Agent Developer Roadmap
构建工具

webpack:从 entry 走到浏览器实际加载的 chunks

核验日期:2026-07-07。示例版本来自 examples/build-tools/package.jsonwebpack 5.108.4、webpack-cli 7.2.1、html-webpack-plugin 5.6.7。

需要掌握的问题

主题读者必须回答的问题原理与边界高频错误代码或观察证据来源
entry → resolve → module ruleswebpack 从哪里开始,怎样把 import 解析成模块?entry 决定起点,resolve 决定请求到文件或包导出的映射,module.rules 决定资源进入哪些 Loader 或内置模块类型默认 ./src/index.js 不存在、alias/exports 条件错、rules 匹配过宽示例 entry: './src/main.ts'、resolve/rules 局部配置webpack concepts
LoaderLoader 为什么只处理单模块内容,pitch/normal 顺序怎样影响结果?Loader 是接收单个 resource 内容并返回 JS/Buffer/map/meta 的函数;pitch 从左到右,normal 从右到左在 Loader 里做全局分包、异步 callback 漏调用、缓存依赖漏登记pitch/normal 最小 Loader、this.addDependencywebpack loadersLoader Interface
Plugin、Compiler、Compilation、TapablePlugin 如何介入完整生命周期?Plugin 通过 apply(compiler) 注册 Tapable hooks;Compiler 管一次构建配置和运行,Compilation 管一次构建的模块、chunk、assets在 Loader 做全局工作、在异步 hook 里既 return Promise 又 callback、用已废弃 asset hookcompiler.hooks.thisCompilationcompilation.hooks.processAssetswebpack pluginsCompiler HooksCompilation Hooks
module graph → chunk graph → runtime静态 import 和动态 import 如何变成主 JS 与 async chunk?module graph 表示模块依赖,chunk graph 表示哪些模块进入哪些 chunk,runtime 负责加载异步 chunk 和 publicPath以为每个动态 import 都一定独立 chunk、HTML 直接加载 async chunk、runtime 被 CDN 缓存错stats.json 证明 detail.ts 在非 initial chunkwebpack concepts
splitChunks什么时候拆共享 chunk,拆错会怎样?optimization.splitChunks 基于缓存组、大小、复用和入口关系抽取共享依赖小项目过度拆包、旧 HTML 漏新 runtime/vendor、缓存命中但加载 404contenthash、runtimeChunk、cacheGroups 对照webpack caching guide
Tree Shaking / sideEffects未使用导出和副作用如何被判断?依赖 ESM、usedExports、minifier 和正确 sideEffects 声明;CSS/注册代码属于副作用sideEffects: false 删除 CSS 或全局注册、只在 development 测试SHOULD_NOT_REACH_BUNDLE 不应出现在产物中webpack tree shaking
Source Map 与 stats如何证明产物可追溯,而不是只看命令退出 0?devtool 控制 Source Map;stats 可输出 assets、chunks、modules、reasons 等结构化构建证据生产暴露源码、map 链断裂、stats 太粗看不到 async chunkdevtool: 'source-map'stats.chunkModulesdevtoolstats
filesystem cache、watch、HMR冷构建、增量、热更新分别依赖什么状态?filesystem cache 持久化编译结果;watch 根据依赖失效;HMR 通过 dev server 和 runtime 接受模块更新缓存键漏环境变量、Loader 未登记外部依赖、HMR accept/dispose 不完整cache 配置、this.addDependency、HMR gotchacacheHMR guide
publicPath、hash、CDN浏览器加载失败时怎样定位是路径、hash 还是缓存问题?emitted assets 通过 output.publicPath 被引用;contenthash 绑定内容;HTML 是入口清单CDN 仍缓存旧 HTML 指向已删除 hash、publicPath 指向错误 originNetwork 404、HTML script/link、stats assetspublicPath
迁移与重大变化webpack 4 到 5、5.108 哪些变化会改变工程决策?webpack 5 引入持久化缓存、资产模块、改进 tree shaking、移除自动 Node polyfill;5.108 继续推进 experimental HTML/CSS/TypeScript 能力把实验能力当默认稳定能力、升级后 Node 内置模块缺失迁移表、实验标注、配置 diffwebpack 5 releasewebpack 5.108

内容边界说明:

