使用 grunt-webpack 将语言包拆分为异步 chunk
解读
在国内前端工程化面试中,“老项目还在用 Grunt,如何借助 grunt-webpack 实现代码分割” 是高频追问。面试官想确认三点:
- 你是否理解 Grunt 与 Webpack 的职责边界(Grunt 负责任务编排,Webpack 负责模块打包);
- 你是否能把 动态 import 与 Webpack 的 splitChunks 配置结合起来,产出按需加载的异步 chunk;
- 你是否能在 Gruntfile.js 里正确接入 grunt-webpack,并保证本地调试、生产构建两条流水线都能输出带 hash 的语言包文件,且路径能被运行时正确解析。
一句话:“让 Grunt 调 Webpack,让 Webpack 把语言包拆成独立 chunk,浏览器只在切换语言时才去拉对应文件。”
知识点
- grunt-webpack:把 Webpack 编译阶段注册成 Grunt 的一个 task,支持多环境配置数组;
- output.publicPath:决定异步 chunk 的拉取路径,国内项目常配 CDN 域名,必须以 / 结尾;
- optimization.splitChunks.cacheGroups[i18n]:通过 test / priority / name / chunks: 'async' 四元组把语言包单独成包;
- 动态 import():返回 Promise,必须配合 @babel/plugin-syntax-dynamic-import 才能在老 Babel 环境编译通过;
- webpack_public_path:运行时变量,在入口顶部赋值可解决“同一份构建产物部署到多个域名”的痛点;
- Grunt 的并发模型:grunt-webpack 默认阻塞式,若后续还有 zip、上传 oss 等任务,需用 grunt-contrib-watch 的 spawn: false 模式防止进程被提前杀掉。
答案
- 安装依赖(公司内网源可替换为 cnpm 或私有 registry)
npm i -D grunt-webpack webpack webpack-cli babel-loader @babel/core @babel/preset-env
npm i -D @babel/plugin-syntax-dynamic-import file-loader
- 在 Gruntfile.js 里声明 grunt-webpack 任务
module.exports = function(grunt) {
const webpackConfig = {
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
entry: './src/app.js',
output: {
path: __dirname + '/dist',
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/i18n/[name].[contenthash:8].js',
publicPath: grunt.option('cdn') || '/assets/' // 支持命令行覆盖
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-syntax-dynamic-import']
}
}
}
]
},
optimization: {
splitChunks: {
cacheGroups: {
i18n: {
test: /[\\/]src[\\/]locales[\\/]/,
name(module, chunks, cacheGroupKey) {
const lang = module.rawRequest.match(/locales\/(\w+)\//)[1];
return `i18n.${lang}`;
},
chunks: 'async',
priority: 20,
reuseExistingChunk: true
}
}
}
}
};
grunt.initConfig({
webpack: {
options: webpackConfig,
dev: { mode: 'development', devtool: 'cheap-source-map' },
prod: { mode: 'production' }
}
});
grunt.loadNpmTasks('grunt-webpack');
grunt.registerTask('default', ['webpack:dev']);
grunt.registerTask('build', ['webpack:prod']);
};
- 业务代码中动态加载语言包
// src/app.js
if (window.__webpack_public_path__) {
__webpack_public_path__ = window.cdnUrl; // 运维注入的变量
}
function loadLocale(lang) {
return import(
/* webpackChunkName: "i18n.[request]" */
`./locales/${lang}/messages.js`
);
}
document.getElementById('lang-switch').addEventListener('change', e => {
loadLocale(e.target.value).then(module => {
i18n.setLocaleMessage(e.target.value, module.default);
});
});
- 运行
# 本地开发
grunt
# 生产构建并上传 CDN
grunt build --cdn=https://cdn.example.com/assets/
构建结束后,dist/js/i18n 目录会产出 i18n.zh.1234abcd.js、i18n.en.1234abcd.js 等异步 chunk,首次加载不包含语言包,切换语言时才触发网络请求,达到拆分目的。
拓展思考
- 降级方案:如果项目仍需兼容 IE11,动态 import 需加 webpack 的 require.ensure 垫片,并在 Gruntfile 里多配一个 entry: 'core-js/stable' 做 polyfill 隔离;
- 缓存策略:国内 CDN 往往强制缓存 1 年,文件名必须带 contenthash;同时给 HTML 注入
<link rel="preload">可让浏览器提前解析 DNS,但切忌提前 preload 所有语言包,否则失去拆分意义; - 灰度发布:大型项目会按用户维度灰度语言包,可在 splitChunks.name 函数里读取 process.env.GRAY_LANG,把灰度语言包拆成 i18n.zh.gray.js,通过 Nginx 的 cookie 判断走灰度 CDN 路径;
- 监控与回滚:在 chunk 加载失败时,监听 import().catch() 并上报 SLS / 阿里 Sentry,同时自动回退到默认语言,防止页面卡死;
- 未来迁移:Grunt 已停止迭代,面试官可能追问“如何无痛迁移到 Vite/Rollup”。可回答:“先让 grunt-webpack 输出 manifest.json,再用 rollup 消费相同 entry,保证 chunk name 与 publicPath 一致,实现灰度迁移。”