描述在 grunt 中实现灰度发布到指定租户
解读
面试官想确认三件事:
- 你是否理解“灰度发布”在国内的真实含义——按租户维度逐步放量,而非简单按 IP 或 UID 切流;
- 你是否能把“灰度”逻辑与 Grunt 的“构建-上传-刷新 CDN”自动化流程打通,让同一份源码产出两套(或多套)差异化产物;
- 你是否具备“配置即代码”思维,用 Gruntfile 把租户名单、比例、版本号、回滚策略全部显性化,而不是写死在脚本里。
回答时要突出“构建侧打标 + 发布侧切流”两步走,并给出可落地的 Grunt 任务链,避免空谈 Jenkins 或 K8s。
知识点
- 多环境产物隔离:利用
grunt.file.write动态生成gray-config.json,把租户白名单、灰度版本号、CDN 路径注入到构建产物里。 - 任务编排:
grunt.registerTask('gray', ['clean:gray', 'webpack:gray', 'string-replace:grayTag', 'upload:gray', 'cdn:refreshGray']),保证 gray 任务与 prod 任务完全隔离。 - 租户维度打标:在 HTML/JS 入口里插入
window.__GRAY_TENANTS__ = <%= grayTenants %>,由 Grunt 模板引擎一次性替换,前端运行时根据当前登录租户 ID 匹配是否走灰度 CDN。 - 上传与刷新:国内云厂商(阿里云 OSS+CDN、腾讯云 COS+ECDN)均提供 OpenAPI 密钥对,用
grunt-http或grunt-exec调用官方 CLI,实现“上传后立刻按目录刷新”,避免老资源缓存。 - 回滚能力:Gruntfile 里维护
grayHistory数组,记录每次灰度的buildNo与CDN 目录,回滚任务直接复用旧目录做刷新,10 秒内完成。
答案
-
目录约定
dist/prod/ // 全量包 dist/gray/{version}/{tenant}/ // 按版本+租户隔离 -
安装依赖
npm i -D grunt-contrib-clean grunt-string-replace grunt-webpack grunt-http -
Gruntfile 核心片段
module.exports = function(grunt) { const grayTenants = grunt.option('tenants')?.split(',') || ['tenantA', 'tenantB']; const grayVersion = grunt.option('ver') || Date.now(); grunt.initConfig({ clean: { gray: ['dist/gray/<%= grayVersion %>'] }, webpack: { gray: { entry: './src/main.js', output: { path: `dist/gray/${grayVersion}/common/`, filename: 'app.[contenthash].js' } } }, 'string-replace': { grayTag: { files: [{ expand: true, cwd: `dist/gray/${grayVersion}/common/`, src: '*.js', dest: `dist/gray/${grayVersion}/common/` }], options: { replacements: [{ pattern: /\/\*GRAY_TAG\*\//, replacement: `window.__GRAY_TENANTS__=${JSON.stringify(grayTenants)};window.__GRAY_VER__='${grayVersion}';` }] } } }, http: { uploadGray: { options: { url: `https://oss-cn-shanghai.aliyuncs.com/${bucket}/gray/${grayVersion}/`, method: 'PUT', headers: { Authorization: generateOSSAuth() }, src: `dist/gray/${grayVersion}/**`, dest: '/' } }, refreshCDN: { options: { url: 'https://cdn.aliyuncs.com/', method: 'POST', form: { ObjectPath: `https://cdn.xxx.com/gray/${grayVersion}/`, Action: 'RefreshObjectCaches' }, headers: { Authorization: generateCDNAuth() } } } } }); grunt.registerTask('gray', function() { if (!grayTenants.length) grunt.fail.warn('必须指定 --tenants'); grunt.task.run(['clean:gray', 'webpack:gray', 'string-replace:grayTag', 'http:uploadGray', 'http:refreshCDN']); grunt.log.ok(`灰度版本 ${grayVersion} 已发布至租户 ${grayTenants.join(',')}`); }); }; -
运行命令
grunt gray --tenants=tenantA,tenantB --ver=20250618 -
前端运行时
if (window.__GRAY_TENANTS__?.includes(currentTenantId)) { __webpack_public_path__ = `https://cdn.xxx.com/gray/${window.__GRAY_VER__}/common/`; } -
回滚
grunt cdnRollback --ver=20250615任务内部直接调用 CDN 刷新接口,把
20250615目录重新刷为最新即可。
拓展思考
- 动态租户列表:把租户白名单放到 Redis 或配置中心,Grunt 任务里先调用接口拉取,实现“零重启”增删灰度租户。
- 比例灰度:如果业务要求“按租户+流量百分比”双层灰度,可在构建阶段只打标版本,真正的切流逻辑下沉到网关层(如阿里云 MSE、腾讯云 CLB),Grunt 负责把灰度版本推送至“灰度池”目录,网关按租户 ID 做一致性哈希。
- 安全合规:国内金融、政企项目要求“灰度包可审计”,可在 Grunt 任务链尾部追加
grunt-zip把dist/gray/${version}打成tar.gz,自动上传至公司内部的 Artifactory 并记录 SHA256,满足监管留痕。