webpack超详解 — 面试回答+代码理解

2,138 阅读13分钟

前言

实际上webpack是一个实操性很强的技术,并不适合用文章进行教学。如果是初学webpack的同学,可以跟着培训机构的视频一起敲一遍,学习效率可能会更高一点。 webpack视频地址(尚学堂)

书籍的话,我建议使用 吴浩麟 的 深入浅出webpack,配合上面的视频一起使用。

本文主要从面试方面(理论部分)来对以上两个资源进行详细的总结,但是也会尽可能放代码加深理解(背下来的知识,面试官是听得出来的)。如果没有实操过webpack的同学建议一定先看视频跟着实操


关于构建工具常识的面试问题

2.1 有哪些主流的构建工具?

Gulp和webpack(作者知识所限)

image.png

Gulp 是一个基于流的自动化构建工具。Gulp 的最大特点是引入了流的概念,同时提供了一系列常用的插件去处理流,流可以在插件之间传递。

实际上,Gulp 被设计得非常简单,只通过下面5个方法就可以胜任几乎所有构建场景:

  1. 通过 gulp.task 注册一个任务;
  2. 通过 gulp.run 执行任务;
  3. 通过 gulp.watch 监听文件变化;
  4. 通过 gulp.src 读取文件;
  5. 通过 gulp.dest 写文件。

大致使用如下:

var gulp = require('gulp'); // 引入 Gulp
// 引入插件
var jshint = require('gulp-jshint'); 
var sass = require('gulp-sass');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');


gulp.task('sass', function() { // 编译 SCSS 任务
  gulp.src('./scss/*.scss') // 读取文件通过管道喂给插件
    .pipe(sass()) // SCSS 插件把 scss 文件编译成 CSS 文件
    .pipe(gulp.dest('./css')); // 输出文件
});

gulp.task('scripts', function() { // 合并压缩 JS 任务
  gulp.src('./js/*.js')
    .pipe(concat('all.js'))
    .pipe(uglify())
    .pipe(gulp.dest('./dist'));
});

gulp.task('watch', function(){ // 监听文件变化
  gulp.watch('./scss/*.scss', ['sass']);// 当 scss 文件被编辑时执行 SCSS 任务
  gulp.watch('./js/*.js', ['scripts']);    
});

image.png

Webpack 是一个打包模块化 JavaScript 的工具,在 Webpack 里一切文件皆模块,通过 Loader 转换文件,通过 Plugin 注入钩子,最后输出由多个模块组合成的文件。Webpack 专注于构建模块化项目。

一切文件:JavaScript、CSS、SCSS、图片、模板,在 Webpack 眼中都是一个个模块,这样的好处是能清晰的描述出各个模块之间的依赖关系,以方便 Webpack 对模块进行组合和打包。 经过 Webpack 的处理,最终会输出浏览器能使用的静态资源。

Webpack的优点是:

  1. 专注于处理模块化的项目,能做到开箱即用一步到位;
  2. 通过 Plugin 扩展,完整好用又不失灵活;
  3. 使用场景不仅限于 Web 开发;
  4. 社区庞大活跃,经常引入紧跟时代发展的新特性,能为大多数场景找到已有的开源扩展;
  5. 良好的开发体验。

Webpack的缺点是只能用于采用模块化开发的项目。

  • 我们来看一个简易的基于webpack开发环境配置
