前端项目体积膨胀时,Webpack的externals配置经常被当作一剂快速减肥药。但实际使用中,它并不是把依赖删掉那么简单,而是改变了依赖在运行时的获取方式。这篇文章从配置、原理到误区完整拆解,帮你把externals用得稳一点,避免构建完一上线就报全局变量未定义。

一、外部扩展Externals解决什么问题
Webpack正常处理第三方依赖时,会从node_modules里读取模块代码,经过解析、转换、压缩后合并进最终的bundle。这样做的优点是项目自包含,但缺点也很明显:像React、Vue、echarts、lodash这类体积较大的库,每次打包都会把完整代码塞进产物,拖慢构建速度,也增加了浏览器需要下载的脚本体积。
externals的作用就是告诉Webpack:某些模块在打包时不要处理,它们在运行时已经由外部环境提供。比如页面通过<script>标签引入了CDN上的jQuery,那么window对象上就会有一个jQuery全局变量。配置externals后,源码里的import $ from 'jquery'会被转换成对全局变量jQuery的引用,而不是把整个jQuery库打包进bundle。
这样做能带来两个直接好处。一是减小bundle体积,让业务代码和第三方库分离;二是可以利用公共CDN缓存,如果用户访问过其他同样使用该CDN地址的站点,浏览器很可能已经缓存过这份脚本,首屏加载会更快。不过需要注意的是,externals只是排除了打包,并没有自动把CDN脚本注入页面,这个动作需要开发者自己完成。
二、Externals的配置方式与生效原理
最常用的写法是对象映射,键是模块名,值是运行时提供的全局变量名。下面这个例子中,jquery模块会被映射到全局jQuery,react映射到React,react-dom映射到ReactDOM。
module.exports = {
externals: {
jquery: 'jQuery',
react: 'React',
'react-dom': 'ReactDOM'
}
};
如果你需要兼容不同的模块系统,比如CommonJS、AMD或者直接挂载在window上的全局变量,可以使用对象详细配置。下面的写法分别指定了不同模块环境下的取值,Webpack会根据打包目标选择对应的字段。
module.exports = {
externals: {
lodash: {
commonjs: 'lodash',
commonjs2: 'lodash',
amd: 'lodash',
root: '_'
}
}
};
除了对象写法,externals还支持正则和函数。正则适合批量匹配模块名,比如把所有以jquery开头的插件都标记为外部扩展。函数形式则更灵活,可以在回调里判断模块名、发起文件的路径,再决定是否以及如何排除。例如下面的代码只处理名为jquery的请求,其他模块正常打包。
module.exports = {
externals: [
function (context, request, callback) {
if (request === 'jquery') {
return callback(null, 'jQuery');
}
callback();
}
]
};
从原理上看,Webpack在解析模块依赖图时,一旦发现某个import请求命中了externals规则,就不会去读取该模块的源码,而是生成一段引用外部变量的代码。对于ES模块打包场景,这段代码通常类似module.exports = jQuery。因此externals是否生效,最终取决于运行时环境里是否真的存在对应全局变量,这也是很多问题产生的根源。
三、容易被忽略的误区和避坑方法
第一个高频误区是只配置externals却不引入CDN脚本。构建阶段不会有报错,因为Webpack不关心运行时是否存在全局变量。上线后浏览器执行到业务代码时才会抛出jQuery is not defined或React is not defined。排查这类问题,先打开浏览器控制台输入window.jQuery或window.React,看看全局对象上是否真的有值。
第二个误区是模块名和全局变量名映射错误。Webpack的externals不会自动转换大小写,例如'react-dom'对应的全局变量是ReactDOM,写成ReactDom就会运行时失败。同样,jQuery插件库里经常用'jquery'作为模块名,而全局变量可能是jQuery而不是$,配置时两者必须严格匹配。
第三个误区是混淆了模块导入方式和实际运行环境。某些库在package.json里同时提供了ES Module、CommonJS、UMD等多种产物,配置externals时如果只指定了root字段,在CommonJS环境下打包可能拿不到预期值。如果项目里有多种模块规范混用,建议用详细对象写法同时声明commonjs、commonjs2、amd和root,减少环境差异带来的问题。
第四个误区是忽略子路径。比如代码中使用了import fp from 'lodash/fp',而externals只配置了lodash,Webpack并不会自动把子路径也排除,最终lodash/fp仍然会被打进bundle。遇到这种场景,需要为子路径单独配置规则,或者用函数形式统一处理。
四、结合CDN与构建优化的实践建议
外部扩展最适合那些稳定、体积大、且不需要频繁更新的第三方库。React、Vue、jQuery、echarts都是典型候选。内部业务代码则不建议排除,因为业务代码改动频繁,外部化后版本管理会变复杂,还容易破坏构建产物的一致性。
在HTML里引入CDN脚本时,建议将<script>标签放在业务bundle之前。下面是一个简单的示例,展示了先加载jQuery,再加载业务脚本的顺序。
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="/dist/main.bundle.js"></script>
如果担心CDN脚本加载阻塞页面渲染,可以使用defer或async属性,但要确保业务脚本依赖这些全局变量时,执行顺序仍然正确。简单的做法是把CDN脚本放在head中使用defer,业务脚本也使用defer,这样两者按顺序执行;或者把所有脚本统一放在body底部,利用HTML文档自上而下的加载顺序保证依赖先就绪。
构建优化方面,externals与splitChunks可以配合使用,但不要把它们看成二选一的关系。splitChunks解决的是重复模块和公共代码的拆分问题,externals解决的是完全不打包特定依赖的问题。一个实用的策略是:将体积大且运行时稳定的库交给CDN,将项目内部抽出的公共模块交给splitChunks管理。同时注意排查问题时要看构建产物,确认目标库已经不在bundle里;再看页面加载的网络请求,确认CDN脚本返回正常;最后在控制台检查全局变量是否存在。三步下来,大部分externals相关的坑都能定位清楚。

