青铜:全局全量引入
最简单粗暴的方案,在HTML或JS入口直接引入完整的polyfill文件。
html
<!-- 在<head>或<body>开头 -->
<script src="./polyfill.js"></script>
js
// 或在JS入口文件第一行
import "./polyfill.js";
优点:
- 引入极其省事,一行代码搞定
- 覆盖全面,不用担心遗漏任何特性
缺点:
- 体积巨大,通常200KB+(未压缩)
- 不管实际用到多少新特性,全部打包进来
- 首屏加载性能灾难,严重影响用户体验
💡 青铜思维:"能用就行,管他体积多大?"
白银:全局按需引入
进阶方案:编写构建策略,在编译时扫描代码,收集实际用到的polyfill依赖,最后生成一个精简的polyfill.js在入口引入。
这个构建策略可以通过AST分析实现------扫描代码中的新特性用法(如Array.prototype.includes、Promise、async/await等),然后只打包对应的polyfill模块。
优点:
- 相比青铜方案,总体积大幅减少(可能从200KB降到50KB)
缺点:
- 首屏仍需加载全部polyfill,只是体积小了
- 对于大型应用,首屏polyfill体积仍可能成为瓶颈
- 无法实现代码分割,所有页面都加载相同的polyfill集合
💡 白银思维:"我知道我要什么,但只能一次性给"
钻石:useBuiltIns: 'usage'
目前业界主流方案,通过Babel的@babel/preset-env配置,让构建工具根据引用树智能分包。
js
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3,
targets: {
chrome: '60',
ie: '11'
}
}]
]
};
Babel会在转译时扫描每个文件,自动注入该文件所需的polyfill。然后再配合rollup等打包工具,可以实现依赖的最优分配方式:公用部分提取成公用chunk、单独使用的,只会和使用它的模块合到同个chunk中。
优点:
- 智能分包,首屏不会加载全部polyfill
- 自动化程度高,无需手动管理polyfill列表
缺点:
-
只支持core-js:大量core-js中没有的polyfill无法支持(如某些DOM API、特殊的浏览器bug修复等)
-
深层依赖不受配置影响:core-js的模块依赖是固定的。很多polyfill底层依赖,比如Object.defineProperty,即使你的目标浏览器原生支持,也会被打包进来
-
无法分辨降级场景:有些新特性是无法完美模拟的(sham),代码中可能存在判断和降级逻辑,如:
javaScriptif (typeof Symbol === 'function') { // 使用原生Symbol } else { // 降级处理 }Babel看到
Symbol就注入polyfill,可能导致判断逻辑失效
💡 钻石思维:"让工具帮我做,但工具有自己的局限"
王者:rollup-plugin-polyfill-inject
这是真正的王者方案------构建时精准注入,零运行时开销,极致的代码体积控制。
这个Rollup插件的核心思想是:在打包阶段分析AST,精确识别需要polyfill的位置,然后智能注入import语句。
核心特性
1. Polluting模式(污染型polyfill)
针对会修改全局原型的polyfill(如core-js):
javaScript
// rollup.config.js
import polyfill from 'rollup-plugin-polyfill-inject';
export default {
plugins: [
polyfill({
polluting: {
".includes": [
"core-js/modules/es.array.includes",
"core-js/modules/es.string.includes"
],
"Set": "core-js/modules/es.set",
"Map": "core-js/modules/es.map"
}
})
]
};
转换效果:
javaScript
// 源代码
console.log([].includes(1));
var set = new Set();
// 构建后自动注入
import "core-js/modules/es.array.includes";
import "core-js/modules/es.set";
console.log([].includes(1));
var set = new Set();
2. Pure模式(非污染型polyfill)
不污染全局,排除typeof判断和window判断:
javaScript
polyfill({
pure: {
".padStart": "core-js-pure/actual/string/pad-start",
"Promise": "core-js-pure/actual/promise"
}
})
转换效果:
javaScript
// 源代码
"abc".padStart(5, "0");
Promise.resolve(42);
// 构建后
import padStart from "core-js-pure/actual/string/pad-start";
import Promise from "core-js-pure/actual/promise";
padStart("abc", 5, "0");
Promise.resolve(42);
3. Getter/Setter转换
更强大的能力------可以替换属性访问为函数调用:
javaScript
polyfill({
getter: {
".textContent": "@/utils/getNodeText",
"document.currentScript": ["sky-core/utils/getCurrentScript", 'getCurrentScript']
},
setter: {
".textContent": "@/utils/setNodeText",
"document.title": ["@/utils/jsBridge", 'setActivityTitle']
}
})
转换效果:
javaScript
// 转换前
var text = document.getElementById("foo").textContent;
document.title = "New Title";
// 转换后
import getNodeText from "@/utils/getNodeText";
import { setActivityTitle } from "@/utils/jsBridge";
var text = getNodeText(document.getElementById("foo"));
setActivityTitle("New Title");
4. Error特殊处理
针对类继承和Error cause特性的精准注入:
javaScript
polyfill({
polluting: {
"AggregateError": "sky-core/polyfill/AggregateError"
},
error: {
"Error": "sky-core/pure/Error" // 只在new Error(..., { cause })时注入
}
})
转换效果:
javaScript
// 转换前
var err = new Error("error");
var err2 = new Error("error", { cause: 1 });
var err3 = new AggregateError([], "msg");
var err4 = new AggregateError([], "msg", { cause: 1 });
// 转换后
import "sky-core/polyfill/AggregateError";
import CauseError from "sky-core/pure/Error";
import CauseAggregateError from "sky-core/pure/AggregateError";
new Error("error");
new CauseError("error", { cause: 1 });
new AggregateError([], "msg");
new CauseAggregateError([], "msg", { cause: 1 });
5. Timer回调参数检测
智能检测setTimeout/setInterval是否传入了参数:
javaScript
polyfill({
timer: {
"setTimeout": "sky-core/pure/setTimeout",
"setInterval": "sky-core/pure/setInterval"
}
})
转换效果:
javaScript
// 源代码
let args = [0, 1, 2];
setTimeout(function() {}, 0); // 不转换
setTimeout(function() {}, ...args); // 转换!
setTimeout.apply(window, args); // 转换!
// 构建后
import argsSetTimeout from "sky-core/pure/setTimeout";
let args = [0, 1, 2];
setTimeout(function() {}, 0);
argsSetTimeout(function() {}, ...args);
argsSetTimeout.apply(window, args);
💡 王者选手的思维:"我要在构建阶段就完成一切优化,运行时零负担"
还有高手?typescript-plugin-polyfill
如果说rollup-plugin是"构建时AST分析"的王者,那typescript-plugin就是 "基于类型系统的精准打击" ------真正的宗师级方案。
核心突破:利用TypeScript类型信息
传统方案的最大痛点:同名方法无法区分类型 。比如.at()方法,数组和字符串都有,但Babel看到xxx.at()时,不知道xxx是数组还是字符串,只能两个polyfill都引入。
typescript-plugin-polyfill利用TypeScript的类型系统,在编译时精确判断变量类型:
typeScript
// 源代码
const arr: string[] = ['a', 'b'];
const str: string = 'hello';
arr.at(0); // TypeScript知道这是数组
str.at(0); // TypeScript知道这是字符串
配置方式:
javaScript
// 配合Rollup使用
import typescript from '@rollup/plugin-typescript';
import polyfillPlugin from 'typescript-plugin-polyfill';
export default {
plugins: [
typescript({
transformers: (program) => ({
before: [
polyfillPlugin(program, {
polluting: {
at: {
Array: 'sky-core/polyfill/Array/prototype/at', // 数组用
String: 'sky-core/polyfill/String/prototype/at' // 字符串用
},
includes: {
Array: 'sky-core/polyfill/Array/prototype/includes',
String: 'sky-core/polyfill/String/prototype/includes'
}
}
})
]
})
})
]
};
精准注入效果:
typeScript
// 转换前
const arr: string[] = ['a', 'b'];
const str: string = 'hello';
arr.at(0);
str.at(0);
// 转换后 - 基于类型只引入需要的polyfill
import 'sky-core/polyfill/Array/prototype/at';
import 'sky-core/polyfill/String/prototype/at';
const arr: string[] = ['a', 'b'];
const str: string = 'hello';
arr.at(0);
str.at(0);
💡 宗师级思维:"类型即契约,利用类型系统做最精准的决策"
总结
一般的polyfill引入方式只是够用,要真正的优雅,还得靠 rollup-plugin-polyfill-inject 和 typescript-plugin-polyfill。当然一般业务开发者是无需考虑兼容性的,处理构建问题是架构层做的。
本文提到的工具:
- (www.npmjs.com/package/rol...) rollup-plugin-polyfill-inject - 构建时精准注入polyfill
- (www.npmjs.com/package/typ...) typescript-plugin-polyfill - 基于TypeScript类型的polyfill注入