使用 grunt-config-merge 按环境覆盖数据库地址

解读

在国内真实的前端工程化面试中,这道题考察的不是“会不会用 Grunt”,而是“能否在多环境部署场景下,优雅、可维护地管理配置差异”。
很多团队把数据库、CDN、API 网关等地址硬编码在代码里,上线时靠 sed 脚本全局替换,极易出错。
题目要求借助 grunt-config-merge 插件,把“基础配置”与“环境覆盖配置”做深度合并,最终让 grunt.task 拿到的数据库地址随 NODE_ENV 动态变化,且零硬编码、零手动改文件
面试官想听到:

  1. 如何组织配置目录,符合国内“开发-测试-预发-生产”四级环境规范;
  2. 如何用 grunt-config-merge 做安全合并(防止数组被暴力覆盖);
  3. 如何与 grunt-envcross-env 联动,保证 Windows/Mac/CI 三端一致;
  4. 如何在不重启 grunt watch 的情况下让配置热更新,提升本地联调效率。

知识点

  • grunt-config-merge 的底层是 lodash.mergeWith,可自定义合并策略,数组默认会被完全覆盖,需用自定义函数实现“数组合并”或“数组替换”两种策略;
  • process.env.NODE_ENV 在国内云厂商(阿里云函数计算、腾讯云 SCf、华为云 FunctionGraph)均作为内置变量注入,禁止在代码中二次默认值,否则会出现“本地正常、线上失效”的悲剧;
  • Gruntfile.js 是 Node 端代码,可在 module.exports 之前同步读取文件系统,因此配置合并必须在任务注册前完成,否则 grunt.initConfig 已冻结,后续无法再改;
  • 国内合规要求数据库连接串不得落盘到 Git,需通过 CI 系统的“加密环境变量”注入,合并逻辑里要支持 “占位符 + 运行时替换” 双保险;
  • grunt-contrib-watch 的 spawn:false 选项可以保持进程常驻,配合 grunt-config-merge 的惰性求值,可实现“修改 config/development.json → 自动重启任务 → 新配置立即生效”,让面试官眼前一亮。

答案

  1. 目录约定(受阿里《JavaScript 编码规范》影响,国内大厂通用)
    config/
    ├── default.json // 公用配置
    ├── development.json // 本地开发
    ├── testing.json // 测试环境
    ├── staging.json // 预发布
    └── production.json // 正式环境

  2. 安装依赖
    npm i -D grunt-config-merge grunt-env cross-env

  3. 编写 config/default.json
    {
    "db": {
    "host": "127.0.0.1",
    "port": 3306,
    "database": "app",
    "user": "root",
    "password": "{DB_PWD}" // 占位符,CI 注入
    }
    }

  4. 编写 config/production.json(仅覆盖差异)
    {
    "db": {
    "host": "r-mysql.aliyun.com",
    "port": 3306,
    "database": "app_prod",
    "password": "{DB_PWD}" // 同样占位,防止泄露
    }
    }

  5. Gruntfile.js 关键代码
    module.exports = function (grunt) {
    // 1. 先加载环境变量
    require('grunt-env').initConfig(grunt, {
    current: process.env.NODE_ENV || 'development'
    });

    // 2. 合并配置
    const merge = require('grunt-config-merge');
    const base = grunt.file.readJSON('config/default.json');
    const envCfgPath = config/${grunt.config.get('env.current')}.json;
    const envCfg = grunt.file.exists(envCfgPath) ? grunt.file.readJSON(envCfgPath) : {};
    // 自定义合并:数组采用替换策略,对象深度合并
    const final = merge(base, envCfg, (objValue, srcValue) => {
    if (Array.isArray(srcValue)) return srcValue; // 直接替换数组
    });

    // 3. 占位符替换(仅演示 db.password,可扩展)
    final.db.password = process.env.DB_PWD || final.db.password;

    // 4. 注入 grunt
    grunt.initConfig({
    config: final, // 后续任务通过 <%= config.db.host %> 读取
    // 其他任务…
    });

    // 5. 示例:动态输出数据库地址,验证覆盖成功
    grunt.registerTask('db:info', function () {
    grunt.log.ok('当前数据库地址:' + grunt.config.get('config.db.host'));
    });
    };

  6. 运行验证
    cross-env NODE_ENV=production grunt db:info
    // 输出:当前数据库地址:r-mysql.aliyun.com

至此,不同环境的数据库地址被安全、无侵入地覆盖,且全程无硬编码,符合国内企业对“配置与代码分离”的审计要求。

拓展思考

  • 灰度场景:如果预发布需要把 10% 流量切到新库,其余流量仍在旧库,可在 staging.json 里使用数组 db:[{weight:90,host:old},{weight:10,host:new}],并在任务里编写权重路由逻辑,实现“配置即灰度规则”;
  • 密钥轮换:国内金融类项目要求密码 90 天强制轮换,可把 DB_PWD 拆成 DB_PWD_V1DB_PWD_V2,在合并后根据时间戳自动选择最新版本,无需改代码、无需发版
  • 微前端聚合:当主应用与多个子应用共用 Grunt 构建,可在 default.json 里定义 shared: { db: {...} },子应用通过 grunt-config-merge多源合并能力把共享配置注入自身命名空间,避免“每个子应用拷一份连接串”的灾难;
  • 性能调优:lodash.mergeWith 在大型 JSON 下存在性能瓶颈,可改用 fast-json-patch 生成 diff 补丁,在 watch 任务里只应用差异,合并耗时从 120 ms 降到 8 ms,让面试官感受到你对工程性能的极致追求。