如何在production环境优雅的使用console.log

前言

前篇文章介绍了如何通过自定义Babel plugin,增强console输出信息:通过自定义Babel plugin增强console.log输出信息 - 掘金

但是现在还有个问题,如果代码输出的log很多,会导致f12里console信息很多,而且现在还加了前缀,特别在production环境上,信息很乱,有些信息不应该展示给客户,大部分信息应该只是给开发人员调试问题用的,而且log过多还可能导致内存泄漏的风险。

所以就想到一个思路,在生产环境上,把代码里console log不打印出来,缓存起来,然后需要分析调试问题时,由开发或测试人员把log输出到f12 console里,或者导出log文件,这个过程也可以通过一些流程和形式让客户自己生成log。

前提条件跟上篇文章一样,假设当前项目已经有大量的console log代码,手动一个个更改工作量很大,所以通过自定义Babel plugin转义console语法,达到需求。

源码:github.com/markz-demo/...

先准备一个环境

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文件。

相关推荐
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
black^sugar5 小时前
纯前端实现更新检测
开发语言·前端·javascript
理想不理想v6 小时前
webpack最基础的配置
前端·webpack·node.js
2401_857600956 小时前
SSM 与 Vue 共筑电脑测评系统:精准洞察电脑世界
前端·javascript·vue.js
2401_857600956 小时前
数字时代的医疗挂号变革:SSM+Vue 系统设计与实现之道
前端·javascript·vue.js
GDAL6 小时前
vue入门教程:组件透传 Attributes
前端·javascript·vue.js
小白学大数据6 小时前
如何使用Selenium处理JavaScript动态加载的内容?
大数据·javascript·爬虫·selenium·测试工具
2402_857583496 小时前
基于 SSM 框架的 Vue 电脑测评系统:照亮电脑品质之路
前端·javascript·vue.js