假设你是一家大型餐厅的老板。顾客点了一份“红烧肉盖饭”,后厨不会把整座菜市场搬出来,而是从这道菜名开始倒推:需要猪五花、米饭、酱油、葱姜……每样食材又各自要经历清洗、切块、腌制、炒制,最后所有环节汇成一份装盘整齐的盖饭,从出菜口端上桌。Webpack,就是前端项目的“中央厨房总调度”。 在这套体系里,entry 决定从哪道菜开始排菜,依赖图记录每道菜需要哪些配菜;loader 是备菜车间,把“生食材”加工成“能下锅的形态”;plugin 则是前厅服务团队,负责摆盘、灯光、台卡这些不碰锅铲却决定体验的杂活。开发时,试菜间(Dev Server + HMR)让你改一勺盐立即尝到味道且不用重开一桌;餐厅连锁后,Module Federation 让分店共享招牌菜,持久化缓存则是“昨天腌好的肉今天直接下锅”。这篇文章,就用这张餐厅地图带你走通 Webpack 的全部核心概念。

岗位关联

把这四件套装进心里,你就读懂了几乎所有前端构建工具的原理;无论是 Webpack、Vite 还是 ESBuild,“入口—转换—插件—产物”这套心智模型完全通用,是前端工程化实战与面试的地基。

目录
  1. 可视化演示
  2. 入口与依赖图
  3. 输出与产物
  4. Loader转换器
  5. Plugin插件
  6. 开发体验优化
  7. Webpack5新特性
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示Webpack核心概念:入口依赖图构建、Loader转译链(Babel/PostCSS)与Plugin生命周期钩子,切换模式观察模块打包全流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

入口与依赖图构建 从entry出发递归解析import依赖 构建完整模块依赖图,拓扑排序 graph = DFS(entry → imports) 依赖图决定模块打包顺序与代码分割

入口与依赖图

顾客走进餐厅,点了一份“红烧肉盖饭”。服务生不需要背下整本菜单,只需要记下这道菜名——后厨立刻能从这张“菜名”出发,倒推出今天要备齐的所有食材。在 Webpack 的世界里,这个“菜名”就是入口(entry)。但光有菜名还不够,主厨得把菜谱翻一遍:猪五花要配冰糖和酱油,米饭要提前蒸,葱姜要切好。食材之间环环相扣的关系被画成一张网,这就是依赖图(dependency graph)。

在 Webpack 配置中,entry 用来指定打包的起点,默认值是 ./src/index.js。Webpack 会从入口文件开始读取代码,扫描每一行里的 import 和 require 语句;每当发现一个新的文件引用,就把这个文件注册为一个“模块”,并继续解析这个模块自己的引用。这个过程不断地“顺藤摸瓜”,直到把所有被引用的文件都摸清,一张完整的依赖图就构建完成。

依赖图可以抽象成一张有向图:节点是项目里的每个文件(模块),边是文件之间的引用关系。Webpack 为每个模块分配唯一 ID,按依赖顺序遍历整张图,从而知道最终产物应该包含哪些文件、以什么顺序拼接。多页面项目可以配置多个入口,Webpack 会为每个入口分别构建依赖图并产出对应的 bundle。依赖图也是代码分割(code splitting)的依据——多个入口共享的模块可以被抽成公共 chunk,避免重复打包。

有了这张依赖图,后厨就知道今天只需准备红烧肉这条流水线需要的食材,而不是把整座菜市场搬回来。同样,Webpack 只把依赖图里的模块打包进产物——没被引用的文件,哪怕静静地躺在项目里,也不会进入最终的 JS 文件。

Gdep={Vmodules, Eimports}\displaystyle \mathcal{G}_{dep} = \{ V_{modules},\ E_{imports} \}
依赖图就是一个“配料关系网”:V 是所有要准备的食材,E 是“谁需要谁”的连线。
  1. 模块集合 V把项目中的每个文件看作一个小方块,所有小方块合在一起构成模块集合 V:
    Vmodules={index.js, utils.js, App.js, style.css}\displaystyle V_{modules} = \{ index.js,\ utils.js,\ App.js,\ style.css \}
  2. 依赖边 E模块之间“谁引用了谁”的关系构成有向边集合 E,表示“谁依赖谁”:
    Eimports={(index.js→utils.js), (index.js→App.js)}\displaystyle E_{imports} = \{ (index.js \rightarrow utils.js),\ (index.js \rightarrow App.js) \}

