使用 grunt-replace 注入 __INITIAL_STATE__ 脚本
解读
在国内前端面试中,这道题表面问“如何用 Grunt 把一段 JSON 塞进 HTML”,实质考察的是“构建阶段如何安全、可维护地把服务端初始数据注入到浏览器端”。
很多候选人直接回答“把一段字符串拼进去”,却忽略了:
- 数据里可能带
</script>导致 XSS; - 构建与运行时的职责边界;
- 多环境(dev、test、pre、prod)下数据怎么差异化注入;
- 与 React/Vue 同构项目结合时,如何与 webpack/vite 共存。
只有把“注入”当成“构建安全切片”而非“字符串替换”,才能拿到高分。
知识点
- grunt-replace 本质:基于
text-replace的 Grunt 多文件文本替换插件,支持patterns正则与template函数两种模式。 - 注入点标记:国内团队通常用
<!--__INITIAL_STATE__-->或<script id="initial-state"></script>做锚点,避免与业务 ID 冲突。 - 序列化安全:必须用
JSON.stringify(obj).replace(/</g,'\\u003c')做 转义防 XSS,不能直接toString()。 - Gruntfile 阶段顺序:
replace任务必须晚于concat、早于htmlmin,否则会被压缩掉占位符。 - 多环境数据:通过
grunt.config.set('runtimeEnv', process.env.BUILD_ENV)把环境变量传入template函数,实现“一份代码,多份数据”。 - 与 CI 集成:在 GitLab-CI 或 Jenkins 中,把后端接口返回的初始 state 写成
initial-state.json,构建前 curl 拉取,再喂给 grunt-replace,保证数据实时性。 - 性能细节:大 JSON(>200 kB)建议改用
script src="//cdn.xxx.com/initial-state.js"外链,减少首屏阻塞,此时 grunt-replace 只替换src地址即可。
答案
- 安装
npm i -D grunt-replace
- 准备数据
在项目根目录放config/initial-state.json:
{"user":{"uid":0,"name":"guest"},"featureFlags":{"homeRedPacket":true}}
- Gruntfile.js 关键配置
module.exports = function(grunt) {
grunt.initConfig({
replace: {
dist: {
options: {
patterns: [
{
match: /<!--\s*__INITIAL_STATE__\s*-->/,
replacement: function () {
const fs = require('fs');
const state = fs.readFileSync('config/initial-state.json', 'utf-8');
// 安全序列化
const serialized = JSON.stringify(JSON.parse(state))
.replace(/</g, '\\u003c')
.replace(/-->/g, '\\u002d\\u002d\\u003e');
return `<script>window.__INITIAL_STATE__=${serialized};</script>`;
}
}
]
},
files: [
{
expand: true,
cwd: 'dist/',
src: '*.html',
dest: 'dist/'
}
]
}
}
});
grunt.loadNpmTasks('grunt-replace');
grunt.registerTask('inject', ['replace']);
};
- 在 HTML 模板里留好占位符
<!doctype html>
<html>
<head>...</head>
<body>
<div id="root"></div>
<!-- __INITIAL_STATE__ -->
<script src="app.js"></script>
</body>
</html>
- 跑任务
grunt inject
构建后自动把注释替换成:
<script>window.__INITIAL_STATE__={"user":{"uid":0,"name":"guest"},"featureFlags":{"homeRedPacket":true}};</script>
全程无人工改文件,CI 可直接发布。
拓展思考
-
与 React 同构结合:
在server/index.js里renderToString完成后,把store.getState()写成dist/initial-state.json,再触发 grunt inject,实现“服务端生成 → 构建注入 → 客户端 hydrate”闭环。 -
灰度发布场景:
利用 grunt-replace 的template函数读取 Consul/Nacos 的灰度配置,把灰度 ID 写进window.__GRAY_ID__,前端路由据此做 A/B。 -
性能极限优化:
如果初始数据超过 500 kB,把 grunt-replace 改为只替换data-state-src属性,然后前端用fetch(window.initialStateSrc).then(r=>r.json())异步拉取,减少首屏 HTML 体积,提升 TTFB。 -
替代方案对比:
- webpack-html-plugin 的
templateParameters:适合新项目,但老项目迁移成本高; - vite-plugin-html 的
inject:需要把 Grunt 任务链全部切到 Vite,在国企/金融等保守场景阻力大; - grunt-inline-source 直接把 JSON 内联,但失去缓存优势。
结论: grunt-replace 在“存量 Grunt 项目 + 需要同构注入”场景下仍是成本最低的方案。
- webpack-html-plugin 的