Why?

为什么要用打包器?
打包器就是前端开发人员用来将 JavaScript 模块打包到一个可以在浏览器中运行的优化的 JavaScript 文件的工具,例如 webapck、rollup、gulp 等。

  • 模块打包。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。
  • 编译兼容。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,通过webpack的Loader机制,不仅仅可以帮助我们对代码做polyfill,还可以编译转换诸如.less, .vue, .jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。
  • 能力扩展。通过webpack的Plugin机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。

entry

入口决定 webapck 从哪个模块开始生成依赖关系图(构建包),每一个入口文件都对应着一个依赖关系图。

  1. //单个字符串
  2. entry:'./src/index.js',
  3. ====>相当于
  4. entry:{
  5. main:'./src/index.js'
  6. }
  7. //数组
  8. entry:[
  9. './src/print.js',
  10. './src/index.js',
  11. ],
  12. =====> 相当于
  13. entry:{
  14. main:[ './src/print.js','./src/index.js']
  15. }
  16. //对象
  17. entry:{
  18. app: './src/print.js',
  19. main: './src/index.js',
  20. },

注意: 当传入单个字符串相比:单个字符串打包的bundle中只有index的内容。 当传入数组的时候,将print和index文件打包到一个bundle中。 当传入对象的时候,生成多个bundle

output

用于告知 webpack 如何构建编译后的文件,可以自定义输出文件的位置和名称

  1. output: {
  2. filename: 'entry/[name]-[hash:8].js', //在dist目录下新建entry文件夹
  3. path: path.resolve(__dirname, 'dist'),//必须为绝对路径,输出文件路径
  4. chunkFilename: 'bundle-[name]-[chunkhash:8].js', // 块名,公共块名(非入口)
  5. jsonpFunction: 'webpackJsonp_block_plug',
  6. publicPath: '/',
  7. },

说明:

  • chunkFilename:指未被列在 entry 中,却又需要被打包出来的 chunk 文件的名称。一般来说,这个 chunk 文件指的就是要懒加载的代码。
  • publicPath:打包生成的 index.html 文件里面引用资源的前缀,也为发布到线上资源的 URL 前缀,使用的是相对路径,默认为’’。静态资源最终访问路径 = output.publicPath + 资源loader或插件等配置路径
  • jsonpFunction: webpack中用于异步加载(async loading)chunk的JSONP函数。(默认是webpackJsonp)。这是用于异步加载 chunk 的时候一个全局变量。如果多 webpack 环境下,为了防止该函数命名冲撞产生问题,最好设置成一个比较唯一的值。


浏览器缓存与 hash 值

对于我们开发的每一个应用,浏览器都会对静态资源进行缓存,如果我们更新了静态资源,而没有更新静态资源名称(或路径),浏览器就可能因为缓存的问题获取不到更新的资源。在我们使用 webpack 进行打包的时候,webpack 提供了 hash 的概念,所以我们可以使用 hash 来打包

Hash:和整个项⽬的构建相关,只要项⽬⽂件有修改,整个项⽬构建的 hash 值就会更改。可以用在开发环境,生产环境不适用
image.png
Chunkhash:和 webpack 打包的 chunk 有关,不同的 entry 会⽣成不同的 chunkhash 值(不同入口中的包中内容改变) 适用于生产环境

Contenthash:根据⽂件内容来定义 hash ,⽂件内容不变,则 contenthash 不变 适用于生产环境

webpack 也允许哈希的切片。如果你写 [hash:8] ,那么它会获取哈希值的前 8 位。

注意:
  • 尽量在生产环境使用哈希
  • 按需加载的块不受 filename 影响,受 chunkFilename 影响
  • 使用 hash/chunkhash/contenthash 一般会配合 html-webpack-plugin (创建 html ,并捆绑相应的打包文件) 、clean-webpack-plugin (清除原有打包文件) 一起使用。

