如何在 grunt 中实现语言包 CDN 动态加载

解读

面试官真正想考察的是:

  1. 你是否理解 Grunt 仅负责“构建期”,而“动态加载”发生在 浏览器运行时
  2. 你能否用 Grunt 把语言包构建成 按语言分片、带版本号、可 CDN 缓存 的静态资源,并生成一份 运行时配置表 供前端按需拉取;
  3. 你是否熟悉国内主流 CDN(阿里云 OSS+CDN、腾讯云 COS+CDN、七牛、又拍)的上传与刷新流程,并能用 Grunt 插件把“上传+刷新”串进流水线;
  4. 你是否考虑 回退策略(CDN 失败走本地包)、缓存策略(文件名带 hash)、跨域策略(CORS 头)等工程化细节。
    一句话:Grunt 不直接“动态加载”,而是“构建出可供动态加载的 CDN 资源”并“生成加载逻辑所需的数据”

知识点

  • Grunt 任务阶段:initConfig → registerTask → run;所有“动态”逻辑必须提前编译成静态映射。
  • 文件指纹:grunt-contrib-rename、grunt-filerev 可为语言包打出 {lang}.{hash}.json 格式,解决 CDN 缓存击穿。
  • 多语言分片:通过 grunt-i18n-extract 或自定义 grunt-multi-dest 把源码中的翻译键值拆成 en.json、zh-CN.json、ja.json 等独立文件。
  • CDN 上传与刷新
    – 阿里云:grunt-oss-upload(支持 sts 临时密钥)+ grunt-oss-cloudfront(刷新边缘节点)。
    – 腾讯云:grunt-cos-upload + grunt-cdn-refresh。
    上传时必须设置 Cache-Control: max-age=31536000 与 **Access-Control-Allow-Origin: ***,否则会出现跨域或缓存失效。
  • 运行时配置:Grunt 在构建尾声生成 i18n-manifest.json,内容示例:
    {"en":"//cdn.example.com/i18n/en.3ab7f2.json","zh-CN":"//cdn.example.com/i18n/zh-CN.8e9c1d.json"}
    前端通过 import(//cdn.example.com/i18n/${lang}.${hash}.json) 实现真正的“动态加载”。
  • 回退与灰度:在 manifest 中保留 "localPath": "/static/i18n/${lang}.json",当 CDN 请求 4xx/5xx 时前端立即回退;同时可利用 grunt-replace 把灰度语言列表写进 HTML 注释,供运行时判断是否加载实验语言包。
  • 性能指标:国内 CDN 首包 30 KB 时延 < 150 ms(华北节点),Grunt 构建阶段需开启 gzip(grunt-contrib-compress)与 brotli(grunt-brotli)双压缩,减少 25% 体积。

答案

  1. 拆分任务链
    grunt.registerTask('i18n-cdn', ['i18n-extract', 'filerev:i18n', 'compress:i18n', 'oss_upload:i18n', 'cdn_refresh:i18n', 'replace:injectManifest']);

  2. 关键配置片段(Gruntfile.js)

grunt.initConfig({
  // 1. 提取语言包
  i18n_extract: {
    src: ['src/**/*.{js,vue}'],
    dest: 'tmp/i18n'
  },
  // 2. 打指纹
  filerev: {
    i18n: {
      src: 'tmp/i18n/*.json',
      dest: 'dist/i18n'
    }
  },
  // 3. 双压缩
  compress: {
    i18n: {
      options: { mode: 'gzip' },
      expand: true,
      cwd: 'dist/i18n',
      src: '*.json',
      dest: 'dist/i18n',
      ext: '.json.gz'
    }
  },
  // 4. 上传阿里云 OSS
  oss_upload: {
    i18n: {
      options: {
        accessKeyId:  process.env.ALI_OSS_AK,
        accessKeySecret: process.env.ALI_OSS_SK,
        bucket: 'static-assets',
        region: 'oss-cn-hangzhou',
        headers: {
          'Cache-Control': 'max-age=31536000',
          'Access-Control-Allow-Origin': '*'
        }
      },
      files: [{
        expand: true,
        cwd: 'dist/i18n',
        src: '*.*',
        dest: 'i18n/'
      }]
    }
  },
  // 5. 刷新 CDN
  cdn_refresh: {
    i18n: {
      urls: grunt.file.expand('dist/i18n/*').map(f => {
        const name = f.replace('dist/i18n/', '');
        return `//cdn.example.com/i18n/${name}`;
      })
    }
  },
  // 6. 把 manifest 注入 HTML
  replace: {
    injectManifest: {
      src: 'dist/index.html',
      dest: 'dist/index.html',
      replacements: [{
        from: '</head>',
        to: '<script>window.__I18N_MANIFEST__ = '+ JSON.stringify(
          grunt.file.expand('dist/i18n/*.json').reduce((m, f) => {
            const lang = f.match(/([^\/]+)\.\w{8}\.json$/)[1];
            m[lang] = `//cdn.example.com/i18n/${f.replace('dist/i18n/','')}`;
            return m;
          }, {})
        ) + ';</script></head>'
      }]
    }
  }
});
  1. 运行时加载伪代码
async function loadLocale(lang) {
  const url = window.__I18N_MANIFEST__[lang];
  if (!url) return Promise.reject('unsupported lang');
  try {
    const res = await fetch(url, { credentials: 'omit' });
    if (!res.ok) throw new Error('cdn failed');
    return res.json();
  } catch (e) {
    // 回退本地
    return fetch(`/static/i18n/${lang}.json`).then(r => r.json());
  }
}
  1. 上线 checklist
  • 构建机拥有 STS 临时令牌,避免 AK/SK 落盘。
  • CDN 刷新完成后再发版,否则边缘节点仍返回旧包。
  • 灰度阶段先在 header 注入 x-oss-meta-gray=1,通过 Grunt 的 grunt-header 插件完成,方便回滚。

拓展思考

  1. 非 JSON 格式怎么办? 若产品要求 AMD/UMD 格式语言包,可在 Grunt 链里再加 grunt-wrap 任务,把 json 包装成 define({...}),实现 <script src> 直接加载。
  2. SSR 场景如何同构? 在 Node 层先读取 __I18N_MANIFEST__,利用 accept-language 头直出对应语言包,减少一次浏览器请求;Grunt 需额外生成 manifest-node.json 供服务端引用。
  3. 小程序无法访问 CDN? 通过 Grunt 生成 分包语言包,利用微信 subpackages 机制,把语言包做成独立分包,用户进入设置页触发 wx.loadSubpackage,实现“动态加载”且不走浏览器 CDN。
  4. CI/CD 一体化:在 GitLab CI 中把 grunt i18n-cdn 作为 pre-deploy 阶段,上传成功后把 manifest 地址写进 环境变量,供下游 Docker 镜像构建时注入,实现“一条命令上线多语言”。