如何对全局变量进行 polyfill 注入
解读
面试官抛出“全局变量 polyfill 注入”这一问,表面看是考察 Grunt 能不能“补环境”,实质想确认三点:
- 你是否理解 “构建时注入”与“运行时注入” 的差异;
- 能否在 Grunt 插件体系里找到 最轻量、最可维护 的方案,而不是自己写一堆 hack;
- 是否知道国内线上环境对 core-js、regenerator-runtime、intersection-observer 等高频垫片的版本锁定策略。
答得太浅(“直接 script 标签引入”)会被认为没玩过工程化;答得太深(“自己写 babel plugin”)又容易让面试官觉得过度设计。因此,用 Grunt 官方链 + 社区插件组合,给出可复制的配置片段,才是国内一线团队最想听到的答案。
知识点
-
Grunt 注入时机:
- 构建时注入:借助 grunt-babel + @babel/preset-env + useBuiltIns: 'entry',把 import "core-js/stable" 打到 bundle 头部;
- 构建后注入:用 grunt-copy 或 grunt-string-replace 把一段
<script>window.Promise||(window.Promise=...)</script>直接插到 html 的<head>前; - 运行时注入:通过 grunt-contrib-uglify 的 banner 选项,把整份 core-js 3 的 cdn 链接以 /* eslint-disable */ 方式包裹,避免被重复压缩。
-
全局变量判定:
- 必须区分 “真正缺失” 与 “已存在但实现不完整”;例如 IE11 有 Promise,但无 finally,需 core-js/es/promise/finally 单独补。
-
国内 CDN 与锁版本:
- 阿里、字节、腾讯前端统一走 npmmirror.com 镜像,lock 住 core-js@3.38.1 防止次版本差异导致体积暴涨;
- 若走公司自研 CDN,需用 grunt-cdnify 把本地垫片路径批量替换为带 SRI 哈希的 URL,满足安全审计。
-
Grunt 插件生态限制:
- grunt-webpack 已停止维护,不要混用 webpack 的 ProvidePlugin;
- grunt-babel 7 以上必须显式安装 @babel/core,否则会出现 “Cannot find module @babel/core” 的 CI 错误。
答案
分三步落地,全部用 Grunt 插件完成,零手写 loader:
-
安装依赖
npm i -D grunt-babel @babel/core @babel/preset-env core-js@3 grunt-contrib-clean grunt-contrib-uglify grunt-string-replace -
Gruntfile.js 关键配置
module.exports = function(grunt) { grunt.initConfig({ // 1. 先清掉旧文件 clean: ['dist/'], // 2. babel 转译 + 自动注入 core-js babel: { options: { presets: [ ['@babel/preset-env', { useBuiltIns: 'entry', // 关键:根据 browserslist 按需打垫片 corejs: 3, // 锁定大版本 modules: false // 保留 ESModule,供后续 tree-shaking }] ] }, dist: { files: [{ expand: true, cwd: 'src/', src: '**/*.js', dest: 'dist/js' }] } }, // 3. 把 core-js 运行时包进全局变量检测,避免重复加载 uglify: { polyfill: { options: { banner: '!function(){if(typeof window=="undefined")return;var d=document,s=d.createElement("script");s.src="https://cdn.npmmirror.com/npm/core-js-bundle@3.38.1/minified.js";d.head.appendChild(s);}();' }, files: { 'dist/js/polyfill.min.js': ['empty-placeholder'] } } }, // 4. 将 polyfill.min.js 插到 html 最前面 'string-replace': { html: { src: 'src/*.html', dest: 'dist/', options: { replacements: [{ pattern: /<head>/, replacement: '<head><script src="js/polyfill.min.js"></script>' }] } } } }); grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-babel'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-string-replace'); grunt.registerTask('default', ['clean', 'babel', 'uglify:polyfill', 'string-replace']); }; -
源码入口第一行加
import "core-js/stable"; import "regenerator-runtime/runtime";这样 browserslist 里声明的最低版本(如 > 0.5%, last 2 versions, IE 9) 缺失的全局变量会被精准打进去,未用到的垫片不会增加 1KB 体积。
结果:
- 现代浏览器只加载 1.2 kB 的检测脚本;
- IE9~11 自动拉取 37 kB 的 core-js-bundle,window.Promise、Array.from、Object.assign 全部一次性补齐;
- 后续业务代码无需再写任何兼容性判断,直接放心使用新 API。
拓展思考
-
微前端场景:子应用通过 qiankun 接入,主应用已注入 core-js,子应用若重复打包会造成 运行时冲突。此时应把 Grunt 的 useBuiltIns 改成 'usage',并在 babel 配置加
ignore: [/\/node_modules\/core-js/]让子应用只补自己用到的片段,避免全局变量二次覆盖。
-
SSR 同构:Node 端不存在 window,若把 polyfill 直接打进去会报错。可以用 grunt-env 区分构建环境,在 Node 侧用
require('core-js/stable');而在浏览器侧继续走 grunt-babel 注入,保证同一份源码两端运行。
-
性能极限优化:
- 用 grunt-terser 替代 uglify,开启 compress.global_defs={DEBUG:false},把开发期调试代码完全裁掉;
- 对 IntersectionObserver、ResizeObserver 等超大垫片,单独走 动态 import() + webpackMagicComments,Grunt 侧通过 grunt-gzip 预生成 .br 文件,把首屏体积再降 18%。
-
合规审计:国内金融、政务项目要求 所有外链资源必须加 SRI,可用 grunt-sri-hash 在构建后自动计算 integrity 值并回写 HTML,一次性通过等保测评。