输出与产物

菜做好之后,不能连锅一起端给顾客。后厨有一个统一的出菜口,规定了盘子规格、标签格式、取餐区域在哪——顾客走到取餐口,看到贴着“红烧肉盖饭”标签的同款餐盒,就知道这是自己那份。这个出菜口,就是 Webpack 的输出(output)。

output 配置回答两个问题:打包产物放哪、叫什么。path 指定产物目录(如 dist),filename 指定文件名规则。最常用的文件名规则是 [name].[contenthash].js:[name] 是入口名称,[contenthash] 是内容哈希。只要模块内容不变,哈希值就保持稳定;内容一改,哈希立刻变化。

contenthash 的价值在于浏览器缓存。用户首次访问后,浏览器会把 bundle.abc123.js 缓存下来;下一次发布,未变动的模块文件名不变,浏览器直接命中缓存,而改动过的模块换了一个新文件名,浏览器自然去请求新文件。publicPath 则指定静态资源在服务器或 CDN 上的基础路径,保证图片、字体、JS 能被正确加载。借助产物分析工具(如 webpack-bundle-analyzer),可以把每个 chunk 的大小可视化,定位到体积过大的依赖,再做优化。

出菜口的盘子规格统一,顾客才知道怎么取餐;contenthash 则像餐盒上的“出锅时间戳”,内容变了标签就变,内容没变标签就稳稳地贴着——这正是长效缓存策略的基础。

文件名=[name]⋅[contenthash]⋅js\displaystyle 文件名 = [name] \cdot [contenthash] \cdot js
文件名公式相当于出菜口的标签规则:菜名 + 出锅时间戳 + 餐盒类型,三者拼出唯一标识。
  1. 入口名 [name]用入口配置中的名称作为前缀,便于区分多入口产物:
    [name]={main, app, vendor}\displaystyle [name] = \{ main,\ app,\ vendor \}
  2. 内容哈希 [contenthash]把模块内容输入哈希函数 H,得到内容指纹,内容一变指纹立刻不同:
    contenthash=H(模块内容) 取前 8 位十六进制\displaystyle contenthash = H(模块内容) \ \text{取前 8 位十六进制}
  3. 扩展名锁定末尾的 .js 标识文件类型,配合输出目录构成完整产物路径:
    输出路径=output.path+文件名\displaystyle 输出路径 = output.path + 文件名

Loader转换器

后厨最忙碌的往往不是掌勺的大厨,而是备菜间:带泥的土豆要削皮,整块的牛肉要切丁,买回来的鲜鱼要刮鳞去内脏。这些“把原始食材改造成能直接下锅形态”的工序,就是 Loader。炒菜师傅从不亲自去削土豆——他只需要接收已经处理好的食材。

浏览器真正认识的代码只有 JavaScript,而现代项目里到处是 .jsx、.vue、.ts、.scss 这类“原始食材”。Webpack 默认只懂 JS,面对其他格式会直接报错。这时就需要 Loader 上场:babel-loader 把 JSX 和 ES6 语法转译成浏览器兼容的 ES5;css-loader 解析 CSS 中的 @import 和 url();style-loader 把 CSS 以 <style> 标签的形式注入页面。

多个 Loader 可以串联成一条加工流水线。在 module.rules 配置里,test 用正则匹配要处理的文件类型,use 数组按从右到左的顺序执行。以 .scss 文件为例,常见链路是 sass-loader → css-loader → style-loader:先把 SCSS 编译成 CSS,再处理 CSS 里的依赖引用,最后注入页面。顺序反了,流水线就会卡壳——就像先切丝再削皮一样荒唐。

备菜间的每道工序看起来不起眼,却是整桌菜的根基。Loader 的“从右到左”流水线,正是把复杂规则拆解为单一步骤、层层加工的工程智慧。

