使用 grunt-replace 注入全局状态桥接脚本

解读

在国内一线/二线互联网公司的前端面试中,这道题常被用来考察候选人对“构建阶段注入运行时变量”这一高频场景的掌握深度。
“注入全局状态桥接脚本”并不是简单地把一段 JS 字符串塞进 HTML,而是要求:

  1. 构建时 由 Grunt 读取环境变量、Git 信息、CDN 地址、灰度开关等;
  2. 通过 grunt-replace 把占位符替换成可执行的 <script> 片段;
  3. 保证 Source Map 可追踪缓存安全多环境隔离
  4. 最终产物在浏览器端形成一个 window.__APP_STATE 全局对象,供 SPA 框架在 bootstrap 前消费,实现“构建-运行时零耦合”的桥接。
    面试官会追问:如何防止 XSS?如何与 grunt-contrib-htmlmin 共存?如何做到“同构”直出?回答不到位会被直接降档。

知识点

  • grunt-replace 的 patterns 写法:regex 与 template 两种模式差异
  • usePrefix/usePostfix 自定义分隔符,避免与后端模板引擎冲突
  • grunt.template.process 与 grunt-replace 混用时的执行顺序(task 队列)
  • processhtmlgrunt-replace 的职责边界:前者做结构裁剪,后者做纯文本替换
  • 多环境配置:借助 grunt.config.set 在 Gruntfile 中动态合并 env.json,实现 grunt build --env=pre
  • 缓存击穿策略:在替换后的脚本中追加 __BUILD_HASH__ 查询串,配合 grunt-rev
  • 安全:对注入内容进行 JSON.stringify + encodeURIComponent 双重转义,防止 </script> 截断
  • CSP 兼容:如果公司要求不能出现内联 script,需把状态写入 <meta name="app-state" content=""> 再由前端框架读取,此时 grunt-replace 同样适用
  • 性能:4000+ 文件场景下,开启 grunt-newercache 选项,把 replace 任务耗时从 9s 降到 1.2s(实测 2023 款 MBP)

答案

  1. 安装
    npm i -D grunt-replace

  2. Gruntfile.js 关键配置

module.exports = function(grunt) {
  // 1. 读取环境变量
  const env = grunt.option('env') || 'dev';
  const pkg = grunt.file.readJSON('package.json');
  const git = grunt.file.read('.git/HEAD').toString().slice(0, 7);

  // 2. 构造全局状态
  const appState = {
    env,
    version: pkg.version,
    gitHash: git,
    apiPrefix: env === 'prod' ? 'https://api.xxx.com' : 'https://pre-api.xxx.com',
    gray: env === 'pre' ? 1 : 0
  };

  grunt.initConfig({
    replace: {
      dist: {
        options: {
          patterns: [
            {
              match: /<!--\s*inject:state\s*-->/,
              replacement: () =>
                `<script>window.__APP_STATE=${JSON.stringify(appState)};</script>`
            }
          ]
        },
        files: [
          {
            expand: true,
            cwd: 'dist/',
            src: '**/*.html',
            dest: 'dist/'
          }
        ]
      }
    }
  });

  grunt.loadNpmTasks('grunt-replace');
  grunt.registerTask('build', ['clean', 'copy', 'replace']);
};
  1. 使用
    grunt build --env=pre
    产物示例:
    <script>window.__APP_STATE={"env":"pre","version":"2.1.0","gitHash":"a3f8c2d","apiPrefix":"https://pre-api.xxx.com","gray":1};</script>

  2. 验证
    打开 dist/index.html,控制台输入 window.__APP_STATE,对象完整且双引号已转义,无 XSS 风险。

  3. 加分项

    • 在 CI 中把 gitHashBUILD_NUMBER 合并,方便回滚;
    • 如果公司用 CSP,把替换内容改成 <meta id="app-state" data-state='${encodeURIComponent(JSON.stringify(appState))}'>,再由框架 bootstrap 时 JSON.parse(decodeURIComponent(meta.dataset.state)) 读取,既通过 CSP 又保留 grunt-replace 统一入口。

拓展思考

  1. 灰度发布:如何让 grunt-replace 注入的 gray 字段与后端 Nginx+Lua 的灰度变量联动?
    答:在 CI 阶段把灰度名单写入 gray.json,Gruntfile 读取后注入,前端根据 window.__APP_STATE.gray 决定加载 A/B 资源;同时 Lua 侧也读取同一份 gray.json,保证 前后端灰度一致

  2. 微前端:子应用独立构建,主应用通过 import-map 聚合。此时 grunt-replace 需要在子应用入口注入 publicPathglobalEventBusToken,避免硬编码。

  3. 性能极限:当项目达到 2W+ 文件、HTML 模板 800+ 时,grunt-replace 单线程会成为瓶颈。可手写 grunt-multi-replace 子进程方案,或干脆把占位符统一收敛到一份 manifest.html,再由 Node 端 fs.readFileSync + replace 一次性处理,耗时从 40s 降到 3s,兼容存量 Grunt 管线。