使用 grunt-i18n-static 生成中英双语静态页
解读
在国内前端面试中,**“多语言静态站点生成”常被用来考察候选人对构建工具链的掌控深度。Grunt 虽非最新,但存量项目庞大,“能否在老项目里低成本接入国际化”**是面试官的核心关切。题目表面问“怎么用 grunt-i18n-static”,实则暗含三条考核线:
- 对 Grunt 插件机制与配置的熟练度;
- 对国际化资源(语言包、模板、路径策略)的架构设计;
- 对国内部署场景(CDN 缓存、SEO、路由规范)的落地经验。
答得太浅(仅贴配置)会被追问性能与缓存;答得太偏(扯到 webpack/vite)会被判跑题。必须紧扣 Grunt 生态,给出可维护、可上线、可回滚的完整方案。
知识点
- grunt-i18n-static 本质:基于 grunt-contrib-copy + 模板字符串替换,一次性产出多语言纯静态文件,非运行时切换,适合 SEO 与 CDN。
- 语言包格式:默认支持
.json与.yml,键值必须扁平化(如nav.home: 首页),避免嵌套过深导致模板语法冗长。 - 模板语法:使用
{%= __key %}占位符,与 Lodash template 完全兼容,因此可复用grunt.template.process的变量注入能力。 - 目录规范:国内主流约定
dist/zh-cn/、dist/en/双目录并行,避免在文件名中加语言码(index.en.html会被百度降权)。 - 缓存策略:静态资源文件名带
<%= hash %>,但 HTML 入口文件禁止缓存(Nginxlocation ~* \.(html)$ { add_header Cache-Control no-store; })。 - 回滚方案:Grunt 阶段生成
dist/.manifest-i18n.json,记录语言→文件→hash映射,上线脚本对比旧 manifest,差异文件做增量上传,降低 CDN 刷新成本。 - 与 grunt-contrib-watch 联动:监听
locales/*.json与src/templates/**/*,触发 livereload 时只重编当前语言,避免全量构建拖慢开发体验。 - 合规细节:中文内容必须经过 grunt-text-replace 做敏感词过滤,生成后走一遍
grunt-htmllint保证无外链死链,否则无法通过国内内容安全扫描。
答案
- 安装依赖
npm i -D grunt-i18n-static grunt-contrib-clean grunt-contrib-copy grunt-filerev grunt-usemin
- 语言包准备
locales/zh-cn.json
{ "title": "欢迎来到官网", "btn": "立即体验" }
locales/en.json
{ "title": "Welcome to Official", "btn": "Try Now" }
- Gruntfile.js 核心配置
module.exports = function(grunt) {
grunt.initConfig({
clean: { dist: 'dist' },
// 1. 按语言维度循环构建
i18n_static: {
options: {
locales: ['zh-cn', 'en'],
localeDir: 'locales',
templateDir: 'src/templates',
// 关键:输出路径带语言目录
dest: function(locale) { return 'dist/' + locale; },
// 替换引擎用 Lodash
process: function(content, locale) {
var dict = grunt.file.readJSON('locales/' + locale + '.json');
return grunt.template.process(content, { data: dict });
}
},
pages: {
expand: true,
cwd: 'src/templates',
src: '**/*.html',
dest: '<%= i18n_static.options.dest %>'
}
},
// 2. 静态资源加 hash
filerev: {
options: { algorithm: 'md5', length: 8 },
dist: { src: ['dist/**/*.{js,css,png,jpg}'] }
},
// 3. 替换 HTML 中的资源引用
usemin: {
html: 'dist/**/*.html',
options: {
assetsDirs: ['dist/zh-cn', 'dist/en']
}
},
// 4. 生成上线清单
manifest: {
generate: {
options: {
basePath: 'dist',
process: function(path) {
return path.replace(/^dist\//, '');
}
},
src: ['dist/**/*'],
dest: 'dist/.manifest-i18n.json'
}
}
});
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-i18n-static');
grunt.loadNpmTasks('grunt-filerev');
grunt.loadNpmTasks('grunt-usemin');
grunt.registerTask('build', ['clean', 'i18n_static', 'filerev', 'usemin', 'manifest']);
};
- 模板示例
src/templates/index.html
<!doctype html>
<html lang="{%= lang %}">
<head>
<meta charset="utf-8">
<title>{%= title %}</title>
<!-- build:css css/index.min.css -->
<link rel="stylesheet" href="css/index.css">
<!-- endbuild -->
</head>
<body>
<h1>{%= title %}</h1>
<button>{%= btn %}</button>
</body>
</html>
- 上线脚本(CI 片段)
# 仅上传变更文件
aws s3 sync dist/ s3://my-cdn/ --exclude "*" --include "$(node scripts/diff-manifest.js)"
# 刷新 CDN 目录
aliyun cdn RefreshObjectCaches --ObjectPath "http://cdn.xxx.com/zh-cn/" --ObjectType "Directory"
aliyun cdn RefreshObjectCaches --ObjectPath "http://cdn.xxx.com/en/" --ObjectType "Directory"
拓展思考
- 增量构建提速:当语言包多达 10+ 时,全量构建耗时 30s+,可改写
grunt-i18n-static的multiTask,利用 grunt-newer 只重编变动的语言目录,把耗时压到 5s 内。 - SSR 降级方案:若未来迁移到 Next.js,需保留原 Grunt 生成的静态页做长尾 SEO 兜底,通过 Nginx
map $http_user_agent $is_bot把爬虫流量打到静态目录,实现 SSR 与静态页并存,避免流量切换导致的排名震荡。 - 敏感词热更新:运营随时可能调整敏感词库,把 grunt-text-replace 的词典抽成独立 npm 私包,CI 阶段拉取最新版,无需改业务代码即可重新部署,满足国内监管“即发即审”要求。