分类本文落点
编程语言与类型系统解释 JS/TS 模块如何进入 graph;示例使用 webpack 5 experimental TypeScript,只做构建解析,不等于完整类型检查
Browser APIs 与持久化能力覆盖浏览器 script/link、动态 import、Source Map、HTTP/CDN 缓存、HMR WebSocket;不讨论 IndexedDB 或权限 API
UI 框架与元框架只说明 webpack 作为底层打包器供框架工具使用;组件调度、渲染和水合不在本文重复
构建工具与运行时完整覆盖输入、解析、转换、依赖图、分块、优化、缓存、插件、产物和故障定位
后端、数据与 Agent 技术只覆盖 Loader/Plugin/config 以开发者权限执行和 CI 信任边界;服务端请求生命周期、数据库一致性和 Agent 评测不适用

entry、resolve、module rules:先把请求变成模块

webpack 的入口问题是:从哪个模块开始构建 dependency graph。默认入口是 ./src/index.js,真实项目通常显式写入口,避免目录结构变化时猜错。

共享示例的入口:

// examples/build-tools/configs/webpack.config.mjs
export default {
  context,
  mode: 'production',
  target: ['web', 'es2022'],
  entry: './src/main.ts',
};

main.ts 里有三类依赖:

import { label, tools } from './catalog.ts';
import './side-effect.ts';
import './styles.css';

button.addEventListener('click', async () => {
  const { renderDetail } = await import('./detail.ts');
  renderDetail(detail, tool);
});

这会形成三种边:

进入 graph 的方式产物影响
静态 JS/TS import编译入口同步依赖进入 initial chunk
CSS 副作用 importexperiments.css 或 Loader 处理生成 CSS asset 或运行时样式
动态 import创建异步依赖块进入非 initial async chunk

resolve 负责把请求映射成文件。最小配置通常长这样:

export default {
  resolve: {
    extensions: ['.ts', '.js'],
    conditionNames: ['browser', 'import', 'default'],
    alias: {
      '@': new URL('../src/', import.meta.url).pathname,
    },
  },
};

module.rules 负责选择 Loader 或内置资源类型:

export default {
  module: {
    rules: [
      { test: /\.svg$/i, type: 'asset/resource' },
      { test: /\.css$/i, use: ['style-loader', 'css-loader'] },
    ],
  },
};

本仓库示例没有写传统 ts-loader 和 CSS Loader,而是启用 webpack 5 的实验能力:

experiments: {
  css: true,
  typescript: true,
}

这不是“webpack 默认稳定支持所有 HTML/TypeScript/CSS 应用构建”的声明。webpack 5.108 官方把 HTML、CSS、TypeScript 方向明确放在 opt-in experiment 叙事里;生产项目迁移时要单独跑回归。

严重问题完整流程:

症状根因观察命令修正
Module not found: Can't resolve './src'未配置 entry,默认入口不存在pnpm --filter @roadmap/build-tools-slice build:webpack 看首个 resolve 错误显式写 entry,不要依赖默认目录
同一个包打进两份resolve 条件、alias 或 symlink 策略不一致stats 中搜索重复 package 路径统一 resolve.conditionNamesalias、workspace 依赖入口
CSS 不出现没有 Loader,也没启用 experiments.css搜索 stats assets 是否有 .css使用稳定 Loader/插件,或明确启用实验 CSS 并验收

Loader:pitch 从左到右,normal 从右到左

Loader 的职责很窄:处理一个模块的 resource 内容。它不是全局优化器,也不应该决定 chunk 结构。

一个同步 Loader:

export default function bannerLoader(source, inputMap) {
  this.cacheable(true);
  this.addDependency(this.resourcePath);
  return `/* built by bannerLoader */\n${source}`;
}

一个异步 Loader:

export default function asyncLoader(source) {
  const callback = this.async();
  someAsyncRead(this.resourcePath, (error, extra) => {
    if (error) return callback(error);
    callback(null, `${extra}\n${source}`);
  });
}

一个 pitch Loader 可以短路后续 Loader:

export function pitch(remainingRequest) {
  if (this.resourceQuery.includes('raw')) {
    return `export default ${JSON.stringify(`-!${remainingRequest}`)};`;
  }
}

Loader 难点完整流程:

症状根因观察命令修正
构建卡住不结束调了 this.async() 但没有调用 callback给 Loader 加超时日志或最小 fixture每条分支都 callback;不要同时 return Promise 和 callback
watch 改外部文件不重编译Loader 读取了额外文件但没 this.addDependency改被读取文件,看 watch 是否触发对文件用 addDependency,对目录用 addContextDependency
filesystem cache 命中旧结果Loader 结果依赖 process.env 或网络,但没进入缓存键改 env 后比较输出 hash把环境写入 options/version,或 cacheable(false)
Loader 里想改多个 chunkLoader 只处理当前模块内容搜索 Loader 是否访问 compilation.chunks改成 Plugin,在 Compilation hook 里做全局工作

