跳到正文
前端知识库
工程化

Webpack 面试真题补充:构建链路、优化与选型

根据编号 9《Webpack 面试真题》补充 HMR、构建对象、资源处理、构建优化和 Webpack 生态选型中的场景追问。

约 7 分钟Webpack · HMR · Loader · Plugin · 构建优化 · 面试

Webpack 面试真题补充:构建链路、优化与选型

本文根据编号 9《Webpack 面试真题》整理。项目已有文章分别介绍了基础概念、Loader、Plugin 和构建流程,这里集中补充 PDF 中容易被追问的边界、配置取舍和排查方法。

1. 从模块化到依赖图

早期项目通过多个 script 标签或命名空间对象拆分代码,依赖顺序由 HTML 和约定维护。IIFE 可以隔离局部变量,但依赖仍需要手工安排。CommonJS 和 ES Modules 把依赖关系写进模块本身,Webpack 再从入口递归构建依赖图。

entry
  -> resolve(找到模块)
  -> loader(把源码转为可分析模块)
  -> dependency graph
  -> chunk
  -> asset

Webpack 的价值不只是把很多文件拼成一个文件,而是让模块、样式、图片和字体进入同一张图,从而可以做转译、按需加载、缓存和资源优化。回答“Webpack 解决了什么问题”时,应该同时提到模块边界、浏览器兼容和产物管理。

2. Compiler、Compilation 与构建阶段

这两个对象经常被混用,但生命周期不同:

对象 关注范围 生命周期
Compiler 整个 Webpack 实例和一次次构建的调度 通常随进程长期存在
Compilation 某一次构建的模块、依赖、Chunk 和资源 每次构建重新创建

一次构建可以按下面的主线回答:

  1. 初始化配置和命令行参数,创建 Compiler。
  2. 注册插件,插件通过 apply(compiler) 订阅 Tapable hooks。
  3. run 或 watch 触发 compile,创建本次构建的 Compilation。
  4. 从 entry 开始解析模块;每个模块按规则执行 Loader,并继续收集 import、require 和动态 import() 依赖。
  5. seal 阶段根据依赖关系组织 Chunk,执行优化和代码生成。
  6. emit 阶段把最终 assets 写入 output.path 或交给开发服务器提供。

make、buildModule、seal、emit 不是孤立的命令,而是插件可以观察或扩展的生命周期节点。面试中能说清“模块构建发生在 Compilation,文件输出发生在 emit”就比只背流程名更有说服力。

3. HMR 的更新链路与失败边界

HMR 不是简单地重新加载整个 bundle。开发服务器会维护编译结果和一个浏览器端 HMR Runtime:

文件变化
  -> Compiler 增量编译
  -> 生成新的 hash、manifest 和 hot-update chunk
  -> HMR Server 通过 WebSocket 推送 hash/状态
  -> Runtime 请求 manifest
  -> Runtime 下载变更的 chunk
  -> 从接受边界开始替换模块

module.hot.accept('./util.js', callback) 表示当前模块愿意处理 util.js 的更新。若依赖链上没有可接受的边界,或者更新会影响应用入口,Runtime 会退化为整页刷新。常见失败原因包括:

  • 代码没有建立可接受的 HMR 边界;
  • WebSocket 被反向代理、容器网络或 HTTPS 配置阻断;
  • Loader 或框架运行时没有正确处理更新后的模块;
  • 更新涉及不可安全替换的全局副作用,例如重新注册全局监听器。

排查时先看浏览器与 dev server 的 WebSocket 状态,再确认 hash、*.hot-update.json 和 *.hot-update.js 是否返回,最后检查模块是否到达 accept 回调。不要只盯着“页面有没有刷新”判断 HMR 是否工作。

4. DevServer Proxy 的边界

开发代理的典型链路是:

浏览器 -> http://localhost:3000/api/users
       -> dev server 代理进程
       -> https://api.example.com/users
       <- 代理把响应转回 localhost

浏览器只访问 localhost:3000,因此浏览器的同源限制不会阻止这次请求;跨域请求发生在服务器进程之间,不是浏览器脚本直接访问目标域名。代理并没有改变生产环境的 CORS 策略,也不能替代服务端鉴权。

module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        pathRewrite: { '^/api': '' },
      },
    },
  },
}

changeOrigin 会调整转发请求的 Host 等信息,某些虚拟主机服务需要它;pathRewrite 只改变目标 URL 路径。Cookie、CSRF、WebSocket 和 HTTPS 证书仍要按实际部署链路配置,不能因为本地代理成功就认为线上跨域问题已经解决。

5. Loader 规则的三个易错点

5.1 执行顺序

同一条 use 数组通常从右到左执行。下面的链路先由 sass-loader 生成 CSS,再由 css-loader 解析 @import 和 url(),最后由 style-loader 把样式注入页面:

{
  test: /\.s[ac]ss$/,
  use: ['style-loader', 'css-loader', 'sass-loader'],
}

enforce: 'pre' 和 enforce: 'post' 可以把某些 Loader 放到普通规则之前或之后,但不应靠堆叠 enforce 修复一份无法解释的配置。大型项目可用 oneOf、include 和 exclude 让每个资源只命中必要的规则。

5.2 老 Loader 与 Webpack 5 资源模块

PDF 中使用了 file-loader、url-loader 和 raw-loader。在 Webpack 5 中,图片、字体和文本通常可以使用内置 Asset Modules:

