聊webpack配置,很多人第一反应是去逐条背module.rules和plugins,可真正决定打包流程走向的,往往是那个看起来不起眼的entry字段。它就像一张依赖地图的起始坐标:webpack拿到它,顺着import递归展开整棵依赖树,最终才能生成一个个chunk和bundle。entry怎么写,不仅决定了构建入口在哪,更决定了产物怎么切分、公共代码能不能抽干净、多页面能不能各归各位。这个字段看似简单,但实际项目里关于它的误解和踩坑,比任何一个loader都多。
这篇文章从一个长期和webpack打交道的人的视角,把entry彻底拆开聊透。你会看到它的四种写法分别适用什么场景,多入口之间怎么配合splitChunks玩出干净的打包结果,还会给出一套从单页到多页项目的完整配置演进路线。无论你是刚开始学webpack的新手,还是已经负责中大型项目构建的老手,里面都有值得直接抄走的配置和排坑经验。
1. entry的本质:打包流程的“起点”与“地图”
1.1 从起点到依赖图:webpack到底在入口处做了什么
entry的核心作用,不是“指定一个文件被丢进打包”这么简单。webpack的执行引擎拿到entry后,会做三件事:定位入口模块、创建模块对象、从该模块开始解析依赖。解析依赖的过程是递归的:入口文件里import了什么,就拉进来;被拉进来的文件里又import了别的文件,继续拉。最终,所有被触达的模块形成一张完整的模块依赖图,这张图才是打包的真正输入。
这里有一个容易忽略的点:模块依赖图的形状完全取决于entry的起点选择。同一个项目,入口指向src/index.js和指向src/main.js,生成的依赖图可能天差地别。比如入口文件里没有引用polyfill,那么polyfill就不会进入当前entry对应的bundle,哪怕它在另一个入口里被引用了。这种“以entry为中心”的辐射逻辑,决定了你在写入口时就必须考虑到整个项目的模块组织方式。
从实现细节来看,webpack对每个入口会创建一个compilation中的入口对象,包含name、chunkGroup等信息。这个chunkGroup是后续所有chunk归类的依据,多入口之间的公共模块抽取、异步模块拆分,本质上都是在chunkGroup的维度上做调度。所以entry的命名不只是一个字符串,它是整张依赖图的“根命名空间”。
1.2 entry与module、chunk、bundle的关系图谱
很多人会把module、chunk、bundle这三个概念混在一起。理清它们,entry的作用就清晰了。
- module:项目里的源文件,一个文件就是一个module,webpack能识别的模块类型包括js、jsx、ts、css、图片等。
- chunk:webpack在构建过程中生成的分组,一个chunk包含一组相互引用关系的module。chunk是构建过程中的中间产物,不直接输出。
- bundle:chunk经过文件输出处理(压缩、加hash)后生成的真实静态文件,也就是最终部署到服务器上的文件。
entry和这三者的关系可以这样概括:入口指向一个或多个module,这些module被归入一个或多个chunk,chunk经处理后输出为bundle。一个单入口项目,通常所有同步模块都会归入一个chunk,最终输出为一个bundle。而多入口项目,每个入口都是一个独立的chunkGroup,每个入口最终可能会输出自己的bundle,同时公共模块也可能被抽成一个公共bundle。
用一个生活化类比:模块是仓库里的各种货物,chunk是仓库工作人员临时分好的打包批次,bundle是已经封箱贴上快递单的包裹。entry就是快递单上填写的发货地址——你填了几个发货地址,仓库就按几个方向去配货封装,但不同方向的订单里如果都有同一件货物,仓库可能额外开一个“公共包裹”统一发送。
1.3 为什么说entry选型决定项目架构走向
技术选型层面,entry的形态基本宣告了项目的前端架构形态。如果你用的是单个字符串入口,那这是一个经典的单页应用(SPA)架构——所有路由、状态管理、业务模块都被收敛进同一棵依赖树,最终打包出一个主bundle。随着业务膨胀,主bundle会越来越大,你会开始考虑代码分割,于是把动态路由的import()引进来,这时webpack会自动生成异步chunk,但这些异步chunk的“上游归属”仍然绑在唯一的入口上。
如果你用的是对象多入口,那项目大概率是传统的多页应用形(MPA)——多个页面各自走独立的依赖树,页面之间通过抽取公共chunk来优化体积。这种配置下,每个页面都可以单独部署、单独更新,非常适合偏静态的站点、后台系统或者强SEO需求的项目。
如果你用了函数式动态入口,那就说明你的入口不是固定的,而是要结合文件系统、环境变量或者构建时的目录扫描来动态决定的。这在一些脚手架工具、低代码平台、或者需要按目录自动生成多页面的项目里非常常见。
因此,entry不该是随手一写的配置,而是项目架构的“第一决策点”。后面所有的loader、plugin、splitChunks策略,都要围绕着entry的选择来展开。
2. entry的四种配置形态,以及各自背后的场景密码
2.1 字符串入口:最基础的单页应用起点
字符串入口是最常见、最基础的写法:
module.exports = { entry: './src/index.js', };注意,这里传入的是一个相对路径字符串。webpack会基于context(默认是process.cwd())来解析这个路径。更严谨的写法可以用绝对路径,避免因为终端启动目录不一致导致解析错误:
const path = require('path'); module.exports = { context: path.resolve(__dirname, '.'), entry: path.resolve(__dirname, 'src/index.js'), };单字符串入口会生成一个默认名为main的chunk,最终输出对应的bundle文件名,由output.filename决定。如果你只写filename: 'bundle.js',那就是固定名;如果你写成filename: '[name].[contenthash].js',其中[name]就会被替换为main。
这个写法适合:纯SPA、项目结构清晰、不需要按页面拆分构建产物的场景。不过要注意,字符串入口不代表你的最终代码只能有一个文件。只要在代码里使用动态import(),webpack就会自动拆分出额外的异步chunk。那些异步chunk也归属于这个入口,只是它们不会成为一个独立HTML入口,而是被主入口在运行时按需加载。
2.2 数组入口:别把它当成多入口,它是一组预加载依赖
数组入口的写法是:
module.exports = { entry: ['./src/polyfill.js', './src/index.js'], };很多人第一次看到这个写法会误以为这是“多入口”。错了,数组入口最终只生成一个bundle,数组中的多个文件会被打包进同一个chunk里。它真正的作用,是把入口依赖的多个模块“硬性”加入到入口chunk中,并且按声明顺序排列。数组的第一个元素会最先被加载,然后是后面的元素。
这在实际中的典型用途,是在需要保证某些代码先于业务代码执行的场景下,比如必须优先引入polyfill、初始化埋点、设置全局环境变量等。你当然可以在index.js里第一行import './polyfill',但数组入口可以避免业务入口里“不自觉”的模块提升顺序问题,同时让需求更显式、可读性更强。
数组入口还有一个冷门用法:配合output.library去开发一些库,多入口文件共同组成一个UMD库的对外形态。不过这种情况很少见,大多数时候我们只需要理解它是“单入口的增强版”。
2.3 对象入口:真正意义上的一对多打包控制
对象入口是支持多页面、多bundle的基础:
module.exports = { entry: { home: './src/home.js', about: './src/about.js', admin: './src/admin.js', }, };每一个key就是一个入口名,对应一个独立的chunkGroup。每个入口可以引用完全不同的依赖树,但也可以引用公共模块。对象入口的价值在于,你可以为每个入口独立配置输出、独立注入HTML,并且通过splitChunks把多个入口共用的模块抽取到一个公共chunk里,方便浏览器在页面间跳转时复用缓存。
举个例子,home页面和about页面都用了react和moment,如果不抽取公共模块,打包后的home.js和about.js里会各自包含一份react和moment的副本,得白白浪费好几百KB。配置了splitChunks后,公共部分会独立成vendors或commonchunk,HTML中需要引入三个文件:公共chunk + home chunk + about chunk才能让页面正常跑起来。
对象入口的key名,会直接成为[name]占位符的实际值,也会成为HtmlWebpackPlugin中chunks参数的引用名。所以入口key的命名要语义化,别写a、b这种毫无意义的短名,否则后面查构建产物、定位问题时都是一场灾难。
2.4 函数动态入口:当入口需要“计算”时怎么玩
函数入口是webpack 4.4.0+版本支持的写法,它接收一个函数,返回值可以是字符串、数组或对象。函数形式最大的价值,是让入口可以根据环境变量、构建上下文动态计算。
比如按环境区分入口文件:
module.exports = { entry: () => { if (process.env.NODE_ENV === 'development') { return { app: './src/dev-entry.js', }; } return { app: './src/prod-entry.js', }; }, };再比如自动扫描目录,生成多页面的入口:
const fs = require('fs'); const path = require('path'); const pagesDir = path.resolve(__dirname, 'src/pages'); const entryMap = {}; fs.readdirSync(pagesDir).forEach((dirName) => { const entryPath = path.join(pagesDir, dirName, 'index.js'); if (fs.existsSync(entryPath)) { entryMap[dirName] = entryPath; } }); module.exports = { entry: entryMap, };函数入口还可以返回一个Promise,允许你在扫描目录时做异步操作。这个特性为复杂的构建系统提供了解耦空间,让插件和脚手架能更灵活地注入入口配置。不过要注意,函数入口一般只影响“依赖图起点”的确定,一旦函数执行完毕返回了具体的入口配置,后续行为和静态配置完全一致。不要试图在函数里做过于耗时的工作,比如大规模I/O,否则每次构建的启动速度都会变慢。
3. entry与chunk拆分:那些“隐藏”的优化机会
3.1 多入口条件下公共依赖的重复打包问题
单入口项目不用太担心重复打包问题,因为所有模块都汇集于一棵依赖树,天然只有一份。一旦切换到多入口,每个入口自己的依赖图里,都可能包含react、lodash之类的公共库。如果不对公共部分做处理,产物就重复了。
更麻烦的是,重复打包不只是体积变大,还会破坏应用的状态共享。比如两个入口都引用了同一个第三方库的单例对象,但每个bundle里各有一份独立的拷贝,那它们之间的共享状态就完全割裂了。虽然多页面应用一般不共享内存状态,但如果某两个入口在同一个页面中被同时引用,这种割裂就会造成真实的功能异常。
解决重复打包有两条路径:一是用SplitChunksPlugin抽取公共chunk,二是使用externals配置把公共库声明为外部依赖,避免打进bundle。entry在这里的作用,是定义抽取的“边界”。只有先有了多个entry,splitChunks才有分组的依据;只有entry命名清晰,splitChunks的cacheGroups规则才能准确地把想抽的内容抽出来。
3.2 splitChunks配置与entry的配合要点
先看一个典型的多入口配置:
module.exports = { entry: { home: './src/pages/home/index.js', about: './src/pages/about/index.js', list: './src/pages/list/index.js', }, optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: 10, }, common: { name: 'common', minChunks: 2, minSize: 0, priority: 5, }, }, }, }, };这里的核心逻辑是:webpack会扫描所有entry的chunk,找到它们之间共享的模块。chunks: 'all'意味着无论模块是同步加载还是异步加载,都会被纳入拆分范围。vendors组把来自node_modules的模块抽到vendors这个chunk中,common组把被至少2个chunk引用的自有模块抽到common中。
配合entry时,要注意几个点:
- cacheGroups里的
name会在产物文件里成为实际文件名前缀,和入口名没有必然关系,但建议保持语义。 - 如果在cacheGroups里不指定
name,webpack会默认生成一个包含多个入口名的chunk名,比如home~about,这种命名在文件多了以后显得很乱。 minChunks的数字指“被多少个chunk引用”。如果A和B两个入口都引用了同一个工具函数,设置minChunks: 2就能抽出来;但如果只有一个入口引用,即使size很大,也不满足条件,不会抽取。minSize用来控制抽取的最小体积,避免把很小的模块也抽出来。默认值是20KB左右,如果你发现公共chunk里出现了一些几个字节的重复代码,可以把minSize调小或置零,让抽取逻辑更激进。
在配置entry时,还有一个和splitChunks强相关的优化选项:entry的dependOn依赖关系。通过它,可以让一个入口依赖另一个入口,比如:
module.exports = { entry: { main: './src/index.js', vendor: './src/vendor.js', admin: { import: './src/admin.js', dependOn: 'vendor', }, }, };dependOn会告诉webpack:admin这个入口的运行依赖vendor入口里的模块。这可以避免splitChunks误把一些本应只由vendor提供的模块重复抽取,同时让入口之间的依赖关系显式化。实际项目里,如果你既想抽取公共依赖,又希望保留入口的独立性,这种配置方式非常有用。
3.3 runtimeChunk:入口里不写但必须懂的隐式chunk
还有一种chunk,虽然不直接由entry决定,却和entry息息相关:runtimeChunk。它承载的是webpack的运行时引导代码,包括模块解析函数、热更新逻辑、异步加载逻辑等。
不配置runtimeChunk时,每个entry生成的bundle里都会内嵌一份完整或部分的runtime代码。如果你有多个entry,每个bundle里的runtime就重复了。更重要的是,runtime代码里经常包含chunk的id映射,一旦任何一个异步chunk的内容变化,入口bundle的hash就会跟着变,导致浏览器缓存失效。把runtime单独抽成一个chunk,可以让入口bundle的hash保持相对稳定。
配置方式:
module.exports = { optimization: { runtimeChunk: 'single', }, };或者更细致地,每个入口都单独生成一个runtime:
module.exports = { optimization: { runtimeChunk: true, // 或 { name: (entryPoint) => `runtime-${entryPoint.name}` } }, };在实际中,我通常建议多入口项目使用runtimeChunk: 'single',把整个应用的运行时统一成一个文件,配合splitChunks抽出的vendors、common,能获得一个非常稳定的缓存策略。单入口项目,如果追求极致缓存,也建议把runtime单独抽出来,否则修改一处业务代码可能导致主bundle的hash发生变化,让用户重新下载整个主文件,这对大项目来说是很亏的。
4. 实战:从单页到多页,entry配置的完整演进路线
4.1 单页应用的entry最小配置
一个最小、够用的单页应用配置,通常长这样:
const path = require('path'); module.exports = { mode: 'production', context: __dirname, entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[contenthash:8].js', clean: true, }, };这里的entry是单字符串,output.filename用了[name],实际会输出类似main.a1b2c3d4.js的文件名。配合HtmlWebpackPlugin时,默认生成的HTML会自动引入这个bundle,不需要额外手写script标签。
在实际业务里,单入口项目随着功能增多会引入代码分割,于是入口代码里会出现大量动态import:
const routes = [ { path: '/user', component: () => import('./pages/User'), }, ];webpack会为每个动态import生成独立的异步chunk,这些chunk的命名默认来自webpack的magic comments或模块路径。你可以在import时手动添加/* webpackChunkName: "user" */来控制分包名。这个操作看似和entry无关,但实际这些异步chunk都归属在main这个entry的chunkGroup下。所以,即使是单入口,你也不可能完全绕开对chunk的认知。
4.2 多页应用(MPA)的entry自动生成方案
多页应用最大的痛点不是“怎么写多个入口”,而是“入口太多、不想手动维护”。手动维护意味着你每新增一个页面,就要在webpack配置里加一行entry,忘加了页面就构建不出来。为了根治这个问题,我推荐用目录扫描的方式自动生成entry。
假设你的目录结构如下:
src/ pages/ home/ index.js index.html about/ index.js index.html list/ index.js index.html那么可以用一个脚本自动生成entryMap:
const fs = require('fs'); const path = require('path'); const pagesDir = path.resolve(__dirname, 'src/pages'); function getEntries() { const entries = {}; fs.readdirSync(pagesDir).forEach((dirName) => { const entryPath = path.join(pagesDir, dirName, 'index.js'); if (fs.existsSync(entryPath)) { entries[dirName] = entryPath; } }); return entries; } module.exports = { entry: getEntries(), };这样,新页面只需要在pages目录下新建一个文件夹并放好index.js,构建就能自动包含它。扩展性的代价是文件名必须严格遵循约定,如果页面文件夹里缺少index.js,它就会被静默跳过,很可能构建产物少了一个页面而不自知。为了解决这个问题,可以在扫描循环里加上日志输出,把每个发现的entry打印出来,或者在缺失时直接抛错,避免静默失败。
4.3 结合HtmlWebpackPlugin的多入口HTML分流
多入口搭配HtmlWebpackPlugin时,最直接的做法是每个页面一个plugin实例:
const HtmlWebpackPlugin = require('html-webpack-plugin'); const entries = getEntries(); const htmlPlugins = Object.keys(entries).map((name) => { return new HtmlWebpackPlugin({ filename: `${name}.html`, template: path.resolve(__dirname, `src/pages/${name}/index.html`), chunks: [name, 'vendors', 'common'], }); }); module.exports = { entry: entries, plugins: [...htmlPlugins], };这里的chunks字段非常关键。如果你不写chunks,默认情况下HtmlWebpackPlugin会把所有entry生成的chunk以及所有split出来的chunk都注入到每一个HTML里。在多页面场景下,这会导致home页面加载了admin页面的js,既浪费流量,又可能因为访问不存在的DOM而报错。手动指定chunks后,每个HTML只加载自己需要的bundle。
另一种写法是使用chunksSortMode来控制chunk的加载顺序,比如chunksSortMode: 'manual'加上数组顺序。不过现代浏览器支持ES Module和defer,顺序问题通常不大,只要保证公共chunk在业务chunk之前加载即可。如果你用传统script标签同步加载,注意common和vendor要放在业务chunk前面,否则运行时会报出“某某模块不存在”的错误。
4.4 开发环境下的entry注意事项:devServer与devtool
开发环境下,很多人会为entry额外添加热更新入口。最典型的例子是配合webpack-dev-server的hot模式,你可能会看到这种配置:
module.exports = { entry: { app: [ 'webpack/hot/dev-server.js', './src/index.js', ], }, devServer: { hot: true, }, };数组入口在这里就派上了用场:把热更新的client脚本和业务入口绑定在一起,构成开发专用的bundle。但实际上,webpack-dev-server的热更新并不依赖你手动添加这个脚本,通常内置的HMR插件会自动注入。如果你在配置里手动添加,反而可能造成重复注入。
更需要注意的是devtool与entry的关系:开发时配置devtool: 'eval-cheap-module-source-map',webpack会对每个模块生成大量source map信息,这些信息会被存进memory,不会污染真实的entry产物。但如果你用entry设置的数组里包含了webpack-dev-server/client,可能会让热更新的初始化时序产生微妙变化。我的经验是:开发环境保持entry干净,把环境差异交给webpack-merge和配置文件拆分处理,而不是在entry里塞各种client脚本。
另外,开发环境可能希望devServer打开时自动指向一个特定页面路径。这可以通过devServer.openPage、或historyApiFallback配合单页面路由来实现,它不直接属于entry的范畴,但需要注意的是:如果你是多页面应用,historyApiFallback默认会把所有未知路径都rewrite到首页,导致刷新about.html时被错误地代理到home页面。这时你需要像这样配置:
devServer: { historyApiFallback: { rewrites: [ { from: /^\/about/, to: '/about.html' }, { from: /^\/home/, to: '/home.html' }, ], }, }多页面开发环境中,直接用/home.html这种真实HTML路径访问即可,让devServer直接服务静态文件,避免rewrite带来的混乱。
5. 常见踩坑实录与排查手册
5.1 数组入口“号称多入口但只产出一个bundle”的真相
我在不少项目里看到过这样的配置:
entry: ['./src/pageA.js', './src/pageB.js'],开发者以为这样就能生成两个bundle、两个页面。结果构建完成后,dist目录里只有一个main.js,打开页面A和B时加载的都是同一个文件。原因前面已经说过了:数组入口本质上是单一入口的“多文件拼接”,所有入口文件都会被合并进同一个chunk。webpack不会因为数组里有多个文件就认为它是多入口。
如果你要的是两个独立页面,必须用对象形式:
entry: { pageA: './src/pageA.js', pageB: './src/pageB.js', },这个坑很隐蔽,尤其在只跑一次构建、不做产物检查时,问题几乎不会暴露。等到真正上线部署,发现页面B加载了页面A的逻辑,才会追悔莫及。
5.2 entry路径大小写与扩展名省略的坑
不同操作系统对路径的大小写敏感度不同。在macOS/Linux上,./src/Index.js和./src/index.js指向的是两个完全不同的文件;在Windows上,文件系统默认不区分大小写,Index.js也能解析到index.js。于是经常出现这样的场景:开发机器是Windows,构建正常;部署到Linux CI机器上,报Module not found: Error: Can't resolve './src/index.js'。
扩展名省略也有风险。webpack默认的resolve.extensions包含.js、.json等,所以你写'./src/index'也能解析到index.js。但如果你在某个文件里把import './utils'写成小写,而源文件名是Utils.js,在大小写敏感的系统上就会失败。entry作为依赖图的根,如果路径错了,整个构建直接挂掉,报错信息往往是一个冷冰冰的Can't resolve。排查方法很简单:用path.resolve拼接绝对路径,并且严格保持文件名字符一致。
5.3 入口文件使用动态require导致打包“假成功”
有时候你会想偷懒,在entry文件里这么写:
const page = process.env.PAGE; require(`./pages/${page}`);这种动态require,webpack在解析依赖时,由于传入的是一个表达式,无法静态分析出确切的模块路径,于是会以./pages/为前缀做一个context module,打包时会把这个目录下的所有可能文件都作为依赖包含进来。结果就是,你本来只想打包一个页面,却把整个pages目录下的所有页面全部打进了bundle,构建还显示“成功”。这会让entry对应的bundle体积无故暴涨,同时运行时的模块选择也完全错误。
如果你确实需要动态选择模块,建议把这个逻辑移到构建配置的function入口里,而不是写在业务代码里。在function入口中读取环境变量,确定返回哪个具体的入口文件路径,让webpack解析到一个明确的静态路径。如果把动态逻辑留在业务代码中,需要使用import()配合变量,但也要尽量使用能推断路径的方式,比如:
const pages = { home: './home/index', about: './about/index', }; import(`./pages/${pages[page]}`);这种写法能帮助webpack定位到确切的模块,生成独立的异步chunk,而不是一锅端。
5.4 多入口时公共模块重复打包与hash不稳定
多入口项目最常见的两个问题:公共模块没有被抽出来,每个bundle体积都很大;或者抽出来了,但入口bundle的hash依然频繁变动,导致缓存失效。
前者很好排查:打开构建产物,对比各entry文件里是否都有一段相同的代码(比如React的运行时)。如果size差异很大,大概率是splitChunks配置没生效。检查一下optimization.splitChunks中是否设置了正确的cacheGroups,以及minChunks和minSize是否合理。有时候不是没生效,而是公共模块被引用次数只有1,没达到minChunks: 2的阈值。这时可考虑把公共库单独用dependOn声明,或者直接把minChunks改成1并加上name,强制把指定模块抽出去。
后者hash不稳定的原因,很可能是runtime没有抽成独立chunk。入口bundle内部引用了其他chunk的id映射关系,一旦异步chunk或split chunk的内容变化,入口文件的hash就会变。解决方案就是前面提过的runtimeChunk: 'single',把运行时独立出来。还有一种情况是output.chunkIds和moduleIds默认使用deterministic算法,但如果你在某些loader或配置中引入了随机生成id的插件,也会影响hash稳定性,这种比较少见,但加密混淆类插件要特别注意。
5.5 实战排查:使用stats分析entry相关chunk
当你不知道entry配置到底产生了哪些chunk时,不要猜,要看构建统计信息。运行webpack构建时增加参数:
npx webpack --json > stats.json然后打开生成的stats.json,查找entrypoints字段:
"entrypoints": { "home": { "name": "home", "chunks": ["home", "vendors", "common"], "assets": [ { "name": "home.a1b2c3d4.js" }, { "name": "vendors.e5f6a7b8.js" } ] } }这个结构能直观地看到每个入口最终关联了哪些chunk、哪些assets。如果发现home这个入口的chunks列表中包含了预料之外的chunk名,比如误被抽到了另一个页面的专属chunk,那就要检查splitChunks的cacheGroups规则是不是太宽泛了。
另一个好用的工具是webpack-bundle-analyzer,它会以图形方式展示每个chunk里到底包含哪些模块。用这个工具可以快速发现某个entry对应的bundle里混入了不该有的模块,比如把另一个页面的路由代码解析了进来。通常这时就是设置了过宽的test正则,或者动态require的context module把所有文件卷了进来。
6. 经验补给站:entry配置的几个进阶技巧
6.1 用函数入口实现按环境/动态目录切换
前面提过函数入口的写法,这里再给一个结合环境变量与多页面的完整示例:
module.exports = (env, argv) => { const target = env.target || 'web'; // 假设有环境参数 if (target === 'web') { return { entry: { app: './src/web/index.js', }, // 其他web相关配置 }; } if (target === 'node') { return { entry: './src/server/index.js', target: 'node', output: { filename: 'server.js', libraryTarget: 'commonjs2', }, }; } throw new Error('Unknown target'); };这种方式特别适合需要一套webpack配置同时构建web端和node端的项目。函数入口可以读argv.mode、env参数,按需返回不同entry。它比“在业务代码里判断环境”更纯粹,因为业务代码依旧保持单一,构建逻辑才做分支。
6.2 entry与output.filename的命名联动
output.filename里的[name]对应entry对象的key。如果你希望不同entry的bundle能区分目录,可以使用output.filename里的函数形式,或结合entry的命名:
output: { filename: (pathData) => { const name = pathData.chunk.name; if (name.startsWith('admin/')) { return 'admin/[name].[contenthash:8].js'; } return '[name].[contenthash:8].js'; }, }但要让[name]包含admin/,entry的key需要写成'admin/index': './src/admin/index.js'。这种带斜杠的entry key在webpack中是被允许的,但注意后续使用HtmlWebpackPlugin的chunks: ['admin/index']时也要保持一致,否则匹配不上。
更规范的做法是保持entry key简洁,在output.filename中通过[name]拼接,然后用output.chunkLoadingGlobal、output.publicPath等控制路径。命名联动不是必须的技巧,但一旦项目变大,合理的命名结构能省下不少后续定位的时间。
6.3 微前端/模块联邦下的entry设计思路
现在很多团队在探讨微前端,webpack 5的ModuleFederationPlugin提供了另一种思路:不通过iframe或script标签拼业务,而是在构建层把模块共享。在这种架构下,entry的角色变得非常微妙。
以某个主应用为例,它的entry可能仍然是一个普通的SPA入口:
const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin; module.exports = { entry: './src/main.js', plugins: [ new ModuleFederationPlugin({ name: 'mainApp', remotes: { app2: 'app2@http://localhost:3002/remoteEntry.js', }, shared: ['react', 'react-dom'], }), ], };这里的entry只是主应用自身的引导入口,但ModuleFederationPlugin会额外生成一个remoteEntry.js,它是一个运行时入口,独立于主entry。这个remoteEntry被其他子应用远程加载,以获取主应用共享的模块。也就是说,在现代前端架构里,“入口”已经不再局限于业务入口,还包含了远程模块暴露的入口。设计时要清晰区分:主entry负责应用引导,remoteEntry负责跨应用模块共享。它们各自的chunk拆分策略也不同,主entry可以splitChunks,remoteEntry通常要保持精简,避免把所有共享代码都塞进remoteEntry,否则子应用加载成本会飙升。
6.4 一些小众但好用的entry相关选项:dependOn、library等
除了常规写法,entry的完整描述对象还可以携带一些额外选项。在webpack 5中,单个entry可以是对象:
module.exports = { entry: { index: { import: './src/index.js', dependOn: 'shared', }, shared: './src/shared.js', }, };dependOn前面说过,用来声明入口之间的依赖,让webpack知道index的运行需要shared先执行。这在需要把基础库单独抽到一个bundle并提前加载时非常有用。
另一个选项是library,虽然通常配置在output上,但也可以基于entry级联使用。在构建类库时,你可能需要多个UMD入口,每个入口对应一个不同的组件库。这时entry的值可以是:
module.exports = { entry: { 'my-lib': { import: './src/index.js', library: { name: 'MyLib', type: 'umd', }, }, }, };不过大部分情况下,把library放在output层级更规范,因为一组入口通常共享同一种输出格式。
还有entry的runtime选项,它允许你为一个入口指定名字,该名字表示对应的runtime chunk名称。结合函数形式的runtimeChunk使用,可以精细控制每个入口的runtime归属。这种用法非常小众,通常只有构建大型微前端架构时才需要动它。
最后再分享一点我的个人经验
entry这个配置项,说难确实不难,但说简单也真不简单。我见过太多项目,一开始为了省事用字符串单入口,业务膨胀后靠动态import硬撑,最后主bundle还是太大,又回过头折腾splitChunks和异步加载;也见过多入口项目为了图快,手动维护一份长长的一两百行的entry对象,每加一个页面都要改配置,肉眼排查错误十分痛苦。
如果你问我有什么核心建议,我会说三条:第一,单页优先用对象入口,哪怕只有一个key,也好过字符串,因为后面扩展时不用改结构;第二,多入口务必让entry生成自动化,用目录扫描代替手动维护;第三,多入口项目一定配splitChunks和runtimeChunk,否则缓存优化无从谈起。entry不是一个写完就不动的配置,它会随着项目架构一起演进。把它理解透,你的webpack才算是真正入了门。