阅读 188

webpack 快速入门 系列 —— 性能

webpack 快速入门 系列 —— 性能

性能

本篇主要介绍 webpack 中的一些常用性能,包括热模块替换、source map、oneOf、缓存、tree shaking、代码分割、懒加载、渐进式网络应用程序、多进程打包、外部扩展(externals)和动态链接(dll)。

准备本篇的环境

虽然可以仅展示核心代码,但笔者认为在一个完整的环境中边看边做,举一反三,效果更佳。

这里的环境其实就是实战一一文完整的示例,包含打包样式、打包图片、以及打包javascript

项目结果如下:

webpack-example3        - src                 // 项目源码     - index.html        // 页面模板     - index.js          // 入口   - package.json        // 存放了项目依赖的包   - webpack.config.js   // webpack配置文件

代码如下:

// index.html <!DOCTYPE html> <html> <head>     <meta charset="UTF-8">     <meta http-equiv="X-UA-Compatible" content="IE=edge">     <meta name="viewport" content="width=`, initial-scale=1.0">     <title>Document</title> </head> <body>     <p>请查看控制台</p>     <span class='m-box img-from-less'></span> </body> </html>
// index.js console.log('hello');
// package.json {   "name": "webpack-example3",   "version": "1.0.0",   "description": "",   "main": "index.js",   "scripts": {     "test": "echo \"Error: no test specified\" && exit 1",     "build": "webpack",     "dev": "webpack-dev-server"   },   "keywords": [],   "author": "",   "license": "ISC",   "devDependencies": {     "@babel/preset-env": "^7.14.2",     "babel-loader": "^8.2.2",     "core-js": "3.11",     "css-loader": "^5.2.4",     "eslint": "^7.26.0",     "eslint-config-airbnb-base": "^14.2.1",     "eslint-webpack-plugin": "^2.5.4",     "file-loader": "^6.2.0",     "html-loader": "^1.3.2",     "html-webpack-plugin": "^4.5.2",     "less-loader": "^7.3.0",     "mini-css-extract-plugin": "^1.6.0",     "optimize-css-assets-webpack-plugin": "^5.0.4",     "postcss-loader": "^4.3.0",     "postcss-preset-env": "^6.7.0",     "url-loader": "^4.1.1",     "webpack": "^4.46.0",     "webpack-cli": "^3.3.12",     "webpack-dev-server": "^3.11.2"   } }
// webpack.config.js const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const ESLintPlugin = require('eslint-webpack-plugin'); process.env.NODE_ENV = 'development' const postcssLoader = {    loader: 'postcss-loader',    options: {     // postcss 只是个平台,具体功能需要使用插件     // Set PostCSS options and plugins     postcssOptions:{       plugins:[         // 配置插件 postcss-preset-env         [           "postcss-preset-env",           {             // browsers: 'chrome > 10',             // stage:            },         ],       ]     }   }  } module.exports = {   entry: './src/index.js',   output: {     filename: 'main.js',     path: path.resolve(__dirname, 'dist')   },   module: {     rules: [       {         test: /\.css$/i,         // 将 style-loader 改为 MiniCssExtractPlugin.loader         use: [MiniCssExtractPlugin.loader, "css-loader", postcssLoader],       },       {         test: /\.less$/i,         loader: [           // 将 style-loader 改为 MiniCssExtractPlugin.loader           MiniCssExtractPlugin.loader,           "css-loader",           postcssLoader,           "less-loader",         ],       },       {         test: /\.(png|jpg|gif)$/i,         use: [           {             loader: 'url-loader',             options: {               // 指定文件的最大大小(以字节为单位)               limit: 1024*6,             },           },         ],       },       // +       {         test: /\.html$/i,         loader: 'html-loader',       },       {         test: /\.js$/,         exclude: /node_modules/,         use: {           loader: 'babel-loader',           options: {             presets: [               [                 '@babel/preset-env',                 // +                 {                   // 配置处理polyfill的方式                   useBuiltIns: "usage",                   // 版本与我们下载的版本保持一致                   corejs: { version: "3.11"},                   "targets": "> 0.25%, not dead"                 }               ]             ]           }         }       }     ]   },   plugins: [     new MiniCssExtractPlugin(),     new OptimizeCssAssetsPlugin(),     new HtmlWebpackPlugin({         template: 'src/index.html'     }),     // new ESLintPlugin({     //   // 将启用ESLint自动修复功能。此选项将更改源文件     //   fix: true     // })   ],   mode: 'development',   devServer: {     open: true,     contentBase: path.join(__dirname, 'dist'),     compress: true,     port: 9000,   }, };

Tip: 由于本篇不需要 eslint,为避免影响,所以先注释。

在 webpack-example3 目录下运行项目:

// 安装项目依赖的包 > npm i // 启动服务 > npm run dev

浏览器会自动打开页面,如果看到”请查看控制台“,控制台也输出了“hello”,说明环境准备就绪。

:笔者运行 npm i 时出现了一些问题,在公司运行 npm i 验证此文是否正确,结果下载得很慢(好似卡住了),于是改为淘宝镜像 cnpm i,这次仅花少许时间就执行完毕,接着运行 npm run dev 却在终端报错。于是根据错误提示安装 babel-loader@7 ,再次重启服务,问题仍旧没有解决。回家后,运行 npm i,依赖安装成功,可能环境也很重要。