结果=Ln(Ln−1(⋯L2(L1(源文件))⋯ ))\displaystyle 结果 = L_n \left( L_{n-1} \left( \cdots L_2 \left( L_1(源文件) \right) \cdots \right) \right)
loader 链路就是一条“函数套函数”的加工流水线,最里层的 loader 最先开工,输出逐层向外传递。
  1. 最右侧 loader 先执行复合函数从内层开始,即 use 数组最右边的 loader 最先处理源文件:
    L1:sass-loader,输入=源.scss文件\displaystyle L_1: sass\text{-}loader,\quad 输入 = 源.scss 文件
  2. 结果逐层向外传递前一个 loader 的输出作为后一个 loader(左侧)的输入:
    L2:css-loader,输入=L1(sass源文件)\displaystyle L_2: css\text{-}loader,\quad 输入 = L_1(sass源文件)
  3. 最外层收尾最后执行的是数组最左边的 loader,其输出就是最终进入依赖图的模块:
    最终模块=Ln(⋯L1(源文件)⋯ )\displaystyle 最终模块 = L_n( \cdots L_1(源文件) \cdots )

Plugin插件

菜做完了,餐厅却还没准备好开门:门口没有招牌、桌上没有台卡、背景灯太暗、排队叫号牌没接通。这些活儿不碰锅铲,却决定了顾客的整体体验——负责它们的是前厅服务团队,对应 Webpack 的插件(Plugin)。

Loader 只管单个文件的转换,而 Plugin 可以介入构建的整个生命周期。Webpack 在构建过程中会广播一系列事件钩子(hooks),比如刚启动时、生成资源时、即将输出文件时。插件的本质,就是“在某个钩子时刻执行一段自己的代码”,从而在恰当的时机干预构建过程与产物。

最常用的 HtmlWebpackPlugin 就是一个典型:开发者提供一个 HTML 模板,插件在构建完成时把带哈希文件名的 JS 和 CSS 以 <script>、<link> 标签自动注入模板,并在 dist 目录生成最终的 index.html。这样你永远不必手写 <script src="bundle.a1b2c3d4.js">——文件名跟着 contenthash 变来变去,插件总会帮你同步写对。类似的还有 MiniCssExtractPlugin(把 CSS 抽成独立文件)、TerserPlugin(压缩 JS)等,它们都在各自的钩子时刻完成使命。

前厅团队不碰一口锅,却让整个餐厅的品质上升一个档次。Plugin 的价值正是“见缝插针”:在构建的关键节点执行自定义逻辑,把重复而繁琐的工程杂活自动化。

HTMLfinal=模板HTML⊕∑iasseti(打包产物)\displaystyle HTML_{final} = 模板_{HTML} \oplus \sum_{i} asset_{i}(打包产物)
这条公式说的是:最终页面 HTML,等于“模板”加上“按顺序拼入的每个产物标签”。
  1. HTML 模板开发者准备一个可读的 template.html,预留注入位置:
    模板HTML:⟨html⟩⋯⟨body⟩⋯⟨/html⟩\displaystyle 模板_{HTML}: \langle html \rangle \cdots \langle body \rangle \cdots \langle /html \rangle
  2. 打包产物资产插件收集 output 目录中的 js/css 等资源,生成标签清单:
    asseti:⟨script src="bundle.a1b2c3d4.js"⟩,⟨link href="main.css"⟩\displaystyle asset_{i}: \langle script\ src="bundle.a1b2c3d4.js" \rangle, \langle link\ href="main.css" \rangle
  3. 注入并落盘插件把标签按预设顺序拼入模板,生成最终 index.html:
    HTMLfinal=HtmlWebpackPlugin(模板,产物清单)\displaystyle HTML_{final} = HtmlWebpackPlugin(模板, 产物清单)

开发体验优化

后厨旁边设了一间“试菜间”,厨师刚把汤出锅,服务生就端一小碗给客人先尝。客人觉得咸了,厨师回锅加一勺水,几秒后又端出来——不用等整桌菜重新做,客人也还坐在原位。这就是开发服务器(Dev Server)和热模块替换(HMR)带来的体验。

传统的开发流程是:改代码 → 手动打包 → 手动刷新浏览器 → 白屏等待。webpack-dev-server 在本地起一个服务器,监听文件变化后自动重新构建,并立即推送更新。而 HMR 更进一步:只把“被修改的那个模块”的最新代码推给页面并原地替换,页面不刷新,表单输入、滚动位置、弹窗状态全部保留。

SourceMap 解决的是“报错看不懂”的问题:打包后的代码压缩、混淆,报错堆栈根本不是人话;SourceMap 建立压缩代码与源码的映射,让浏览器开发者工具直接定位到出错的那行源码。mode 配置则区分两种场景:开发模式(development)保留未压缩代码与详细日志,方便调试;生产模式(production)自动启用压缩、Tree Shaking 等优化,追求最小的产物体积。

