1. 入口在构建流程中的真实角色不只是配置文件里的一行路径很多人第一次接触 webpack都是跟着某篇教程把 entry 写成./src/index.js构建跑通了就不管了。直到某天项目需要做多页面、需要拆包、需要把不同环境的入口分开才发现自己对入口的理解其实停留在“告诉 webpack 从哪个文件开始”这个层面上。实际上没那么简单。entry 不是一个路径字段而是整个依赖图的第一块多米诺骨牌。webpack 从入口出发顺着import/require语句不断递归解析把项目的文件组织成一棵依赖树再把这棵树的叶子按规则打包成最终的 bundle。入口写在哪里、怎么写、写几个直接决定了依赖图的形状、chunk 的切分粒度、运行时的加载逻辑甚至会影响构建性能和线上缓存策略。拿一个最直观的例子说。同样一份代码如果用单入口打包所有模块都会汇入同一个 chunk如果拆成两个入口webpack 会各自维护一棵依赖子树公共模块如果没做额外配置就会在两边各打一份。这个“各打一份”不是 bug而是依赖图分离后的必然结果——因为从入口 A 出发的那份代码并不知道入口 B 也在用同一个模块。理解到这里你才算摸到 entry 的第一层逻辑入口定义了项目的边界而边界决定依赖的归属。本文不会端到端讲 webpack 的所有配置只盯着 entry 这一个点往下刨它会以什么形态出现、多入口场景怎么设计、动态入口怎么写、入口对象里几个容易被忽略的字段到底怎么影响运行时、以及我在实际项目里反复踩过的那些和入口直接相关的坑。适合对 webpack 有一定基础、但想从“会配”走向“懂原理”的开发者也适合正在维护多页面项目、正在为打包体积头疼的人。2. 单入口与多入口三种写法背后的依赖图差异2.1 字符串与数组写法默认行为与实际应用边界webpack 的 entry 支持字符串、数组、对象三种形态每种形态对应不同的依赖图构造行为。零配置情况下默认值是./src/index.js这也是字符串写法的直白应用module.exports { entry: ./src/index.js };这种写法告诉 webpack把index.js作为唯一起点递归收集它的依赖最后输出一个 bundle。适合单一入口的 SPA、简单的 SSR 前端资源构建。在这个形态下webpack 不会在意你入口文件内部有多少个子模块它只关心从起点能摸到哪些模块这些模块最终会合并到同一个 chunk 里。数组写法平时用得少但偶尔有奇效module.exports { entry: [./src/polyfill.js, ./src/index.js] };数组的语义不是“多个入口各自成包”而是把这些文件合并成一个入口 chunk。数组元素按顺序写入最终 bundle并且后面的文件能使用前面文件声明的全局变量或模块。最典型的场景是 polyfill 前置——把core-js、regenerator-runtime这类基础库放在业务代码前面加载。看起来和多个入口文件很接近实际效果却完全不同数组最终只产生一个入口 chunk。2.2 对象写法多入口场景的语法基础当一个项目开始需要多个页面、多个独立的挂载点就需要对象语法出场了module.exports { entry: { home: ./src/home/index.js, about: ./src/about/index.js, contact: ./src/contact/index.js } };对象的每个 key 代表一个 chunk 名也常被称为 entry namevalue 是这个入口的模块路径。webpack 会为每个 key 独立生成一条依赖链最终每个入口对应一个 bundle具体输出规则还受output.filename影响这点后面细说。选择对象语法意味着你明确告诉 webpack这些页面之间没有强关联它们需要各自独立的依赖图。这也是多页面应用MPA最基本的结构。前面说过各自独立会导致公共模块被重复打包这并非 webpack 设计缺陷而是依赖图独立的必然后果——除非你引入splitChunks在 chunk 层面做二次整合否则“共享”这个概念在入口阶段是不存在的。2.3 三种写法怎么选按依赖共享程度决策选哪种写法不应该看项目大小而应该看页面之间依赖共享的程度。单页应用、依赖高度内聚、只需要一个 HTML用字符串。需要在入口启动前注入基础代码、且这些代码没有模块化关联用数组。有多个相对独立的页面、每个页面都有自己的渲染入口用对象。想让不同入口的模块共享一份公共依赖对象 splitChunks 方案。我见过不少团队从单页做多页直接把entry从字符串改成对象然后发现构建产物体积暴涨。问题的根源不是多页本身而是没有同步配置 chunk 拆分策略。入口决定了依赖图的边界但边界内模块的归属还要靠splitChunks进一步规划。记住这句话entry 划界splitChunks 理内。3. 入口对象的三层字段dependency、runtime、filename 的运行机制3.1 入口 item 的完整写法webpack 5 中entry 对象的值不只是字符串还可以是一个对象用来分别控制入口的模块来源、运行时行为和输出文件名module.exports { entry: { home: { import: ./src/home/index.js, filename: pages/home.js, dependOn: shared, runtime: runtime-home }, about: { import: ./src/about/index.js, dependOn: shared, runtime: runtime-about }, shared: { import: ./src/shared/index.js, filename: common/shared.js, runtime: runtime-shared } } };这里的import就是原本字符串形式的模块入口路径filename可以单独覆盖全局output.filename规则dependOn声明当前入口依赖哪些其他入口的模块runtime则为当前入口指定一个独立的运行时 chunk 名称。3.2 runtime 字段把运行时与业务代码分离不理解 runtime就很容易在这个字段上犯错。webpack 的每个入口 chunk 在浏览器里启动时需要一段运行时代码来负责模块注册、懒加载脚本的调度、chunk 加载逻辑等默认情况下这段运行时会被注入到每个入口 chunk 中。如果入口很多运行时会被重复打进每个 bundle带来额外的字节开销。runtime字段允许你指定一个独立的 chunk 来存放运行时代码多个入口可以共享同一个运行时 chunk从而避免重复注入。比如三四个入口共用一个runtime-appentry: { home: { import: ./src/home/index.js, runtime: runtime-app }, about: { import: ./src/about/index.js, runtime: runtime-app } }但这里有一个极易踩的坑同一个 runtime 名分配给多个入口时webpack 会认为这些入口共享一套运行时。你要小心如果这些入口的依赖关系各不相同共享运行时可能带来无法预料的加载顺序问题。我的建议是如果你不确定运行时之间的依赖关系宁可让 webpack 自动生成也不要手动硬塞一个相同的 runtime 名。手动指定 runtime 的精髓是“你比 webpack 更清楚运行时应该怎么组织”而非为了消除报错而随手填一个名字。3.3 dependOn 字段共享依赖的显式声明dependOn解决的问题是“入口 B 的模块在初始化时入口 A 的模块必须已经就绪”。典型场景是多个入口都依赖一套初始化的业务公共模块比如通用埋点、全局事件总线、统一请求实例的初始化。entry: { core: ./src/core/index.js, pageA: { import: ./src/pageA/index.js, dependOn: core }, pageB: { import: ./src/pageB/index.js, dependOn: core } }这样当pageA的 chunk 被加载时webpack 会保证core的 chunk 也同步加载并且core中的模块都先于pageA的模块执行。dependOn本质上定义了入口之间的显式依赖顺序和import语句在模块内部建立的依赖关系是两种不同的维度。但要注意dependOn和splitChunks的自动拆包逻辑有重叠也有冲突的风险。如果core里的公共模块同时又满足splitChunks的抽取条件webpack 可能生成两个含义重叠的 chunk反而导致构建体积降不下去或运行时加载顺序错乱。我惯用的取舍是如果一个库或模块天然就该全局共享用dependOn显式声明如果只是恰好被多个入口用到但并非一定有共享语义交给splitChunks自动处理。手动和自动的边界确立得越早后续的维护成本越低。3.4 filename 字段为什么比全局 output 更灵活filename字段用于覆盖output.filename的模板规则让不同入口可以输出到不同目录、使用不同的文件命名策略。比如一个项目里既有面向用户的 C 端页面也有内部运营后台的入口你希望它们分别输出到dist/client/和dist/admin/entry: { client: { import: ./src/client/index.js, filename: client/[name].[contenthash].js }, admin: { import: ./src/admin/index.js, filename: admin/[name].[contenthash].js } }这个能力在迁移旧项目时特别有用——可以维持旧的线上文件目录结构而不用一次性改完所有 HTML 引用。还有一点容易忽略filename中可以使用[name]占位符它会自动取 entry 的 key。如果 key 带了目录信息比如pages/home输出路径也相应带有目录层级这一点在动态入口场景会反复出现。4. 动态入口用脚本扫描目录从“配入口”升级为“生成入口”4.1 为什么需要动态入口中小型项目手动写死三五个 entry 是没问题的但当页面数量增长到几十个、甚至需要按约定目录自动添加页面时手动维护 entry 对象就成了一件体力活。动态入口的核心思路是不手写 entry 对象而是用脚本扫描文件系统根据目录结构自动生成 entry 配置。这在多页面脚手架、后台管理系统、活动页平台这类“按页面目录增量开发”的项目中非常常见。4.2 基于 fs 和 glob 的入口扫描实现我常用glob包来匹配目录下的入口文件规则是约定src/pages/下每个目录代表一个页面目录内的index.js作为页面入口。扫描逻辑大概这样const glob require(glob); const path require(path); const pagesDir path.resolve(__dirname, ../src/pages); function getEntry() { const entryFiles glob.sync(${pagesDir}/**/index.js); const entry {}; entryFiles.forEach((file) { const relativePath path.relative(pagesDir, file); const pageName relativePath.replace(/\\/g, /).replace(/index.js, ); entry[pageName] { import: file, filename: ${pageName}/[name].[contenthash].js }; }); return entry; }这个脚本有几个关键细节值得展开。第一路径分隔符要统一。在 Windows 上path.relative会返回反斜杠直接拿来做 entry 的 key 虽然能构建但最终生成的文件名在线上 URL 里会出现%5C之类的转义非常难看。所以replace(/\\/g, /)这步是必需的。第二filename 模板里用了双占位符。${pageName}/[name].[contenthash].js会输出类似home/main.8f3d2a.js的路径——实际上[name]在这里等于 entry 的 key也就是home输出会变成home/home.8f3d2a.js。这有点冗余。更常见的做法是写成${pageName}/index.[contenthash].js或者干脆用entry[pageName] file配合全局output.filename: [name].[contenthash].js。第三动态入口不等于动态 HTML。entry 生成之后还需要让html-webpack-plugin知道每个 HTML 页面应该加载哪些 chunk否则构建产物和 HTML 对不上。通常会配合扫描 HTML 模板或手动数组统一配置。4.3 动态入口与 splitChunks 的关系不配置拆分页面越多体积越爆炸动态入口让新增页面变得简单但它也把“依赖共享”问题放大了。几十个页面每个页面都独立捎上一份 react 或 lodash构建产物体积会快速增长。动态入口项目里我建议必配splitChunksoptimization: { splitChunks: { chunks: all, cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, priority: 10, name: vendors } } } }chunks: all意味着同步和异步引入的依赖都能被纳入拆分范围test筛选出第三方库统一打成vendorschunk。这样每个页面的入口只负责自己的业务代码公共依赖全部收敛到一个共享包。配合前面的filename模板最终的产物结构会非常清晰vendors.xxx.js、home/index.xxx.js、about/index.xxx.js。需要特别提醒的是vendors这个 name 一旦固定就不要轻易改动否则线上缓存全部失效。你可以把vendors按库再拆细比如react-vendors、utils-vendors但原则是拆包粒度一旦上线就要在很长一段时间内保持稳定。4.4 动态入口的边界问题哪些场景不该用动态动态入口不是银弹。以下情况我宁可手动配置页面数量少比如少于 5 个手动配置一目了然扫描反而引入了一层抽象。页面入口路径不符合统一约定规则扫描逻辑会越写越复杂最终比手动还难维护。需要精细控制每个页面的runtime和dependOn关系脚本生成的入口没法体现这种差异。换句话说动态入口适合的是“规则统一且页面数量持续增长”的项目。规则一旦出现例外脚本就慢慢长成一座需要人肉维护的“小系统”这往往比直接手写 entry 更糟糕。5. 入口相关的高频故障根因、排查链路与修复方案5.1 多入口构建后页面 HTML 引用了错误的 JS 路径有一次我在一个多页面项目里排查线上脚本 404构建产物明明都在但某个 HTML 引用路径指向了assets/js/main.js而实际文件在assets/js/pages/main.js。问题出在全局output.filename和入口的filename字段混用上了。链路是这样的项目初始只有单入口配置是output.filename: assets/js/[name].js一切正常。后来改成多页面新增页面时我顺手在 entry 的对象里写了filename: pages/[name].js结果home入口输出到了assets/js/pages/home.js但某个引入方式比较老的 HTML 模板仍然写死引用了assets/js/home.js。这类问题排查的关键点是先看产物目录结构和 HTML 引用的是否一一对应再确认有没有入口级 filename 覆盖了全局规则。如果页面多且 filename 规则复杂我建议输出一份构建产物清单用脚本比对每个 HTML 的 script 引用是否都能命中真实文件。这种自动化校验比任何口头约定都可靠。5.2 两个入口引用了同一库打包体积翻倍增长这是把 entry 从单入口改成多入口之后最常见的现象但它本身不是 bug而是依赖图独立的自然结果。我在一个内部工具项目里见过完整的过程两个后台页面都引用了同一个 UI 组件库单入口阶段完全正常改成多入口后构建产物体积从 400KB 涨到 800KB 出头。排查链路是先看编译日志确认是否真的生成了两个体积都很大的 chunk。用webpack-bundle-analyzer查看每个 chunk 内容发现同一个组件库被打进了两份。确认入口配置没有dependOn、optimization.splitChunks也处于默认状态。加上splitChunks.chunks: all重新构建公共库被抽到单独的 vendors chunk业务代码体积回归正常。这个案例想说明的问题很简单多入口本身是原因但修复的方案不在入口层而在拆分策略层。入口的职责是定义依赖图边界边界确定后公共模块的归属由splitChunks重新分配。两个层次各有分工不要试图靠调整入口路径来解决问题那是南辕北辙。5.3 入口对象中 runtime 与 dependOn 冲突导致的构建异常某次在改造一个老项目时我为了提高多页面共享代码的复用率给所有入口都手动加了runtime: shared-runtime并且用dependOn把公共模块入口挂到每个页面下面。构建直接报错错误信息大致指向 runtime chunk 重复声明的问题。根因是同一份 runtime chunk 名称被多个入口引用但 webpack 内部的运行时 chunk 是按其所属入口和依赖关系区分的你不能让多个完全独立的入口共用一个你指定的运行时名。报错其实是 webpack 在保护你——它知道这种配置在运行时根本不可控。修复方式很直接要么让每个入口拥有独立的运行时名要么手动管理一个真正的共享运行时 chunk并确保它是一个独立的入口 key。我在实际项目中最终选择了后者entry: { shared: { import: ./src/shared/index.js, runtime: shared-runtime }, pageA: { import: ./src/pageA/index.js, dependOn: shared }, pageB: { import: ./src/pageB/index.js, dependOn: shared } }这次踩坑让我记住了一个原则runtime是运行时序的编排dependOn是依赖关系的声明二者是可以相互配合的但不能被滥用成“随便填一个名字让构建通过”的工具。手动配置入口项其实是在替 webpack 承担部分运行时设计工作前提是你必须真正理解你想达到的顺序效果。5.4 入口文件写得太厚导致每个页面首屏无谓地拖慢入口文件的“厚度”指的是它内部直接引用的业务模块数量。一个常见的坏味道是入口文件里既初始化路由、又挂载全局状态、又注册插件、又执行上报逻辑甚至包含一些页面首屏根本用不到的工具函数。这些问题在构建阶段很难被发现因为入口文件再厚也只是把更多模块拉进依赖图。真正的代价体现在浏览器端入口 chunk 被 HTML 直接引用是首屏加载的首个脚本它的体积直接决定白屏时间。入口文件引用了 10 个模块首屏可能只需要其中 2 个剩下 8 个仍要全部下载并执行。修复的思路有两个层面把非首屏必要的功能改成动态import让它们进入异步 chunk不要在入口同步引用。入口文件保持“薄”只负责框架挂载、路由初始化、全局容器创建具体的业务逻辑全部延后到路由层面。这个问题的隐蔽性在于——它在构建技能够用、页面也能跑、体积也不算离谱的情况下依然会持续拖慢线上真实体验。我在接手某些项目时会特意打开入口文件看第一屏代码量这是最直接的“入口健康度”指标。6. 从入口出发的构建规模控制思路聊完故障再往远处看一眼。入口在一个大型项目里的影响力往往会随着模块联邦、微前端架构的出现而变复杂。以 webpack 5 的模块联邦为例remote 应用的入口不再只承担页面启动职责还承担对外暴露模块的能力需要额外生成一份 remote entry chunk。此时入口配置会多出exposes、remotes这样的关联配置和传统 MPA 的动态入口思路完全不同。不过不管架构怎么演进入口设计始终遵循同一个底层逻辑依赖边界的划分要与运行时加载顺序匹配。入口定多了公共模块被分散首屏脚本数量可能失控入口定少了依赖图过于集中可缓存边界变得粗糙任何一个入口引用的模块变化都可能影响大范围 chunk 失效。我在实践中的一个倾向是入口粒度宁可偏大也不要无限切碎。这里的“偏大”不是说把所有页面都塞进一个入口而是指尽量让入口对应真实的业务模块边界比如一个频道、一个子系统、一整套功能而不是对应一个细小的路由。入口设得太多会让 splitChunks 的策略变得极其繁琐构建产物碎片化线上 HTTP 请求数量上涨收益却未必明显。有一种看似聪明、实则容易失控的做法为每个路由都生成一个 entry希望借此做到精准缓存。但路由级入口意味着每个入口都重复包含框架初始化代码一份公共框架代码被拆进几十个入口最终缓存收益被巨大的重复代码淹没了。正确的拆法是入口负责定义稳定的业务模块边界公共框架交给 cacheGroup 收敛路由级的变化交给动态 import 的异步 chunk 去消化。三者各有分工不该互相替代。7. 我自己在入口设计上的一次完整取舍最后分享一个具体的决策过程。之前做一个中后台项目规划时有两个方案方案 A所有页面共用一个单入口路由用react-router处理页面组件全部动态 import。方案 B每个页面一个入口结合多页面 HTML 和 splitChunks 抽公共包。从技术热度看方案 B 很像“多页面标准实践”但结合实际场景去算一笔账事情就不一样了。这个项目页面虽多但都属于同一个业务后台共享一套布局、一套权限模块、一套通信协议公共依赖占比极高。如果所有页面各自为政真正独特的部分反而很少splitChunks 处理起来费劲每个页面的入口还要重复配置 html-webpack-plugin。方案 A 反而更贴合实际。单入口 路由懒加载框架代码只编译一份动态 import 自动生成异步 chunk公共代码天然只在主 chunk 里出现一次。页面多并不等于必须多入口。入口数量应该由“业务是否彼此独立”决定而不是由“页面数量”决定。这个项目上线后主 bundle 不算大异步 chunk 按路由天然拆分用户访问哪个页面就加载哪个页面对应 chunk体验和缓存都很平滑。如果当时被“页面多就该多入口”的惯性带偏最终产物可能就复杂度倍增了。入口设计的本质是一次“边界抽象”的权衡。你在入口层面做的每一个决定都会在数月后的构建体积、缓存效率、加载体验上得到验证或惩罚。读懂 entry 不仅是为了通过某个面试题更是为了在未来某个项目规模增长的节点上掌握那条分岔路的走向。
阅读完成 · 觉得有帮助?