很多刚用esbuild做打包的前端同学,一提老浏览器兼容,第一个动作就是在配置里把target设成es5,以为这样就搞定了,结果发版到IE11或者老Chrome,控制台照样红一片——要么是“flat is not a function”,要么是莫名其妙的语法错误,这坑我踩过好几次,今天就把亲测有效的解决流程说清楚:除了改target,必须加上构建自动化的兼容性校验和未知API预防,这俩才是发布前不能省的步骤。
一、为什么只改target不够?
很多人对esbuild的target参数有误解,觉得设成老版本就解决所有问题,实则不然。target的核心作用是把新语法转成老浏览器能识别的旧语法,但它管不了内置API。比如用了数组的flat方法,target设成es5只能把可选链或者异步函数转成es5能懂的写法,但flat是es2019新增的数组原型方法,IE11的Array根本没有这个方法,所以打包后代码在老浏览器跑,就会报“flat is not a function”;再比如用了Optional Chaining(可选链),如果target设成es5,esbuild会把它转成&&的形式,语法没问题,但如果是ES模块语法,老浏览器不支持的话,还是会报错。
1.1 踩坑实例:语法转了,但API没兼容
我之前做过一个企业内部系统,兼容IE11,打包的时候target设成了es5,结果上线后同事反馈页面空白,一看控制台,报的是“Promise.allSettled is undefined”,因为target转语法,但allSettled是Promise的静态方法,IE11的Promise根本没有这个方法。那段代码大概是这样的:
// 业务源码,用了es2020的Promise.allSettled和数组flat
async function batchLoadData() {
const res = await Promise.allSettled([
fetch('/api/data1'),
fetch('/api/data2')
]);
return res.map(item => item.value?.data).flat();
}
用esbuild打包后的代码,可选链和异步函数都转成了es5的写法,语法没问题,但Promise.allSettled和flat方法在IE11里完全不存在,所以直接报错。如果我只改target,哪怕设成es3,也补不全这些API的缺失,这就是问题的核心。
二、构建自动化的兼容性校验:提前把问题挡在打包前
既然target管不了API,那怎么提前发现代码里的兼容问题?答案是做构建自动化的静态校验,在打包前就检查代码里的API、语法在目标浏览器里是否支持,通不过就不打包,这样就能把问题掐死在开发阶段。
2.1 用eslint-plugin-compat做静态检查
要做这个校验,最常用的工具是eslint-plugin-compat,它是eslint的一个插件,会根据你设置的目标浏览器(比如browserslist里的配置),检查代码里用到的所有API、语法、方法是否在目标浏览器里存在。比如你目标是IE11,它就会检测你有没有用Promise.allSettled、flat这些IE11不支持的东西,有就直接报错,不会让代码进打包流程。
2.2 把校验集成到构建流程里
光装插件没用,得把它加进打包的命令里,这样每次打包都会自动校验。我一般是在package.json的scripts里加两步:先跑兼容性校验,校验通过了才执行打包,校验不通过就直接终止。具体配置如下:
{
"scripts": {
"check-compat": "eslint src --ext .js --plugin compat --rule 'compat/compat: error'",
"build": "npm run check-compat && esbuild src/index.js --bundle --outfile=dist/bundle.js --target=es5"
},
"browserslist": ["ie 11", "chrome >= 50", "firefox >= 45"]
}
这里解释一下:check-compat命令会调用eslint,检查src下所有js文件的兼容性,--plugin compat是启用兼容性插件,--rule 'compat/compat: error'是把兼容性问题设成错误级别的,只要有问题就直接报错。build命令先跑check-compat,过了才执行esbuild打包,这样就从流程上杜绝了有兼容问题的代码上线。
三、未知API缺失的预防:按需补polyfill才是正解
哪怕做了静态校验,有时候还是会漏,比如用了某个第三方库的新API,自己没注意到,或者校验工具没覆盖到,这时候就需要给缺失的API补polyfill,但补polyfill不能乱补,全量polyfill会让打包体积变大,加载变慢,最好是按需补用到的API,只补项目里实际用到的那些。
3.1 esbuild搭配core-js的配置
要做按需polyfill,我用的是@esbuild-plugins/core-js这个插件,它能让esbuild在打包的时候,只给用到的API加对应的core-js polyfill,不会加全量。配置的时候要注意和browserslist、core-js版本对应,不然补的polyfill不对。具体的esbuild配置文件如下:
// esbuild.config.js,这里用的是esbuild + core-js@3
import { defineConfig } from 'esbuild';
import coreJsPlugin from '@esbuild-plugins/core-js';
export default defineConfig({
entryPoints: ['src/index.js'], // 入口文件
bundle: true, // 打包成单文件
outfile: 'dist/bundle.js', // 输出路径
target: 'es5', // 语法转成es5
plugins: [
coreJsPlugin({
version: '3', // core-js的版本,必须和项目里安装的一致
targets: 'ie 11', // 目标浏览器,和browserslist一致
proposals: true // 包含还在提案阶段的API,避免漏补
})
]
});
这个配置的好处是,打包的时候,只要项目里用到flat、Promise.allSettled这些API,插件就会自动给这些API加上对应的polyfill,不会加多余的,比如如果项目里没用到flat,就不会补flat的polyfill,打包体积控制得很好。
3.2 配合package.json的browserslist
刚才的配置里target设成了es5,但更准确的是用browserslist里的配置,比如刚才的browserslist是["ie 11", "chrome >= 50", "firefox >= 45"],这样插件会根据这个列表来补对应的polyfill,而不是笼统的es5,这样补的polyfill更精准,比如IE11需要补Promise.all的polyfill,但Chrome50已经支持了,就不会补。
四、这个流程的适用场景和优缺点
4.1 适合哪些项目?
这个流程最适合需要兼容老旧浏览器的项目,比如企业内部管理系统(很多老员工用IE11)、面向金融/政务的项目(有合规兼容要求)、第三方组件库(要兼容不同版本的宿主环境)。如果是C端新业务,只需要兼容最新的Chrome/Firefox,就没必要这么麻烦,但如果是ToB或者兼容老系统的项目,这个流程能省很多线上排查的功夫。
4.2 优点和要注意的点
优点很明显:一是自动化,每次构建前自动检查,不用手动测;二是精准,按需补polyfill,体积不会变大;三是彻底,既管语法又管API,解决老浏览器的大部分问题。 要注意的点也有几个:一是browserslist要和所有工具(eslint-plugin-compat、core-js插件)的目标一致,不然校验和补polyfill的浏览器不一样,会出问题;二是不要依赖core-js的旧版本,要用最新的3.x版本,API覆盖更全;三是这个流程会增加一点点构建时间(大概几毫秒到几十毫秒),对大型项目来说可以忽略,但如果是超小型项目,可根据情况调整;四是要定期更新browserslist,去掉已经不用兼容的旧浏览器,减少不必要的polyfill。
五、最后总结
很多人在前端兼容上的误区是只改打包工具的target,殊不知target只管语法,管不了API的缺失。要彻底解决老浏览器的兼容问题,必须在构建流程里加上两步:一是自动化的兼容性静态校验,提前发现代码里的兼容问题;二是按需补polyfill,避免API缺失的错误。这两个步骤加上改target,才是发布前的必备流程,能帮你避免大部分线上的兼容故障,尤其是对需要兼容老旧浏览器的项目来说,这几步绝对不能省。
评论
围绕“esbuild压缩后的产物在老旧浏览器中出现语法错误,除修改target外,构建自动化的兼容性校验流程并预防未知API缺失是发布前的必备步骤。”参与讨论