集成 Intl Polyfill 并在 grunt 中按地区打包

解读

面试官想考察三件事:

  1. 你是否理解 Intl API 的浏览器兼容性缺口 以及 polyfill 的体积代价
  2. 能否在 Grunt 体系 里把「条件引入 + 地区分包」做成 可维护、可缓存、可增量构建 的工程化方案;
  3. 是否具备 国内落地意识:CDN 合规备案、ES5 语法向下兼容、Gzip/Brotli 双压缩、SourceMap 回源策略、小程序 WebView 白名单等。

知识点

  • Intl 细分Intl.DateTimeFormatIntl.NumberFormatIntl.CollatorIntl.PluralRulesIntl.RelativeTimeFormat
  • polyfill 选型intl, intl-locales-supported, @formatjs/intl-* 系列;
  • locale-data 按需加载cldr-datacldr-core 的 npm 镜像源(淘宝源)同步;
  • Grunt 多任务范式grunt.registerMultiTask + this.files 动态映射;
  • 条件注入模板grunt.template.process + lodash.template 生成 entry.js
  • 文件指纹grunt-filerev 配合 grunt-assets-inline 把 locale 包路径写进 HTML;
  • 差异化压缩grunt-contrib-uglifycompress.global_defs 剔除 DEBUG 代码;
  • 国内部署:阿里云 OSS 分地区 Bucket、华为云 FunctionGraph 边缘 gzip、微信开发者工具 ES6 转 ES5 开关。

答案

  1. 安装依赖

    npm i -D intl intl-locales-supported cldr-data
    npm i -D grunt-contrib-clean grunt-contrib-copy grunt-contrib-uglify grunt-contrib-concat grunt-filerev grunt-replace
    
  2. 目录约定

    src/
      js/
        entry.js
        intl-shim.js
    locale-data/
      zh.json
      en.json
    dist/
      js/
        app.<hash>.js
        intl/<locale>/polyfill.<hash>.js
    
  3. Gruntfile.js 核心片段

    module.exports = function(grunt) {
      const localeList = ['zh', 'en', 'ja'];          // 国内业务常用
      grunt.initConfig({
        clean: { dist: 'dist' },
    
        // 1. 拷贝并按 locale 拆包
        copy: {
          intl: {
            files: localeList.map(l => ({
              expand: true,
              cwd: 'node_modules/intl/locale-data/jsonp',
              src: `intl-${l}*.js`,
              dest: `dist/js/intl/${l}/`,
              rename: () => `polyfill.js`
            }))
          }
        },
    
        // 2. 合并 shim + locale 数据
        concat: {
          intl: {
            files: localeList.map(l => ({
              src: ['src/js/intl-shim.js', `dist/js/intl/${l}/polyfill.js`],
              dest: `dist/js/intl/${l}/polyfill.js`
            }))
          }
        },
    
        // 3. 压缩并加指纹
        uglify: {
          intl: {
            files: localeList.map(l => ({
              src: `dist/js/intl/${l}/polyfill.js`,
              dest: `dist/js/intl/${l}/polyfill.js`
            }))
          }
        },
        filerev: {
          intl: { src: 'dist/js/intl/**/*.js' }
        },
    
        // 4. 把带 hash 的路径写回 HTML
        replace: {
          html: {
            src: 'dist/*.html',
            overwrite: true,
            replacements: localeList.map(l => ({
              from: `__INTL_${l.toUpperCase()}__`,
              to: grunt.filerev.summary[`dist/js/intl/${l}/polyfill.js`]
            }))
          }
        }
      });
    
      grunt.registerTask('intl', ['copy:intl', 'concat:intl', 'uglify:intl', 'filerev:intl', 'replace:html']);
      grunt.registerTask('default', ['clean', 'intl']);
    };
    
  4. 运行时加载策略(entry.js)

    const locale = window.LOCALE || navigator.language.slice(0, 2);
    if (!window.Intl || !Intl.DateTimeFormat.supportedLocalesOf(locale).length) {
      const s = document.createElement('script');
      s.src = `__INTL_${locale.toUpperCase()}__`;   // 被 grunt-replace 替换
      s.async = false;
      document.head.appendChild(s);
    }
    
  5. 国内优化细节

    • cldr-data 镜像指向 淘宝 npm 镜像 加速 CI;
    • 产出文件同时生成 .br.gz,OSS 配置 Brotli 优先级
    • 针对 微信 WebView 强制 ES5 语法, uglify 输出 ie8: true
    • 若走 小程序 web-view,域名需加入 业务域名白名单 并上 HTTPS;
    • 为了 SEO 预渲染,在 grunt-phantom 任务里把 locale 包预加载,避免首屏空白。

拓展思考

  1. 如果业务扩张到 50+ 地区,Grunt 的「枚举式」配置会爆炸,可改用 grunt-contrib-requirejsinclude + exclude 动态算子,或者把任务迁移到 Rollup 的代码分割 再回退到 Grunt 调用。
  2. 为了 SSR 同构,需要在 Node 端同样做 locale 分包,可用 intl-locales-supportedEgg.js 中间件 里按需 require(),避免把全部 cldr 数据打进 Docker 镜像。
  3. 国内 隐私合规 要求不能把用户语言偏好回传海外 CDN,可在 边缘函数(阿里云 EdgeRoutine) 里根据 Accept-Language 直接 302 到最近的地区包,减少回源。