解释在 grunt 中实现 feature flag 注入的两种方案
解读
国内一线团队面试时,这道题考察的是“在不改业务源码的前提下,如何借助 Grunt 把开关变量注入到产物里”,从而支持灰度发布、A/B Test、环境隔离。回答必须体现出“构建阶段注入”而非“运行时拉取”,并给出两种可落地的 Grunt 插件级方案,同时说明各自优缺点与典型使用场景。
知识点
- 构建时预编译常量:在打包阶段把占位符替换成布尔值或对象,产物中无多余请求,性能最好。
- 文件内联替换:借助 grunt-string-replace、grunt-replace 等通用文本替换插件,把形如
__FEATURE_X__的标记一次性替换成 true/false。 - AST 级注入:使用 grunt-babel-plugin 或 grunt-contrib-uglify 的自定义 transform,在语法树里插入
window.FF = {…},保证压缩后仍能被业务代码引用。 - 环境隔离:通过
grunt.template.process读取process.env.NODE_ENV或自定义grunt.option('env'),实现“同一份 Gruntfile,不同命令出不同包”。 - 缓存与增量编译:替换方案需兼容 grunt-contrib-watch,避免每次全量打包导致 dev 体验下降。
答案
在真实项目中,我落地过以下两种主流方案,均通过 Grunt 完成“零运行时请求”的 feature flag 注入:
方案一:字符串模板替换(grunt-string-replace)
- 在源码里预留占位符,如
if (__ENABLE_PAY__) { /* 支付逻辑 */ } - 在 Gruntfile 中配置 grunt-string-replace 任务:
stringReplace: { dist: { files: [{ expand: true, cwd: 'src', src: '**/*.js', dest: 'dist' }], options: { replacements: [{ pattern: /__ENABLE_PAY__/g, replacement: grunt.option('env') === 'prod' ? 'false' : 'true' }] } } } - 构建命令
grunt build --env=prod即可产出带关闭开关的包。
优点:配置简单、无学习成本;缺点:只能做纯文本替换,压缩后若被重命名可能失效,需配合/*#__PURE__*/注释保留。
方案二:AST 级全局常量定义(grunt-babel + babel-plugin-inline-replace-variables)
- 安装 grunt-babel 及自定义 babel 插件,在
.babelrc中声明:{ "plugins": [["inline-replace-variables", { "__FF__": { "pay": false, "login": true } }]] } - Gruntfile 中通过
grunt.config.set('babel.options.env', { flags: require('./flag.' + grunt.option('env')) })动态把环境变量喂给 babel。 - 业务代码直接写
if (__FF__.pay) {…},构建后自动编译成if (false){…},uglify 阶段会删除死代码,包体积最小。
优点:语法树级别,安全且可 tree-shaking;缺点:需要团队熟悉 Babel 插件开发,首次配置成本略高。
两种方案都能与国内常见的蓝绿发布、钉钉/飞书机器人通知流水线无缝集成,选择时根据“是否需要死代码消除”与“团队 Babel 技术储备”权衡即可。
拓展思考
- 灰度动态化:如果业务要求“上线后仍可实时调整开关”,可在 Grunt 构建阶段只注入“取号逻辑”
window.FF = <%= grunt.file.read('remoteFlag.json') %>,然后接入公司自研的“配置中心 SDK”,实现“构建+远程”混合策略。 - 多产物并行:在 GitLab-CI 里并行跑
grunt build --env=gray与grunt build --env=prod,产出两份包,由 nginx 根据 cookie 分流,Grunt 侧只需保证两次构建缓存隔离即可。 - 合规与审计:国内金融项目需留痕,可在替换同时把最终注入的 flag 快照写入
dist/manifest.flag.json,由运维归档,方便监管回查。