使用 grunt 注入 SSR 错误边界脚本并捕获异常
解读
这是一道**“老工具新场景”的实战题,考察候选人能否把 Grunt 的“文件流转”能力与 Node 服务端渲染(SSR)的异常治理结合起来。国内主流 SSR 框架(Next.js、Nuxt、Egg + React/Vue)普遍把错误边界放在运行时,但线上灰度或降级场景**需要“兜底脚本”在 HTML 首屏就注入,确保:
- 服务端抛错时,浏览器能立即感知并上报;
- 错误信息不泄露源码路径;
- 注入过程可灰度、可回滚、可溯源。
Grunt 虽“老”,但在存量项目、内网构建、Jenkins 流水线中仍大量存在,面试官想看你是否能用“土办法”解决“新问题”,而不是直接换 Vite/Webpack。
知识点
- Grunt 任务生命周期:init → loadNpmTasks → registerTask → grunt.task.run
- grunt-contrib-copy / grunt-string-replace / grunt-injector 等“文件中间态”插件的执行顺序与管道机制
- Node SSR 异常分类:renderToString 同步错误、asyncData 异步错误、资源加载 404、ChunkLoadError
- 浏览器端错误边界与 window.onerror / window.addEventListener('unhandledrejection') 的拾取范围差异
- source-map-support 与 error-stack-parser 在 SSR 环境下的映射原理
- 国内监控体系:阿里 SLS、腾讯 RUM、字节 APM,均要求统一 errorId + 用户行为回溯
- 灰度密钥:通过 grunt-replace 注入
window.__GRAY_KEY__,与 CDN 边缘规则联动 - 安全合规:生产环境需脱敏 stack,防止路径泄露;内网源站需屏蔽
x-powered-by
答案
- 安装依赖
npm i -D grunt grunt-contrib-copy grunt-string-replace grunt-injector cheerio
- 在
Gruntfile.js中定义“注入 + 捕获”双任务
module.exports = function(grunt) {
grunt.initConfig({
// 1. 先拷贝构建产物,形成“可写副本”
copy: {
ssrHtml: {
src: '.next/server/pages/**/*.html',
dest: 'dist/ssr/',
expand: true,
flatten: false
}
},
// 2. 注入错误边界脚本
'string-replace': {
errorBoundary: {
files: [{
expand: true,
cwd: 'dist/ssr/',
src: '**/*.html',
dest: 'dist/ssr/'
}],
options: {
replacements: [{
pattern: '</head>',
replacement: function() {
const code = `
<script>
(function(){
window.__SSR_ERROR_ID__='<%= grunt.template.today("yyyymmddHHMMss") %>';
window.onerror=function(m,u,l,c,o){window.__SSR_ERROR__=o;};
window.addEventListener('unhandledrejection',function(e){
window.__SSR_ERROR__=e.reason;
});
if(window.__NEXT_DATA__&&window.__NEXT_DATA__.err){
window.__SSR_ERROR__=window.__NEXT_DATA__.err;
}
})();
</script></head>`;
return code;
}
}]
}
}
},
// 3. 上报脚本(可灰度)
injector: {
options: {
starttag: '<!-- injector:js -->',
endtag: '<!-- endinjector -->',
transform: function(filePath) {
return '<script src="' + filePath + '?key=' + grunt.option('grayKey') + '"></script>';
}
},
local: {
files: {
'dist/ssr/**/*.html': ['static/report.js']
}
}
}
});
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-string-replace');
grunt.loadNpmTasks('grunt-injector');
grunt.registerTask('ssr-error', function() {
// 从环境变量读取灰度 key,缺省走全量
grunt.option('grayKey', process.env.GRAY_KEY || '0000');
grunt.task.run(['copy:ssrHtml', 'string-replace:errorBoundary', 'injector:local']);
});
// 4. 捕获异常并写日志(供 Jenkins 采集)
grunt.registerTask('ssr-capture', function() {
const done = this.async();
const glob = require('glob');
const fs = require('fs');
const path = require('path');
glob('dist/ssr/**/*.html', function(er, files) {
if (er) grunt.fail.fatal(er);
files.forEach(f => {
const html = fs.readFileSync(f, 'utf8');
const $ = cheerio.load(html);
const err = $('script').text().match(/window\.__SSR_ERROR__\s*=\s*(.+);/);
if (err) {
const logFile = path.join('logs', path.basename(f, '.html') + '.json');
grunt.file.write(logFile, JSON.stringify({
file: f,
error: err[1],
buildTime: grunt.template.today('isoDateTime')
}));
}
});
grunt.log.ok('SSR 异常捕获完成,共 ' + files.length + ' 文件');
done();
});
});
grunt.registerTask('default', ['ssr-error', 'ssr-capture']);
};
- 在 CI(Jenkins / GitLab Runner)里调用
GRAY_KEY=1234 grunt default
产物 dist/ssr/ 下的 HTML 已注入边界脚本,日志目录 logs/ 会被 SonarQube 插件采集,实现“构建即治理”。
拓展思考
- 灰度闭环:把
grayKey写入 HTML 注释,边缘节点(阿里云 CDN、腾讯云 ECDN) 可根据 Cookie 或 Header 做按号段灰度,回源时带上?grayKey=1234,实现“脚本灰度 + 内容灰度”双通道。 - 性能权衡:错误边界脚本仅 1.2 kB(gzip),但仍有阻塞风险;可改用
requestIdleCallback延迟上报,或把report.js改成async属性,首屏时间增加 < 5 ms。 - 与新版工具链共存:老项目用 Grunt,新项目用 Vite,可在统一容器镜像里把
grunt default作为prebuild钩子,产物供 Vite 静态托管,实现“双引擎构建,同一套监控”。 - 合规升级:等保 2.0 要求“用户敏感信息不出域”,可把
window.__SSR_ERROR__做AES 对称加密,密钥走内网 KMS 接口动态获取,Grunt 任务里通过grunt-secret插件注入,满足金融、政府项目审计。 - 未来迁移:一旦团队决定废弃 Grunt,可把上述逻辑抽象成独立 npm 包(
grunt-ssr-error-boundary),向下兼容 Grunt,向上暴露 Webpack/Vite 插件入口,实现“老任务平滑退役,监控能力零丢失”。