🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] # 概念总结 <span style="font-family: 楷体; font-size: 20px; color: #007fff; font-weight: bold;">entry points(入口)</span> 指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。 进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。 <span style="font-family: 楷体; font-size: 20px;color: #007fff; font-weight: bold;">output(输出)</span> output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist <span style="font-family: 楷体; font-size: 20px;color: #007fff; font-weight: bold;">loader </span> loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。 loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。 test 属性:用于标识出应该被对应的 loader 进行转换的某个或某些文件 use 属性:表示进行转换时,应该使用哪个 loader <span style="font-family: 楷体; font-size: 20px;color: #007fff; font-weight: bold;">plugins</span> 插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。 想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins 数组中。 多数插件可以通过选项(option)自定义。 你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new 操作符来创建它的一个实例。 <span style="font-family: 楷体; font-size: 20px;color: #007fff; font-weight: bold;">module、chunk、bundle</span> - module:webpack 支持 commonJS、ES6 等模块化规范,简单来说就是你通过 import 语句引入的代码就可以视为一个个的 module。 - chunk: chunk 是 webpack 根据功能拆分出来的,包含三种情况: 1、你的项目入口(entry) 2、通过 import() 动态引入的代码 3、通过 splitChunks 拆分出来的代码 chunk 包含着 module,可能是一对多也可能是一对一。 - bundle:bundle 是 webpack 打包之后的各个文件,一般就是和 chunk 是一对一的关系(也可能是多个 chunk 对应一个 bundle),bundle 就是对 chunk 进行编译压缩打包等处理之后的产出。 # Webpack 生命周期(流程) [https://juejin.im/post/5beb8875e51d455e5c4dd83f#heading-14](https://juejin.im/post/5beb8875e51d455e5c4dd83f#heading-14) 这里只是大概梳理一下,具体分析可阅读参考链接 1、初始化参数:从配置文件和 `Shell` 语句中读取与合并参数,得出最终的参数; 2、开始编译:用上一步得到的参数初始化 `Compiler` 对象,加载所有配置的插件,执行对象的 `run` 方法开始执行编译; 3、确定入口:根据配置中的 entry 找出所有的入口文件 4、编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理; 5、完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系; 6、输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 `Chunk`,再把每个 `Chunk` 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会; 7、输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。 在以上过程中,`webpack` 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 `webpack` 提供的 API 改变 `webpack` 的运行结果。 # Entry 与 Output 配置 ## 基础 先来看最基础的写法: ```js const config = { entry: './path/to/my/entry/file.js' }; // 是下面的简写 const config = { entry: { main: './path/to/my/entry/file.js' } }; ``` 那么其表示什么意思呢? 入口起点(entry point) 指示 webpack 应该使用哪个模块,来作为构建其内部 **依赖图** 的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。 每个依赖项随即被处理,最后输出到称之为 bundles 的文件中。可以把依赖图理解为一个表示模块间依赖关系的数据结构,有了这个数据结构才可以做一些代码分割,tree shaking 之类的操作。 ## 配置多个入口 注意多个入口并不是指多页面,即如下的配置并不会生成多个 html 文件,那么这表示什么意思呢?这告诉 webpack 需要 2 个独立分离的依赖图。 ```js { entry: { app: './src/app.js', search: './src/search.js' }, output: { filename: '[name].js', // 使用占位符确保生成的文件名不同 path: __dirname + '/dist' } } // 写入到硬盘:./dist/app.js, ./dist/search.js ``` ## output 中的常用配置 - filename:用于输出文件的文件名 - path:目标输出目录的绝对路径 - publicPath:如果配置了给选项,所有的 src 都会增加 publicPath 前缀(把静态资源放在 CDN 上才使用这个配置) 假设有如下的配置: ```js module.exports = { mode: 'development', entry: { main: './src/index.js', sub: './src/index.js' }, output: { publicPath: 'http://cdn.com.cn', filename: '[name].js', path: path.resolve(__dirname, 'dist') } } ``` 那么我们的 dist 目录结构会是这样的: ```js |-- dist |-- index.html |-- main.js |-- sub.js ``` 生成的 html 文件会是这样的:所以说其 src 都会加上 publicPath 前缀 ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>html 模版</title> </head> <body> <div id='root'></div> <script type="text/javascript" src="http://cdn.com.cn/main.js"></script><script type="text/javascript" src="http://cdn.com.cn/sub.js"></script></body> </html> ``` ## 多页面配置 多页面简单来说就是打包生成的 html 文件不止一个,呃...至少记住你要生成几个 html 文件 entry 就要有几个,然后对应的 HtmlWebpackPlugin 也要 new 几次。可以参考这篇文章:[https://blog.csdn.net/nongweiyilady/article/details/79255746](https://blog.csdn.net/nongweiyilady/article/details/79255746) # 常用 loader ## file-loader 官网:[https://www.webpackjs.com/loaders/file-loader/](https://www.webpackjs.com/loaders/file-loader/) 安装:`npm install --save-dev file-loader` 用途:可用于加载 .png .jpg .gif 等格式的图片(似乎可以被 url-loader 替代?) 默认情况下,生成的文件的文件名就是文件内容的 MD5 哈希值并会保留所引用资源的原始扩展名 ```js module.exports = { module: { rules: [ { test: /\.(png|jpg|gif)$/, use: [ { loader: 'file-loader', options: {} } ] } ] } } ``` ## url-loader 官网:[https://www.webpackjs.com/loaders/url-loader/](https://www.webpackjs.com/loaders/url-loader/) 安装:`npm install --save-dev url-loader` 用途:功能类似于 file-loader,但是在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL。(即 base64 格式,图片被合并到 JS 代码中,这样可以节省 HTTP 请求,但是对大文件不应该这么处理,因为这么做会影响对 JS 文件大小有较大的影响) PS:如果对一个 3KB 的图片做 base64 处理,那么其合并到的 JS 文件增加的体积几乎是略多于 3KB 的 下图就是 base64 的形式: ![](https://box.kancloud.cn/14d04fde146e1d8281807dacea27bad5_484x162.png) 一般配置: ```js module.exports = { module: { rules: [ { test: /\.(jpg|png|gif)$/, use: { loader: 'url-loader', options: { name: '[name]_[hash].[ext]', // 占位符 [name]:文件原本的名字 [ext]:扩展名 outputPath: 'images/', // 生成到 dist 目录下 images 文件夹下 limit: 2048 // 2048 = 2KB } } } ] } } ``` ## 处理样式文件(以 sass 为例) `npm install css-loader style-loader -D` - css-loader:解释 @import 和 url() ,会 import / require() 后再解析(resolve)它们 - style-loader:将 CSS 解析为 \<style> 标签后添加到 DOM 中 `npm install sass-loader node-sass –save-dev` - sass-loader:读取 SASS/SCSS 文件并将其编译为 CSS 文件 配置 webpack 时需要注意 loader 的顺序:从 use 数组中最后一个 loader 开始 ```js module.exports = { ... module: { rules: [ { test: /\.scss$/, use: [{ loader: 'style-loader' // 将 JS 字符串生成 style 节点 }, { loader: 'css-loader' // 将 CSS 转化成 CommonJS 模块 }, { loader: 'sass-loader' // 将 sass 编译成 css }] } ] } } ``` 一般也会加上 postcss-loader (如自动为 CSS3 属性添加厂商前缀,还有其他功能) 官网:[https://www.webpackjs.com/loaders/postcss-loader/](https://www.webpackjs.com/loaders/postcss-loader/) `npm i postcss-loader -D` `npm i autoprefixer -D` 可以像下面这样配置,也可以将 postcss.config.js 抽离出来 ```js { test: /\.scss$/, use: [ 'style-loader', // 将 JS 字符串生成为 style 节点 'css-loader', // 将 css 转换成 CommonJS 模块 'postcss-loader' // 添加其他 css 需求,如处理厂商前缀 'sass-loader', // 将 Sass 编译成 css ] } ``` ## 处理字体样式 基本上就是复制到输出目录,做个哈希处理 ```js { test: /\.(woff|woff2|eot|ttf|otf)$/, use: [ 'file-loader' ] } ``` # 常用 plugin ## HtmlWebpackPlugin 官网:[https://www.webpackjs.com/plugins/html-webpack-plugin/](https://www.webpackjs.com/plugins/html-webpack-plugin/) 安装:`npm install --save-dev html-webpack-plugin` 用途: 打包时并没有把 index.html 放入 dist 目录,使用这个插件可以解决这个问题,其还帮助我们自动把生成的 script 文件添加到 index.html 中(即使每次打包生成的文件名不同,其也可以正确地引入) 我们使用脚手架构建的 vue 或 react 项目一般都有一个 html 模板: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>html 模版</title> </head> <body> <div id='root'></div> </body> </html> ``` HtmlWebpackPlugin 就可以帮助我们整合这个模板到输出目录的 index.html 中: ```js const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); module.exports = { entry: { main: './src/index.js' }, module: { ... }, plugins: [new HtmlWebpackPlugin({ template: 'src/index.html' // template 配置接收一个模板文件 }), new CleanWebpackPlugin(['dist'])], output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') } } ``` 最后生成的 index.html 中就会多一行如 ```html <script src="bundle.js"></script> ``` ## CleanWebpackPlugin 上面也有写到这个插件,这个插件在每次重新构建前帮我们清理 /dist 文件夹 安装:`npm install clean-webpack-plugin --save-dev` 像上面那样 require 引入然后 new CleanWebpackPlugin(['dist']) 即可 ## DLLPlugin [https://www.webpackjs.com/plugins/dll-plugin/](https://www.webpackjs.com/plugins/dll-plugin/) 该插件主要是针对第三方模块,减少对这些模块的重复的不必要的分析;这个插件主要用于提升打包速度而不是减小打包生成的文件的体积。 其原理就是把网页依赖的基础模块抽离出来打包到 dll 文件中,当需要导入的模块存在于某个 dll 中时,这个模块不再被打包,而是去 dll 中获取。 >TODO:配置 ## workbox-webpack-plugin(PWA 插件) 简言之:在你第一次访问一个网站的时候,如果成功,做一个缓存,当服务器挂了之后,你依然能够访问这个网页 ,这是 PWA 的特性之一。这个只需要在线上环境,才需要做 PWA 的处理,以防不测。 ` cnpm i workbox-webpack-plugin -D ` ```js const WorkboxPlugin = require('workbox-webpack-plugin') // 引入 PWA 插件 const prodConfig = { plugins: [ // 配置 PWA new WorkboxPlugin.GenerateSW({ clientsClaim: true, skipWaiting: true }) ] } 在入口文件加上 // 判断该浏览器支不支持 serviceWorker if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker .register('/service-worker.js') .then(registration => { console.log('service-worker registed') }) .catch(error => { console.log('service-worker registed error') }) }) } ``` # 其他常用配置项及概念介绍 ## SourceMap 官网:[https://www.webpackjs.com/configuration/devtool/](https://www.webpackjs.com/configuration/devtool/) sourceMap 通过 devtool 选项来配置,sourceMap 的作用就是建立一个源代码和打包生成的代码的映射关系。 假设打包生成的 dist 目录下的 main.js 文件 96 行出错了,如果有 sourceMap,那么它就可以帮我们找到是源代码 src 目录下 index.js 中的第一行(我们写代码的位置)出错了。 常用配置: - 开发环境(development):cheap-module-eval-source-map - 线上环境(production):一般不建立 source-map,如果出错了需要排查再使用 另外,关于其可选值的一些理解: - inline:即 source-map 文件以 data-url 的形式放在打包生成的文件中 - cheap:简化提示信息(减少某些不必要的映射关系如第三方模块),可以减少打包时间 - module:与第三方模块的错误映射以及一些 loader 出错相关 - eval:错误会在打包生成的代码中以 eval 的形式提示 ```js module.exports = { mode: 'development', // development devtool: 'cheap-module-eval-source-map', // production devtool: 'cheap-module-source-map' or 'none', + devtool: 'cheap-module-eval-source-map', entry: { main: './src/index.js' }, module: { ... }, plugins: [ ... ], output: { filename: '[name].js', path: path.resolve(__dirname, 'dist') } } ``` ## WebpackDevServer 官网:[https://www.webpackjs.com/configuration/dev-server/#devserver-proxy](https://www.webpackjs.com/configuration/dev-server/#devserver-proxy) 通过 devServer 选项来进行配置,只用于开发环境,帮助我们快速进行开发。随便说几个功能就比如可以做代理服务,修改代码自动刷新(重新打包后自动刷新)等。 ```js module.exports = { mode: 'development', devtool: 'cheap-module-eval-source-map', entry: { main: './src/index.js' }, + devServer: { + contentBase: './dist', // 服务器根路径:当前目录 dist 文件下 + open: true, // 启动 webpack-dev-server 时帮我们自动启动浏览器并访问服务 + port: 8080 // 为什么需要以 web 服务器的形式启动? 如果以文件形式打开 URL 是 file 而不是 http 协议,不能发 AJAX 请求 }, output: { filename: '[name].js', path: path.resolve(__dirname, 'dist') } } ``` ## 开启 Hot Module Replace(HMR,模块热替换) 官网:[https://www.webpackjs.com/guides/hot-module-replacement/](https://www.webpackjs.com/guides/hot-module-replacement/) 如果只进行上述 WebPackDevserver 的配置你会发现你的操作会被重置,这种完全刷新对于 vuex 这类状态管理工具是不友好的(Vue Loader,React Hot Loader 都内置了一些这种 module.hot 的判断来帮我们实现模块热替换)。 该功能允许在运行时更新各种模块,而无需进行完全刷新,注意以下几个要点: - 方便我们修改 CSS 直接查看效果,不影响 JS - 需要注意一些 JS 操作,比如添加节点,我们更改代码后是删除之前的节点再重新执行添加节点的操作还是直接重新执行添加节点的操作?(前者是1个节点后者是2个,看官网提供的例子) - HMR 不适用于生产环境,应该只在开发环境使用 - 打包生成的文件是在内存中的,因此我们看不到生成的文件 我们只需要更新 webpack-dev-server 的配置,以及使用 HMR 的插件 - hot:开启 HMR - hotOnly:即使 HMR 不生效,也不让浏览器自动刷新 另外,HMR 插件是 webpack 自带的,所以 require(‘webpack’) 即可,new webpack.HotModuleReplacementPlugin() ```js const webpack = require('webpack'); module.exports = { mode: 'development', devtool: 'cheap-module-eval-source-map', entry: { main: './src/index.js' }, devServer: { contentBase: './dist', open: true, port: 8080, + hot: true, + hotOnly: true }, module: { rules: [{ ... }] }, plugins: [ + new webpack.HotModuleReplacementPlugin() ], output: { filename: '[name].js', path: path.resolve(__dirname, 'dist') } } ``` ### HMR 的实现原理 来源:[https://zhuanlan.zhihu.com/p/30669007](https://zhuanlan.zhihu.com/p/30669007) ![](https://img.kancloud.cn/41/26/4126a99a4e29d3af8f141c704e574016_720x749.png) 根据这张流程图来分析: 1. 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。 2. 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API 对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。 3. 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了 devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。 4. 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。 5. webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。 6. HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。 7. 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。 8. 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。 ## tree shaking 官方文档:[https://www.webpackjs.com/guides/tree-shaking/](https://www.webpackjs.com/guides/tree-shaking/) - 开发环境默认不开启 tree shaking - 线上环境默认开启,也就是说你只要配置 mode 即可 tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。 它依赖于 ES2015 模块系统中的静态结构特性,例如 import 和 export。这个术语和概念实际上是兴起于 ES2015 模块打包工具 rollup。 只支持 import 和 export 的模块化方法。 简单来说就是把代码中不需要引入的模块去掉,或者说只引入模块的一部分。 比如我们定义一个 math.js 文件: ```js // math.js export const add = (a, b) => { console.log( a + b ); } export const minus = (a, b) => { console.log( a - b ); } ``` 然后 index.js 只引入 add 方法: ```js // index.js // Tree Shaking 只支持 ES Module import { add } from './math.js'; add(1, 2); ``` 如果没有开启 tree shaking,你会发现 minus 方法也被添加到打包生成的文件中,如果使用了 tree shaking,那么 minus 方法就属于未被引用的代码,丢弃。 > 你可以将应用程序想象成一棵树。绿色表示实际用到的源码和 library,是树上活的树叶。灰色表示无用的代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。 package.json 中可以配置 sideEffects ,值为 false 为一个数组,如果为数组,数组中的模块表示不需要进行 tree shaking 的 副作用的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。 注意,任何导入的文件都会受到 tree shaking 的影响。这意味着,如果在项目中使用类似 css-loader 并导入 CSS 文件,则需要将其添加到 side effect 列表中,以免在生产模式中无意中将它删除: ```js // package.json { "name": "your-project", "sideEffects": [ "./src/some-side-effectful-file.js", "*.css" ] } ``` ## Code Spliting(代码分离) 官网:[https://webpack.js.org/configuration/optimization/](https://webpack.js.org/configuration/optimization/) 用途:将代码分离到不同的 bundle 中,然后便可以按需加载或并行加载这些文件。 为什么要把整个应用分为一个个的 bundle ?如果全部打包为一个 bundle 将会有以下后果: - 首屏加载时间长 - 影响缓存性能:如果修改了整个代码中的任何一部分,那么用户下次访问时将重新加载全部的文件;如果分为 bundle,我们修改了一部分的 bundle,那么用户下次访问只需要重新加载这一个 bundle,其他的 bundle 可以直接从缓存中读取 有两种方式来实现:看你的引入模块的方式 1.如果是静态加载(import 语句),只需要配置最外层的 optimization 选项 ```js const path = require('path'); module.exports = { mode: 'development', entry: { index: './src/index.js', another: './src/another-module.js' }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') }, + optimization: { + splitChunks: { + chunks: 'all' + } + } }; ``` 2.如果是动态加载(使用 import() 语法),无需上述配置,webpack 会解析该语法帮我们做代码分离 其区别大概是这样? ```js import _ from 'lodash' // 同步引入模块的方式 var element = document.createElement('div') element.innerHTML = _.join(['Hello'], 'world') document.body.appendChild(element) // do something... // 异步方式 function getComponent () { // 下面的注释称为 magicname return import(/* webpackChunkName:"lodash" */ 'lodash').then(({ default: _ }) => { var element = document.createElement('div') element.innerHTML = _.join(['Hello'], 'world') return element }) } getComponent().then(element => { document.body.appendChild(element) }) ``` > 可能解释的有错,可以去看官网 ## SplitChunksPlugin 我们可以看到上面的 optimization 其实就是使用了该插件,它有如下的默认配置项: ```js splitChunks: { chunks: "async", // 做代码分割时只对异步代码生效 async(默认) / all / initial minSize: 30000, // 引入的模块大于 30000 字节才做代码分割 30KB minChunks: 1, // 当一个模块至少被引用了多少次才做代码分割 maxAsyncRequests: 5, // 最多同时加载的请求数,一般不动 maxInitialRequests: 3, // 不动 automaticNameDelimiter: '~', // 打包生成的文件的连接符 vendors~main.js name: true, // 不动 cacheGroups: { // 分割出的代码的文件名,缓存组:符合同等要求的第三方库会打包到一起 vendors: { // 配置组 test: /[\\/]node_modules[\\/]/, // 如果在node_modules 中 priority: -10, // 优先级 // filename: ‘vendors.js’ 则放到 vendors.js 文件中 }, default: { // 如果上述组不匹配 minChunks: 2, priority: -20, reuseExistingChunk: true // 防止模块的重复引入 } } } ``` ## 懒加载 官网:[https://www.webpackjs.com/guides/lazy-loading/](https://www.webpackjs.com/guides/lazy-loading/) 懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。 许多框架和类库对于如何用它们自己的方式来实现(懒加载)都有自己的建议。这里有一些例子: * React:[Code Splitting and Lazy Loading](https://reacttraining.com/react-router/web/guides/code-splitting) * Vue:[Lazy Load in Vue using Webpack's code splitting](https://alexjoverm.github.io/2017/07/16/Lazy-load-in-Vue-using-Webpack-s-code-splitting/) * AngularJS:[AngularJS + Webpack = lazyLoad](https://medium.com/@var_bin/angularjs-webpack-lazyload-bb7977f390dd)by[@var\_bincom](https://twitter.com/var_bincom) ## resolve 配置项 resolve 配置项一般用于配置 **别名** 和 **默认引入文件** 例如,一些位于 src/ 文件夹下的常用模块: ```js alias: { '@': path.join(__dirname, 'src'), Utilities: path.resolve(__dirname, 'src/utilities/'), Templates: path.resolve(__dirname, 'src/templates/') } ``` 现在,替换「在导入时使用相对路径」这种方式,就像这样: ```js import Utility from '../../utilities/utility'; ``` 你可以这样使用别名: ```js import Utility from 'Utilities/utility'; ``` 再看下面这段代码: ```js resolve: { extensions: ['.js', '.jsx'] } ``` 这段配置表示:当我们引入一个 js 模块时,先找 js 文件,再找 jsx 文件,这样我们就可以写成 `import Child from ‘./child` 而不是 `import Child from ‘./child.jsx’` 但是注意如果 extensions 过多,反而会影响文件的查找速度。 一般资源类的文件要求写后缀(.css .png),逻辑类(.vue .jsx .js)可以不写(自己配) ```js resolve: { extensions: ['.js', '.jsx'], mainFiles: ['index', 'child'] } ``` mainFiles 配置表示:当你引入一个目录时,默认找名字为 index 的文件,如果找不到就找名字为 child 的文件。 ## babel 配置 官网:[https://www.babeljs.cn/docs/](https://www.babeljs.cn/docs/) 官方提供的 webpack 配置说明:[https://www.babeljs.cn/setup#installation](https://www.babeljs.cn/setup#installation) >TODO: babel 配置 ## library 打包 library 主要是你在写一个库才要注意这些问题,你写的库可能被别人用 import、require 等方式引入,要注意很多问题,还要保证文件体积尽可能小。 ```js const path = require('path') // 作为一个 library / 库 别人会怎么引入呢? // 1.import 2.require 3.AMD ... 如何配置? module.exports = { mode: 'production', entry: './src/index.js', externals: 'lodash', // 考虑这么一种情况:用户引入我们的库又引入了 lodash, 该配置项会忽略(不打包)lodash,但会要求用户先引入 lodash output: { path: path.resolve(__dirname, 'dist'), filename: 'library.js', library: 'library', // 支持 <script src="library.js"></script> 方式引入 libraryTarget: 'umd' // 不管是哪种模块化方式引入,都可正确配置 还有 this,window,global 可选值 } } ``` 如果我们想发布库到 npm 仓库,需要先注册 npm 账号,然后改一下入口文件(package.json),然后 npm publish,然后别人就可以 npm isntall 了 比如我们的打包结果是 dist 目录下的 library.js 那么 package.json 中配置如下一行即可: `"main": "./dist/library.js"` ## TypeScript 的打包配置 使用 typeScript 最大的好处就是规范我们的代码,报错信息更详细(类型检查等) 官网:[https://www.webpackjs.com/guides/typescript/#%E5%9F%BA%E7%A1%80%E5%AE%89%E8%A3%85](https://www.webpackjs.com/guides/typescript/#%E5%9F%BA%E7%A1%80%E5%AE%89%E8%A3%85) 1.npm install typescript ts-loader 以支持 typescript 打包 2.需要在根路径下创建 tsconfig.json ```js // tsconfig.js { "compilerOptions": { "outDir": "./dist/", "sourceMap": true, "noImplicitAny": true, "module": "es6", "target": "es5", "allowJs": true } } ``` 3.配置 loader ```js module: { rules: [ { test: /\.ts?$/, use: 'ts-loader', exclude: /node_modules/ } ] }, ``` 4.使用第三方库并使用 TypeScript 做类型检验时需要先安装例如 @types/lodash ## 开发环境(Development)与线上环境(Production) 其大致有如下区别: 1. sourceMap:开发环境比较详细,线上环境可以不需要 2. 代码压缩:开发环境一般不进行代码压缩,线上环境 webpack 自动?帮我们进行代码压缩 3. 线上环境不需要 devServer 配置 4. 线上环境不需要热重启 5. 开发环境打包生成的文件是存储在内存中的(便于热重启),线上环境直接以文件形式存储 对于重复的配置代码,可以提取到 webpack.common.js 中,`npm install webpack-merge -D`,然后可以这么做合并: ```js + const merge = require('webpack-merge'); + const common = require('./webpack.common.js'); + + module.exports = merge(common, { + devtool: 'inline-source-map', + devServer: { + contentBase: './dist' + } + }); ``` # 自己写一个 loader 和 plugin ## loader 如果需要都代码做一些统一的替换或者包装之类的,可以利用 webpack 的 loader API:[https://www.webpackjs.com/api/loaders/#%E5%90%8C%E6%AD%A5-loader](https://www.webpackjs.com/api/loaders/#%E5%90%8C%E6%AD%A5-loader) loader 相当于一个函数,对你的源代码进行处理后再返回,webpack 规定了与配置对应的 API,比如参数如何获取之类的; 我们写一个 replaceloader.js 文件如下:需要先引入 loader-tuils `npm install loader-utils -D` ```js const loaderUtils = require('loader-utils') // source 为 compiler 传递给 Loader 的一个文件的原内容 module.exports = function (source) { // 获取到用户给当前 Loader 传入的 options const options = loaderUtils.getOptions(this); return source.replace('lee', 'world') } ``` 这个 loader 的作用很简单,就是代码中的 'lee' 字符串替换成 'world',其参数为资源文件 然后 webpack 配置如下: ```js const path = require('path'); module.exports = { mode: 'development', entry: { main: './src/index.js' }, resolveLoader: { modules: ['node_modules', './loaders'] }, module: { rules: [{ test: /\.js/, use: [ { loader: 'replaceLoader', } ] }] }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js' } } ``` 打包后 index.js 中的代码就会被这个 loader 处理 ## plugin plugin 比 loader 麻烦些,plugin 相当于一个个的钩子,在打包过程中的某个时刻起作用,webpack 就提供了一系列钩子供我们使用:[https://www.webpackjs.com/api/plugins/](https://www.webpackjs.com/api/plugins/) 真要写的话得又得研究一堆 API 了...... 假设我们写一个 copyright-webpack-plugin.js 文件如下: ```js class CopyrightWebpackPlugin { apply(compiler) { compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => { console.log('compiler'); }) compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => { debugger; compilation.assets['copyright.txt']= { source: function() { return 'copyright by dell lee' }, size: function() { return 21; } }; cb(); }) } } module.exports = CopyrightWebpackPlugin; ``` 我们的配置文件这么写就可以使用我们写的插件了: ```js const path = require('path'); const CopyRightWebpackPlugin = require('./plugins/copyright-webpack-plugin'); module.exports = { mode: 'development', entry: { main: './src/index.js' }, plugins: [ new CopyRightWebpackPlugin() ], output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js' } } ``` # 其他 ## 提升 Webpack 打包速度 ① 升级 webpack、node 、npm、yarn 的版本 ② 在尽可能少的模块上应用 Loader,使用 include / exclude 配置,一般忽略 node_modules 下的 js 文件 ③ Plugin 尽可能精简并确保可靠,首先要注意开发环境和线上环境的一些区别:如代码是否需要压缩,SourceMap 是否需要准确,devServer 是否启用等;另外最好使用官方网站使用的插件,插件会影响打包速度 ## 打包分析工具 推荐[https://github.com/webpack-contrib/webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) ## 搭建 vue 和 react 的 webpack 配置 vue:[https://juejin.im/post/5cc55c336fb9a032086dd701#heading-21](https://juejin.im/post/5cc55c336fb9a032086dd701#heading-21) react:[https://juejin.im/post/5cfe4b13f265da1bb13f26a8#heading-2](https://juejin.im/post/5cfe4b13f265da1bb13f26a8#heading-2) react-webpack4:[https://github.com/Z6T/react-webpack4-cook](https://github.com/Z6T/react-webpack4-cook) 再去看看 vue 和 react 的脚手架的 webpack 的配置...我 TM 都学了点啥...... ![](https://img.kancloud.cn/29/7d/297d4deb5a234da0641e32493fe8f17f_480x270.gif) ## 参考资料 [https://juejin.im/post/5cfe4b13f265da1bb13f26a8#heading-20](https://juejin.im/post/5cfe4b13f265da1bb13f26a8#heading-20) [https://segmentfault.com/a/1190000015088834](https://segmentfault.com/a/1190000015088834)