试菜间把“改一勺盐”的反馈时间从几分钟压缩到几百毫秒,开发者的思路不断线。Webpack 的开发体验优化,本质上就是在压缩“改动 → 反馈”这条路径的每一步耗时。

O(热更新)=O(变更模块),O(全量刷新)=O(全部模块)\displaystyle O(热更新) = O(变更模块), \qquad O(全量刷新) = O(全部模块)
HMR 的成功秘诀是“只补坏掉的那块砖”:改多少、换多少,而不是把整栋楼推倒重建。
  1. 变更模块 ΔHMR 只重新编译用户改动的那几个模块以及直接依赖它们的模块:
    Δ模块={被修改文件}∪{依赖它的模块}\displaystyle \Delta_{模块} = \{ 被修改文件 \} \cup \{ 依赖它的模块 \}
  2. 全部模块全量刷新需要重新打包、重载整个应用的所有模块:
    O(全部),改动一行代码也需重建整个项目\displaystyle O(全部), \quad 改动一行代码也需重建整个项目
  3. 代价对比改动范围通常远小于整体,因此热更新耗时远低于全量刷新:
    ∣Δ∣≪∣全部∣,tHMR≪t刷新\displaystyle |\Delta| \ll |全部|,\quad t_{HMR} \ll t_{刷新}

Webpack5新特性

餐厅越开越多,成了连锁品牌。每家分店不必闷头做所有菜:招牌红烧肉由中央厨房统一配送,分店之间还能互相借菜;中央厨房的冷库里存着昨天卤好的半成品,今天加热就能上桌。这就是 Webpack 5 的 Module Federation 和持久化缓存带来的改变。

Module Federation(模块联邦)允许应用 A 在运行时“远程加载”应用 B 暴露的模块,两端还能共享同一份公共依赖,不必把重复代码各打一份。这让微前端架构真正实现了“跨店点菜”式的组件共享。持久化缓存通过 cache 配置开启,把编译中间结果写入磁盘(如 node_modules/.cache),第二次构建时未变化的模块直接复用,构建时间肉眼可见地下降。

Webpack 5 还引入资源模块(Asset Modules),通过 asset/resource、asset/inline 等规则直接处理图片、字体等资源,替代了旧版需要额外安装的 file-loader 和 url-loader。内置的 Tree Shaking 和压缩优化也进一步减小产物体积。对团队而言,升级 Webpack 5 意味着更快的 CI 构建、更流畅的微前端协作,以及更精简的依赖列表。

连锁餐厅的共享中央厨房与半成品库存,直接决定了开新店的速度和出餐效率。Webpack 5 用同样的思路,让大型前端项目的构建与协作在新时代里跑得更快。

t构建=t全量×(1−r缓存命中)\displaystyle t_{构建} = t_{全量} \times (1 - r_{缓存命中})
构建耗时的计算公式:全量时间 × 没被缓存救下的那部分比例,缓存命中越多,构建越快。
  1. 全量构建时间首次构建无缓存,需要处理全部模块,记为基础耗时:
    t全量=首次完整构建耗时\displaystyle t_{全量} = 首次完整构建耗时
  2. 缓存命中率未变化的模块直接复用磁盘缓存,命中率表示可跳过的比例:
    r命中=未变更模块数全部模块数∈[0,1]\displaystyle r_{命中} = \frac{未变更模块数}{全部模块数} \in [0, 1]
  3. 实际构建时间构建耗时近似为全量耗时乘以未命中比例:
    t构建=t全量×(1−r命中)\displaystyle t_{构建} = t_{全量} \times (1 - r_{命中})

AI 应用场景

场景 1:AI 辅助搭建前端构建流水线

阶段:应用开发与工程化部署

解决的问题:前端项目首次初始化时,手动配置 entry/output/loader/plugin 链路很容易出错,比如忘记给 CSS 配 style-loader、文件名忘记带 contenthash。借助 AI 生成并解释 webpack.config.js,可以让配置过程从“查文档试错”变成“对话式生成”。

具体案例:团队要搭建一个 AI 可视化大屏项目,AI 助手根据技术栈自动生成配置:入口指向 src/index.js,输出带 contenthash 的文件名,接入 babel-loader 处理 JSX,配好 HtmlWebpackPlugin 注入产物。生成后 AI 还会逐行解释每个字段的含义,新人也能看懂。

