前言
前篇文章介绍了如何通过自定义Babel plugin,增强console输出信息:通过自定义Babel plugin增强console.log输出信息 - 掘金。
但是现在还有个问题,如果代码输出的log很多,会导致f12里console信息很多,而且现在还加了前缀,特别在production环境上,信息很乱,有些信息不应该展示给客户,大部分信息应该只是给开发人员调试问题用的,而且log过多还可能导致内存泄漏的风险。
所以就想到一个思路,在生产环境上,把代码里console log不打印出来,缓存起来,然后需要分析调试问题时,由开发或测试人员把log输出到f12 console里,或者导出log文件,这个过程也可以通过一些流程和形式让客户自己生成log。
前提条件跟上篇文章一样,假设当前项目已经有大量的console log代码,手动一个个更改工作量很大,所以通过自定义Babel plugin转义console语法,达到需求。
先准备一个环境
javascript
// src/index.js
import { test1 } from './index2';
btn1.onclick = function () {
console.log(123);
console.warn('abc');
console.log(123, 'abc', true, [1, 2, 3], { key: 'value' });
test1();
}
// src/index2.js
export function test1() {
console.log('index2 test1');
}
javascript
// src/logger.js
const logger = {
log: function (method, location, ...args) {
const date = new Date().toLocaleString();
const prefix = `[${date}] [${location}]`;
console[method](prefix, ...args);
},
};
export default logger;
这里定义一个button click方法,里面输出log,然后再调用另一个文件里的test1方法,再输出log。
思路
思路是先定义一个logger api文件暴露方法,然后自定义Babel plugin,检测哪些文件有console语法,然后再检测这些文件有没有import引用logger文件,如果没引用就添加引用,然后把console语法替换成logger.log
方法。
因为需要检测文件是否引用logger文件,如果用都用相对路径引用就会各文件不一,没法判断,所以需要用Webpack alias
给logger文件加个别名,为@logger
。
javascript
resolve: {
alias: {
'@logger': path.resolve(__dirname, "./src/logger.js"),
}
},
然后按上面的思路写Babel plugin,具体步骤参考注释。
javascript
const basename = require("path").basename;
const { addDefault } = require("@babel/helper-module-imports");
module.exports = (babel) => {
const { types: t, template } = babel;
return {
name: 'test-log',
visitor: {
CallExpression(path, state) {
if (t.isMemberExpression(path.node.callee) // 判断是不是对象成员,比如console.log
&& path.node.callee.object.name === 'console'
&& ['log', 'info', 'warn', 'error'].includes(path.node.callee.property.name)
) {
const filename = basename(state.file.opts.filename); // 获取文件名 比如index.js
const location = `${filename} ${path.node.loc.start.line}:${path.node.loc.start.column}`; // 获取console调用位置 比如7:8
const programPath = path.hub.file.path;
let importTrackerId = "";
programPath.traverse({
// 解析当前文件import
ImportDeclaration(path) {
const sourceValue = path.get("source").node.value;
if (sourceValue === '@logger') { // 判断是否有引用@logger
const specifiers = path.get("specifiers.0");
// 如果当前文件有引用@logger,记录import的变量名存在importTrackerId
importTrackerId = specifiers.get("local").toString();
path.stop();
}
},
});
if (!importTrackerId) {
// 如果当前文件没有引用@logger,则需要添加一个 import _loggerXX from '@logger',并且记录变量名存在importTrackerId
importTrackerId = addDefault(programPath, '@logger', {
nameHint: programPath.scope.generateUid("_logger"), // 会生成一个随机变量名,比如_logger2
}).name;
}
const window = t.identifier(importTrackerId); // 生成 _logger
const my_console = t.memberExpression(window, t.identifier('log')); // 生成 _logger.log
const args = [t.stringLiteral(path.node.callee.property.name), t.stringLiteral(location), ...path.node.arguments]; // 定义参数:"log", "index.js 7:8", ...args
const expression = t.callExpression(my_console, args); // 把参数传入方法中 _logger.log(method, location, ...args)
path.replaceWith(expression); // 替换当前console语法
}
}
}
};
}
转换大概效果如下:
javascript
console.log(123);
// 转换成--->
import _logger from "@logger";
_logger.log("log", "index.js 9:8", 123);
测试结果:
达到了预期,到这里只是替换了logger.log方法,还需要加上log缓存和下载log文件功能。
log缓存与下载
修改log
方法,把log存入缓存list。
javascript
const logCacheList = [];
const logger = {
log: function (method, location, ...args) {
const date = new Date().toLocaleString();
// 存入cache
logCacheList.push({ method, date, location, args });
},
}
添加print
方法console输出所有缓存log
javascript
const logger = {
...
print: function () {
// 遍历cache,console输出
logCacheList.forEach(({ method, date, location, args }) => {
window.console[method](`[${date}] [${location}]`, ...args);
});
},
添加export
方法,把所有log生成文件流并download为csv文件。
javascript
const logger = {
...
export: function () {
// 定义csv header,一共5列:number,type,date,location,message
const header = ['number', 'type', 'date', 'location', 'message'];
// 遍历cache生成csv格式数据,比如:"0","log","2023/11/6 16:52:53","index.js 9:8","123"
const logs = logCacheList.map(({ method, date, location, args }, i) =>
[i, method, date, location, ...args].map(v => {
if (typeof v === 'object') {
v = JSON.stringify(v);
}
v = String(v).replaceAll(`"`, `'`);
return '"' + v + '"';
}).join(',')
);
const content = [header, ...logs].join('\n');
const blob = new Blob([content], { type: 'text/plain,charset=UTF-8' }); // 生成blob
const url = URL.createObjectURL(blob); // 转换成url
// 通过a标签的download属性,调起浏览器下载
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = 'log.csv';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
},
运行结果
这样就达到了预期效果,代码里调用console
时,并不会在f12 console里输出,输入执行_logger.print()
时才会输出,执行_logger.export()
则会把log下载为csv文件。
优化
以上只是从技术角度探索下一些需求的可行性,有了这些技术机制就可以扩展优化下需求,比如:
客户操作页面时,如果发现请求异常,或者js抛错异常(弹框等形式)时,不仅要给support反馈correlation id等服务器端唯一标识,给后台开发查找后端log,也需要提供前端log。 可以在页面header提供一个feedback入口,点击可以触发上面的print或export方法,然后通过手动发送或者直接用程序发邮件附件log文件给support,这样就可以收集到出错时的前端log,而且客户端操作也很方便。 我这边还没有做过类似的交互需求,客户js抛错了基本都是跟客户要console截图,感觉太low了所以就有此想法,欢迎大佬分享下比较成熟的设计方案。
总结
本文介绍了通过Babel plugin,自动import底层logger js,并且转换代码里的console语法,替换为自定义底层logger方法,并且支持在生产环境缓存log及下载log csv文件。