module.exports = {
  module: {
    rules: [
      { test: /\.png$/, type: 'asset/resource' },
      { test: /\.svg$/, type: 'asset/inline' },
      { test: /\.txt$/, type: 'asset/source' },
    ],
  },
}

小文件是否内联应以体积预算和缓存策略为依据,不要把所有图片都转成 Base64。资源规则还要明确输出路径和 publicPath,否则开发环境能显示、生产环境却可能出现 404。

5.3 Loader 只负责转换

Loader 接收资源内容并返回转换结果,复杂的全局优化、产物生成和生命周期工作应交给 Plugin。异步 Loader 必须通过 this.async() 获取回调,并保证只回调一次;处理二进制内容时要设置合适的 raw 选项。

6. Plugin、资源输出与构建观测

Plugin 通过 apply 订阅 Compiler 或 Compilation 的 hooks,可以生成新文件、修改 assets 或记录构建指标。常见职责可以这样分:

需求 常用方案 关键边界
生成 HTML HtmlWebpackPlugin 模板、注入 Chunk、压缩策略
清理旧产物 CleanWebpackPlugin 或 output.clean 不要误删外部目录
抽离 CSS MiniCssExtractPlugin 开发与生产的加载方式不同
注入环境常量 DefinePlugin 注入的是编译期字面量,不是运行时密钥
复制静态文件 CopyWebpackPlugin 明确 from、to 和忽略规则
体积分析 Bundle Analyzer、size plugin 结果要接入 CI 预算

如果需要写一个 Plugin,先确定“哪个阶段能观察到所需数据”,再选择 hook。emit 时可以读取最终 assets,但如果需要参与模块分析,应更早订阅 compilation 相关 hook。不要在插件之间共享可变的全局 compiler 或 compilation 引用。

7. 构建速度要先测量再优化

构建慢的回答不应只是罗列插件。建议按“定位、缩小范围、缓存、并行”四步展开:

  1. 用 webpack --profile --json、speed-measure-webpack-plugin 或构建日志定位最慢的 Loader、Plugin 和模块。
  2. 给 Babel、TypeScript、图片处理等规则设置准确的 include,排除 node_modules 和不需要转换的资源。
  3. Webpack 5 优先使用 cache: { type: 'filesystem' };cache-loader、hard-source-webpack-plugin 属于旧版本方案,升级时不要无条件叠加。
  4. 只有单个任务足够重时才启用 thread-loader 或并行压缩;进程启动和序列化成本可能让小项目变慢。
  5. resolve.extensions、resolve.modules 和别名应保持精简,避免无意义的文件系统查找。

DLL 预编译可以减少稳定第三方依赖的重复构建,但配置、版本同步和缓存失效成本较高。Webpack 5 持久化缓存、合理的拆包和 CI 构建缓存通常更易维护。

8. 产物体积、长缓存与 Tree Shaking

8.1 Hash 的选择

  • hash 反映整个构建,任意文件变化都可能让所有文件失效。
  • chunkhash 反映 Chunk,适合按 Chunk 缓存,但同一 Chunk 中 CSS 和 JS 变化会互相影响。
  • contenthash 反映单个资源内容,适合长期缓存的 JS/CSS 文件。
output: {
  filename: '[name].[contenthash:8].js',
  chunkFilename: '[name].[contenthash:8].chunk.js',
}

8.2 Tree Shaking 的前提

Tree Shaking 依赖 ESM 的静态 import/export、生产优化阶段以及正确的 sideEffects 声明。CommonJS 的动态导出难以在编译期判断,效果通常较弱。声明 sideEffects: false 前必须确认模块没有注册全局样式、Polyfill 或其他导入即执行的副作用,否则可能把必要代码删掉。

8.3 SplitChunks 的取舍

入口拆分、动态 import() 和 SplitChunksPlugin 是三种常见代码分割方式。chunks: 'all' 可以同时处理同步和异步 Chunk,但拆得越细不一定越快:请求数量、运行时开销和缓存命中都要一起考虑。首屏必要的小型 runtime 可以内联,但应评估 CSP、缓存和调试成本。

9. Source Map 与生产安全

Source Map 让压缩后的代码映射回源码,但公开部署完整源码映射可能泄露业务实现。开发环境可以优先调试速度,生产环境按错误监控需求选择 hidden-source-map、nosources-source-map 或受限访问的 source-map。应确保:

  • .map 文件不被公共静态目录无条件暴露;
  • 错误监控服务能安全接收并保存映射;
  • 不把 Token、密钥或构建机路径写入源码映射;
  • 生产产物和映射文件使用同一版本标识,避免错配。

10. Webpack、Rollup、Parcel、Snowpack 与 Vite 怎么选

工具 主要特点 更适合
Webpack 依赖图和 Loader/Plugin 生态成熟,定制能力强 复杂应用、历史项目、多资源处理
Rollup ESM 优先、产物简洁、Tree Shaking 出色 类库和小型模块包
Parcel 零配置起步 约束少的简单应用或原型
Snowpack 早期以 Unbundled 开发体验见长 历史项目,现多被 Vite 取代
Vite 开发时原生 ESM,生产通常交给 Rollup 现代应用、快速启动和 HMR

选型应结合已有生态、浏览器兼容、构建插件、团队维护成本和产物指标,而不是只比较一次冷启动时间。Webpack 仍适合需要深度控制 Loader、Plugin、模块联邦或复杂历史配置的项目。

Q&A 索引

本页的短答已合并到同目录的 99-高频追问Q&A.md;本页保留 PDF 中需要展开说明的原理、代码和场景,避免同一答案在两处维护。