解释在 grunt 中实现平台特性开关

解读

“平台特性开关”在国内前端工程里通常指按运行环境(dev、sit、uat、prod)或渠道(微信、钉钉、WebView、Electron)动态启用/禁用某段代码或某个任务
面试官问这道题,想看三点:

  1. 你是否理解 Grunt 的“配置即代码”哲学,能用 Gruntfile.js 把环境差异显性化;
  2. 你是否熟悉 grunt-env、grunt-replace、grunt-preprocess 等社区方案,能在不侵入业务代码的前提下完成“编译期开关”;
  3. 你是否能把开关逻辑与 CI/CD 变量(Jenkins/GitLab 参数、Docker ARG、Node 环境变量) 打通,让同一条流水线打出不同包,满足国内“一次构建、多处部署”的安全合规要求。

知识点

  1. process.envgrunt.option() 的优先级与差异
  2. grunt.template.process() 在配置阶段的求值时机
  3. grunt-env 插件:把外部变量固化到 process.env,解决 Windows 与 Linux 脚本差异
  4. grunt-replace / grunt-preprocess:在拷贝、合并、压缩任务里做“条件编译”,生成不同的静态文件
  5. grunt-contrib-uglifyglobal_defs:把开关直接压成死代码,实现真正的“ Tree-Shaking ”
  6. 多任务(multi-task)+ 任务别名(registerTask):把“dev”“prod”两条链路彻底隔离,避免手滑
  7. 国内合规要点:npm 私服(Verdaccio/Cnpmjs)与离线构建机场景下,插件版本必须锁死(package-lock.json + nrm 切换源)

答案

“我通常分三步做平台特性开关:
第一步,用 grunt-env 把外部变量收敛。在 Jenkins 里传参 ENV=prod,Gruntfile 顶部 grunt.loadNpmTasks('grunt-env'),配置

env: { prod: { ENV: 'prod', API_PREFIX: 'https://api.xxx.com' } }

保证 Windows 构建机也能读到同一套变量。

第二步,用 grunt-replace 做编译期替换。在 concat/uglify 之前插一条 replace 任务,把源码里的

/* @if ENV=='prod' */
console.log = function(){};  
/* @endif */

直接消掉,产出干净代码;同时把 __API_PREFIX__ 替换成真实域名,避免前端再发一次配置请求。

第三步,用 uglify 的 global_defs 压死代码。在 prod 链路里加

uglify: { options: { compress: { global_defs: { DEBUG: false, FEAT_X: false } } } }

让闭包里的 if(DEBUG){...} 被完全删除,减少 10% 体积。

最后注册两条别名:

grunt.registerTask('dev', ['env:dev', 'replace:dev', 'concat', 'less', 'watch']);
grunt.registerTask('rel', ['env:prod', 'replace:prod', 'uglify:prod', 'cssmin', 'filerev']);

CI 里只要执行 grunt rel --env=prod,就能一次性打出可回溯、可审计、无敏感日志的生产包。”

拓展思考

  1. 如果开关粒度要细化到“用户白名单”级别,就不能只在构建期处理,需要把 开关配置文件抽成独立 JSON,让运维侧在 Nginx 层做 灰度下发,Grunt 只负责把 JSON 打进包并加 SRI 哈希,防止被篡改。
  2. 当项目迁移到 vite/webpack 后,可用 rollup 的 @rollup/plugin-replace 做同样的事,但 Grunt 思路依旧适用:环境变量 → 条件编译 → 任务别名,只是配置语法从 JSON 变成 ESModule。
  3. 国内金融、政务项目要求“不可变制品”,即一次构建、到处部署。此时 Grunt 的开关只能做“编译期裁剪”,不能把密钥写进包。正确姿势是把敏感开关放到 Apollo/Nacos 这类配置中心,Grunt 构建时只打占位符,容器启动后再通过 entrypoint.sh 做 sed 注入,既满足监管,又保留自动化。