查漏补缺
通过如何利用 webpack 来提升前端开发效率 (一) 的学习, 我们已经能够通过 webpack 的 loader 和 piugin 机制来处理各种文件资源. 细心的小伙伴们发现了缺少了对字体文件和 html 中 < img > 标签的资源处理, 那让我们先来解决这个问题.
接上篇文章, 我们的目录结构, 如图所示:
首先是对字体文件的处理, 修改 webpack.config.JS
- // webpack.config.JS
- // 新增对字体的 loader
- {
- test: /\.(eot|woff|woff2|ttf)$/,
- use: [{
- loader: 'url-loader',
- options: {
- name: '[name].[hash:7].[ext]',
- limit: 8192,
- outputPath: 'font', // 打包到 dist/font 目录下
- }
- }]
- },
如何我们从网上随意下载了一种字体, 放置于 src 文件夹下, 并修改 src/index.HTML
<!-- src/index.html -->
- <!DOCTYPE HTML>
- <HTML lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta http-equiv="X-UA-Compatible" content="ie=edge">
- <title>
- my-webpack
- </title>
- </head>
- <body>
- <h1>
- webpack 大法好!! 前端大法好!!
- </h1>
- </body>
- </HTML>
在 index.SCSS 中引入字体
- /* src/index.SCSS */
- /* 添加以下样式 */
- @font-face {
- font-family: 'myFont';
- src: url('./font/ZaoZiGongFangQiaoPinTi-2.ttf');
- }
- h1 {
- font-family: 'myFont';
- }
在此之前, 每次重新打包都要删除 dist 文件夹, 实在是麻烦, 现在我们可以借助 clean-webpack-plugin, 它能够在每次打包时删除指定的文件夹, 我们在命令行执行 NPM i clean-webpack-plugin -D
修改 webpack.config.JS
- // webpack.config.JS
- // 新增以下引入
- const CleanWebpackPlugin = require('clean-webpack-plugin');
- // 新增以下插件
- plugins: [
- new CleanWebpackPlugin(['dist'])
- ],
随后在命令行执行 NPM run build, 我们的 dist 文件夹会被自动删除, 并输出以下结果, 可以看到我们虽然成功打包了字体文件, 但字体文件是在太大, 连 webpack 都发出了警告[big].
这里我们一般有以下解决方案:
对字体文件开启 CDN 加速
通过设计给出已经制作好的样式图
利用 font-spider 对字体进行压缩
我们实践一下第三种方案, 也是我推荐的方案 在命令行依次执行
- NPM i font-spider -D
- font-spider ./dist/index.HTML
可以看到将近 4MB 的字体文件体积瞬间压缩至不足 6KB!!! 而页面效果和之前一模一样.
而对于 HTML 文档中
<img > 标签的引入问题
, 我们需要借助 HTML-loader, 它能将 HTML 文档中 img.src 解析成 require, 从而实现引入图片, 话不多说, 我们直接看效果. 在命令行执行
NPM i HTML-loader -D
修改以下文件
- // webpack.config.JS
- // 新增对 HTML 的 loader
- {
- test: /\.HTML$/,
- use: {
- loader: 'html-loader',
- options: {
- attrs: ['img:src'] // img 代表解析标签, src 代表要解析的值, 以 key:value 形式存在于 attrs 数组中
- }
- }
- }
- <!-- src/index.html -->
- <body>
- + <img src="./leaf.png" alt="">
- </body>
在命令行执行 NPM run build, 查看 dist/index.HTML, 看来已经成功啦
动态加载
设想如果我们的入口文件很大(包含了所有的业务逻辑代码), 就会造成首屏加载变慢, 用户体验感下降. 这里我们从两个方面解决:
模块解耦, 将入口文件解耦, 将基础模块 (UI, 工具类) 和业务模块分离, 即能方便代码维护拓展, 也能减少入口文件的体积.
动态加载, 用户不可能一开始就用到所有的功能, 这时候我们可以将次要的, 需要事件触发的模块, 在之后的交互过程中, 动态引入. 在 src 目录下新增 dynamic.JS
- // dynamic.JS
- export default () => {
- console.log('Im dynamically loaded.');
- }
修改以下文件
- <!-- src/index.html -->
- <body>
+ <button id="btn">点击我, 动态加载 dynamic.JS</button>
</body>
- // src/index.JS
- // 新增以下内容
- const btn = document.getElementById('btn');
- // 点击按钮, 动态加载 dynamic.JS
- btn.onclick = () => {
- import(/* webpackChunkName: "dynamic" */ './dynamic.js').then(function (module) {
- const fn = module.default;
- fn();
- })
- }
执行 NPM run build, 可以看到
如果未设置
/* webpackChunkName: "dynamic" */
, 则是
可以得出得结论是: 设置 ChunkName 为 "dynamic" 是必要的, 否则打包完成会是以自动分配的, 可读性很差的 id 命名的 JS 文件. 且没有 Chunk Names 标识.
现在我们打开 dist/index.HTML, 此时
当我点击该按钮时
控制台打印出
Network 网络请求显示, 动态加载了 dynamic.JS
至此, 我们成功实现了动态加载.
分离开发环境和生产环境
回头看我们的 webpack.config.JS, 不知不觉就写了这么多代码, 鉴于我们在开发实际项目时, 是开发和生产两套工作模式, 各司其职, 我们不如做个了断, 分离配置.
命令行执行 NPM i webpack-merge cross-env -D
webpack-merge 可以合并 webpack 配置项, cross-env 可以设置及使用环境变量.
新增 webpack.base.JS, 提供基本的 webpack loader plugin 配置
- const path = require('path');
- const htmlWebpackPlugin = require('html-webpack-plugin');
- const MiniCssExtractPlugin = require('mini-css-extract-plugin');
- const pathResolve = (targetPath) => path.resolve(__dirname, targetPath);
- const devMode = process.env.NODE_ENV !== 'production';
- // 在 node 中, 有全局变量 process 表示的是当前的 node 进程.
- // process.env 包含着关于系统环境的信息.
- // NODE_ENV 是用户一个自定义的变量, 在 webpack 中它的用途是来判断当前是生产环境或开发环境.
- // 我们可以通过 cross-env 将 NODE_ENV=development 写入 NPM run dev 的指令中, 从而注入 NODE_ENV 变量.
- module.exports = {
- entry: {
- index: pathResolve('js/index.js')
- },
- output: {
- path: pathResolve('dist'),
- },
- module: {
- rules: [
- {
- test: /\.HTML$/,
- use: {
- loader: 'html-loader',
- options: {
- attrs: ['img:src']
- },
- },
- },
- {
- test: /\.(eot|woff|woff2|ttf)$/,
- use: [{
- loader: 'url-loader',
- options: {
- name: '[name].[hash:7].[ext]',
- limit: 8192,
- outputPath: 'font',
- },
- }],
- },
- {
- test: /\.(sa|sc|c)ss$/,
- use: [
- devMode ? 'style-loader' : { // 如果处于开发模式, 则无需再外链 CSS, 直接插入到 < style > 标签中
- loader: MiniCssExtractPlugin.loader,
- options: {
- publicPath: '../'
- }
- },
- 'css-loader',
- 'postcss-loader',
- 'sass-loader',
- ],
- },
- {
- test: /\.(PNG|jpg|jpeg|svg|gif)$/,
- use: [{
- loader: 'url-loader',
- options: {
- limit: 8192,
- name: '[name].[hash:7].[ext]',
- outputPath: 'img',
- },
- }],
- },
- ],
- },
- plugins: [
- new htmlWebpackPlugin({
- minify: {
- collapseWhitespace: true, // 移除空格
- removeAttributeQuotes: true, // 移除引号
- removeComments: true // 移除注释
- },
- filename: pathResolve('dist/index.html'),
- template: pathResolve('src/index.html'),
- })
- ]
- };
新增 webpack.dev.JS, 服务于开发模式下
const path = require('path'); const webpack = require('webpack'); const base = require('./webpack.base.js'); const { smart } = require('webpack-merge'); const pathResolve = (targetPath) => path.resolve(__dirname, targetPath); module.exports = smart(base, { mode: 'development', output: { filename: 'js/[name].[hash:7].js' }, devServer: { contentBase: pathResolve('dist'), port: '8080', inline: true, historyApiFallback: true, hot: true }, plugins: [ new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin() ] })
新增 webpack.prod.JS, 服务于生产模式下
const path = require('path'); const base = require('./webpack.base.js'); const { smart } = require('webpack-merge'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const pathResolve = (targetPath) => path.resolve(__dirname, targetPath); module.exports = smart(base, { mode: 'production', devtool: 'source-map', // 会生成对于调试的完整的. map 文件, 但同时也会减慢打包速度, 适用于打包后的代码查错 output: { filename: 'js/[name].[chunkhash:7].js', chunkFilename: 'js/[name].[chunkhash:7].js', }, plugins: [ new CleanWebpackPlugin(['dist']), new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash:7].css', }), ], });
相应的, package.JSON 也需要修改
// 新增以下两条命令 // cross-env 决定运行环境 --config 决定运行哪个配置文件 "dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack.dev.js", "build": "cross-env NODE_ENV=production webpack --config webpack.prod.js"
缓存之争
缓存在前端的地位毋庸置疑, 正确的利用缓存就能极大地提高应用的加载速度和性能. webpack 利用了 hash 值作为文件名的组成部分, 能有效利用缓存. 当修改文件, 重新打包时, hash 值就会改变, 导致缓存失效, HTTP 请求重新拉取资源.
而 webpack 有三种 hash 处理策略, 分别是:
hash
属于项目工程级别的, 即每次修改任何一个文件, 所有文件名的 hash 值都将改变. 所以一旦修改了任何一个文件, 整个项目的文件缓存都将失效. 如将整个项目的 filename 的命名策略改为 name.[hash:7](:7 的意思是从完整 hash 值中截取前七位), 我们可以看到, 打包后的文件 hash 值是一样的, 所以对于没有改变的模块而言, hash 也被更新了, 导致缓存失效了.
chunkhash
chunkhash 根据不同的入口文件 (Entry) 进行依赖文件解析, 构建对应的 chunk, 生成对应的哈希值. 如将整个项目 filename 的命名策略改为 name.[chunkhash:7], 我们可以看到 Chunk Names 为 "index" 的文件 hash 值一致, 而不同 chunk 的 hash 值不同. 这也就避免了修改某个文件, 整个工程 hash 值都将改变的情况.
contenthash
但问题随之而来, index.SCSS 是作为模块导入到 index.JS 中的, 其 chunkhash 值是一致的, 只要其中之一改变, 与其关联的文件 chunkhash 值也会改变. 这时候就要用到 contenthash, 它是根据文件的内容计算, 该文件的内容改变了, contenthash 值才会改变. 我们将 CSS 文件的命名策略改为 name.[contenthash:7], 并修改 src/index.JS, 不改动其他文件, 再次打包, 发现:
生产环境的配置优化
tree-shaking
字面意思理解为从一棵树上把叶子摇晃下来, 这样数的重量就减轻了, 类比程序, 就如同从我们的应用上删除没用的代码, 从而减少体积. 借于 ES6 的模块引入是静态分析的, 故而 webpack 可以在编译时正确判断到底加载了什么代码, 即没有被引用的模块不会被打包进来, 减少我们的包大小, 缩小应用的加载时间, 呈现给用户更佳的体验. 那么怎么使用呢?
新建 src/utils.JS
// src/utils.JS const square = (num) => num ** 2; const cube = num => num * num * num; // 导出了两个方法 export { square, cube }
新建 src/shake.JS
// src/shake.JS import { cube } from './utils.js'; // 只使用了 cube 方法 console.log('cube(3) is' + cube(3));
在 webpack.base.JS 中新增入口文件 shake.JS
entry: { + shake: pathResolve('src/shake.js') },
命令行执行 NPM run build, 查看打包后的 shake.JS, 并没有发现 square 方法没有被打包进来, 说明 tree-shaking 起作用了. 而这一切都是 webpack 在 production 环境下自动为我们实现的.
splitChunks
字面意思为拆分代码块, 默认情况下它将只会影响按需加载的代码块, 因为改变初始化的代码块将会影响 HTML 中运行项目需要包含的 script 标签. 还记得我们在 src/index.JS 中动态引入了 src/dynamic.JS 吗, 最终 dynamic.JS 被独立打包, 就是归功于 splitChunks.
在实际生产中, 我们经常会引入第三方库(jQuery,Lodash), 往往这些第三方库体积高达几十 KB 掺杂在业务代码中, 并且不会像业务代码一样经常更新, 这时候我们就需要将他们拆分出来, 既能保持第三方库持久缓存, 又能缩减业务代码的体积.
修改 webpack.prod.JS
// 在 module.exports 中新增如下内容 optimization: { runtimeChunk: { name: 'manifest', // 被注入了 webpackJsonp 的定义及异步加载相关的定义, 单独打包模块信息清单, 利于缓存 }, splitChunks: { cacheGroups: { // 缓存组, 默认将所有来源于 node_modules 的模块分配到叫做'venders'的缓存组, 所有引用超过两次的模块分配到'default'缓存组. vendor: { chunks: "all", // all, async, initial 三选一, 插件作用的 chunks 范围, 推荐 all test: /[\\/]node_modules[\\/]/, // 缓存组所选择的的模块范围 name: "vendor", // Chunk Names 及打包出来的文件名 minChunks: 1, // 引用次数>=1 maxInitialRequests: 5, // 页面初始化时加载代码块的请求数量应该<=5 minSize: 0, // 代码块的最小尺寸 priority: 100, // 缓存优先级权重 }, } } },
命令行执行 NPM i lodash -S
修改 src/index.JS
// 新增以下内容 import _ from 'lodash';
执行 NPM run build, 可以看到优化前 lodash 被打包进 index.JS, 优化后 lodash 被打包进 vendor.JS.
压缩代码, 去除冗余
往往在 CSS 代码中, 存在很多我们没有用到的样式, 它们是冗余的, 我们需要将它们剔除, 并压缩剩余的 CSS 样式, 以减少 CSS 文件体积.
在命令行执行 NPM i glob optimize-CSS-assets-webpack-plugin purifycss-webpack purify-CSS -D
修改 webpack.prod.JS
// 新增以下引入 const glob = require('glob'); // 匹配所需文件 const PurifyCssWebpack = require('purifycss-webpack'); // 去除冗余 CSS const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); // 压缩 CSS // 新增以下插件 new PurifyCssWebpack({ paths: glob.sync(pathResolve('src/*.html')) // 同步扫描所有 HTML 文件中所引用的 CSS, 并去除冗余样式 }) // 新增以下优化 optimization: { minimizer: [ new OptimizeCSSAssetsPlugin({}) // 压缩 CSS ] }
执行 NPM run build
可以看到, 去除了冗余 CSS, 并压缩至一行. 接下来, 我们需要压缩 JS 代码. 由于我们使用的是
uglifyjs-webpack-plugin
, 它需要 ES6 的支持, 所以我们先让工程支持 ES6 的语法. Babel 是一个 JavaScript 编译器. 它能把下一代 JavaScript 语法转译成 ES5, 以适配多种运行环境.
@babel/core 提供了 babel 的转译 API, 如 babel.transform 等, 用于对代码进行转译. 像 webpack 的 babel-loader 就是调用这些 API 来完成转译过程的.
@babel/preset-env 可以根据配置的目标浏览器或者运行环境来自动将 ES2015 + 的代码转换为 ES5.
先在命令行执行 NPM i @babel/core @babel/preset-env babel-loader @babel/plugin-syntax-dynamic-import -D
新建. babelrc 文件
{ "presets": [ // 配置预设环境 ["@babel/preset-env", { "modules": false }] ], "plugins": [ "@babel/plugin-syntax-dynamic-import" // 处理 src/index.JS 中动态加载 ] }
修改 webpack.base.JS
// 新增 JS 的解析规则 { test: /\.(JS|jsx)$/, use: 'babel-loader', exclude: /node_modules/ },
然后命令行执行 NPM i uglifyjs-webpack-plugin -D
修改 webpack.prod.JS
// 新增以下引入 const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); // 新增以下优化 optimization: { minimizer: [ + new UglifyJsPlugin({ // 压缩 JS cache: true, parallel: true, sourceMap: true }) ] }
执行 NPM run build, 可以看到打包的文件体积大大减少, 大功告成, JS 也被压缩了.
以 index.HTML 为例, 我们可以打开 Chrome 的开发者工具, 选择 More tools, 点击 Coverage 面板, 可以看到 JS,CSS 等文件的使用率, 配合我们定制的 webpack 配置进行极致优化.
多页面
有时候, 我们需要同时构建多个页面, 借助 HTML-webpack-plugin, 只需在 plugins 中添加新页面的配置项.
新增 src/main.HTML
<!DOCTYPE HTML> <HTML lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title> main page </title> </head> <body> <h1> I am Main Page </h1> </body> </HTML>
修改 webpack.base.JS
// 修改以下内容 plugins: [ new htmlWebpackPlugin({ // 配置 index.HTML minify: { collapseWhitespace: true, removeAttributeQuotes: true, removeComments: true }, filename: pathResolve('dist/index.html'), template: pathResolve('src/index.html'), chunks: ['manifest', 'vendor', 'index', ] // 配置 index.HTML 需要用的 chunk 块, 即加载哪些 JS 文件, manifest 模块管理的核心, 必须第一个进行加载, 不然会报错 }), new htmlWebpackPlugin({ // 配置 main.HTML minify: { collapseWhitespace: true, removeAttributeQuotes: true, removeComments: true }, filename: pathResolve('dist/main.html'), template: pathResolve('src/main.html'), chunks: ['manifest', 'shake'] // 配置 index.HTML 需要用的 chunk 块, 加载 manifest.JS,shake.JS }), ],
执行 NPM run build, 成功构建了 index.HTML,main.HTML.
结语
至此, 我们摆脱了第三方脚手架的的禁锢, 循序渐进的搭建了属于自己的前端流程工具, 做到了即改即用, 功能俱全, 快速便捷, 复用性强的特点. 希望小伙伴能亲自动手, 别老是纸上谈 webpack, 要理解它的构建, 优化原理, 得心应手得融入到自己的工程项目中, 拒绝再用以前繁琐, 不规范的开发流程, 不做 "CV 工程师", 创建属于自己的知识体系, 工作流程, 提高前端的开发效率.
最后, 本项目源码已部署在 GitHub 上, 并增加了许多额外优化(Less 的支持, ESLint 检测, 针对图片格式的压缩...), 让大家可以直接下载体验, 并协助项目开发, 日后也会持续维护, 希望小伙伴们可以互相学习, 提提建议.
easy-frontend 一个快速, 简单, 易用的前端开发效率提升工具 https://github.com/B2D1/easy-frontend
Star 该项目, 就是你们对我最大的的鼓励!!
前端路上, 不忘初心, 祝大家早日发财!!
来源: https://juejin.im/post/5c41a4866fb9a049f7467d73