解释在 grunt 中实现 A/B 实验埋点分桶
解读
面试官并非想听“Grunt 能不能跑 A/B 测试”,而是考察候选人能否把前端构建流程与数据驱动业务打通:
- 用 Grunt 的插件体系在编译时把“分桶逻辑”注入代码;
- 保证分桶结果可复现、可灰度、可回滚;
- 让埋点字段与分桶 ID 严格一致,避免数据偏差。
回答时要体现“构建即实验”的思想:把实验配置当成代码的一部分,在 Gruntfile 里版本化、自动化、可视化。
知识点
- Grunt 任务生命周期:initConfig → registerTask → run;
- npm conf 或 grunt-replace 做“编译期字符串替换”,把实验号写进代码;
- grunt-crypto 或自建 Node 脚本做“一致性哈希分桶”(uuid→bucketId);
- grunt-contrib-uglify 开启 sourceMap,保证压缩后仍能映射实验号;
- grunt-contrib-watch + grunt-contrib-connect 实现本地“多桶并行预览”(localhost:3000/?exp=b1);
- 埋点规范:spm、gmkey-gokey、dt-traceid 等国内主流字段;
- 灰度发布:结合阿里云 OSS/腾讯云 COS 的“目录级灰度”(如
/exp/1.0.0_b001/); - 数据回传:埋点必须带 expId+bucketId+uidHash,方便实时计算显著性。
答案
-
实验配置代码化
在config/ab.json中声明实验:{ "expId": "homepage_btn_color_2025Q2", "buckets": { "CONTROL": 50, "RED": 25, "BLUE": 25 }, "salt": "2025Q2_salt_xxx" }文件随 Git 版本化,任何变更走 MR+CodeReview。
-
Gruntfile 注入分桶常量
安装grunt-replace:npm i -D grunt-replace在 Gruntfile.js 里:
grunt.initConfig({ replace: { ab: { options: { patterns: [{ match: /__AB_CONFIG__/, replacement: () => JSON.stringify(grunt.file.readJSON('config/ab.json')) }] }, files: [{ src: 'src/entry.js', dest: '.tmp/entry.js' }] } }, uglify: { build: { files: { 'dist/js/entry.min.js': '.tmp/entry.js' } } } }); grunt.registerTask('build', ['replace:ab', 'uglify']); -
运行时一致性分桶
在.tmp/entry.js顶部注入:window.AB = JSON.parse('__AB_CONFIG__'); function getBucket(uid) { const hash = md5(uid + AB.salt).slice(0, 4); const n = parseInt(hash, 16) % 100; let acc = 0; for (const [k, v] of Object.entries(AB.buckets)) { acc += v; if (n < acc) return k; } return 'CONTROL'; }该算法纯前端、无后端依赖,且同 UID 永远进同桶。
-
埋点联动
按钮渲染时:const bucket = getBucket(Cookies.get('uid') || uuid()); document.querySelector('#buyBtn').classList.add(bucket); // 埋点 sendUbt({ spm: 'a2145.home.buy', expId: AB.expId, bucketId: bucket, uidHash: md5(uid).slice(0, 6) }); -
本地多桶并行验证
用grunt-contrib-connect起三个端口:grunt.initConfig({ connect: { control: { options: { port: 3000, base: 'dist', middleware: (c,o,n)=>n(c,o.setHeader('x-ab-bucket','CONTROL')) } }, red: { options: { port: 3001, base: 'dist', middleware: (c,o,n)=>n(c,o.setHeader('x-ab-bucket','RED')) } }, blue: { options: { port: 3002, base: 'dist', middleware: (c,o,n)=>n(c,o.setHeader('x-ab-bucket','BLUE')) } } } }); grunt.registerTask('serve:ab', ['build', 'connect', 'watch']);测试同学可同时打开三窗口验证 UI 差异。
-
线上灰度与回滚
构建产出按#{expId}_#{bucket}目录隔离,通过 CDN 边缘规则把 10% 流量切到实验目录;若指标异常,回滚只需把流量切回原版目录,无需重新打包。
拓展思考
- 服务端分层分桶:如果实验涉及接口字段,需把 Grunt 生成的
ab.json上传到配置中心,让 Node/Java 服务用同一套 salt 与比例,保证前后端桶一致,避免“前端红、后端蓝”的数据错乱。 - 实时显著性监控:埋点数据直接打到阿里云 SLS或腾讯灯塔,用 SQL
WHERE expId='xxx' GROUP BY bucketId每 5 分钟计算置信区间,Grunt 任务里可集成grunt-sls-upload把构建元数据(commitId、builder、时间)一并上传,实现版本-实验-指标三联追踪。 - SSR/边缘渲染场景:Grunt 构建时把分桶脚本打成
ab-loader.js,在CDN EdgeRoutine里先执行分桶,再回源带X-AB-Bucket头,实现边缘决定桶,减少前端闪烁。 - 合规与隐私:国内《个人信息保护法》要求“最小必要”,因此 UID 必须脱敏哈希,且实验配置中禁止出现用户明文属性;Grunt 任务加一道
grunt-contrib-jshint规则,扫描代码是否意外上传了 openid、手机号等敏感字段。