如何动态读取 browserslist 配置决定 polyfill 注入

解读

这道题表面问“怎么读 browserslist”,实质考察候选人能否把 “构建目标 → 浏览器范围 → 代码转换策略” 这条链路在 Grunt 里跑通。国内项目普遍要兼容 微信内置 WebView、QQ 浏览器、360 极速、Safari iOS 9+、Chrome 51+ 等“钉子户”,polyfill 一旦打多包体爆炸,打少线上白屏。面试官想听的是:

  1. 你能在 Grunt 任务里实时拿到 browserslist(不是写死);
  2. 能根据结果动态收紧或放宽 @babel/preset-env 的 useBuiltIns
  3. 整个流程对现有任务无侵入、可缓存、可调试

知识点

  1. browserslist 的解析顺序:package.json 字段 → .browserslistrc → grunt 任务参数 → 默认 "> 0.5%, last 2 versions"。
  2. browserslist 官方解析库browserslist 本身返回字符串数组,可再交给 @babel/helper-compilation-targets 得到 { chrome: '51', ios: '9' } 结构。
  3. Grunt 运行时序grunt.initConfig 之前执行 grunt.file.readJSON 同步读包,在任务体里再读会触发 IO 抖动;正确姿势是在 Gruntfile 顶部一次性解析,挂到 grunt.config('meta.browserslist') 上供后续任务消费。
  4. babel-loader / grunt-babel 的 dynamic 配置options.presets[0][1].targets 必须是一个对象,不能传字符串数组,否则 @babel/preset-env 会再次调用 browserslist 导致双重解析。
  5. polyfill 粒度控制
    • useBuiltIns: 'entry' 需要在入口 import 'core-js/stable'
    • useBuiltIns: 'usage' 无需手写 import,但要求 @babel/plugin-transform-runtime 关闭 corejs 避免重复注入
  6. 国内常见坑
    • 公司私有 Nexus 源里 core-js 版本锁 2.x,必须显式声明 corejs: 3,否则 babel 会静默降级;
    • 微信小程序 web-view 页面会强制缓存首次 JS,Grunt 任务里需加 ?v=[hash] 让入口重新触发 entry polyfill 注入。

答案

  1. 安装依赖

    npm i -D grunt-babel @babel/core @babel/preset-env core-js browserslist
    
  2. 在 Gruntfile 顶部同步读取并解析 browserslist

    const browserslist = require('browserslist');
    const { default: getTargets } = require('@babel/helper-compilation-targets');
    
    // 读取项目根目录下的实际配置
    const browsers = browserslist(); // 会自动走 .browserslistrc > package.json
    const targets = getTargets(browsers, {});
    
    module.exports = function (grunt) {
      grunt.initConfig({
        meta: { browserslist: targets }, // 缓存给后面任务用
        babel: {
          options: {
            presets: [
              ['@babel/preset-env', {
                targets: '<%= meta.browserslist %>', // 动态注入
                useBuiltIns: 'usage',
                corejs: 3
              }]
            ]
          },
          dist: {
            files: [{
              expand: true,
              cwd: 'src',
              src: '**/*.js',
              dest: 'dist'
            }]
          }
        }
      });
    
      grunt.loadNpmTasks('grunt-babel');
      grunt.registerTask('build', ['babel']);
    };
    
  3. 验证
    运行 grunt build --verbose,控制台会打印

    [Babel] Based on your targets: { chrome: '51', ios: '9' }, added polyfills: es.promise, es.object.assign …
    

    证明 browserslist 被实时解析并精准注入,未出现 core-js@2 或多余 polyfill。

拓展思考

  1. 多环境构建:国内常见“内网演示机”只需兼容 Chrome 最新,而“生产包”需拉回全量 browserslist。可在 Gruntfile 里用 grunt.option('env') 区分,动态切换两个 browserslist 查询语句,实现“一套源码,两套靶标”。
  2. 缓存提速grunt-babel 每次全量跑太慢,可引入 grunt-cache-babel 或自建 grunt-newer把 browserslist 字符串作为缓存 key 的一部分,配置一旦变更即失效重编。
  3. 非 JS 资源:postcss、autoprefixer 同样读 browserslist,在 Grunt 侧统一解析一次后通过 grunt.config('meta.browserslist') 共享,避免三处重复查询。
  4. 微前端场景:子应用独立仓库,母应用通过 browserslist-config-xxx 包下发统一靶标。Grunt 任务里 require('browserslist-config-xxx') 拿到数组再解析,实现“集团统一、项目零感知”