/*
  开发环境配置:能让代码运行
    运行项目指令:
      webpack 会将打包结果输出出去
      npx webpack-dev-server 只会在内存中编译打包,没有输出
*/
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/js/index.js', //入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
  output: {  //输出结果,在 Webpack 经过一系列处理并得出最终想要的代码后输出结果
    filename: 'js/built.js',
    path: resolve(__dirname, 'build')
  },
  module: { //模块,在 Webpack 里一切皆模块,一个模块对应着一个(种)文件
    rules: [
      // loader的配置
      {  // 输出后,less是和js在一起的,不需要像图片一样单独设置outputPath
        test: /\.less$/, // 正则化表达式匹配less资源
        use: ['style-loader', 'css-loader', 'less-loader'] //loader:模块转换器,用于把模块原内容按照需求转换成新内容
      },
      {
        test: /\.(jpg|png|gif)$/, // 处理图片资源
        loader: 'url-loader',//url-loader是根据file-loader封装的库,其可以使用base64压缩
        options: {
          limit: 8 * 1024,
          name: '[hash:10].[ext]',
          esModule: false, // 关闭es6模块化
          outputPath: 'imgs'
        }
      }
    ]
  },
  
  plugins: [ // plugins:扩展插件,在 Webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情。
    new HtmlWebpackPlugin({ 
      template: './src/index.html'
    })
  ],
  mode: 'development',
  
  // DevServer 会启动一个 HTTP 服务器用于服务网页请求
  //同时会帮助启动 Webpack ,并接收 Webpack 发出的文件更变信号,通过 WebSocket 协议自动刷新网页做到实时预览。
  devServer: {
    contentBase: resolve(__dirname, 'build'),
    compress: true,
    port: 3000,
    open: true
  }
};

2.2 Gulp和webpack有什么不同?/你为什么选择webpack?

我选择webpack的理由:实习的公司就是用webpack (手动狗头)

  • 侧重点(思想)不同 Gulp侧重于前端开发的整个过程的控制管理(像是流水线),我们可以通过给gulp配置不通的task来让gulp实现不同的功能,从而构建整个前端开发流程。

Webpack更侧重于模块打包,当然我们可以把开发中的所有资源(图片、js文件、css文件等)都可以看成模块。Webpack是通过loader(加载器)和plugins(插件)对资源进行处理的。

  • 选择webpack的原因

大多数团队在开发新项目时会采用紧跟时代的技术,这些技术几乎都会采用“模块化+新语言+框架”,Webpack 可以为这些新项目提供一站式的解决方案

1、模块化

目前有三种模块化方案:CommonJS、AMD、ES6 模块化

  • CommonJS代码可复用于 Node.js 环境下并运行,而且通过 NPM 发布的很多第三方模块都采用了 CommonJS 规范。 但是采用了CommonJS的代码无法直接运行在浏览器环境下,必须通过工具转换成标准的 ES5
  • AMD 规范主要是为了解决针对浏览器环境的模块化问题,它采用异步的方式去加载依赖的模块,代码可运行在浏览器环境和 Node.js 环境下。 AMD 的缺点在于JavaScript 运行环境没有原生支持 AMD,需要先导入实现了 AMD 的库后才能正常使用。
  • ES6 模块化终极JavaScript 模块化规范,它在语言的层面上实现了模块化。浏览器厂商和 Node.js 都宣布要原生支持该规范。它将逐渐取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。但是ES6兼容性有问题,目前无法直接运行在大部分 JavaScript 运行环境下,必须通过工具转换成标准的 ES5 后才能正常运行

webpack并不强制你使用某种模块化方案,而是通过兼容所有模块化方案让你无痛接入项目,你可以随意选择你喜欢的模块化方案,至于怎么处理模块之间的依赖关系及如何按需打包,webpack会帮你处理好的。

2、新语言

主要指的是TypeScript,其无法直接运行在浏览器或 Node.js 环境下。但实际上无论是Glup或是webpack都具有将TS转成JS的能力

3、框架

webpack对Vue和React都具有良好的支持


2.3 为什么要使用构建工具? / 构建工具的功能有哪些?

  • 为什么要用构建工具
  1. 各种可以提高开发效率的新思想和框架被发明。但是这些东西都有一个共同点:源代码无法直接运行,必须通过转换后才可以正常运行。构建就是做这件事情,把源代码转换成发布到线上的可执行 JavaScrip、CSS、HTML 代码

  2. 构建其实是工程化、自动化思想在前端开发中的体现,把一系列流程用代码去实现,让代码自动化地执行这一系列复杂的流程,解放生产力

  • 构建工具的功能
  1. 代码转换:TypeScript 编译成 JavaScript、SCSS 编译成 CSS 等。
  2. 文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片等。
  3. 代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载。
  4. 模块合并:在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件。
  5. 自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器。
  6. 代码校验:在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过。
  7. 自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统。

2.4 webpack的构建流程是什么?

面试的时候选一个回答就可以了,面试建议选第二个

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  3. 确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