Plugin、Compiler、Compilation、Tapable 生命周期

Plugin 解决的问题是:当你需要跨模块、跨 chunk 或改 assets 时,单模块 Loader 不够用。

class AssetListPlugin {
  apply(compiler) {
    compiler.hooks.thisCompilation.tap('AssetListPlugin', (compilation) => {
      compilation.hooks.processAssets.tap(
        {
          name: 'AssetListPlugin',
          stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT,
        },
        (assets) => {
          const body = Object.keys(assets).sort().join('\n');
          compilation.emitAsset(
            'asset-list.txt',
            new compiler.webpack.sources.RawSource(body),
          );
        },
      );
    });
  }
}

生命周期分工:

对象生命周期能看见什么典型用途
Compiler一份 webpack 配置的一次运行或 watch 容器options、resolver、文件系统、hooks、watch 状态初始化插件、注册全局 hooks、创建 Compilation
Compilation一次具体构建modules、chunks、assets、errors、warnings、module graph、chunk graph分析模块、改 assets、生成报告
Tapable hooks同步、异步、瀑布、bail 等 hook 协议hook 参数决定上下文控制插件执行时机和异步完成语义
Plugin用户传入 plugins 数组的对象apply(compiler) 入口介入 webpack 完整生命周期

异步 hook 严重问题:

症状根因观察命令修正
产物偶发缺文件tapAsync 中提前 callback,异步写入还没完成processAssets 打印开始/结束时间使用 tapPromiseawait,或最后再 callback
webpack 5 报废弃 hook仍使用 optimizeAssetsadditionalAssets 等旧资产 hook开启 warnings,读 compilation warnings改用 compilation.hooks.processAssets 和 stage
persistent cache 报序列化问题Plugin 把不可序列化对象塞进模块/asset metadata看 cache warning 和 plugin 最近改动只存可序列化数据,或用官方 compiler.webpack.sources 类型

module graph、chunk graph 与 runtime:动态导入不是 HTML 标签

webpack 先建立 module graph:哪些模块依赖哪些模块。随后建立 chunk graph:哪些模块放进哪些 chunk。最后 runtime 负责在浏览器里加载异步 chunk。

关键边界:

机制解决的问题不保证
module graph找出所有可达模块和依赖关系每个模块都一定进入同一个文件
chunk graph把模块分配到 initial、async、shared chunks每个 dynamic import 都独占一个文件
runtime在浏览器执行模块包装、加载 async chunk、维护模块缓存CDN 一定有文件、publicPath 一定正确
HTML 插件或 HTML experiment把 initial assets 注入 HTML不应该直接 script 加载 async chunk

共享示例要求 detail.ts 是非 initial chunk。观察命令:

pnpm --filter @roadmap/build-tools-slice build:webpack
node -e "const s=require('./examples/build-tools/dist/webpack/stats.json'); console.log(s.chunks.map(c => ({id:c.id, initial:c.initial, files:c.files, modules:(c.modules||[]).map(m=>m.name||m.nameIdentifier)})))"

预期结果:至少一个 initial: false 的 chunk,其 modules 或嵌套 modules 能追溯到 src/detail.tsindex.html 只加载 initial JS/CSS,不直接加载该 async chunk。

splitChunks、Tree Shaking 与 sideEffects:优化会改变语义边界

splitChunks 解决“共享依赖不要重复下载”的问题。Tree Shaking 解决“可证明没用的导出不要进入最终产物”的问题。sideEffects 解决“哪些模块即使导出没用也必须执行”的问题。

export default {
  optimization: {
    usedExports: true,
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
        },
      },
    },
  },
};

取舍表:

方案性能可靠性使用难度维护成本适用规模
不配 splitChunks小项目请求少、配置少简单可靠示例、很小应用
默认 splitChunks避免明显重复webpack 默认经验值较稳普通应用
自定义 vendor/runtime长期缓存更稳定配错会增加请求和旧 HTML 风险中高中高多页面、大型应用
激进 sideEffects false体积可能更小容易误删 CSS/注册代码仅适合可证明纯 ESM 包

严重问题完整流程:

症状根因观察命令修正
CSS 或全局注册消失包级 sideEffects: false 误删副作用模块搜索产物是否有 CSS asset 和 preserved 文本用数组保留 ["*.css", "./src/register.js"]
SHOULD_NOT_REACH_BUNDLE 仍在产物中ESM 静态分析失败或 minify 未执行rg "SHOULD_NOT_REACH_BUNDLE" dist/webpack保留 ESM,开启 production/minimize,检查 CommonJS 包
vendor hash 每次都变runtime 或 module id 不稳定连续两次构建比较 asset 名使用 deterministic ids、抽 runtime、避免构建时间写入 bundle
初始请求过多splitChunks 过度拆分DevTools Network 看 initial JS 数量和 waterfall放宽 cacheGroups,按真实复用和 HTTP/2/3 条件调整

Source Map 与 stats:用证据证明产物结构

devtool 控制 Source Map。示例使用:

export default {
  devtool: 'source-map',
  stats: {
    all: false,
    assets: true,
    chunks: true,
    chunkModules: true,
    modules: true,
  },
};

Source Map 取舍:

devtool 思路适用风险
source-map生产需要外部 .map 调试公开 .map 会暴露源码
hidden-source-map上传错误平台,不在 JS 里暴露 URL发布系统要保证 map 与 hash 对齐
eval-*开发快速 rebuild不适合生产,安全策略和可读性差
false极简或高度保密发布事故定位困难

stats 不是装饰品,它是构建证据。共享测试会用 stats.json 验证:

const stats = JSON.parse(await readFile('examples/build-tools/dist/webpack/stats.json', 'utf8'));
const asyncDetail = stats.chunks.find((chunk) =>
  chunk.initial === false &&
  chunk.modules?.some((module) =>
    /(?:^|[\\/])detail\.ts/.test(module.nameIdentifier ?? module.name ?? ''),
  ),
);

错误完整流程:

症状根因观察命令修正
浏览器错误只指向压缩 JS没生成外部 Source Map 或没部署检查 JS 尾部 sourceMappingURL.map 文件设置 devtool: 'source-map' 或错误平台上传 hidden map
stats 看不到 chunk modulesstats 配置太少jq '.chunks[0]' stats.json打开 chunkschunkModules
map sources 泄露绝对路径loader 或 devtool 输出了本机路径`rg "$PWDfile:" dist/webpack`

filesystem cache、watch 与 HMR:三种“快”的边界不同

webpack 的快有三种:

能力作用状态来源常见误判
filesystem cache跨进程复用编译结果源码、配置、依赖、版本、cache buildDependencies命中不代表结果一定符合新环境
watch文件变化后局部重建文件/目录依赖图Loader 漏 addDependency 会不触发
HMR浏览器运行时替换模块dev server、HMR runtime、模块 accept/disposeHMR 成功不代表生产 bundle 正确

最小 filesystem cache:

export default {
  cache: {
    type: 'filesystem',
    version: process.env.BUILD_TARGET ?? 'local',
    buildDependencies: {
      config: [new URL('./webpack.config.mjs', import.meta.url).pathname],
    },
  },
};

HMR 最小边界:

if (import.meta.webpackHot) {
  import.meta.webpackHot.accept('./detail.ts', async () => {
    const next = await import('./detail.ts');
    console.log('detail module updated', next);
  });
}

严重问题完整流程:

症状根因观察命令修正
切换 BUILD_TARGET 后仍用旧产物filesystem cache version 或 buildDependencies 漏环境清缓存前后比较 bundle 内容把环境写进 cache.version 或禁用跨环境 cache
watch 不重建 Sass/模板 includeLoader 没登记二级文件依赖改 include 文件,观察 rebuildLoader 调 this.addDependencythis.addContextDependency
HMR 后点击仍是旧函数DOM 事件 handler 持有旧闭包浏览器控制台看 HMR 日志和交互行为在 accept 回调中重新绑定,必要时 dispose 清理
开发正常生产失败dev server 内存产物、publicPath、minify、splitChunks 与生产不同同时跑 production build 和静态预览不用 HMR 结果替代生产验收

publicPath、contenthash 与 CDN:加载链路的严重问题

webpack 输出的文件不等于浏览器一定能加载。浏览器先拿 HTML,再按 HTML 和 runtime 拼出的 URL 请求 JS/CSS/async chunk。

export default {
  output: {
    filename: 'assets/[name].[contenthash:8].js',
    chunkFilename: 'assets/[name].[contenthash:8].js',
    cssFilename: 'assets/[name].[contenthash:8].css',
    publicPath: './',
    clean: true,
  },
};

加载链路故障表:

症状根因观察命令修正
首屏 JS 404output.publicPath 指到错误 CDN/origin/pathDevTools Network 看请求 URL;检查 stats.publicPath按部署路径设置 publicPath,或使用 auto 并验证
点击后 async chunk 404runtime 拼出的 chunk base path 错只点击触发动态 import,观察 chunk URL修正 publicPath,不要只看初始 HTML
部署后用户拿旧 HTML 指向已删除 hashCDN/浏览器缓存 HTML 太久,clean 删除旧 asset查看旧 HTML 中 hash 是否还存在于 dist/CDNHTML 短缓存,assets 长缓存;保留上个版本 assets 或原子发布
CSS 200 但样式不生效HTML 未引用 CSS 或 MIME/cache 错查看 index.html link 和 Response headers确保 CSS asset 被注入,CDN 返回正确 Content-Type
Source Map 404 或错配map 没随 hash 上传,或清理策略删了 map检查 JS 尾部 map 文件名上传同版本 map;生产可改 hidden map

webpack 4 到 5,再到 5.108:迁移和重大变化

webpack 5 的重大变化集中在长期缓存、Node polyfill、资产模块、Module Federation、内部 graph/runtime 和持久化缓存。webpack 5.108 继续推进 universal target、experimental HTML modules、CSS、Tree Shaking 和 typed config。

变化webpack 4 常见状态webpack 5 / 5.108 状态迁移动作
Node polyfill许多 Node 内置模块自动 polyfillwebpack 5 移除自动 Node polyfill浏览器代码删除 Node 依赖,或显式 resolve.fallback
持久化缓存主要依赖 loader cache 或外部缓存内置 filesystem cache配置 cache.buildDependencies,防止环境漏 key
资产处理file-loaderurl-loader 常见Asset Modules 内置小步替换 Loader,比较 URL、hash、大小
Graph 内部plugin 可能读旧模板对象module graph / chunk graph / runtime modules 更明确迁移 Plugin 到新 API,少碰内部私有属性
HTML主要依赖 html-webpack-plugin5.107/5.108 有 experimental HTML modules 和 HTML output不把实验能力当默认;生产继续验收插件方案
TypeScript常用 ts-loaderbabel-loaderfork-ts-checker5.107+ 有 experimental TypeScript support明确“不等于完整类型检查”;保留 tsc --noEmit
CSScss-loaderstyle-loadermini-css-extract-plugin 常见原生 CSS 能力仍需实验标注和限制核查项目逐项迁移,不一次性删 Loader

升级严重问题完整流程:

症状根因观察命令修正
cryptostreambuffer 找不到webpack 5 不再自动 polyfill Node 内置模块stats errors 看 resolve 请求删除浏览器端 Node 依赖;必要时显式 fallback
自研 Plugin 访问 compilation.assets 报警告webpack 5 资产处理迁移到 processAssets构建 warningscompilation.emitAsset/updateAsset 和 processAssets stage
启用 HTML experiment 后页面行为和插件方案不同experimental HTML modules 与 html-webpack-plugin 尚非完全等价对比 HTML、chunks、HMR、模板能力只在实验分支试用;生产按回归结果决定
TypeScript 构建过了但类型错webpack 构建解析 TS 不等于类型检查单独跑 tsc --noEmit把类型检查留给 TypeScript 工具链

共享示例:webpack 构建浏览器入口

运行命令:

pnpm --filter @roadmap/build-tools-slice build:webpack

输入:

examples/build-tools/src/main.ts
examples/build-tools/src/catalog.ts
examples/build-tools/src/detail.ts
examples/build-tools/src/side-effect.ts
examples/build-tools/src/styles.css

预期结果:

examples/build-tools/dist/webpack/index.html
examples/build-tools/dist/webpack/stats.json
examples/build-tools/dist/webpack/assets/*.js
examples/build-tools/dist/webpack/assets/*.css
examples/build-tools/dist/webpack/assets/*.js.map
examples/build-tools/dist/webpack/assets/*.css.map

验证观察:

find examples/build-tools/dist/webpack -type f | sort
node --test examples/build-tools/test/artifacts.test.mjs

预期输出要点:

观察点预期结果
index.html通过相对 script src 加载 initial JS,通过 link rel="stylesheet" 加载 CSS
stats.json存在非 initial chunk,并且该 chunk 能追溯到 detail.ts
主 JS source mapsources 至少能追溯到 main.ts
async JS source mapsources 至少能追溯到 detail.ts
CSS source mapsources 至少能追溯到 styles.css
bundled JS包含 Babelpreserved,不包含 SHOULD_NOT_REACH_BUNDLE

如果只运行 webpack 命令退出 0,还不能证明产物正确;至少要检查 HTML 引用、async chunk、CSS、Source Map、stats 和运行时可见文本。

资料入口

在 GitHub 上编辑此页

本页目录