使用 grunt-replace 注入 __INITIAL_STATE__ 脚本

解读

在国内前端面试中,这道题表面问“如何用 Grunt 把一段 JSON 塞进 HTML”,实质考察的是“构建阶段如何安全、可维护地把服务端初始数据注入到浏览器端”
很多候选人直接回答“把一段字符串拼进去”,却忽略了:

  1. 数据里可能带 </script> 导致 XSS;
  2. 构建与运行时的职责边界;
  3. 多环境(dev、test、pre、prod)下数据怎么差异化注入;
  4. 与 React/Vue 同构项目结合时,如何与 webpack/vite 共存。
    只有把“注入”当成“构建安全切片”而非“字符串替换”,才能拿到高分。

知识点

  1. grunt-replace 本质:基于 text-replace 的 Grunt 多文件文本替换插件,支持 patterns 正则与 template 函数两种模式。
  2. 注入点标记:国内团队通常用 <!--__INITIAL_STATE__--><script id="initial-state"></script> 做锚点,避免与业务 ID 冲突
  3. 序列化安全:必须用 JSON.stringify(obj).replace(/</g,'\\u003c')转义防 XSS,不能直接 toString()
  4. Gruntfile 阶段顺序replace 任务必须晚于 concat、早于 htmlmin,否则会被压缩掉占位符。
  5. 多环境数据:通过 grunt.config.set('runtimeEnv', process.env.BUILD_ENV) 把环境变量传入 template 函数,实现“一份代码,多份数据”
  6. 与 CI 集成:在 GitLab-CI 或 Jenkins 中,把后端接口返回的初始 state 写成 initial-state.json构建前 curl 拉取,再喂给 grunt-replace,保证数据实时性
  7. 性能细节:大 JSON(>200 kB)建议改用 script src="//cdn.xxx.com/initial-state.js" 外链,减少首屏阻塞,此时 grunt-replace 只替换 src 地址即可。

答案

  1. 安装
npm i -D grunt-replace
  1. 准备数据
    在项目根目录放 config/initial-state.json
{"user":{"uid":0,"name":"guest"},"featureFlags":{"homeRedPacket":true}}
  1. 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']);
};
  1. 在 HTML 模板里留好占位符
<!doctype html>
<html>
<head>...</head>
<body>
  <div id="root"></div>
  <!-- __INITIAL_STATE__ -->
  <script src="app.js"></script>
</body>
</html>
  1. 跑任务
grunt inject

构建后自动把注释替换成:

<script>window.__INITIAL_STATE__={"user":{"uid":0,"name":"guest"},"featureFlags":{"homeRedPacket":true}};</script>

全程无人工改文件,CI 可直接发布。

拓展思考

  1. 与 React 同构结合
    server/index.jsrenderToString 完成后,把 store.getState() 写成 dist/initial-state.json再触发 grunt inject,实现“服务端生成 → 构建注入 → 客户端 hydrate”闭环。

  2. 灰度发布场景
    利用 grunt-replace 的 template 函数读取 Consul/Nacos 的灰度配置,把灰度 ID 写进 window.__GRAY_ID__,前端路由据此做 A/B。

  3. 性能极限优化
    如果初始数据超过 500 kB,把 grunt-replace 改为只替换 data-state-src 属性,然后前端用 fetch(window.initialStateSrc).then(r=>r.json()) 异步拉取,减少首屏 HTML 体积,提升 TTFB

  4. 替代方案对比

    • webpack-html-plugintemplateParameters:适合新项目,但老项目迁移成本高;
    • vite-plugin-htmlinject:需要把 Grunt 任务链全部切到 Vite,在国企/金融等保守场景阻力大
    • grunt-inline-source 直接把 JSON 内联,但失去缓存优势
      结论: grunt-replace 在“存量 Grunt 项目 + 需要同构注入”场景下仍是成本最低的方案。