Webpack

返回 相关私有笔记 · 相关私有笔记

构建流程

  1. 初始化参数:从配置文件和 shell 语句读取参数并合并,获得最终的参数
  2. 开始编译:用初始化参数初始化 compiler 对象,加载所有配置的 plugin,执行 compiler 对象的 run 方法开始编译流程
  3. 确定入口:根据 entry 找出入口文件
  4. 编译模块:从入口文件开始,根据配置的 loader 对模块进行转译,如果该模块还有依赖,则递归进行转译,直到所有模块都转译完成
  5. 完成模块编译:经过 loader 转译完所有模块后,获得每个模块之间的依赖关系图 ModuleGraph
  6. 输出资源:根据入口和模块之间的依赖关系,生成一个个包含多个模块的 chunk,再把每个 chunk 转换成一个单独的文件加入到输出列表中
  7. 输出完成:根据输出项的配置,将文件内容写到系统中

上述流程简化之后,可以大致分为以下几个阶段:

  1. 初始化阶段:合并计算配置参数,创建 CompilerCompilation 等基础对象,并初始化 Plugin,并最终根据 entry 配置,找到所有入口模块
  2. 构建模块:从 entry 开始,调用 loader 转译对应的模块,调用 Acorn 将代码转换为 AST 结构,遍历 AST 从中构建出完整的模块依赖关系图(递归操作)
  3. 生成阶段:根据 entry 配置,根据模块生成一个个 chunk 对象,之后转译 chunk 代码并封装为 asset,最后写出到文件系统

如果启用了 watch,则在单次构建完成之后不会退出 webpack 进程,而是持续监听文件变化,发生变化时回到构建模块阶段重新执行构建

从资源转换角度看构建流程

  • compiler.make 阶段

    • entry 文件以 dependence 对象形式加入 compilation 的依赖列表 ,dependence 对象记录了 entry 的相关信息
    • 根据 dependency 创建 对应的 module 对象,之后读入 module 对应的文件内容, 调用 loader-runner 对内容做转化, 转化结果若有对其他依赖则继续读入依赖资源, 重复此过程直到所有的依赖均被转换为 module
  • compilation.seal 阶段

    • 遍历 module 集合, 根据 entry 配置以及引入资源的方式, 将 module 分配到不同的 Chunk
    • Chunk 之间最终形成 ChunkGraph 结构
    • 遍历 ChunkGraph 调用 compilation.emitAssets 方法标记 chunk 的输出规则, 及转换为 assets 集合
  • compiler.emitAssets 阶段

    • assets 写入文件系统

分包策略

在 webpack 打包过程中,经常出现 vendor.jsapp.js 单个文件较大的情况,这偏偏又是网页最先加载的文件,这就会使得加载时间过长,从而使得白屏时间过长,影响用户体验。所以我们需要有合理的分包策略。

webpack 中常见的三种代码分割方式:

  1. 使用 entry 配置手动分离代码
  2. 在模块中使用 import('./xxx') 来加载模块代码
  3. 使用 splitChunks 来去重和分离

文件监听

开启文件监听后,webpack 会轮询访问文件的最后修改时间。当发现文件的最后修改时间变化后,会先缓存变化后的文件,等到 aggregateTimeout 再统一执行

开启文件监听方式:可以在构建时带上 --watch 参数或者设置 watch:true,而 watchOptions 则可以对监听的细节进行定制

watch: true,
watchOptions: {
    //不监听的文件或者文件夹忽略一些大型的不经常变化的文件可以提高构建速度
    ignored: /node_modules/,
    //监听到变化会等多少时间再执行
    aggregateTimeout: 300,
    //判断文件是否发生变化是通过不断轮询指定文件有没有变化实现的
    poll: 1000
}

热更新 (HMR)

如何开启热更新?⬇️
通过设置 devServer: {hot: true} 开启,开启后便可以在发生改变后局部刷新改变的部分

原理:

  1. 使用 webpack-dev-server(WDS) 托管静态资源,同时以 runtime 的形式注入 HMR 客户端代码
  2. 浏览器加载后,与 WDS 建立 `websocket` 连接
  3. webpack 监听到文件变化之后,增量构建发生变更的模块,并通过 websocket 发送 hash 事件
  4. 浏览器接受到 hash 事件之后,请求 manifest 资源文件,确认增量变更范围
  5. 浏览器加载发生变更的增量模块
  6. webpack runtime 触发变更模块的 module.hot.accept 回调,执行变更逻辑

常见问题

loader 和 plugin 的区别?

loader

loader 负责将文件内容转化为 webpack 可以处理的 js 代码

plugin

plugin 基于 tapable 事件流框架来监听 webpack 在构建 / 打包过程中的 hooks,通过自定义的逻辑和功能来改变输出结果

如何保证 loader 按照想要的顺序执行?

webpack 会按照 use 定义的顺序从前往后执行 Pitch Loader 从后往前执行 Normal Loader 我们可以将一些预处理的逻辑放在 Pitch

可以通过 enforce 来强制控制 Loader 的执行顺序 (pre 表示在所有正常的 loader 执行之前执行,post 则表示在之后执行)

loader 的执行有两个阶段:

  1. pitching 阶段:loader 上的 pitch 方法,按照 后置(post)行内(inline)普通(normal)前置(pre) 的顺序调用。更多详细信息,请查看 Pitching Loader
  2. normal 阶段:loader 上的常规方法,按照 前置(pre)、普通(normal)、行内(inline)、后置(post) 的顺序调用。模块源码的转换,发生在这个阶段