简单化流程

  1. Webpack 启动后会从 Entry 里配置的 Module 开始递归解析 Entry 依赖的所有 Module。
  2. 每找到一个 Module, 就会根据配置的 Loader 去找出对应的转换规则,对 Module 进行转换后,再解析出当前 Module 依赖的 Module
  3. 这些模块会以 Entry 为单位进行分组,一个 Entry 和其所有依赖的 Module 被分到一个组也就是一个 Chunk
  4. 最后 Webpack 会把所有 Chunk 转换成文件输出 (在整个流程中 Webpack 会在恰当的时机执行 Plugin 里定义的逻辑)

关于 webpack 处理模块的问题

3.1 loader 是什么?有哪些常见的 Loader?这些常见的 loader 是解决什么问题的?

  • loader是放置在module中的rules中的应用规则

module是决定如何处理模块的部分,而 rules是一个数组,里面放置配置模块的读取和解析规则,Loader就放置在数组的对象的一个属性use中。

先用一个生动的例子来说明loader的作用:

对一个单独的 module 对象定义了 rules 属性,里面包含两个必须属性:test 和 use。这告诉 webpack 编译器(compiler) 如下信息:

“嘿,webpack 编译器,当你碰到JS文件时,在你对它打包之前,先 use (使用) babel-loader 转换一下。”

const path = require('path');

module.exports = {
  output: {
    filename: 'my-first-webpack.bundle.js',
  },
  module: {
    rules: [
    {
      test: /\.js$/, // 命中 JavaScript 文件
      // 用 babel-loader 转换 JavaScript 文件
      // ?cacheDirectory 表示传给 babel-loader 的参数,用于缓存 babel 编译结果加快重新编译速度
      use: ['babel-loader?cacheDirectory']
    }],
  },
};

我们还可以放置多个load,对文件进行链式操作,一步一步地完成需要的功能。所以开发上需要严格遵循“单一职责”,每个 Loader 只负责自己需要负责的事情。

比如下面一个例子:use 数组中 loader 执行顺序:从右到左,从下到上 依次执行。对于 less 文件,先执行 less-loader,再执行 css-loader,最后执行 style-loader,顺序是不能出错的!

module: {
    rules: [
      // 不同文件必须配置不同loader处理
      {
        test: /\.css$/,
        use: [
          'style-loader', // 创建style标签,将js中的样式资源插入进行,添加到head中生效
          'css-loader' // 将css文件变成commonjs模块加载js中,里面内容是样式字符串
        ]
      },
      {
        test: /\.less$/,
        use: [
          'style-loader',
          'css-loader', // 将less文件编译成css文件
          'less-loader'
        ]
      }
    ]
  },
  • 常见的loader

先以比较简单的打包资源作为例子

  module: {
    rules: [
      {
        test: /\.less$/, // 处理less资源
        use: ['style-loader', 'css-loader', 'less-loader']
      },
      {
        // less和css是和js在一起的,不需要像图片一样单独设置outputPath
        test: /\.css$/, // 处理css资源
        use: ['style-loader', 'css-loader']
      },
      {
        loader: 'postcss-loader',
        options: {
          ident: 'postcss',
          plugins: () => [
            // postcss的插件
            require('postcss-preset-env')()
          ]
        }
      },
      {
        test: /\.(jpg|png|gif)$/, // 处理图片资源
        loader: 'url-loader',//url=loader是根据file-loader封装的库,其可以使用base64压缩
        options: {
          limit: 8 * 1024,
          name: '[hash:10].[ext]',
          esModule: false, // 关闭es6模块化
          outputPath: 'imgs'
        }
      },
      {
        exclude: /\.(html|js|css|less|jpg|png|gif)/,  // 处理其他资源
        loader: 'file-loader',
        options: {
          name: '[hash:10].[ext]',
          outputPath: 'media'
        }
      }
    ]
  },

根据上面代码中loader出现的顺序(再次注意,和执行顺序不一样,在use数组中,是从右到左执行的),有以下一些loader

  1. css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
  2. style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS
  3. less-loader:将 LESS 代码转换成CSS
  4. postcss-loader:扩展 CSS 语法,解决兼容性问题,使用下一代 CSS。可以配合 autoprefixer 插件自动补齐 CSS3 前缀
  5. url-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)。用户可以设置一个阈值,大于阈值会交给 file-loader 处理,小于阈值时返回文件 base64 形式编码 (处理图片和字体)
  6. file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)

