如何在 grunt 中实现语言包 CDN 动态加载
解读
面试官真正想考察的是:
- 你是否理解 Grunt 仅负责“构建期”,而“动态加载”发生在 浏览器运行时;
- 你能否用 Grunt 把语言包构建成 按语言分片、带版本号、可 CDN 缓存 的静态资源,并生成一份 运行时配置表 供前端按需拉取;
- 你是否熟悉国内主流 CDN(阿里云 OSS+CDN、腾讯云 COS+CDN、七牛、又拍)的上传与刷新流程,并能用 Grunt 插件把“上传+刷新”串进流水线;
- 你是否考虑 回退策略(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% 体积。
答案
-
拆分任务链
grunt.registerTask('i18n-cdn', ['i18n-extract', 'filerev:i18n', 'compress:i18n', 'oss_upload:i18n', 'cdn_refresh:i18n', 'replace:injectManifest']); -
关键配置片段(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>'
}]
}
}
});
- 运行时加载伪代码
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());
}
}
- 上线 checklist
- 构建机拥有 STS 临时令牌,避免 AK/SK 落盘。
- CDN 刷新完成后再发版,否则边缘节点仍返回旧包。
- 灰度阶段先在 header 注入 x-oss-meta-gray=1,通过 Grunt 的 grunt-header 插件完成,方便回滚。
拓展思考
- 非 JSON 格式怎么办? 若产品要求 AMD/UMD 格式语言包,可在 Grunt 链里再加 grunt-wrap 任务,把 json 包装成
define({...}),实现<script src>直接加载。 - SSR 场景如何同构? 在 Node 层先读取
__I18N_MANIFEST__,利用accept-language头直出对应语言包,减少一次浏览器请求;Grunt 需额外生成 manifest-node.json 供服务端引用。 - 小程序无法访问 CDN? 通过 Grunt 生成 分包语言包,利用微信 subpackages 机制,把语言包做成独立分包,用户进入设置页触发
wx.loadSubpackage,实现“动态加载”且不走浏览器 CDN。 - CI/CD 一体化:在 GitLab CI 中把
grunt i18n-cdn作为 pre-deploy 阶段,上传成功后把 manifest 地址写进 环境变量,供下游 Docker 镜像构建时注入,实现“一条命令上线多语言”。