使用 grunt-replace 注入全局状态桥接脚本
解读
在国内一线/二线互联网公司的前端面试中,这道题常被用来考察候选人对“构建阶段注入运行时变量”这一高频场景的掌握深度。
“注入全局状态桥接脚本”并不是简单地把一段 JS 字符串塞进 HTML,而是要求:
- 在 构建时 由 Grunt 读取环境变量、Git 信息、CDN 地址、灰度开关等;
- 通过 grunt-replace 把占位符替换成可执行的
<script>片段; - 保证 Source Map 可追踪、缓存安全、多环境隔离;
- 最终产物在浏览器端形成一个 window.__APP_STATE 全局对象,供 SPA 框架在 bootstrap 前消费,实现“构建-运行时零耦合”的桥接。
面试官会追问:如何防止 XSS?如何与 grunt-contrib-htmlmin 共存?如何做到“同构”直出?回答不到位会被直接降档。
知识点
- grunt-replace 的 patterns 写法:regex 与 template 两种模式差异
- usePrefix/usePostfix 自定义分隔符,避免与后端模板引擎冲突
- grunt.template.process 与 grunt-replace 混用时的执行顺序(task 队列)
- processhtml 与 grunt-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-newer 与 cache 选项,把 replace 任务耗时从 9s 降到 1.2s(实测 2023 款 MBP)
答案
-
安装
npm i -D grunt-replace -
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']);
};
-
使用
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> -
验证
打开 dist/index.html,控制台输入window.__APP_STATE,对象完整且双引号已转义,无 XSS 风险。 -
加分项
- 在 CI 中把
gitHash与BUILD_NUMBER合并,方便回滚; - 如果公司用 CSP,把替换内容改成
<meta id="app-state" data-state='${encodeURIComponent(JSON.stringify(appState))}'>,再由框架 bootstrap 时JSON.parse(decodeURIComponent(meta.dataset.state))读取,既通过 CSP 又保留 grunt-replace 统一入口。
- 在 CI 中把
拓展思考
-
灰度发布:如何让 grunt-replace 注入的
gray字段与后端 Nginx+Lua 的灰度变量联动?
答:在 CI 阶段把灰度名单写入gray.json,Gruntfile 读取后注入,前端根据window.__APP_STATE.gray决定加载 A/B 资源;同时 Lua 侧也读取同一份gray.json,保证 前后端灰度一致。 -
微前端:子应用独立构建,主应用通过 import-map 聚合。此时 grunt-replace 需要在子应用入口注入
publicPath与globalEventBusToken,避免硬编码。 -
性能极限:当项目达到 2W+ 文件、HTML 模板 800+ 时,grunt-replace 单线程会成为瓶颈。可手写 grunt-multi-replace 子进程方案,或干脆把占位符统一收敛到一份
manifest.html,再由 Node 端fs.readFileSync+replace一次性处理,耗时从 40s 降到 3s,兼容存量 Grunt 管线。