在来一个在生产环境打包的简单例子(去除了上述重复部分)

 module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        enforce: 'pre', // 优先执行
        loader: 'eslint-loader',
        options: {
          fix: true // 自动修复eslint的错误
        }
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          presets: [
            [
              '@babel/preset-env', // 先做基本兼容性处理,如promise高级语法不能转换
              {
                useBuiltIns: 'usage', // 然后使用core-js根据浏览器版本进行按需加载
                corejs: {version: 3}, // 指定core-js版本
                targets: { // 指定兼容性做到哪个版本浏览器
                  chrome: '60',
                  firefox: '50'
                }}]]}
       }
      }
    ]
  },
  1. eslint-loader:进行语法检查。检查每个成员的代码风格,只检查项目成员的源代码,第三方的库是不用检查的。
  2. label-loader: js兼容性处理,比如把 ES6 转换成 ES5。

还有很多常用的loader,比如

  1. ts-loader: 和label-loader近似,将 TypeScript 转换成 JavaScript
  2. svg-inline-loader:和style-loader类似,将压缩后的 SVG 内容注入代码中
  3. sass-loader:和less-loader类似,将SCSS/SASS代码转换成CSS 如果不想仅限于面试的同学请移步 webpack中文文档-loader

3.2 Plugin 是什么?有哪些常见的 Plugin?这些常见的 Plugin 是解决什么问题的?

插件嘛!就是扩展功能用的啊!这也是为什么webpack既不臃肿又功能全面的原因。在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

Plugin 在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。

我们先来看一个例子

我们要解决一个问题:在上述的代码方案中,css和less是集成在js中的,但是先加载js在挂载css的话可能会出现闪屏现象,也会有兼容性问题。 我们要用把css文件单独抽取出来

const { resolve } = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
  entry: './src/js/index.js',
  output: {
    filename: 'js/built.js',
    path: resolve(__dirname, 'build')
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader, //代替style-loader
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      minify: { // 压缩html代码
        collapseWhitespace: true, // 移除空格
        removeComments: true // 移除注释
      }
    }),
    new MiniCssExtractPlugin({
      filename: 'css/built.css' // 对输出的css文件进行重命名
    }),
    new OptimizeCssAssetsWebpackPlugin() // 压缩css
  ],
  mode: 'development'
};

根据上面代码中Plugin出现的顺序,有以下一些Plugin

  1. mini-css-extract-plugin:分离样式文件,CSS 提取为独立文件,支持按需加载,还可以结合postcss进行兼容性处理(postcss配置项放在最后,也就是第一个执行)
  2. html-webpack-plugin:简化了 HTML 文件的创建,以便为你的 webpack 包提供服务。比如例子中,在该插件中就可以实现html的压缩后输出
  3. optimize-css-assets-webpack-plugin:压缩css代码

关于 webpack 优化的问题

4.1 在你开发的时候,是否使用过一些webpack性能优化或者开发优化的相关手段?

有啊。

开发时,可以使用热替换HMR 提升打包构建速度, 用source-map优化代码调试

到生产环境时,提升打包速度的话,使用babel缓存、oneOf正则匹配等

提升代码运行性能的话,使用文件资源缓存(hash-chunkhash-contenthash), tree shaking、code split、懒加载/预加载、pwa

(没有把握的别说,因为后面大概率要问其中一两个)

能不能再详细一点?

