1. webpack 实现模块化原理
1.1 CommonJs 原理
- 定义一个 add 函数并导出
1 2 3 4
| const add = (num1, num2) => { return num1 + num2 } module.exports = add
|
- webpack 打包后的代码为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
var __webpack_modules__ = { "./src/js/add.js": function (module) { const add = (num1, num2) => { return num1 + num2; }; module.exports = add; }, };
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) { var cachedModule = __webpack_module_cache__[moduleId]; if (cachedModule !== undefined) { return cachedModule.exports; } var module = (__webpack_module_cache__[moduleId] = { exports: {}, }); __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports; }
!(function () { const add = __webpack_require__("./src/js/add.js");
console.log(add(10, 20)); })();
|
1.2 EsModel 原理
- 定义一个 add 函数并导出
1 2 3
| export const add = (num1, num2) => { return num1 + num2; };
|
- webpack 打包后的代码为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| var __webpack_modules__ = { "./src/js/add.js": function ( __unused_webpack_module, __webpack_exports__, __webpack_require__ ) { __webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, { add: function () { return add; }, }); const add = (num1, num2) => { return num1 + num2; }; }, };
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) { var cachedModule = __webpack_module_cache__[moduleId]; if (cachedModule !== undefined) { return cachedModule.exports; } var module = (__webpack_module_cache__[moduleId] = { exports: {}, });
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports; }
!(function () { __webpack_require__.d = function (exports, definition) { for (var key in definition) { if ( __webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key) ) { Object.defineProperty(exports, key, { enumerable: true, get: definition[key], }); } } }; })();
!(function () { __webpack_require__.o = function (obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }; })();
!(function () { __webpack_require__.r = function (exports) { if (typeof Symbol !== "undefined" && Symbol.toStringTag) { Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); } Object.defineProperty(exports, "__esModule", { value: true }); }; })();
var __webpack_exports__ = {};
!(function () { __webpack_require__.r(__webpack_exports__); var _js_add_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/js/add.js");
console.log((0, _js_add_js__WEBPACK_IMPORTED_MODULE_0__.add)(10, 20)); })();
|
2.1 source-map
我们的代码通常运行在浏览器上时,是通过打包压缩的:
也就是真实跑在浏览器上的代码,和我们编写的代码其实是有差异的;
比如 ES6 的代码可能被转换成 ES5;
比如对应的代码行号、列号在经过编译后肯定会不一致;
比如代码进行丑化压缩时,会将编码名称等修改;
比如我们使用了 TypeScript 等方式编写的代码,最终转换成 JavaScript;
但是,当代码报错需要调试时(debug),调试转换后的代码是很困难的 n 但是我们能保证代码不出错吗?不可能。
那么如何可以调试这种转换后不一致的代码呢?答案就是 source-map
下面几个值不会生成 source-map
- false:不使用 source-map,也就是没有任何和 source-map 相关的内容。
- none:production 模式下的默认值,不生成 source-map。
- eval:development 模式下的默认值,不生成 source-map
- 但是它会在 eval 执行的代码中,添加 //# sourceURL=;
- 它会被浏览器在执行时解析,并且在调试面板中生成对应的一些文件目录,方便我们调试代码;
socurce map 文件
- version:当前使用的版本,也就是最新的第三版;
- sources:从哪些文件转换过来的 source-map 和打包的代码(最初始的文件);
- names:转换前的变量和属性名称(因为我目前使用的是 development 模式,所以不需要保留转换前的名 称);
- mappings:source-map 用来和源文件映射的信息(比如位置信息等),一串 base64 VLQ(veriablelength quantity 可变长度值)编码; pfile:打包后的文件(浏览器加载的文件);
- sourceContent:转换前的具体代码信息(和 sources 是对应的关系);
- sourceRoot:所有的 sources 相对的根目录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| { "version": 3, "file": "main.js", "mappings": ";;;;;;;;;;;;;;AAAO;AACP;AACA;;;;;;;UCFA;UACA;;UAEA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;;UAEA;UACA;;UAEA;UACA;UACA;;;;;WCtBA;WACA;WACA;WACA;WACA,yCAAyC,wCAAwC;WACjF;WACA;WACA;;;;;WCPA,8CAA8C;;;;;WCA9C;WACA;WACA;WACA,uDAAuD,iBAAiB;WACxE;WACA,gDAAgD,aAAa;WAC7D;;;;;;;;;;;;ACNiC;AACjC;AACA;AACA;AACA,YAAY,+CAAG", "sources": [ "webpack://webpack_text/./src/js/add.js", "webpack://webpack_text/webpack/bootstrap", "webpack://webpack_text/webpack/runtime/define property getters", "webpack://webpack_text/webpack/runtime/hasOwnProperty shorthand", "webpack://webpack_text/webpack/runtime/make namespace object", "webpack://webpack_text/./src/index.js" ], "sourcesContent": [ "export const add = (num1, num2) => {\r\n return num1 + num2\r\n}\r\n", "// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n", "// define getter functions for harmony exports\n__webpack_require__.d = function(exports, definition) {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};", "__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }", "// define __esModule on exports\n__webpack_require__.r = function(exports) {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};", "import { add } from './js/add.js'\r\n\r\nconsole.log(asd)\r\n\r\nconsole.log(add(10, 20))\r\n" ], "names": [], "sourceRoot": "" }
|
2.2 eval-source-map
会生成 sourcemap,但是 source-map 是以 DataUrl 添加到 eval 函数的后面
1 2 3 4 5 6 7 8 9 10 11 12
| eval( '\n; {\r\n return num1 + num2\r\n}\r\n\n; (add(10, 20))\r\n utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiODI1LmpzIiwibWFwcGluZ3MiOiI7O0FBQU87QUFDUDtBQUNBOz s7QUNGaUM7QUFDakM7QUFDQSxZQUFZLEdBQUciLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly93ZWJwYWNrX3RleHQvLi9 zcmMvanMvYWRkLmpzPzA4ZjQiLCJ3ZWJwYWNrOi8vd2VicGFja190ZXh0Ly4vc3JjL2luZGV4LmpzP2I2MzU iXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGNvbnN0IGFkZCA9IChudW0xLCBudW0yKSA9PiB7XHJcbi AgcmV0dXJuIG51bTEgKyBudW0yXHJcbn1cclxuIiwiaW1wb3J0IHsgYWRkIH0gZnJvbSAnLi9qcy9hZG QuanMnXHJcblxyXG5jb25zb2xlLmxvZyhhZGQoMTAsIDIwKSlcclxuIl0sIm5hbWVzIjpbXSwic291 cmNlUm9vdCI6IiJ9\n )
|
2.3 inline-source-map
会生成 sourcemap,但是 source-map 是以 DataUrl 添加到 bundle 文件的后面
1 2 3 4 5 6 7 8 9
| !function(){"use strict";console.log(30)}();
uIjozLCJmaWxlIjoibWFpbi5qcyIsIm1hcHBpbmdzIjoieUJBRUFBLFFBQVFDLElDRENDLEciL CJzb3VyY2VzIjpbIndlYnBhY2s6Ly93ZWJwYWNrX3RleHQvLi9zcmMvaW5kZXguanMiLCJ3ZWJ wYWNrOi8vd2VicGFja190ZXh0Ly4vc3JjL2pzL2FkZC5qcyJdLCJzb3VyY2VzQ29udGVudCI6WyJ pbXBvcnQgeyBhZGQgfSBmcm9tICcuL2pzL2FkZC5qcydcclxuXHJcbmNvbnNvbGUubG9nKGFkZCg xMCwgMjApKVxyXG4iLCJleHBvcnQgY29uc3QgYWRkID0gKG51bTEsIG51bTIpID0+IHtcclxuICB yZXR1cm4gbnVtMSArIG51bTJcclxufVxyXG4iXSwibmFtZXMiOlsiY29uc29sZSIsImxvZyIsI m51bTEiXSwic291cmNlUm9vdCI6IiJ9
|
2.4 cheap-source-map
会生成 sourcemap,但是会更加高效一些(cheap 低开销),因为它没有生成列映射(Column Mapping), 因为在开发中,我们只需要行信息通常就可以定位到错误了
2.5 cheap-module-source-map
会生成 sourcemap,类似于 cheap-source-map,但是对源自 loader 的 sourcemap 处理会更好。
cheap-source-map 和 cheap-module-source-map 的区别
如果源码需要使用 loader 进行处理,比如 bable,那么 cheap-source-map 报错的位置和部分代码会和写的不一样,cheap-module-source-map 和自己编写的一样
2.6 hidden-source-map
会生成 sourcemap,但是不会对 source-map 文件进行引用,相当于删除了打包文件中对 sourcemap 的引用注释,如果我们手动添加进来,那么 sourcemap 就会生效了
2.7 nosources-source-map
会生成 sourcemap,但是生成的 sourcemap 只有错误信息的提示,不会生成源代码文件
2.8 多个值的组合
事实上,webpack 提供给我们的 26 个值,是可以进行多组合的。
组合的规则如下:
- inline-|hidden-|eval:三个值时三选一;
- nosources:可选值;
- cheap 可选值,并且可以跟随 module 的值;
==[inline-|hidden-|eval-] [][nosources-][cheap-[module-]] source-map==
那么在开发中,最佳的实践是什么呢?
- 开发阶段:推荐使用 source-map 或者 cheap-module-source-map 这分别是 vue 和 react 使用的值,可以获取调试信息,方便快速开发;
- 测试阶段:推荐使用 source-map 或者 cheap-module-source-map 测试阶段我们也希望在浏览器下看到正确的错误提示;
- 发布阶段:false、缺省值(不写)
3. babel
Babel 是一个工具链,主要用于旧浏览器或者缓解中将 ECMAScript 2015+代码转换为向后兼容版本的 JavaScript;
3.1 babel 编译器执行原理
3.2 webpack 中使用 bable
1.安装@babel/core 和 babel-loader
1
| npm install babel-loader @babel/core -D
|
2.在 webpack 中配置
1 2 3 4 5 6 7 8
| module: { rules: [ { test: /\.js$/, use: "babel-loader", }, ]; }
|
但是这样配置并没有将 es6 的代码转成 es5,因为我们需要安装插件
- 比如我们需要转换箭头函数,那么我们就可以使用箭头函数转换相关的插件
1
| npm install @babel/plugin-transform-arrow-functions -D
|
1
| npm install @babel/plugin-transform-block-scoping -D
|
在 webpack 中配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| module: { rules: [ { test: /\.js$/, use: { loader: "babel-loader", options: { plugins: [ "plugin-transform-arrow-functions", "plugin-transform-block-scoping", ], }, }, }, ]; }
|
如果我们一个个去安装使用插件,那么需要手动来管理大量的 babel 插件,我们可以直接给 webpack 提供一个 preset,webpack 会根据我们的预设来加载对应的插件列表,并且将其传递给 babel。
1
| npm install @babel/preset-env -D
|
在 webpack 中配置
1 2 3 4 5 6 7 8 9 10 11 12 13
| module: { rules: [ { test: /\.js$/, use: { loader: "babel-loader", options: { presets: ["babel/preset-env"], }, }, }, ]; }
|
我们最终打包的 JavaScript 代码,是需要跑在目标浏览器上的,需要通过 browserslist 工具来告知 babel 需要适配的浏览器。
3.3 babel 的配置文件
像之前一样,我们可以将 babel 的配置信息放到一个独立的文件中,babel 给我们提供了两种配置文件的编写:
- babel.config.json(或者.js,.cjs,.mjs)文件;
- .babelrc.json(或者.babelrc,.js,.cjs,.mjs)文件;
它们两个有什么区别呢?目前很多的项目都采用了多包管理的方式(babel 本身、element-plus、umi 等);
- .babelrc.json:早期使用较多的配置方式,但是对于配置 Monorepos 项目是比较麻烦的;
- babel.config.json(babel7):可以直接作用于 Monorepos 项目的子包,更加推荐;
babel.config.js
1 2 3
| module.exports = { presets: ["@babel/preset-env"], };
|
webpack.config.js
1 2 3 4 5 6 7 8 9
| module: { rules: [ { test: /\.js$/, use: 'babel-loader', exclude: /(node_modules)/ } ] }
|
3.4 polyfill
为什么时候会用到 polyfill 呢?
- 比如我们使用了一些语法特性(例如:Promise, Generator, Symbol 等以及实例方法例如 Array.prototype.includes 等)
- 但是某些浏览器压根不认识这些特性,必然会报错;
- 我们可以使用 polyfill 来填充或者说打一个补丁,那么就会包含该特性了;
- 安装
1
| npm install core-js regenerator-runtime --save
|
- 在 babel.config.js 配置
useBuiltIns 属性有三个常见的值
第一个值:false
第二个值:usage
- 会根据源代码中出现的语言特性,自动检测所需要的 polyfill;
- 这样可以确保最终包里的 polyfill 数量的最小化,打包的包相对会小一些;
- 可以设置 corejs 属性来确定使用的 corejs 的版本;
第三个值:entry
- 如果我们依赖的某一个库本身使用了某些 polyfill 的特性,但是因为我们使用的是 usage,所以之后用户浏览器 可能会报错;
- 所以,如果你担心出现这种情况,可以使用 entry;
- 并且需要在入口文件中添加 `import ‘core-js/stable’; import ‘regenerator-runtime/runtime’;
- 这样做会根据 browserslist 目标导入所有的 polyfill,但是对应的包也会变大;
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| module.exports = { presets: [ [ "@babel/preset-env", { useBuiltIns: "usage", corejs: 3, }, ], ], };
|
如果使用 entry 需要在 index.js 导入
1 2
| import "core-js/stable"; import "regenerator-runtime/runtime";
|
3.5 编译 react
- 安装
1
| npm install @babel/preset-react -D
|
- 在 babel.config.js 配置
1 2 3 4 5 6 7 8 9 10 11 12
| module.exports = { presets: [ [ "@babel/preset-env", { useBuiltIns: "entry", corejs: 3, }, ], ["@babel/preset-react"], ], };
|
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import React, { Component } from "react"; import ReactDOM from "react-dom/client";
class App extends Component { constructor(props) { super(props);
this.state = { message: "Hello React", }; }
render() { return ( <div> <h2>{this.state.message}</h2> </div> ); } } const root = ReactDOM.createRoot(document.getElementById("app")); root.render(<App />);
|
3.6 编译 TypeScript
3.6.1 使用 ts-loader 编译
1.安装
1 2
| npm install ts-loader -D tsc --init // 生成ts配置文件
|
2.webpack.config.js 配置
1 2 3 4
| { test: /\.ts$/, use: 'ts-loader' }
|
3.6.2 使用 babel-loader 编译
- 安装
1
| npm install @babel/preset-typescript -D
|
- webpack.config.js 配置
1 2 3 4
| { test: /\.ts$/, use: 'babel-loader' }
|
- babel.config.js 配置
1 2 3 4 5 6 7 8 9 10 11 12
| module.exports = { presets: [ [ '@babel/preset-env', { useBuiltIns: 'usage', corejs: 3 } ], ['@babel/preset-typescript'] ] }
|
3.6.3 ts-loader 和 babel-loader 选择
那么我们在开发中应该选择 ts-loader 还是 babel-loader 呢?
使用 ts-loader(TypeScript Compiler)
- 来直接编译 TypeScript,那么只能将 ts 转换成 js;
- 如果我们还希望在这个过程中添加对应的 polyfill,那么 ts-loader 是无能为力的;
- 我们需要借助于 babel 来完成 polyfill 的填充功能;
使用 babel-loader(Babel)
- 来直接编译 TypeScript,也可以将 ts 转换成 js,并且可以实现 polyfill 的功能;
- 但是 babel-loader 在编译的过程中,不会对类型错误进行检测;
那么在开发中,我们如何可以同时保证两个情况都没有问题呢?
- 我们使用 Babel 来完成代码的转换,使用 tsc 来进行类型的检查。
3.7 编译 Vue
1.安装相关依赖
1 2
| npm install vue-loader -D npm install vue-template-compiler -D
|
4. 代码分离
Webpack 中常用的代码分离有三种:
- 多入口起点:使用 entry 配置手动分离代码;
- 防止重复:使用 Entry Dependencies 或者 SplitChunksPlugin 去重和分离代码;
- 动态导入:通过模块的内联函数调用来分离代码;
4.1 多入口起点
1 2 3 4 5 6 7 8 9 10 11 12
| const path = require("path"); module.exports = { mode: "development", entry: { index: "./src/index.js", main: "./src/main.js", }, output: { filename: "[name].build.js", path: path.join(__dirname + "./build"), }, };
|
假如我们的 index.js 和 main.js 都依赖 axios 库,我们可以对他们进行共享
1 2 3 4 5 6 7 8 9 10 11 12 13
| const path = require("path"); module.exports = { mode: "development", entry: { index: { import: "./src/index.js", dependOn: "shared" }, main: { import: "./src/main.js", dependOn: "shared" }, shared: ["axios"], }, output: { filename: "[name].build.js", path: path.join(__dirname + "./build"), }, };
|
4.2 动态导入
另外一个代码拆分的方式是动态导入时,webpack 提供了两种实现动态导入的方式
- 第一种,使用 ECMAScript 中的 import() 语法来完成,也是目前推荐的方式;
- 第二种,使用 webpack 遗留的 require.ensure,目前已经不推荐使用
index.js 使用动态导入, 点击按钮才会加载 main.js, 可以加快首屏渲染速度
1 2 3 4 5 6 7 8
| const btnEl = document.createElement('button') btnEl.textContent = 'show'
btnEl.onclick = function () { import('./js/main') }
document.body.appendChild(btnEl)
|
动态导入的文件命名
1 2 3 4 5 6 7 8 9 10 11 12 13
| btnEl.onclick = function () { import( './js/main') }
output: { filename: 'js/build.js', path: path.resolve(__dirname, './build'), chunkFilename: '[name]_chunk.js' }
|
4.3 SplitChunks
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| module.exports = { optimization: { splitChunks: { chunks: "all", maxSize: 20000, minSize: 10000, minChunks: 2, chunkIds: "deterministic", cacheGroups: { vendors: { test: /node_modules/, filename: "[id]_[hsah:6]_vendors.js", }, }, }, }, };
|
4.4 Prefetch 和 Preload
在声明 import 时,使用下面这些内置指令,来告知浏览器:
- prefetch(预获取):将来某些导航下可能需要的资源
- preload(预加载):当前导航下可能需要资源
与 prefetch 指令相比,preload 指令有许多不同之处
- preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开 始加载。
- preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
- preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻
1 2 3 4 5 6 7
| btnEl.onclick = function () { import( "./js/main" ); };
|
4.5. CDN
CDN 称之为内容分发网络(Content Delivery Network 或 Content Distribution Network,缩写:CDN)
使用 cdn
- 在 webpack.config.js 配置
1 2 3 4 5 6 7 8
| module.exports = { // 排除不需要进行打包的包 // key为框架的名字 value为cdn地址提供的名字 externals: { react: 'React', axios: 'axios' } };
|
- 在 index.html 中引入
1 2
| <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.2.2/axios.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/react/18.2.0/cjs/react-jsx-dev-runtime.development.js"></script>
|
4.6 分离 css
安装 css 分离插件
1
| npm install mini-css-extract-plugin -D
|
配置 rules 和 plugins
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| const miniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = { module: { rules: [ { test: /\.css$/, use: [ miniCssExtractPlugin.loader, { loader: "css-loader", options: { importLoaders: 1, }, }, { loader: "postcss-loader", options: { postcssOptions: { plugins: [require("postcss-preset-env")], }, }, }, ], }, ], }, plugins: [ new miniCssExtractPlugin({ filename: "css/[name].css", chunkFilename: "css/[name]_chunk.css", }), ], };
|
5. 代码压缩
5.1 手动压缩 js 文件
- 在 webpack 中有一个 minimizer 属性,在 production 模式下,默认就是使用 TerserPlugin 来处理我们的代码的;
- 如果我们对默认的配置不满意,也可以自己来创建 TerserPlugin 的实例,并且覆盖相关的配置
- 不过一般不需要手动配置,因为在 production 模式下 webacpk 已经配置好了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| const TerserPlugin = require("terser-webpack-plugin"); module.exports = { optimization: { minimize: true, minimizer: [ new cssMinimizerWebpackPlugin(), ], }, };
|
5.2 CSS 的压缩
- CSS 压缩通常是去除无用的空格等,因为很难去修改选择器、属性的名称、值等;
- CSS 的压缩我们可以使用另外一个插件:css-minimizer-webpack-plugin
- css-minimizer-webpack-plugin 是使用 cssnano 工具来优化、压缩 CSS(也可以单独使用)
安装
1
| npm install css-minimizer-webpack-plugin -D
|
在 webpack.config.js 中配置
1 2 3 4 5 6 7
| const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); module.exports = { optimization: { minimize: true, minimizer: [new CssMinimizerPlugin()], }, };
|
5.3 压缩 html
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { plugins: [ new HtmlWebpackPlugin({ minify: { removeComments: true, // 移除注释 collapseWhitespace: true, // 移除空格 removeRedundantAttributes: true, // 移除默认属性 removeEmptyAttributes: true, // 移除空的属性 比如id='' minifyCSS: true // 压缩内联css样式 } }), ] }
|
5.4 gzip 压缩
webpack 中相当于是实现了 HTTP 压缩的第一步操作,我们可以使用 CompressionPlugin
安装
1
| npm install compression-webpack-plugin -D
|
6. Tree Shaking
6.1 js 实现 Tree Shaking
6.1.1 usedExports
事实上 webpack 实现 Tree Shaking 采用了两种不同的方案:
usedExports:通过标记某些函数是否被使用,之后通过 Terser 来进行优化的;
sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用
将 mode 设置为 development 模式:
为了可以看到 usedExports 带来的效果,我们需要设置为 development 模式
因为在 production 模式下,webpack 默认的一些优化会带来很大额影响。
设置 usedExports 为 true 和 false 对比打包后的代码:
在 usedExports 设置为 true 时,会有一段注释:unused harmony export mul;
这段注释的意义是什么呢?告知 Terser 在优化时,可以删除掉这段代码;
这个时候,我们讲 minimize 设置 true:
usedExports 设置为 false 时,mul 函数没有被移除掉;
usedExports 设置为 true 时,mul 函数有被移除掉;
所以,usedExports 实现 Tree Shaking 是结合 Terser 来完成的
1 2 3 4 5 6 7 8 9 10 11
| const TerserPlugin = require('terser-webpack-plugin') module.exports = { optimization: { usedExports:true, minimize: true, minimizer: [ new TerserPlugin() ] } }
|
6.1.2 sideEffects
sideEffects 用于告知 webpack compiler 哪些模块时有副作用的:
- 副作用的意思是这里面的代码有执行一些特殊的任务,不能仅仅通过 export 来判断这段代码的意义;
在 package.json 中设置 sideEffects 的值:
- 如果我们将 sideEffects 设置为 false,就是告知 webpack 可以安全的删除未用到的 exports;
- 如果有一些我们希望保留,可以设置为数组;
package.json
1 2 3 4 5 6 7 8
| { "name": "webpack_demo", "version": "1.0.0", "sideEffects": [ "./src/format.js", "*.css" ], }
|
比如我们有一个 format.js、style.css 文件:
- 该文件在导入时没有使用任何的变量来接受;
- 那么打包后的文件,不会保留 format.js、style.css 相关的任何代码
6.2 css 实现 Tree Shaking
安装
1
| npm install purgecss-webpack-plugin -D
|
配置 PurgeCss
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const path = require("path"); const glob = require("glob"); const { PurgeCSSPlugin } = require("purgecss-webpack-plugin");
module.exports = { plugins: [ new PurgeCSSPlugin({ paths: glob.sync(`${path.resolve(__dirname, "../src")}/**/*`, { nodir: true, }), safelist() { return { standard: ["html"], }; }, }), ], };
|
6.3 Scope Hoisting 作用域提升
什么是 Scope Hoisting 呢?
Scope Hoisting 从 webpack3 开始增加的一个新功能;
功能是对作用域进行提升,并且让 webpack 打包后的代码更小、运行更快;
默认情况下 webpack 打包会有很多的函数作用域,包括一些(比如最外层的)IIFE:
无论是从最开始的代码运行,还是加载一个模块,都需要执行一系列的函数;
cope Hoisting 可以将函数合并到一个模块中来运行;
使用 Scope Hoisting 非常的简单,webpack 已经内置了对应的模块:
在 production 模式下,默认这个模块就会启用;
在 development 模式下,我们需要自己来打开该模块
7. 打包文件分析
安装
1
| npm install webpack-bundle-analyzer -D
|
在 webpack 中配置
1 2 3 4 5
| const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin; module.exports = { plugins: [new BundleAnalyzerPlugin()], };
|