场景 2:微前端运行时共享组件

阶段:多应用集成与模型服务接入

解决的问题:一个数据中台同时承载报表、模型训练、数据标注三个应用,三者都要用同一套图表组件和权限组件。传统方式要么复制代码、要么抽公共包发版,维护成本极高。

具体案例:三个应用通过 Module Federation 暴露各自的共享组件;中台升级了新版图表组件后,报表和标注应用在运行时即可拉取最新版本,无需各自重新发版部署。

场景 3:AI 编程助手下的 HMR 高效调试

阶段:迭代开发与调试反馈

解决的问题:AI 代码生成工具每次会给出大段改动,全量刷新会让页面状态丢失,调试效率低;HMR 只替换变更模块,配合 SourceMap 定位报错,极大提升反馈速度。

具体案例:开发者用 AI 助手生成一个新表单组件,保存后页面在保留原输入内容的情况下热替换新组件,同时 SourceMap 把组件内一处报错直接指向源码第 12 行,开发者秒级定位问题。

Python 演示

python77 行
# -*- coding: utf-8 -*-"""迷你打包器: 用 Python 模拟 Webpack 的 entry + 依赖图 + loader + plugin""" import reimport hashlib  class MiniBundler:    """极简模块打包器, 演示 Webpack 核心流程"""     def __init__(self, entry):        self.entry_name = entry        self.module_map = {}        self.read_count = 0     def read_module(self, module_name):        """读取模块并扫描 import, 递归构建依赖图"""        self.read_count += 1        sources = {            "entry.js": "import { log } from './utils.js';\nlog('hello');",            "utils.js": "export function log(msg) { console.log(msg); }"        }        source = sources[module_name]        deps = re.findall(r"from\s+['\"](.*?)['\"]", source)        module_id = len(self.module_map)        self.module_map[module_id] = {            "name": module_name,            "deps": deps,            "source": source        }        for dep in deps:            known = [self.module_map[m]["name"] for m in self.module_map]            if dep not in known:                self.read_module(dep)        return module_id     def run_loader(self, source):        """模拟 Babel Loader: 把 ESM 语法转成 CommonJS 风格"""        source = re.sub(            r"import\s+\{([^}]+)\}\s+from\s+['\"]([^'\"]+)['\"];",            r"var {\1} = require('\2');",            source        )        return source.replace("export function", "function")     def run_plugin(self, bundle_source, template):        """模拟 HtmlWebpackPlugin: 把打包产物注入 HTML 模板"""        return template.replace("<!--inject-->", "<script>" + bundle_source + "</script>")     def build(self):        """完整流程: 入口 -> 依赖图 -> loader -> 合并输出 -> plugin 注入"""        self.read_module(self.entry_name)        pieces = []        for mid in sorted(self.module_map.keys()):            pieces.append(self.run_loader(self.module_map[mid]["source"]))        bundle_source = ";(function(){" + ";".join(pieces) + "})();"        hash_hex = hashlib.sha256(bundle_source.encode("utf-8")).hexdigest()[:8]        bundle_name = "bundle." + hash_hex + ".js"        final_html = self.run_plugin(            bundle_source,            "<html><body><!--inject--></body></html>"        )        return {            "module_count": len(self.module_map),            "bundle_name": bundle_name,            "bundle": bundle_source,            "html": final_html        }  if __name__ == "__main__":    bundler = MiniBundler("entry.js")    out = bundler.build()    print("依赖图节点数:", out["module_count"])    print("产物文件名:", out["bundle_name"])    print("打包代码:", out["bundle"])    print("HTML 注入结果:", out["html"])
目的:用 80 行 Python 复刻 Webpack 的核心流水线——从入口递归构建依赖图,经过 loader 转译、合并产物、插件注入 HTML,最后用内容哈希生成文件名。
  1. deps = re.findall(r"from\s+['\"](.*?)['\"]", source)用正则把 import 语句里的依赖路径都捞出来,相当于“读菜谱找配菜”
  2. self.module_map[module_id] = {...}给每个模块一个 id,登记名字、依赖和源码,这就是依赖图的一个节点
  3. known = [self.module_map[m]["name"] for m in self.module_map]列出已收录的模块名,避免重复读取同一个文件
  4. source.replace("export function", "function")Loader 的转译动作:把 ESM 导出语法降级为普通函数声明
  5. hashlib.sha256(bundle_source.encode("utf-8")).hexdigest()[:8]对打包结果算内容哈希,模拟 [contenthash]:内容一变文件名就变
  6. template.replace("<!--inject-->", "<script>" + bundle_source + "</script>")Plugin 把打包产物以 script 标签注入 HTML 模板,就是 HtmlWebpackPlugin 干的事
  7. sorted(self.module_map.keys())按注册顺序合并所有模块代码,形成单一 bundle