如何编写 loader?

即使没有实际地编写过一个 webpack loader,但是当面试官问你这个问题的时候,实际上是要考察你对 loader 这方面知识的掌握程度,所以,你可以讲出一个 loader 的编写要注意的点应该是这样的:

  • loader 的主要职责就是将文件内容转换为 webpack 可以理解的 js 代码;并且 loader 支持链式调用,所以上一个 loader 的执行结果会作为下一个 loader 的入参
    • 要在 loader 内部通过 return / this.callback 返回转换后的结果,并且这个返回值必须是标准的 js 字符串或者 AST
  • loader 应该尽量保持单一功能原则
  • webpack 默认缓存 loader 的执行结果,可以通过 this.cacheable 来控制
  • loader 接收三个参数
    • source:对于第一个执行的 loader,这个为文件的内容,后续执行的 loader 则为前一个 loader 的执行结果
    • sourceMap (optional):
    • data (optional):其他需要在 loader 链上传递的数据
  • loader 异常信息处理
    • 一般尽量使用 this.emitError
    • 明确需要警示用户的错误,优先使用 this.emitError
    • 对于严重的编译错误,使用 callback
  • loader 的 this 指向的是 loader-runnerloaderContext
  • loader 分为 pitch 和 normal 两个阶段

如何编写 plugin?

核心的点有两个:

  1. compiler:全局的单例对象,代表了 webpack 从开启到关闭的整个生命周期,负责启动编译和监听文件
  2. compilation每次构建的上下文对象(也就意味着每次热更新和重新编译都会重新成生一个),包含当次构建所需要的信息

那编写 plugin 时的思路和要注意的点如下:

  • plugin 一定是一个函数或者包含 apply() 的对象,这样才可以监听 compiler 对象
  • 传递给插件的 compilercompilation 都是同一个引用
  • 基于 tapable 来完成对 hooks 的复杂的订阅以及响应
  • 监听一些具有特定意义的 hook 来影响构建
    • compiler.hooks.compilationwebpack 刚启动完并创建 `compilation` 对象后触发
    • compiler.hooks.makewebpack 开始构建时触发
    • compiler.hooks.donewebpack 完成编译时触发,此时可以通过 stats 对象得知编译过程中的各种信息
  • 使用 schema-utils 等开发工具
  • 正确处理插件日志信息以及插件信息
    • 使用 stats 汇总插件的统计数据
    • 使用 ProgressPlugin 插件的 reportProgress 接口上报执行进度
    • 通过 compilation.getLogger 获取分级日志管理器
    • 使用 compilation.errors / warning 处理异常信息(eslint-webpack-plugin 的做法)
  • 测试插件
    • 通过分析 compilation.error/warn 数组来判断 webpack 是否运行成功
    • 分析构建产物判断插件功能是否符合预期

什么是文件指纹?

文件指纹是指文件打包后的一连串后缀,通常有两个作用:

版本管理: 在发布版本时,通过文件指纹来区分修改的文件和未修改的文件。
使用缓存: 浏览器通过文件指纹是否改变来决定使用缓存文件还是请求新文件。

种类:

  • Hash:和整个项目的构建相关,只要项目有修改(compilation 实例改变),Hash 就会更新
  • Contenthash:和文件的内容有关,只有内容发生改变时才会修改
  • Chunkhash:和 webpack 构建的 chunk 有关,不同的 entry 会构建出不同的chunk (不同 Chunkhash 之间的变化互不影响)

如何使用:

  • JS 文件:使用 Chunkhash
  • CSS 文件:使用 Contenthash
  • 图片等静态资源: 使用 hash
module.exports = {
    entry: {
        app: "./scr/app.js",
        search: "./src/search.js",
    },
    output: {
        filename: "[name][chunkhash:8].js",
        path: __dirname + "/dist",
    },
    // 通过 MiniCssExtractPlugin 插件设置 css 文件指纹
    plugins: [
        new MiniCssExtractPlugin({
            filename: `[name][contenthash:8].css`,
        }),
    ],
    module: {
        // 通过 rules 配置图片的文件指纹设置
        // 设置file-loader的name,使用hash。
        rules: [
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [
                    {
                        loader: "file-loader",
                        options: {
                            name: "img/[name][hash:8].[ext]",
                        },
                    },
                ],
            },
        ],
    },
};

什么是 source map?如何使用?

source map 是将编译打包后的代码映射回源码

可以通过 devtool 配置项来设置,或者通过 SourceMapDevToolPlugin 实现更加精细粒度的控制

  • 开发环境:cheap-eval-source-map,这种 source map 速度最快,并且由于开发环境下没有代码压缩,所以不会影响断点调试
  • 生产环境:hidden-source-map,由于进行了代码压缩,所以并不会占用多大的体积

Tree shaking

webpack 使用 Tree shaking 的三个必要条件

  • 使用 ESM 规范编写模块代码
  • 配置 optimization.usedExportstrue 启动标记功能
  • 启动代码优化功能 可以通过如下方法实现
    • 配置 mode = production
    • 配置 optimization.minimize = true
    • 提供 optimization.minimizer 数组

对于使用了 babel-loader 或者根据 loader 对代码进行转译的时候,注意应该关闭对于导入/导出语句的转译,因为这会影响到后续的 tree shaking

比如应该将 babel-loaderbabel-preset-envmodules 配置为 false

如何利用 Webpack 来优化项目性能

动态导入

reference:webpack 是如何实现动态导入的