// 终端报错 ...  babel-loader@8 requires Babel 7.x (the package '@babel/core'). If you'd like to use Babel 6.x ('babel-core'), you should install 'babel-loader@7'.

热模块替换

模块热替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新所有类型的模块,而无需完全刷新。

Tip: HMR 不适用于生产环境,这意味着它应当用于开发环境

下面我们就从 html、css 和 js 三个角度来体验热模块替换。

启用 hmr

此功能可以很大程度提高生产效率。我们要做的就是更新 webpack-dev-server 配置, 然后使用 webpack 内置的 HMR 插件。

配置 hot: true 就能启用 hmr。

// webpack.config.js module.exports = {   devServer: {     // 开启热模块替换     hot: true   } }

css 使用 hmr

新建一个 css 文件,通过 index.js 引入:

// a.css p{color:blue;}
// index.js import './a.css'

首先我们先不开启 hmr,重启服务(npm run dev),浏览器文字显示蓝色。如果改为红色(color:red;),你会发现整个页面都刷新了,文字变为红色。

接着开启hmr(hot: true),重启服务,再次修改颜色,文字的颜色会改变,但整个页面不会刷新。

Tip:如果觉得每次重启服务,都会自动打开浏览器页面,你可以注释掉 open: true 来关闭这个特征。

这里 css 热模块之所以生效,除了在 dev-server 中开启了 hmr,另一个是借助了 mini-css-extract-plugin 这个包;而借助 style-loader 使用模块热替换来加载 CSS 也这么简单。

html 使用 hmr

没有开启热模块替换之前,修改 index.html 中的文字,浏览器页面会自动刷新;而开启之后,修改 html 中的文字,浏览器页面就不会自动刷新。

将 index.html 也配置到入口(entry)中:

// webpack.config.js module.exports = {   - entry: './src/index.js',   // 将 index.html 也作为入口文件   + entry: ['./src/index.js', './src/index.html'], }

重启服务,再次修改 index.html,浏览器页面自动刷新,热模块替换对 html 没生效。

// index.html - <p>请查看控制台</p> + <p>请查看控制台2</p>

Tip:热模块替换,就是一个模块发生了变化,只变更这一个,其他模块无需变化;而 index.html 不像 index.js 会有多个模块,index.html 只有一个模块,就是它自己,所以也就不需要热模块替换。

js 使用 hmr

首先在 dev-server 中开启 hmr,然后创建一个 js 模块,接着在 index.js 中引入:

// a.js const i = 1; console.log(i);
// index.js // 引入 a.js 模块 import './a';

此刻,你若修改 i 的值(const i = 2;),则会发现浏览器页面会刷新。

要让热模块替换在 js 中生效,我们需要修改代码:

// index.js // 引入 a.js 模块 import './a'; if (module.hot) {   module.hot.accept('./a', () => {     console.log('Accepting the updated printMe module!');   }); }

再次修改 i 的值,控制台会输出新的值,但浏览器页面不会再刷新。

此时,如果你尝试给入口文件(index.js)底部增加一条语句 console.log('a');,你会发现浏览器还是会刷新。

所以这种方式对入口文件无效,只能处理非入口 js。

:如果一个 js 模块没有 HMR 处理函数,更新就会冒泡(bubble up)。

小结

模块热替换比较难以掌握。

社区还提供许多其他 loader,使 HMR 与各种框架和库平滑地进行交互:

  • Vue Loader: 此 loader 支持 vue 组件的 HMR,提供开箱即用体验。

  • React Hot Loader: 实时调整 react 组件。

source map

source map,提供一种源代码到构建后代码的映射,如果构建后代码出错了,通过映射可以方便的找到源代码出错的地方。

初步体验

我们先故意弄一个语法错误,看浏览器的控制台如何提示:

// a.js const i = 1; // 下一行语法错误 console.log(i)();
// 控制台提示 a.js 第3行出错 Uncaught TypeError: console.log(...) is not a function         a.js:3

点击“a.js:3”,显示内容为:

var i = 1; // 下一行语法错误 console.log(i)();

定位到了源码,很清晰。

假如换成 es6 的语法,点击进入的错误提示就没这么清晰了。请看示例:

// a.js class Dog {     constructor(name) {         this.name = name;     }     say() {         console.log(this.name)();     } } new Dog('xiaole').say();
... var Dog = /*#__PURE__*/function () {   function Dog(name) {     _classCallCheck(this, Dog);     this.name = name;   }   _createClass(Dog, [{     key: "say",     value: function say() {       console.log(this.name)(); // {1}     }   }]);   return Dog; }(); new Dog('xiaole').say();

错误提示会定位了行{1},我们看到的不在是自己编写的源码,而是通过 babel 编译后的代码。

接下来我们通过配置 devtool,选择一种 source map 格式来增强调试过程。不同的值会明显影响到构建(build)和重新构建(rebuild)的速度。

Tip:Devtool 控制是否生成,以及如何生成 source map。

// webpack.config.js module.exports = {   devtool: 'source-map' }

重启服务,通过错误提示点击进去,则会看到如下代码:

class Dog {   constructor(name) {     this.name = name;   }   say() {     console.log(this.name)(); // {1}   } } new Dog('xiaole').say();

不在是编译后的代码,而是我们的源码,而且在行{1}处,对错误也有清晰的提示。

不同的值

source map 格式有多种不同的值,以下是笔者对其中几种值的研究结论:

  • devtool: 'source-map'

> npm run build 1. 会生成一个 dist/main.js.map 文件 2. 在 dist/main.js 最后一行,有如下一行代码: //# sourceMappingURL=main.js.map 3. 上文我们知道,调试能看到源码,官网文档的描述是 `quality 是 original` 4. 构建(build)速度和重建(rebuild)速度都是最慢(slowest) 5. 官网推荐其可作为生产的选择
  • devtool: inline-source-map

> npm run build 1. 没生成一个 dist/main.js.map 文件 2. 在 dist/main.js 最后一行,有如下一行代码: //# sourceMappingURL=data:application/json;charset= 3. 调试能看到源码 4. 构建(build)速度和重建(rebuild)速度都是最慢(slowest)
  • devtool: eval-source-map

> npm run build 1. 没生成一个 dist/main.js.map 文件 2. 在 dist/main.js 中有 15 处 sourceMappingURL。而 inline-source-map 只有一处。 3. 调试能看到源码 4. 构建(build)速度最慢(slowest),但重建(rebuild)速度正常(ok) 5. 官网推荐其可作为开发的选择
  • devtool: hidden-source-map

> npm run build 1. 生成一个 dist/main.js.map 文件 2. 点击错误提示,看到的是编译后的代码 Uncaught TypeError: console.log(...) is not a function   main.js:11508 3. 构建(build)速度和重建(rebuild)速度都是最慢(slowest)

:官网说 hidden-source-map 的品质是 original,但笔者这里却是编译后的!

如何选择

source map 有很多不同的值,我们该如何选择?

幸好官网给出了建议。

开发环境,我们要求构建速度要快,方便调试:

  • eval-source-map,每个模块使用 eval() 执行,并且 source map 转换为 DataUrl 后添加到 eval() 中。初始化 source map 时比较慢,但是会在重新构建时提供比较快的速度,并且生成实际的文件。行数能够正确映射,因为会映射到原始代码中。它会生成用于开发环境的最佳品质的 source map。

生成环境,考虑到代码是否要隐藏,是否需要方便调试:

  • source-map,整个 source map 作为一个单独的文件生成。它为 bundle 添加了一个引用注释,以便开发工具知道在哪里可以找到它。官网推荐其可作为生产的选择。

  • (none)(省略 devtool 选项),不生成 source map,也是一个不错的选择

Tip:若你还有一些特别的需求,就去官网寻找答案

oneOf

oneof 与下面程序的 break 作用类似:

let count = 1 for(; count < 10; count++){   if(count === 3){     break;   } } console.log(`匹配了${count}次`) // 匹配了3次

这段代码,只要 count 等于 3,就会被 break 中断退出循环。

通常,我们会这样定义多个规则:

module: {     rules: [{         test: /\.css$/i,         loader: ...       },       {         test: /\.css$/i,         loader: ...       },       {         test: /\.less$/i,         loader: ...       },       {         test: /\.(png|jpg|gif)$/i,         loader: ...       }       ...     ]

当 a.css 匹配了第一个规则,还会继续尝试匹配剩余的规则。而我希望提高一下性能,只要匹配上,就不在匹配剩余规则。则可以使用 Rule.oneOf,就像这样:

module: {     rules: [       {         oneOf: [{             test: /\.css$/i,             loader: ...           },           {             test: /\.less$/i,             loader: ...           },           {             test: /\.(png|jpg|gif)$/i,             loader: ...           }           ...         ]       }     ]

如果同一种文件需要执行多个 loader,就像这里 css 有 2 个 loader。我们可以把其中一个 loader 提到 rules 中,就像这样:

module: {     rules: [       {         test: /\.css$/i,         // 优先执行         enforce: 'pre'         loader: ...       },       {         oneOf: [{             test: /\.css$/i,             loader: ...           },           ...        ]       }     ]

Tip: 可以通过配置 enforce 指定优先执行该loader

缓存

babel 缓存

让第二次构建速度更快。

配置很简单,就是给 babel-loader 添加一个选项:

{   loader: 'babel-loader',   options: {     presets: [       ...     ],     // 开启缓存     cacheDirectory: true   } }

Tip:因为要经过 babel-loader 编译,如果代码量太少,就不太准确,建议找大量的 es6 代码自行测试。

静态资源的缓存

Tip: 本小节讲的其实就是 hash、chunkhash和conenthash。

通常我们将代码编译到 dist 目录中,然后发布到服务器上,对于一些静态资源,我们会设置其缓存。

具体做法如下:

通过命令 npm run build 将代码编译到 dist 目录;

接着通过 express 启动服务,该服务会读取 dist 中的内容,相当于把代码发布到服务器上:

// 安装依赖 > npm i -D express@4
// 在项目根目录下创建一个服务:server.js const express = require('express') const app = express() const port = 3001 app.use(express.static('dist')); // 监听服务 app.listen(port, () => {   console.log(`Example app listening at http://localhost:${port}`) })
> nodemon server.js   [nodemon] 2.0.7 [nodemon] to restart at any time, enter `rs` [nodemon] watching path(s): *.* [nodemon] watching extensions: js,mjs,json [nodemon] starting `node server.js` Example app listening at http://localhost:3001

通过浏览器访问 http://localhost:3001,多刷新几次,在网络中会看见 main.js 的状态是 304,笔者这里的时间在2ms或5ms之间。

Tip:304 仍然会发送请求,通常请求头中 If-Modified-Since 的值和响应头中 Last-Modified 的值是相同的。

If-Modified-Since: Sat, 17 Jul 2021 02:34:06 GMT Last-Modified: Sat, 17 Jul 2021 02:34:06 GMT

接下来我给静态资源增加缓存,这里就增加一个 10 秒的缓存:

// server.js - app.use(express.static('dist')); + app.use(express.static('dist', { maxAge: 1000 * 10 }));

再次请求,发现 main.js 首先是 304,接下来10秒内状态码则是200,大小则指示来自内存,时间也变为 0 ms。过10秒后再次请求,又是 304。

现在有一个问题,在强缓存期间,如果出现了bug,我们哪怕修复了,用户使用却还是缓存中有问题的代码。

我们模拟一下这个过程图:先将缓存改长一点,比如 1 天,用户访问先输出 1,让浏览器缓存后,我们再修改代码让其输出 2,用户再次访问会输出什么?

// server.js app.use(express.static('dist', { maxAge: '1d' }));
// index.js console.log('1');

重新打包生成 dist,接着用户通过浏览器访问,控制台输出 1。

修改 js,重新打包生成 dist,再次访问,控制台还是输入 1。

// index.js console.log('2');

:不要强刷,因为用户不知道强刷,也不会去管。

于是我们打算从文件名入手来解决此问题,我们依次来看看 hash、chunkhash和conenthash。

hash

核心代码如下:

// index.js import './a.css' console.log('1');
// a.css p{color:red;}
// webpack.config.js module.exports = {   output: {     filename: 'main.[hash:10].js',   },   plugins: [     new MiniCssExtractPlugin({       filename: "[name].[hash:10].css",     })   ] }

重新打包:

> npm run build > webpack-example3@1.0.0 build > webpack Hash: b2e057d598ca9092abd3 Version: webpack 4.46.0 Time: 4837ms Built at: 2021-07-14 8:17:54 ├F10: PM┤                  Asset       Size  Chunks                         Chunk Names             index.html  417 bytes          [emitted]    main.b2e057d598.css   12 bytes    main  [emitted] [immutable]  main     main.b2e057d598.js   5.22 KiB    main  [emitted] [immutable]  main Entrypoint main = main.b2e057d598.css main.b2e057d598.js main.b2e057d598.js.map

主要看生成的 css 和 js 文件,名字中都带有相同的值 b2e057d598,取的是生成的 Hash 的前10位。index.html 中也会自动引入对应的文件名。

现在浏览器访问,文字是红色,控制台输出1。

接着模拟修复缺陷,将文字改为蓝色,再次打包。

p{color:blue;}
> npm run build > webpack-example3@1.0.0 build > webpack Hash: ed2cd907a36536276d20 Version: webpack 4.46.0 Time: 4771ms Built at: 2021-07-14 8:29:14 ├F10: PM┤                  Asset       Size  Chunks                         Chunk Names             index.html  417 bytes          [emitted]    main.ed2cd907a3.css   13 bytes    main  [emitted] [immutable]  main     main.ed2cd907a3.js   5.22 KiB    main  [emitted] [immutable]  main

浏览器访问,文字确实变为蓝色。但 js 和 css 都重新请求了,再看打包生成的文件,js 和 css 也都重新生成了新的文件名。这个会导致一个问题,只修改一个文件,其他的所有缓存都会失效。

Tip:这里修复的是 css,如果修复 js 也同样会导致所有缓存失效。

chunkhash

hash 会导致所有缓存失效,我们将其改为 chunkhash,还是存在相同的问题。请看示例:

将 hash 改为 chunkhash:

// webpack.config.js module.exports = {   output: {     filename: 'main.[chunkhash:10].js',   },   plugins: [     new MiniCssExtractPlugin({       filename: "[name].[chunkhash:10].css",     })   ] }

修改 css,然后重新打包,发现 js 和 css 文件也都重新生成了,虽然 chunkhash 与 hash 值不相同,但 main.js 和 main.css 中的 chunkhash 是一样的:

> npm run build > webpack-example3@1.0.0 build > webpack Hash: 8c1c035175aae3d36fea Version: webpack 4.46.0 Time: 5000ms Built at: 2021-07-14 9:16:46 ├F10: PM┤                  Asset       Size  Chunks                         Chunk Names             index.html  417 bytes          [emitted]    main.619734f520.css   13 bytes    main  [emitted] [immutable]  main     main.619734f520.js   5.22 KiB    main  [emitted] [immutable]  main

Tip: 通过入口文件引入的模块都属于一个 chunk。这里 css 是通过入口文件(index.js)引入的,所以 main.js 和 main.css 的 chunkhash 值相同。

contenthash

contenthash 是根据文件内容来的,可以较好的解决以上问题。请看示例:

将 chunkhash 改为 contenthash,然后打包:

// webpack.config.js module.exports = {   output: {     filename: 'main.[contenthash:10].js',   },   plugins: [     new MiniCssExtractPlugin({       filename: "[name].[contenthash:10].css",     })   ] }
> npm run build > webpack-example3@1.0.0 build > webpack Hash: 12994324788654e2ffc4 Version: webpack 4.46.0 Time: 5115ms Built at: 2021-07-14 9:26:59 ├F10: PM┤                  Asset       Size  Chunks                         Chunk Names             index.html  417 bytes          [emitted]    main.21668176f0.css   12 bytes    main  [emitted] [immutable]  main     main.8983191438.js   5.22 KiB    main  [emitted] [immutable]  main

这次,js 和 css 的 hash 值不在相同。通过浏览器访问多次后,main.js 和 main.css 也都被强缓存。

修改css:

p{color:yellow;}

打包发现 js(main.8983191438.js) 没有变,只有 css 变了:

> npm run build > webpack-example3@1.0.0 build > webpack Hash: 1598c3794090ebc6964c Version: webpack 4.46.0 Time: 4905ms Built at: 2021-07-14 9:31:14 ├F10: PM┤                  Asset       Size  Chunks                         Chunk Names             index.html  417 bytes          [emitted]    main.0241bb73c4.css   13 bytes    main  [emitted] [immutable]  main     main.8983191438.js   5.22 KiB    main  [emitted] [immutable]  main

再次通过浏览器访问,发现 css 请求了新的文件,而 js 还是来自缓存。

Tip: 是否要将 hash 清除?

注:此刻运行 npm run build 会报错,为了不影响下面的介绍,所以将 hash 去除,source map 也不需要,一并删除。

ERROR in chunk main [entry] Cannot use [chunkhash] or [contenthash] for chunk in 'main.[contenthash:10].js' (use [hash] instead)

tree shaking

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。

使用树摇非常简单,只需要满足两个条件:

  • 使用 es6 模块化

  • 模式(mode)开启production

直接演示,请看:

a.js 中导出 a 和 b,但在index.js 中只使用了a:

// a.js export let a = 'hello' export let b = 'jack'
// index.js import { a } from './a.js' console.log(a);

首先在开发模式下测试,发现 a.js 中的”hello“和”jack“都打包进去了,请看示例:

module.exports = {   mode: 'development', }
// dist/main.js // a 和 b 都被打包进来,尽管 b 没有被用到 var a = 'hello'; var b = 'jack';

而在生成模式下,只有用到的 a 才被打包进去,请看示例:

module.exports = {   mode: 'production', }
// dist/main.js // 只找到 hello,没有找到 jack console.log("hello")

将文件标记为 side-effect-free(无副作用)

在一个纯粹的 ESM 模块世界中,很容易识别出哪些文件有副作用。然而,我们的项目无法达到这种纯度,所以,此时有必要提示 webpack compiler 哪些代码是“纯粹部分”。

通过 package.json 的 "sideEffects" 属性,来实现这种方式。

{   "sideEffects": false }

如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false,来告知 webpack 它可以安全地删除未用到的 export。

Tip:"side effect(副作用)" 的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。

我们通过一个例子说明下:

在入口文件引入 css 文件:

// index.js import './a.css' import { a } from './a.js' console.log(a);
// a.css p{color:yellow;}
// webapck.config.js mode: 'production'

打包会生成 css:

> npm run build      Asset       Size  Chunks             Chunk Names index.html  342 bytes          [emitted]   main.css   13 bytes       0  [emitted]  main    main.js    1.3 KiB       0  [emitted]  main

在 package.json 添加 "sideEffects": false,标注所有代码都不包含副作用:

{   "sideEffects": false }

再次打包,则不会生成 css:

> npm run build      Asset       Size  Chunks             Chunk Names index.html  303 bytes          [emitted]    main.js    1.3 KiB       0  [emitted]  main

:所有导入文件都会受到 tree shaking 的影响。这意味着,如果在项目中使用类似 css-loader 并 import 一个 CSS 文件,则需要将其添加到 side effect 列表中,以免在生产模式中无意中将它删除:

// package.json {   "sideEffects": [     "*.css",     "*.less"   ] }

代码分割

将一个文件分割成多个,加载速度可能会更快,而且分割成多个文件后,还可以实现按需加载。

optimization.splitChunks

对于动态导入模块,默认使用 webpack v4+ 提供的全新的通用分块策略(common chunk strategy) —— SplitChunksPlugin。

开箱即用的 SplitChunksPlugin 对于大部分用户来说非常友好。

webpack 将根据以下条件自动拆分 chunks:

  • 新的 chunk 可以被共享,或者模块来自于 node_modules 文件夹

  • 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积)

  • 当按需加载 chunks 时,并行请求的最大数量小于或等于 30

  • 当加载初始化页面时,并发请求的最大数量小于或等于 30

Tip: SplitChunksPlugin的默认配置如下:

// webpack.config.js module.exports = {   optimization: {     splitChunks: {       chunks: 'async',       minSize: 20000,       minRemainingSize: 0,       minChunks: 1,       maxAsyncRequests: 30,       maxInitialRequests: 30,       enforceSizeThreshold: 50000,       cacheGroups: {         defaultVendors: {           test: /[\\/]node_modules[\\/]/,           priority: -10,           reuseExistingChunk: true,         },         default: {           minChunks: 2,           priority: -20,           reuseExistingChunk: true,         },       },     },   }, };

默认配置很多,如果我们不需要修改,则不用管它们,下面我们来体验一下 splitChunks.chunks:

Tip:splitChunks.chunks,表明将选择哪些 chunk 进行优化。当提供一个字符串,有效值为 all,async 和 initial。设置为 all 可能特别强大,因为这意味着 chunk 可以在异步和非异步 chunk 之间共享。

> npm i lodash@4
// index.js import _ from 'lodash'; console.log(_);

打包只生成一个 js:

> npm run build      Asset       Size  Chunks             Chunk Names index.html  303 bytes          [emitted]    main.js   72.7 KiB       0  [emitted]  main

配置splitChunks.chunks:

// webapck.config.js module.exports = {   optimization: {     splitChunks: {       chunks: 'all',     },   }, };

再次打包,这次生成两个 js,其中Chunk Names 是 vendors~main 对应的就是 loadsh:

> npm run build      Asset       Size  Chunks             Chunk Names  1.main.js   71.5 KiB       1  [emitted]  vendors~main index.html  336 bytes          [emitted]    main.js    1.9 KiB       0  [emitted]  main

同一个 chunk 中,如果 index.js 和 a.js 都引入 loadash,会如何打包?请看示例:

// index.js import {a} from './a.js' import _ from 'lodash'; console.log(a) console.log(_);
// a.js export let a = 'hello' export let b = 'jack'
> npm run build      Asset       Size  Chunks             Chunk Names  1.main.js   71.5 KiB       1  [emitted]  vendors~main index.html  336 bytes          [emitted]    main.js   1.92 KiB       0  [emitted]  main

同样是两个 js,而且 loadash 应该是公用了,因为 main.js 较上次只增加了 0.02 kb。

动态导入

使用动态导入可以分离出 chunk。

请看示例:

上文我们知道,这段代码打包会生成两个 js,其中 main.js 包含了 a.js。

// index.js import {a} from './a.js' import _ from 'lodash'; console.log(a) console.log(_);

将其中的 a.js 改为动态导入的方式:

// index.js import _ from 'lodash'; // 动态导入 import(/* webpackChunkName: 'a' */'./a').then((aModule) => {     console.log(aModule.a); }); console.log(_);

打包:

> npm run build      Asset       Size  Chunks             Chunk Names  0.main.js  192 bytes       0  [emitted]  a  2.main.js   94.6 KiB       2  [emitted]  vendors~main index.html  336 bytes          [emitted]    main.js   2.75 KiB       1  [emitted]  main

其中 a.js 被单独打包成一个js(从 Chunk Names 为 a 可以得知)

懒加载

懒加载就是用到的时候在加载。

请看示例:

我们在入口文件注册一个点击事件,只有点击时才加载 a.js。

// index.js document.body.onclick = function () {     // 动态导入     import(/* webpackChunkName: 'a' */'./a').then((aModule) => {         console.log(aModule.a);     }); };
// a.js console.log('moduleA'); export let a = 'hello' export let b = 'jack'

启动服务,测试:

> npm run dev 第一次点击:moduleA hello 第二次点击:hello

只有第一次点击,才会请求 a.js 模块。

Tip:懒加载其实用到的就是上文介绍的动态导入

预获取

思路可能是这样:

  1. 首先使用普通模式

  2. 普通模式下,一次性加载太多,而 a.js 这个文件又有点大,于是就使用懒加载,需要使用的时候在加载 a.js

  3. 触发点击事件,懒加载 a.js,但 a.js 很大,需要等待好几秒中才触发,于是我想预获取来减少等待的时间

将懒加载改为预获取:

// index.js document.body.onclick = function () {     // 动态导入     import(/* webpackChunkName: 'a', webpackPrefetch: true*/'./a').then((aModule) => {         console.log(aModule.a);     }); };

刷新浏览器,发现 a.js 被加载了;触发点击事件,输出 moduleA hello,再次点击,输出 hello。

Tip:浏览器中有如下一段代码:

// 指示着浏览器在闲置时间预取 0.main.a3f7d94cb1.js <link rel="prefetch" as="script" href="0.main.a3f7d94cb1.js">

预获取和懒加载的不同是,预获取会在空闲的时候先加载。

渐进式网络应用程序

渐进式网络应用程序(progressive web application - PWA),是一种可以提供类似于 native app(原生应用程序) 体验的 web app(网络应用程序)。PWA 可以用来做很多事。其中最重要的是,在离线(offline)时应用程序能够继续运行功能。这是通过使用名为 Service Workers 的 web 技术来实现的。

我们首先通过一个包来启动服务:

> npm i -D http-server@0
// package.json {   "scripts": {     "start": "http-server dist"   }, }
> npm run build

启动服务:

> npm run start > webpack-example3@1.0.0 start > http-server dist Starting up http-server, serving dist Available on:   http://192.168.85.1:8080   http://192.168.75.1:8080   http://192.168.0.103:8080   http://127.0.0.1:8080 Hit CTRL-C to stop the server

:多个 url 与适配器有关:

> ipconfig 以太网适配器 VMware Network Adapter VMnet1:    IPv4 地址 . . . . . . . . . . . . : 192.168.85.1    以太网适配器 VMware Network Adapter VMnet8:    IPv4 地址 . . . . . . . . . . . . : 192.168.75.1 无线局域网适配器 WLAN:    IPv4 地址 . . . . . . . . . . . . : 192.168.0.103

通过浏览器访问 http://127.0.0.1:8080。如果我们将服务器关闭,再次刷新页面,则不能再访问。

接下来我们要做的事:通过离线技术让网页再服务器关闭时还能访问。

请看示例:

添加 workbox-webpack-plugin 插件,然后调整 webpack.config.js 文件:

> npm i -D workbox-webpack-plugin@6
// webapck.config.js   const WorkboxPlugin = require('workbox-webpack-plugin');   module.exports = {     plugins: [      new WorkboxPlugin.GenerateSW({        // 这些选项帮助快速启用 ServiceWorkers        // 不允许遗留任何“旧的” ServiceWorkers        clientsClaim: true,        skipWaiting: true,      }),     ],   };

完成这些设置,再次打包,看下会发生什么:

> npm run build               Asset       Size  Chunks             Chunk Names           0.main.js  192 bytes       0  [emitted]  a           2.main.js   94.6 KiB       2  [emitted]  vendors~main          index.html  336 bytes          [emitted]             main.js   2.75 KiB       1  [emitted]  main   service-worker.js   1.11 KiB          [emitted] workbox-15dd0bab.js   13.6 KiB          [emitted]

生成了两个额外的文件:service-worker.js 和 workbox-15dd0bab.js。service-worker.js 是 Service Worker 文件。

值得高兴的是,我们现在已经创建出一个 Service Worker。接下来我们注册 Service Worker。

// index.js document.body.onclick = function () {     // 动态导入     import(/* webpackChunkName: 'a', webpackPrefetch: true*/'./a').then((aModule) => {         console.log(aModule.a);     }); }; if ('serviceWorker' in navigator) {     window.addEventListener('load', () => {         navigator.serviceWorker.register('/service-worker.js').then(registration => {             console.log('SW registered: ', registration);         }).catch(registrationError => {             console.log('SW registration failed: ', registrationError);         });     }); }

再次运行 npm run build 来构建包含注册代码版本的应用程序。然后用 npm start 启动服务。访问 http://127.0.0.1:8080/ 并查看 console 控制台。在那里你应该看到:

SW registered

Tip:如果没有看见 SW registered,可以尝试强刷

现在来进行测试。停止 server 并刷新页面。如果浏览器能够支持 Service Worker,应该可以看到你的应用程序还在正常运行。然而,server 已经停止 serve 整个 dist 文件夹,此刻是 Service Worker 在进行 serve。

Tip:更过 pwa 可以参考 "mdn 渐进式应用程序";淘宝(taobao.com)以前有 pwa,现在却没有了。

多进程打包

通过多进程打包,用的好可以加快打包的速度,用得不好甚至会更慢。

这里使用一个名为 thread-loader 包来做多进程打包。每个 worker 是一个单独的 node.js 进程,开销约 600 毫秒,还有一个进程间通信的开销。

:仅将此加载器用于昂贵的操作!比如 babel

我们演示一下:

未使用多进程打包时间是 3122ms:

// index.js import _ from 'lodash' console.log(_);
> npm run build Hash: a4868f457d7ce754335b Version: webpack 4.46.0 Time: 3031ms

加入多线程:

> npm i -D thread-loader@3
// webpack.config.js -> module.exports -> module.rules {   test: /\.js$/,   exclude: /node_modules/,   use: [     'thread-loader',     {       loader: 'babel-loader',       ...     }   ] }
> npm run build Hash: a4868f457d7ce754335b Version: webpack 4.46.0 Time: 3401ms

构建时间更长。

Tip: 可能是代码中需要 babel 的 js 代码太少,所以导致多线程效果不明显。

外部扩展(externals)

externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。

externals

防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。

例如 jQuery 这个库来自 cdn,则不需要将 jQuery 打包。请看示例:

Tip: 为了测试看得更清晰,注释掉 pwa 和 splitChunks。

> npm i jquery@3
// index.js import $ from 'jquery'; console.log($);

打包生成一个 js,其中包含了 jquery:

> npm run build               Asset       Size  Chunks             Chunk Names           1.main.js     88 KiB       1  [emitted]  vendors~main          index.html  336 bytes          [emitted]             main.js    1.9 KiB       0  [emitted]  main

由于开启了 splitChunks,这里 1.main.js 就是 jquery。

使用 external 将 jQuery 排除:

// webpack.config.js module.exports = {   externals: {     // jQuery 是jquery暴露给window的变量名,这里可以将 jQuery 改为 $,但 jquery 却不行     jquery: 'jQuery'   } };

在 index.html 中手动引入 jquery:

// src/index.html <script src="https://cdn.bootcdn.net/ajax/libs/jquery/1.7.2/jquery.min.js"></script>

Tip: 我们使用 bootstrap cdn。

再次打包,则不在包含 jquery:

> npm run build               Asset        Size  Chunks             Chunk Names          index.html   303 bytes          [emitted]             main.js    1.35 KiB       0  [emitted]  main

Tip:如果你在开发模式(mode: 'development')下打包,你会发现 main.js 中会有如下这段代码:

/***/ "jquery": /*!*************************!*\   !*** external "jQuery" ***!   \*************************/ /*! no static exports found */ /***/ (function(module, exports) { eval("module.exports = jQuery;\n\n//# sourceURL=webpack:///external_%22jQuery%22?"); /***/ })

这里的 jQuery 来自我们手动通过 <script src=> 引入 jquery 所产生的全局变量。

动态链接(dll)

所谓动态链接,就是把一些经常会共享的代码制作成 DLL 档,当可执行文件调用到 DLL 档内的函数时,Windows 操作系统才会把 DLL 档加载存储器内,DLL 档本身的结构就是可执行档,当程序有需求时函数才进行链接。透过动态链接方式,存储器浪费的情形将可大幅降低。

对于 webpack 就是事先将常用又构建时间长的代码提前打包好,取名为 dll,后面打包时则直接使用 dll,用来提高打包速度

vue-cli 删除了 dll

在 vue-cli 提交记录中发现:remove DLL option。

原因是:dll 选项将被删除。 Webpack 4 应该提供足够好的性能,并且在 Vue CLI 中维护 DLL 模式的成本不再合理。

Tip: 详情请看issue

核心代码

附上项目最终核心文件,方便学习和解惑。

webapck.config.js

const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const WorkboxPlugin = require('workbox-webpack-plugin'); const ESLintPlugin = require('eslint-webpack-plugin'); process.env.NODE_ENV = 'development' const postcssLoader = {     loader: 'postcss-loader',     options: {         // postcss 只是个平台,具体功能需要使用插件         // Set PostCSS options and plugins         postcssOptions: {             plugins: [                 // 配置插件 postcss-preset-env                 [                     "postcss-preset-env",                     {                         // browsers: 'chrome > 10',                         // stage:                      },                 ],             ]         }     } } module.exports = {     entry: './src/index.js',     entry: ['./src/index.js', './src/index.html'],     output: {         filename: 'main.js',         // filename: 'main.[contenthash:10].js',         path: path.resolve(__dirname, 'dist')     },     module: {         rules: [             {                 test: /\.css$/i,                 // 将 style-loader 改为 MiniCssExtractPlugin.loader                 use: [MiniCssExtractPlugin.loader, "css-loader", postcssLoader],             },             {                 test: /\.less$/i,                 loader: [                     // 将 style-loader 改为 MiniCssExtractPlugin.loader                     MiniCssExtractPlugin.loader,                     "css-loader",                     postcssLoader,                     "less-loader",                 ],             },             {                 test: /\.(png|jpg|gif)$/i,                 use: [                     {                         loader: 'url-loader',                         options: {                             // 指定文件的最大大小(以字节为单位)                             limit: 1024 * 6,                         },                     },                 ],             },             // +             {                 test: /\.html$/i,                 loader: 'html-loader',             },             {                 test: /\.js$/,                 exclude: /node_modules/,                 use: [                     // 'thread-loader',                     {                         loader: 'babel-loader',                         options: {                             presets: [                                 [                                     '@babel/preset-env',                                     // +                                     {                                         // 配置处理polyfill的方式                                         useBuiltIns: "usage",                                         // 版本与我们下载的版本保持一致                                         corejs: { version: "3.11" },                                         "targets": "> 0.25%, not dead"                                     }                                 ]                             ],                             // 开启缓存                             cacheDirectory: true                         }                     }]             }         ]     },     plugins: [         // new MiniCssExtractPlugin(),         new MiniCssExtractPlugin({             // filename: "[name].[contenthash:10].css",         }),         new OptimizeCssAssetsPlugin(),         new HtmlWebpackPlugin({             template: 'src/index.html'         }),         // new ESLintPlugin({         //   // 将启用ESLint自动修复功能。此选项将更改源文件         //   fix: true         // }),         new WorkboxPlugin.GenerateSW({             // 这些选项帮助快速启用 ServiceWorkers             // 不允许遗留任何“旧的” ServiceWorkers             clientsClaim: true,             skipWaiting: true,         }),     ],     mode: 'development',     // mode: 'production',     devServer: {         // open: true,         contentBase: path.join(__dirname, 'dist'),         compress: true,         port: 9000,     },     devServer: {         // 开启热模块替换         hot: true     },     // devtool: 'eval-source-map',     optimization: {         splitChunks: {             chunks: 'all',         },     },     externals: {         // jQuery 是jquery暴露给window的变量名,这里可以将 jQuery 改为 $,但 jquery 却不行         jquery: 'jQuery'     } };

package.json

{   "name": "webpack-example3",   "version": "1.0.0",   "description": "",   "main": "index.js",   "scripts": {     "test": "echo \"Error: no test specified\" && exit 1",     "build": "webpack",     "dev": "webpack-dev-server",     "start": "http-server dist"   },   "keywords": [],   "author": "",   "license": "ISC",   "devDependencies": {     "@babel/preset-env": "^7.14.2",     "babel-loader": "^8.2.2",     "core-js": "3.11",     "css-loader": "^5.2.4",     "eslint": "^7.26.0",     "eslint-config-airbnb-base": "^14.2.1",     "eslint-webpack-plugin": "^2.5.4",     "express": "^4.17.1",     "file-loader": "^6.2.0",     "html-loader": "^1.3.2",     "html-webpack-plugin": "^4.5.2",     "http-server": "^0.12.3",     "less-loader": "^7.3.0",     "mini-css-extract-plugin": "^1.6.0",     "optimize-css-assets-webpack-plugin": "^5.0.4",     "postcss-loader": "^4.3.0",     "postcss-preset-env": "^6.7.0",     "thread-loader": "^3.0.4",     "url-loader": "^4.1.1",     "webpack": "^4.46.0",     "webpack-cli": "^3.3.12",     "webpack-dev-server": "^3.11.2",     "workbox-webpack-plugin": "^6.1.5"   },   "dependencies": {     "jquery": "^3.6.0",     "lodash": "^4.17.21",     "vue": "^2.6.14"   },   "sideEffects": false }

来源https://www.cnblogs.com/pengjiali/p/15024605.html

文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