常见误区

误区“项目报错了?装个 loader 试试;还不行?再装一个。反正 loader 能把所有文件变成 JS,问题解决就好,插件是什么感觉不重要。”

正解Loader 的职责边界是“文件 → 模块”的内容转换:Babel 转语法、css-loader 解析依赖、sass-loader 编译 SCSS。而流程级操作——注入 HTML、抽取 CSS、压缩产物——必须由 Plugin 在生命周期钩子里完成。就像备菜间只管加工食材,布置餐厅是前厅的活;让土豆削皮机去放背景音乐,机器会当场罢工。

为何常见:初学时 Webpack 报错大多与文件解析有关,加 loader 往往立竿见影,容易形成“loader 万能”的错觉,忽略了 Plugin 的互补分工。

误区“use 数组跟普通数组一样,从左往右读,那执行顺序也应该从左到右呗,跟看代码顺序一致。”

正解实际执行顺序是从右到左。这对应函数复合模型:最右边的 loader 先处理源文件,输出再交给左边下一个 loader。类比备菜流水线:先削皮(sass-loader)→ 再切丝(css-loader)→ 最后下锅(style-loader)。如果写反,style-loader 会拿到还没解析 import 的原始 CSS,直接报错。

为何常见:人类阅读数组的自然方向是左到右,而 Webpack 的 loader 机制仿照函数复合“内层先算”,两种直觉冲突,导致这个经典误区。

误区“文件名里加 hash 就是为了让浏览器不缓存,反正每次打包文件名都会变,缓存策略根本用不上。”

正解contenthash 是“内容指纹”:由模块内容通过哈希函数计算得出,内容不变则哈希不变。它恰恰是为了缓存而生的——未修改的 chunk 文件名稳定,浏览器继续命中旧缓存;修改过的 chunk 文件名变化,浏览器自动请求新文件。只有内容真正改变,标签才会改变。

为何常见:很多人把 contenthash 与构建时间戳混淆,或者观察到“改完代码后名字确实变了”,就误以为它每次都变,没意识到变的原因是内容变了。

误区“热更新不就是改完代码浏览器自动刷新吗?反正都不用我手动按 F5,效果差不多。”

正解自动刷新是整页重载:页面白屏一下,所有内存状态(输入框、滚动条、弹窗)全部清零。HMR 是模块级替换:只把变更模块的新代码推给运行中的页面并原地执行,页面不刷新、状态全保留。它更像是“换掉墙上的一块砖”,而不是“把整栋楼推倒重建”。

为何常见:从使用者视角看,两者都是“保存代码后页面自动更新”,表面相似;只有深入观察页面状态是否保留,才能体会到 HMR 的精细粒度。

面试题

Q1. Webpack 中入口配置 entry 的主要作用是?

单选题 · easy

  • A.指定打包产物的输出目录
  • B.指定依赖图构建的起点模块← 正确答案
  • C.负责将 CSS 文件转成 JS
  • D.自动打开浏览器调试页面

答案:B。入口就像餐厅里顾客点的第一道菜名,后厨从它开始推演食材清单(依赖图)。输出目录由 output 配置规定,CSS 转 JS 是 loader 的职责,自动开浏览器是 devServer 的事,均与 entry 无关。

Q2. 以下哪项是 Loader 的典型职责?

单选题 · easy

  • A.在构建结束时发送邮件通知
  • B.转译 ES6 新语法为 ES5 代码← 正确答案
  • C.负责启动本地开发服务器
  • D.生成 HTML 并注入打包脚本

答案:B。Loader 是备菜车间,把“生食材”变成“能下锅的形态”:Babel 把高版本 JS 语法转成浏览器兼容的低版本语法。发送邮件、启动服务器、生成并注入 HTML 都不属于“文件格式转换”,自然不是 loader 的活。

Q3. Webpack 中多个 loader 的 use 数组执行顺序是?