下面是我复制别人的文章片段 ...... 我是菜鸡,详细不了(手动狗头)

  • 使用高版本的 Webpack 和 Node.js

  • 多进程/多实例构建

    1. thread-loader
  • 多进程并行压缩

    1. webpack-paralle-uglify-plugin
    2. uglifyjs-webpack-plugin 开启 parallel 参数 (不支持ES6)
    3. terser-webpack-plugin 开启 parallel 参数
  • 图片压缩

    1. 使用基于 Node 库的 imagemin (很多定制选项、可以处理多种图片格式)
    2. 配置 image-webpack-loader
  • 缩小打包作用域

    1. exclude/include (确定 loader 规则范围)
    2. resolve.modules 指明第三方模块的绝对路径 (减少不必要的查找)
    3. resolve.mainFields 只采用 main 字段作为入口文件描述字段 (减少搜索步骤,需要考虑到所有运行时依赖的第三方模块的入口文件描述字段)
    4. resolve.extensions 尽可能减少后缀尝试的可能性
    5. noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)
    6. IgnorePlugin (完全排除模块)
  • 提取页面公共资源

    1. 使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中
    2. 使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件
  • DLL

    1. 使用 DllPlugin 进行分包,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间。
    2. HashedModuleIdsPlugin 可以解决模块数字id问题
  • 充分利用缓存提升二次构建速度

    1. babel-loader 开启缓存
    2. terser-webpack-plugin 开启缓存
    3. 使用 cache-loader 或者 hard-source-webpack-plugin
  • Tree shaking

    1. 打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率
    2. 禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking
    3. 使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码。purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议)
  • Scope hoisting

    1. 构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突
    2. 必须是ES6的语法,因为有很多第三方库仍采用 CommonJS 语法,为了充分发挥 Scope hoisting 的作用,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法
  • 动态Polyfill

    1. 建议采用 polyfill-service 只给用户返回需要的polyfill,社区维护。 (部分国内奇葩浏览器UA可能无法识别,但可以降级返回所需全部polyfill)


4.2 你刚才提到了SplitChunks,可以聊一下吗?

再借用一下大佬的话

代码分割的本质其实就是在源代码直接上线和打包成唯一脚本main.bundle.js这两种极端方案之间的一种更适合实际场景的中间状态。

用可接受的服务器性能压力增加来换取更好的用户体验

源代码直接上线:虽然过程可控,但是http请求多,性能开销大。 打包成唯一脚本:服务器压力小,但是页面空白期长,用户体验不好。

来看一个例子

module.exports = {
    // 多入口
    entry: {
        index: './src/js/index.js',
        test: './src/js/test.js'
    },
    output: {filename: 'js/[name].[contenthash:10].js', path: resolve(__dirname, 'build')},
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html',
            minify: {collapseWhitespace: true, removeComments: true}
        })
    ],
//可以将node_modules中代码单独打包一个chunk最终输出
//自动分析多入口chunk中,有没有公共的文件。如果有会打包成单独一个chunk,比如所有文件中都有jQuery
    */
    optimization: {
        splitChunks: {
            chunks: 'all'
        }
    },
    mode: 'production'
};

4.3 聊一聊热替换(hot module replacement)和它的原理吧?

如果只改动了一个模块,而导致了所有模块都要重新打包的话,会严重影响打包速度

我们可以在webpack.config.js中设置devServer中的hot属性开启HMR,一个模块发生变化,只会重新打包这一个模块(而不是打包所有模块),可以极大提升构建速度

module.export = {
  devServer: {
    contentBase: resolve(__dirname, 'build'),
    compress: true,
    port: 3000,
    open: true,
    hot: true // 开启HMR功能
  }
}
  • HMR的原理

image.png

  1. 在 webpack 监听(watch)到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码保存在内存中
  2. webpack-dev-sever 调用 webpack 暴露的 API 对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
  3. webpack-dev-server 监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新(为的是更新静态资源,比如jpg,如果只需要更新静态资源的话,刷新浏览器即可),和 HMR 是两个概念。
  4. webpack-dev-server 通过 sockjs 在浏览器端和服务端之间建立一个 websocket 长连接,服务端传递的最主要信息还是新模块的 hash 值,让客户端与上一次资源进行对比,后面的步骤根据这一 hash 值来进行模块热替换。当然,也传递第三步中 Server 监听静态文件变化的信息
  5. 客户端的 HotModuleReplacement.runtime 对比出差异后,会向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
  6. HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。HotModulePlugin 提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。

4.4 聊一聊 source-map吧?

做过线上发布的都知道,发布到生产环境的代码,一般都有如下步骤:

  1. 压缩混淆,减小体积
  2. 多个文件合并,减少HTTP请求数
  3. 通过编译或者转译,将其他语言编译成JavaScript 这三个步骤,都使得实际运行的代码不同于开发代码,不管是 debug 还是捕获线上的报错,都会变得困难重重。