配置解析策略resolve

  1. module.exports = {
  2. resolve: {
  3. // 设置模块导入规则,import/require时会直接在这些目录找文件
  4. // 可以指明存放第三方模块的绝对路径,以减少寻找,
  5. // 默认 node_modules
  6. modules: [path.resolve(`${project}/components`), 'node_modules'],
  7. // import导入时省略后缀
  8. // 注意:尽可能的减少后缀尝试的可能性
  9. extensions: ['.js', '.jsx', '.react.js', '.css', '.json'],
  10. // import导入时别名,减少耗时的递归解析操作
  11. alias: {
  12. '@components': path.resolve(`${project}/components`),
  13. '@style': path.resolve('asset/style'),
  14. },
  15. // 很多第三方库会针对不同的环境提供几份代码
  16. // webpack 会根据 mainFields 的配置去决定优先采用那份代码
  17. // 它会根据 webpack 配置中指定的 target 不同,默认值也会有所不同
  18. mainFields: ['browser', 'module', 'main'],
  19. },
  20. }

配置优化optimization(webpack4)

主要涉及两方面的优化:

  • 最小化包
  • 拆包

    1. 最小化包

  • 使用 optimization.removeAvailableModules 删除已可用模块

  • 使用 optimization.removeEmptyChunks 删除空模块
  • 使用 optimization.occurrenceOrder 标记模块的加载顺序,使初始包更小
  • 使用 optimization.providedExportsoptimization.usedExportsconcatenateModulesoptimization.sideEffects 删除死代码
  • 使用 optimization.splitChunks 提取公共包
  • 使用 optimization.minimizer || TerserPlugin 来最小化包

    2. 拆包

    当包过大时,如果我们更新一小部分的包内容,那么整个包都需要重新加载,如果我们把这个包拆分,那么我们仅仅需要重新加载发生内容变更的包,而不是所有包,有效的利用了缓存。 ``` module.exports = { //… optimization: { splitChunks: {
    1. chunks: 'async',
    2. minSize: 30000,
    3. maxSize: 0,
    4. minChunks: 1,
    5. maxAsyncRequests: 5,
    6. maxInitialRequests: 3,
    7. automaticNameDelimiter: '~',
    8. name: true,
    9. cacheGroups: {
    10. vendors: {
    11. test: /[\\/]node_modules[\\/]/,
    12. priority: -10
    13. },
    14. default: {
    15. minChunks: 2,
    16. priority: -20,
    17. reuseExistingChunk: true
    18. }
    19. }
    } } };
  1. **参数说明 **
  2. - chunks:表示从哪些chunks里面抽取代码,除了三个可选字符串值 initialasyncall 之外,还可以通过函数来过滤所需的 chunks
  3. **async** 只对对动态(异步)导入的模块进行分离<br /> **initial** 对所有模块进行分离,如果一个模块既被异步引用,也被同部引用,那么会生成两个包<br /> **all** 对所有模块进行分离,如果一个模块既被异步引用,也被同部引用,那么只会生成一个共享包<br /> indexA中异步引入moduleAindexB中同步引入moduleAinitialmoduleA会出现两个打包块中,all模式下只会出现一个】
  4. - minSize:表示抽取出来的文件在压缩前的最小大小
  5. - maxSize:表示抽取出来的文件在压缩前的最大大小,默认为 0,表示不限制最大大小;
  6. - minChunks:表示被引用次数,默认为1
  7. - maxAsyncRequests:最大的按需(异步)加载次数
  8. - maxInitialRequests:最大的初始化加载次数
  9. - automaticNameDelimiter:抽取出来的文件的自动生成名字的分割符,默认为 ~;
  10. - name:抽取出来文件的名字,默认为 true,表示自动生成文件名,文件名就是cacheGroups中定义的key值;
  11. - **cacheGroups**: 缓存组。(这才是配置的关键)可以继承/覆盖上面 splitChunks 中所有的参数值,除此之外还额外提供了三个配置,分别为:test, priority reuseExistingChunk
  12. **test**: 表示要过滤 modules,默认为所有的 modules,可匹配模块路径或 chunk 名字,当匹配的是 chunk 名字的时候,其里面的所有 modules 都会选中;<br />**priority**:表示抽取权重,数字越大表示优先级越高。因为一个 module 可能会满足多个 cacheGroups 的条件,那么抽取到哪个就由权重最高的说了算;<br />**reuseExistingChunk**:表示是否使用已有的 chunk,如果为 true 则表示如果当前的 chunk 包含的模块已经被抽取出去了,那么将不会重新生成新的。
  13. demo之前,安装webpack-bundle-analyzer
  14. **stat:**这个大小是在进行压缩等转换之前的输入文件大小;它叫做stat大小,是因为它是从webpack的[stat](https://webpack.js.org/configuration/stats/)对象里得到的。<br />**parsed:**这个是你的文件最终的输出的尺寸大小。如果你正在使用Uglify等插件,这个值将会是你代码打包的最小尺寸。<br />**gzip:**这个大小是你的包经过gzip压缩后的大小

const {BundleAnalyzerPlugin} = require(‘webpack-bundle-analyzer’); plugins:[ new BundleAnalyzerPlugin(), ]

  1. <a name="YD30T"></a>
  2. #### demo1:initial all async
  3. index1中异步引入lodash、eacharts,同步引入React<br />index2中同步引入lodash、React、eacharts

setTimeout(() => { import(/ webpackChunkName: ‘echarts’/ “echarts”).then(function (echarts) { console.log(“异步加载 echarts”); }); }, 3000);

setTimeout(() => { import(/ webpackChunkName: ‘lodash’/ “lodash”).then(function (lodash) { console.log(“异步加载 lodash”); }); }, 3000);

entry: { main: “./src/index.js”, main2:’./src/index2.js’, },

cacheGroups: { vendors: { chunks: “async” , //async || “initial” || “all”, test: /[\/]node_modules[\/]/, minSize:3000, priority:1 }, }

  1. **async模式下**,只是单独打包异步加载的模块,lodashecharts被单独打包出来<br />index1对应的main包中,没有lodash echarts,只有同步引入的react<br />index2对应的main2包中lodash echarts react<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/248010/1614683405916-59e67651-606e-4fbe-83c8-67f554ad670b.png#crop=0&crop=0&crop=1&crop=1&height=194&id=xeelH&margin=%5Bobject%20Object%5D&name=image.png&originHeight=388&originWidth=1564&originalType=binary&ratio=1&rotation=0&showTitle=false&size=113170&status=done&style=none&title=&width=782)<br />**initial模式下 **如果一个模块既被异步引用,也被同部引用,那么会生成两个包<br />vendors~main~main2 包含react ,代表main和main2中都引入的包,index1和index2中同步引入的react<br />vendors~main2 包含 echarts和lodash,index2中同步引入<br />echarts和lodash中包含异步引入的
  2. <a name="zatWW"></a>
  3. #### ![image.png](https://cdn.nlark.com/yuque/0/2021/png/248010/1614683999021-ba1ba099-187c-4c61-919c-075f04d021e2.png#crop=0&crop=0&crop=1&crop=1&height=352&id=IYdPq&margin=%5Bobject%20Object%5D&name=image.png&originHeight=704&originWidth=1984&originalType=binary&ratio=1&rotation=0&showTitle=false&size=264479&status=done&style=none&title=&width=992)
  4. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/248010/1614684757728-24594888-28c2-46b7-8e0e-f6ec7f79d2d9.png#crop=0&crop=0&crop=1&crop=1&height=616&id=cU1Lq&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1232&originWidth=2284&originalType=binary&ratio=1&rotation=0&showTitle=false&size=452707&status=done&style=none&title=&width=1142)<br />**all模式下 **如果一个模块既被异步引用,也被同部引用,那么只会生成一个共享包
  5. main2中包含reactindex2react同步引入<br />vendors~main中包含reactindex1中同步引入react<br />vendors~echarts~main2vendors~lodash~main2 包含同步和异步引入的echartslodash<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/248010/1614684809945-c773ee95-f3fc-4da5-97ac-72399336d6ff.png#crop=0&crop=0&crop=1&crop=1&height=312&id=pfPrP&margin=%5Bobject%20Object%5D&name=image.png&originHeight=624&originWidth=1932&originalType=binary&ratio=1&rotation=0&showTitle=false&size=230965&status=done&style=none&title=&width=966)
  6. <a name="twaVl"></a>
  7. ####
  8. <a name="LBMhV"></a>
  9. #### demo2:拆分三方库
  10. 如果项目中依赖很多三方库,如果不单独拆分出来,每个入口文件中引入一次,就会被重复打包,通过splitChunks可以将三方库单独打包出来。<br />项目中有index.js,index1.js,index3.js,三个文件中都引用了 import _ from 'lodash'; 设置entry为单入口<br />默认情况下,chunk取值为async,也就是只会将异步加载的模块进行拆分,不会将lodash单独打包出来,可以看到node_modules被打包到了main中<br />TODO:为什么main中是1MB?<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/248010/1614662658571-9f6a5893-7e02-431c-8f57-8230913f53cb.png#crop=0&crop=0&crop=1&crop=1&height=280&id=azu79&margin=%5Bobject%20Object%5D&name=image.png&originHeight=560&originWidth=1794&originalType=binary&ratio=1&rotation=0&showTitle=false&size=163285&status=done&style=none&title=&width=897)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/248010/1614663487291-8bf7e3b6-f03b-46b2-bf1d-5a0ac225950d.png#crop=0&crop=0&crop=1&crop=1&height=512&id=WjpiS&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1024&originWidth=2202&originalType=binary&ratio=1&rotation=0&showTitle=false&size=872949&status=done&style=none&title=&width=1101)
  11. 我们更改chunkall,并且打印module,可以看到,module有很多属性,可以看到node中的依赖被打到了vendor~main

chunks: ‘all’, cacheGroups: { vendors: { test: /[\/]node_modules[\/]/, name(module) { // 获取第三方包名 console.log(22222,module.context) const packageName = module.context.match(/[\/]node_modules\/([\/]|$)/)[1]; console.log(33333,packageName) // npm 软件包名称是 URL 安全的,但是某些服务器不喜欢@符号 // return npm.${packageName.replace('@', '')}; }, priority: -10 },

  1. ```
  2. 22222 '/Users/guan/Desktop/webpacDemo/node_modules/lodash'
  3. 33333 'lodash'
  4. 22222 '/Users/guan/Desktop/webpacDemo/node_modules/webpack/buildin'
  5. 33333 'webpack'
  6. 22222 '/Users/guan/Desktop/webpacDemo/node_modules/webpack/buildin'
  7. 33333 'webpack'
  8. 22222 '/Users/guan/Desktop/webpacDemo/node_modules/mini-css-extract-plugin/dist/hmr'
  9. 33333 'mini-css-extract-plugin'
  10. 22222 '/Users/guan/Desktop/webpacDemo/node_modules/mini-css-extract-plugin/dist/hmr'
  11. 33333 'mini-css-extract-plugin'
  12. 11111 NormalModule {
  13. dependencies:
  14. [ ConstDependency {
  15. module: null,
  16. weak: false,
  17. optional: false,
  18. loc: [SourceLocation],
  19. expression: '',
  20. range: [Array],
  21. requireWebpackRequire: undefined },
  22. CommonJsRequireDependency {
  23. module: [NormalModule],
  24. weak: false,
  25. optional: false,
  26. loc: [SourceLocation],
  27. request: './normalize-url',
  28. userRequest: './normalize-url',
  29. range: [Array] },
  30. RequireHeaderDependency {
  31. module: null,
  32. weak: false,
  33. optional: false,
  34. loc: [SourceLocation],
  35. range: [Array] } ],
  36. blocks: [],
  37. variables: [],
  38. type: 'javascript/auto',
  39. context:
  40. '/Users/guan/Desktop/webpacDemo/node_modules/mini-css-extract-plugin/dist/hmr',
  41. debugId: 1011,
  42. hash: u

image.png

image.png
如果我们将entry设置为多入口,且chunk为async,可以看到每个入口文件中都重复打包了lodash,导致总体积4MB

  1. entry: {
  2. app1: "./src/index.js",
  3. app2:'./src/index2.js',
  4. app3:'./src/index3.js'
  5. },

image.png

image.png

我们将chunk修改为all,对比下总体积1.63MB,而且lodash被打到了一个单独的包中
image.png
image.png

demo3:动态加载

现在我们已经对包拆分的很彻底了,但以上的拆分仅仅是对浏览器缓存方面的优化,减小首屏加载时间,实际上我们也可以使用按需加载的方式来进一步拆分,减小首屏加载时间:
没有动态加载的时候,我们通过lighthouse可以看到,FCP的时间是2.4s
image.png
在index文件中,我们通过import()动态加载,将动态加载的包单独打到commonchunk包中,因为lodash同时符合vendor和commonchunk,通过priority设置优先级,看到FCP的时间为1.2s

  1. import('lodash').then(_ => {
  2. // Do something with lodash (a.k.a '_')...
  3. element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  4. console.log(
  5. _.join(['Another', 'module', 'loaded!'], ' ')
  6. );
  7. })
  8. cacheGroups: {
  9. vendors: {
  10. test: /[\\/]node_modules[\\/]/,
  11. priority: -10
  12. },
  13. common:{
  14. name:'commonchunk',
  15. chunks:"async",
  16. test: /[\\/]node_modules[\\/]/,
  17. // test(module,chunks){
  18. // //chunks引用该模块的chunk
  19. // //module.contex 当前文件模块的目录,该目录下包含多个文件
  20. // //module.resource当前模块的绝对路径
  21. // //'/Users/guan/Desktop/webpacDemo/node_modules/lodash' '/Users/guan/Desktop/webpacDemo/node_modules/lodash/lodash.js'
  22. // },
  23. minChunks:1,
  24. priority:1
  25. },
  26. }

image.png
image.png
index中异步引入lodash,index2中同步引入
将comonchunk中的chunks改成initial,
image.png

demo4 runtimeChunk

runtimeChunk ,作用是将包含chunks映射关系的list单独从app.js里提取出来,因为每一个chunk的id基本都是基于内容hash出来的,所以你每次改动都会影响它,如果不把它提取出来的话,app.js每次都会改变,缓存就失效了。
在使用 CommonsChunkPlugin的时候,我们也通常把webpack runtime 的基础函数提取出来,单独作为一个chunk,毕竟code splitting想把不变的代码单独抽离出来,方便浏览器缓存,提升加载速度。
其实就是单独分离出webpack的一些运行文件。

拆包策略

  • node_moudlus下的单独拆成vendor
  • 项目中lib common公用方法—-common;
  • 三方库使用一次 打包到页面js中,使用多次打包到common中

loader

webpack 开箱即用只支持 JS 和 JSON 两种文件类型,通过 Loaders 去支持其它文件类型并且把它们转化成有效的模块,并且可以添加到依赖图中。
本身是一个函数,接受源文件作为参数,返回转换的结果

  1. module: {
  2. rules: [
  3. {
  4. test: /\.js$/,
  5. exclude: /(node_modules)/, // (不处理node_modules 和 bower_components下的js文件) 优化处理加快速度
  6. use: {
  7. loader: "babel-loader",
  8. options: {
  9. presets: ["@babel/preset-env"] // presets设置的就是当前js的版本
  10. }
  11. }
  12. },
  13. {
  14. test: /\.(png|jpg|gif)$/,
  15. use: [
  16. {
  17. loader: "url-loader",
  18. options: {
  19. limit: 198192, //如果大于或等于8192Byte,则按照相应的文件名和路径打包图片;如果小于8192Byte,则将图片转成base64格式的字符串。
  20. name: "images/[name]-[hash:8].[ext]" //images:图片打包的文件夹;
  21. //[name].[ext]:设定图片按照本来的文件名和扩展名打包,不用进行额外编码
  22. //[hash:8]:一个项目中如果两个文件夹中的图片重名,打包图片就会被覆盖,加上hash值的前八位作为图片名,可以避免重名。
  23. }
  24. }
  25. ]
  26. },
  27. {
  28. test: /\.css$/,
  29. use: ["style-loader", "css-loader"]
  30. },
  31. {
  32. test: /\.png|jpg|gif$/,
  33. use: "file-loader"
  34. },
  35. {
  36. test: /\.txt$/,
  37. use: "raw-loader"
  38. }
  39. ]
  40. }

常见loader:
babel-loader:处理ES6语法,将其编译为浏览器可以执行的js语法。 参见 babel
ts-loader:处理ts语法
css-loader/style-loader、less-loader、sass-loader
raw-loader:将文件以字符串形式导入
file-loader:图片和字体
url-loader: 处理图片和字体

  • 图片资源进行base64编码转换并不能压缩文件(反而会略有增加)
  • 只有较小的图片资源适合进行base64编码转换,因为进行base64编码转换的图片资源往往是放在css中,过大的图片资源转换后会导致css文件膨胀,进而影响页面加载效率(css会阻塞页面的渲染,而图片则不会),而较小的图片转换后虽然css大小略有增加,但可以减少一个http请求。

    plugin

    插件⽤于 bundle ⽂件的优化,资源管理和环境变量注⼊
    作⽤于整个构建过程

常见plugin
HtmlWebpackPlugin:在dist中创建html文件承载输出的bundle
CleanWebpackPlugin:清理dist目录中上一次编译的文件

  1. const { CleanWebpackPlugin } = require('clean-webpack-plugin');
  2. plugins:[
  3. new CleanWebpackPlugin(),
  4. ]

image.png
image.png
BundleAnalyzerPlugin:分析打包情况
DefinePlugin:webpack内置的plugin

  1. new webpack.DefinePlugin({
  2. PRODUCTION: JSON.stringify(true),
  3. VERSION: JSON.stringify('5fa3b9'),
  4. BROWSER_SUPPORTS_HTML5: true,
  5. 'process.env.NODE_ENV': JSON.stringify('production')
  6. })
  7. console.log(PRODUCTION,process.env.NODE_ENV,VERSION)

MiniCssExtractPlugin

单独提取css文件,可以指定css存放路径,对css文件进行压缩

每个包含 CSS 的 JS 文件创建一个单独的 CSS 文件,并支持 CSS 和 SourceMap 的按需加载。注意:这里说的每个包含 CSS 的 JS 文件,并不是说组件对应的 JS 文件,而是打包之后的 JS 文件

提取JS中的CSS样式,单独打css包,减少JS文件的大小,用link外部引入,简称CSS样式分离

MiniCssExtractPlugin插件不能和style-loader共用

  1. const MiniCssExtractPlugin = require('mini-css-extract-plugin');
  2. module.exports = {
  3. plugins: [
  4. new MiniCssExtractPlugin({
  5. filename: "[name].css",
  6. chunkFilename: "[id].css"
  7. })
  8. ],
  9. module: {
  10. rules: [
  11. // {
  12. // test: /\.css$/,
  13. // use: ["style-loader", "css-loader"]
  14. // },
  15. {
  16. test: /\.css$/i,
  17. use: [MiniCssExtractPlugin.loader, 'css-loader'],
  18. },
  19. ],
  20. },
  21. };
  • 如图,通过MiniCssExtractPlugin,打包的时候,css文件单独打包,而且通过link标签引入;相比之前,css打包在了js文件中,通过style-loader,生成style标签引入css
  • 通过optimize-css-assets-webpack-plugin插件可以对css进行压缩,与此同时,必须指定js压缩插件(例子中使用terser-webpack-plugin插件),否则webpack不再对js文件进行压缩;

image.png
image.pngimage.png

mode

Mode ⽤来指定当前的构建环境是:production、development 还是 none
设置 mode 可以使⽤ webpack 内置的函数,默认值为 production

生产模式:代码压缩
开发模式:热更新等

  • webpack.config.base.js:通用的配置,比如入口,出口,插件,loader等。以下两个配置文件会引入此配置,再修改添加其他配置。
  • webapck.config.dev.js:开发模式下,启动 webpack-dev-server。
  • webapck.config.prod.js:生产模式下,编译打包。

production 模式下给你更好的用户体验:

  • 较小的输出包体积
  • 浏览器中更快的代码执行速度
  • 忽略开发中的代码
  • 不公开源代码或文件路径
  • 易于使用的输出资产

development 模式会给予你最好的开发体验:

  • 浏览器调试工具
  • 快速增量编译可加快开发周期
  • 运行时提供有用的错误消息

    文件监听

    HMR 不适用于生产环境,这意味着它应当只在开发环境使用

⽂件监听是在发现源码发⽣变化时,⾃动重新构建出新的输出⽂件。

以上在开发过程中,控制台会被中断,且手动刷新页面

Watch Mode

webpack 开启监听模式,有两种⽅式:

  • 启动 webpack 命令时,带上 —watch 参数
  • 在配置 webpack.config.js 中设置 watch: true
    1. "build": "webpack --watch"
    或者
    1. module.export = {
    2. //默认 false,也就是不开启
    3. watch: true,
    4. //只有开启监听模式时,watchOptions才有意义
    5. wathcOptions: {
    6. //默认为空,不监听的文件或者文件夹,支持正则匹配
    7. ignored: /node_modules/,
    8. //监听到变化发生后会等300ms再去执行,默认300ms
    9. aggregateTimeout: 300,
    10. //判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
    11. poll: 1000
    12. } }
    配置之后,之执行npm run build之后,控制台不会中断,显示 watching files for updates…
    webpack is watching the files…
    但是需要手动刷新浏览器,如果可以自动刷新浏览器就好啦!

webpack-dev-server

webpack-dev-server 提供了一个简单的 web 服务器,并且能够实时重新加载(live reloading)
https://github.com/webpack/webpack-dev-server

Error: Cannot find module ‘webpack-cli/bin/config-yargs’
“webpack-cli”: “^3.3.0”,
“webpack-dev-server”: “^3.11.0”
“webpack”: “^4.32.2”

  1. devServer: {
  2. contentBase: "./HMRDist", // 热启动文件所指向的文件目录
  3. port:8100,
  4. host:'0.0.0.0',
  5. hot: true, // 热更新
  6. open: true, // 服务启动后,自动打开浏览器
  7. historyApiFallback: true, // 找不到的都可替换为 index.html
  8. proxy: { // 后端不帮我们处理跨域,本地设置代理
  9. '/api': 'http://localhost:3000', // 接口中有 '/api' 时代理到 'http://localhost:3000'
  10. },
  11. },
  12. //package.json
  13. "start": "webpack-dev-server --open "

更改代码,不用手动刷新页面,且控制台不会中断,而且控制台中会有hmr的标识,同时生成hot-update.js和hot-update.json文件

文件系统中一个文件(或者模块)发生变化,webpack监听到文件变化对文件重新编译打包,每次编译生成唯一的hash值,根据变化的内容生成两个补丁文件:说明变化内容的manifest(文件格式是hash.hot-update.json,包含了hash和chundId用来说明变化的内容)和更新的chunk js(hash.hot-update.js)模块。
image.png
image.png
image.png
image.png

模块热替换(HMR - Hot Module Replacement)

在配置文件中新增

  1. new webpack.NamedModulesPlugin(),
  2. new webpack.HotModuleReplacementPlugin(),

更改 print.jsconsole.log 的输出内容,你将会在浏览器中看到如下的输出。

  1. import "./style.css";
  2. import printMe from './print.js';
  3. function component() {
  4. const element = document.createElement("div");
  5. element.innerHTML = "Hello Webpack,我要好好学习,天天向上啊!我知道了,一定会的!!奇怪 ";
  6. element.classList.add("hello");
  7. return element;
  8. }
  9. document.body.appendChild(component());
  10. if (module.hot) {
  11. module.hot.accept('./print.js', function() {
  12. console.log('111111111 Accepting the updated printMe module!');
  13. printMe();
  14. })
  15. }

image.png

sourceMap

  • 方便调试,友好的提示日志信息
  • 线上问题溯源、定位

    1. //会生成对应的main.js.map
    2. devtool: "", //cheap-module-eval-source-map source-map inline-source-map
    1. {
    2.   version : 3,
    3. file: "out.js",
    4. sourceRoot : "",
    5. sources: ["foo.js", "bar.js"],
    6. names: ["src", "maps", "are", "fun"],
    7. mappings: "AAgBC,SAAQ,CAAEA"
    8. }
  • version:sourcemap版本(现在都是v3)

  • file:转换后的文件名。
  • sourceRoot:转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空。
  • sources:转换前的文件。该项是一个数组,表示可能存在多个文件合并。
  • names:转换前的所有变量名和属性名。
  • mappings:记录位置信息的字符串。
    mappings 信息是关键,它使用Base64 VLQ 编码,包含了源代码与生成代码的位置映射信息。mappings的编码原理详解可见:http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html,这里就不再详述。

image.png

我们分别看下各种情况的demo
入口文件: console.lg会报错

  1. const name = 'Joy Guan'
  2. console.lg(1111)

devtool:’source-map’

source-map增加映射文件,可以帮我们调试源代码 构建时间774ms

打包后生成了map文件
image.png
打包后main.42536c64.js 文件底部有//# sourceMappingURL = …

  1. console.lg(1111);
  2. //# sourceMappingURL=main.42536c64.js.map

image.png
控制台中
image.png

devtool:’eval’

eval 模式会把每个 module 封装到 eval 里包裹起来执行,并且会在末尾追加注释。构建时间 495ms

我们先看看单独的eval配置,这个配置相对于其他会特殊一点 。因为配置里没有sourceMap,实际上它也会生出map,只是它映射的是转换后的代码,而不是映射到原始代码。
打包后成成txt文件
image.png
image.png
image.png

devtool:’hidden-source-map’

构建时间 707 ms
image.png
image.png
image.png

devtool:’cheap-source-map’

“cheap(低开销)” 的 source map,因为它没有生成列映射(column mapping),只是映射行数 。

构建时间 511 ms
cheap-source-map生成的 js.map 的内容却比 source-map 生成的.js.map 要少很多代码,我们对比一下 source-map 生成的js.map 的结果,发现 source 属性里面少了names信息

image.png
image.png
image.png
可以看到错误信息只有行映射,但实际上开发时我们有行映射也基本足够了,所以开发场景下完全可以使用cheap 模式 ,来节省sourceMap的开销
image.png

devtool:’inline-source-map’

inline配置将map作为DataURI嵌入,不单独生成.map文件。

构建时间 769 ms
打包后没有生成map
可以看到末尾的注释 sourceMap 作为 DataURL 的形式被内嵌进了 bundle 中,由于 sourceMap 的所有信息都被加到了 bundle 中,整个 bundle 文件变得硕大无比。
image.png
image.png
image.png

devtool:’eval-source-map’

和 eval 类似,没有生成map文件,但是把注释里的 sourceMap 都转为了 DataURL 构建时间 739 ms
官方是比较推荐开发场景下使用的,因为它能cache sourceMap,从而rebuild的速度会比较快
image.png
image.png

1、开发环境
综上所述,考虑到我们在开发环境对sourceMap的要求是:快(eval),信息全(module),且由于此时代码未压缩,我们并不那么在意代码列信息(cheap),所以开发环境比较推荐配置:devtool: cheap-module-eval-source-map
2、生产环境
一般情况下,我们并不希望任何人都可以在浏览器直接看到我们未编译的源码,所以我们不应该直接提供sourceMap给浏览器。
但我们又需要sourceMap来定位我们的错误信息, 这时我们可以设置hidden-source-map

一方面webpack会生成sourcemap文件以提供给错误收集工具比如sentry,另一方面又不会为 bundle 添加引用注释,以避免浏览器使用。

开发环境推荐:
cheap-module-eval-source-map
生产环境推荐:
cheap-module-source-map

原因如下:

  1. 使用 cheap 模式可以大幅提高 souremap 生成的效率。大部分情况我们调试并不关心列信息,而且就算 sourcemap 没有列,有些浏览器引擎(例如 v8) 也会给出列信息。
  2. 使用 eval 方式可大幅提高持续构建效率。参考官方文档提供的速度对比表格可以看到 eval 模式的编译速度很快。
  3. 使用 module 可支持 babel 这种预编译工具(在 webpack 里做为 loader 使用)。
  4. 使用 eval-source-map 模式可以减少网络请求。这种模式开启 DataUrl 本身包含完整 sourcemap 信息,并不需要像 sourceURL 那样,浏览器需要发送一个完整请求去获取 sourcemap 文件,这会略微提高点效率。而生产环境中则不宜用 eval,这样会让文件变得极大。

References

https://github.com/sisterAn/blog
https://mp.weixin.qq.com/s/j3jVPNgg4WCnI7RBJTxktA
https://mp.weixin.qq.com/s/To_p4eYJx_dkJr1ApcR4jA
https://mp.weixin.qq.com/s/PeSbdScrvsO0ctECGGY1bQ