单选题 · mid

  • A.严格按照从左到右依次执行
  • B.严格按照从右到左依次执行← 正确答案
  • C.按文件大小动态决定执行顺序
  • D.由插件随机插入执行时机

答案:B。loader 的 use 数组按“函数复合”模型执行:最右边先执行,结果作为左边 loader 的输入。这类似备菜先削皮再切丝,顺序反了会得到无法下锅的中间产物。

Q4. 关于 output 的 [contenthash] 说法正确的是?

单选题 · mid

  • A.每次构建都会生成不同哈希值
  • B.内容不变时哈希保持稳定不变← 正确答案
  • C.用来加密压缩打包后的代码
  • D.等于 Git 仓库的提交编号

答案:B。contenthash 是内容的指纹,由哈希函数根据模块内容算出:内容不变则指纹不变,浏览器可放心缓存;内容一变哈希立刻不同,部署时浏览器自动拉取新文件。它不是加密、也不是提交编号。

Q5. HtmlWebpackPlugin 的核心作用是什么?

单选题 · hard

  • A.压缩 HTML 以减少网络传输体积
  • B.注入打包产物到 HTML 模板文件← 正确答案
  • C.将 HTML 文件转化为 JS 模块
  • D.校验 HTML 标签的嵌套合法性

答案:B。HtmlWebpackPlugin 的工作是在构建完成后把带哈希文件名的 JS/CSS 注入 HTML 模板并产出 index.html。压缩 HTML 属于 minify 选项,转成 JS 模块、校验标签合法性都不在它的职责内。

Q6. HMR 热模块替换与整页刷新的关键区别是?

单选题 · mid

  • A.HMR 会清空页面中的全部状态
  • B.HMR 保留页面状态仅替换模块← 正确答案
  • C.HMR 只在生产环境才会生效启用
  • D.HMR 可以跳过代码编译直接执行

答案:B。HMR 是模块级热替换:页面不刷新、状态不丢失,只替换变更的模块本身。生产环境默认不开启 HMR;它仍然需要先编译再推送,“跳过编译直接执行”是不可能的。

Q7. Webpack 5 的 Module Federation 主要用于?

单选题 · mid

  • A.加快本地开发服务器的热更新速度
  • B.实现应用之间运行时共享模块代码← 正确答案
  • C.自动压缩所有静态图片资源文件
  • D.将 JSON 配置转成 YAML 格式文件

答案:B。Module Federation 解决“跨应用运行时共享代码”的问题,让微前端各应用不用重复打包公共依赖。热更新提速、图片压缩、格式转换都不是它的目标,它们分别属于 devServer、loader 和工具链的范畴。

Q8. 关于 Webpack 依赖图构建,下列说法正确的有?

多选题 · mid

  • A.从入口出发递归解析所有引用语句← 正确答案
  • B.依赖图中的每个节点是一个模块← 正确答案
  • C.只有 JS 文件才会被加入依赖图
  • D.依赖图决定最终打包的内容顺序← 正确答案

答案:ABD。依赖图从入口出发,把文件当节点、引用当边;loader 处理的 CSS、图片等资源同样会成为图中的模块节点,因此 C 错误。图的内容与遍历顺序直接决定最终打包产物,A、B、D 正确。

Q9. 关于 Loader 与 Plugin 的区别,正确的是?

多选题 · hard

  • A.Loader 负责单个文件内容的格式转换← 正确答案
  • B.Plugin 基于构建生命周期钩子执行← 正确答案
  • C.Loader 是 Plugin 的一种高级形式
  • D.Plugin 无法影响任何输出文件内容

答案:AB。Loader 负责“文件格式转换”,Plugin 负责“流程钩子介入”,两者分工不同、无法相互替代;Plugin 可以修改输出文件(如注入 HTML、抽取 CSS),因此 C 和 D 都是对两者关系的错误理解。

Q10. 关于 Webpack 5 持久化缓存,下列说法正确的有?

多选题 · hard

  • A.缓存数据写入磁盘可跨构建复用← 正确答案
  • B.通过 cache 配置项开启缓存功能← 正确答案
  • C.缓存命中后模块无需重新编译处理← 正确答案
  • D.缓存会改变业务代码的运行逻辑

答案:ABC。Webpack 5 持久化缓存把编译中间产物写入磁盘,命中后跳过该模块的编译,大幅加速二次构建;它只影响构建速度,不改变业务代码的运行逻辑,因此 D 错误。A、B、C 描述正确。