sourceMap里保存的,是转换后代码的位置,和对应的转换前的位置。有了它,出错的时候,通过断点工具可以直接显示原始代码,而不是转换后的代码

而webpack中的source-map 是将编译、打包、压缩后的代码映射回源代码的过程。因为打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre-map(其实浏览器的开发者工具也可以定位bug)

要使用 source-map,可以在devtool进行设置

module.exports = {
  entry: ['./src/js/index.js', './src/index.html'],
  output: {...},
  module: {...},
  devtool: 'eval-source-map'
};

devtool的取值其实可以由 source-map、eval、inline、hidden、cheap、module 这六个关键字随意组合而成。 这六个关键字每个都代表一种特性,它们的含义分别是:

  • eval:用 eval 语句包裹需要安装的模块;
  • source-map:生成独立的 Source Map 文件;
  • hidden:不在 JavaScript 文件中指出 Source Map 文件所在,这样浏览器就不会自动加载 Source Map;
  • inline:把生成的 Source Map 转换成 base64 格式内嵌在 JavaScript 文件中;
  • cheap:生成的 Source Map 中不会包含列信息,这样计算量更小,输出的 Source Map 文件更小;同时 Loader 输出的 Source Map 不会被采用;
  • module:来自 Loader 的 Source Map 被简单处理成每行一个模块;

  • devtool建议参数

在开发环境下,建议把 devtool 设置成 cheap-module-eval-source-map,因为生成这种 Source Map 的速度最快,能加速构建。由于在开发环境下不会做代码压缩,Source Map 中即使没有列信息也不会影响断点调试;

在生产环境下,建议把 devtool 设置成 hidden-source-map,意思是生成最详细的 Source Map,但不会把 Source Map 暴露出去。由于在生产环境下会做代码压缩,一个 JavaScript 文件只有一行,所以需要列信息。


4.5 聊一聊文件指纹(Hash/Chunkhash/Contenthash)吧?


webpack内置的hash有三种

  • hash:每次构建会生成一个hash。和整个项目有关,只要有项目文件更改,就会改变hash
  • contenthash:和单个文件的内容相关。指定文件的内容发生改变,就会改变hash。
  • chunkhash:和webpack打包生成的chunk相关。每一个entry,都会有不同的hash。

这三种的应用场景

  • chunkhash用法:一般来说,针对于输出文件,我们使用chunkhash。因为webpack打包后,最终每个entry文件及其依赖会生成单独的一个js文件。此时使用chunkhash,能够保证整个打包内容的更新准确性。
// 设置 output 的 filename,用 chunkhash
module.exports = {
    entry: {app: './scr/app.js', search: './src/search.js'},
    output: {filename: '[name][chunkhash:8].js', path: __dirname + '/dist'}
}

  • contenthash用法:对于css文件来说,一般会使用MiniCssExtractPlugin将其抽取为一个单独的css文件。此时可以使用contenthash进行标记,确保css文件内容变化时,可以更新hash。
//设置 MiniCssExtractPlugin 的 filename,使用 contenthash。
module.exports = {
    entry: {app: './scr/app.js', search: './src/search.js'},
    output: {filename: '[name][chunkhash:8].js', path: __dirname + '/dist'},
    plugins: [new MiniCssExtractPlugin({filename: `[name][contenthash:8].css`})]
}
  • hash用法:一般来说,没有什么机会直接使用hash。因为hash会更据每次工程的内容进行计算,很容易造成不必要的hash变更,不利于版本管理。

file-loader的hash

可能有同学会表示有以下疑问。

明明经常看到在处理一些图片,字体的file-loader的打包时,使用的是[name]_[hash:8].[ext]

但是如果改了其他工程文件,比如index.js,生成的图片hash并没有变化。

这里需要注意的是,file-loader的hash字段,这个loader自己定义的占位符,和webpack的内置hash字段并不一致。

const path = require('path');
module.exports = {
    entry: './src/index.js',
    output: {filename: 'bundle.js', path: path.resolve(__dirname, 'dist')},
    module: {
        rules: [{
            test: /\.(png|svg|jpg|gif)$/,
            use: [{loader: 'file-loader', options: {name: 'img/[name][hash:8].[ext]'}}]
        }]
    }
}