打开浏览器的开发者工具,在控制台里敲下window.console,回车。屏幕上弹出来的不是一个简单的打印函数,而是一个对象,里面整整齐齐排着25个实用方法——log、error、info、table、warn、debug……等等,几乎你能想到的前端调试姿势,全在这个小窗口里等着你调用。可就是这25个方法,正在你的线上代码里心安理得地躺着,成为一个个随时可能引爆的数据泄漏点。

别觉得我在危言耸听。你现在去翻翻刚接手的老项目,或者问问团队里新来的实习生,有多少console.log语句根本没删?又有多少在敏感接口返回后,顺手把用户手机号、邮箱、定位、token直接打印了出来?这些日志一旦被任何人在浏览器里看到,就等于把裤兜翻给人看。而在生产环境,全世界只要用浏览器打开你的页面,就能在控制台里欣赏你精心调试的“内部数据展览”。

打开网易新闻 查看精彩图片

这不是小概率事件。但凡做过几次线上故障排查的人,都可能在崩溃的堆栈里顺带发现几行奇奇怪怪的日志,然后倒吸一口凉气:这东西还在线上跑着?更离谱的是,有些团队在开发阶段把console当成随身笔记本,代码里到处都是log,功能写完直接上线,根本没想过要不要关掉。原因很简单——关是不可能一个个去关的,项目里几百个文件,删都删不过来。

于是有人会说,那你倒是找个全局开关啊。还真有,而且办法简单到让人觉得憋屈。因为它本来就是一个对象上的方法,直接覆盖掉就行了。

你可能已经在stackoverflow上见过那句著名的代码:console.log = ()=>{}。很多人只把它当成一句玩笑,一个段子,一个随手涂鸦。但它确确实实就是猴子补丁(monkey patching)技术在浏览器端最赤裸的体现。所谓猴子补丁,通俗点说就是直接动手修改浏览器自带的内置行为,把原本会输出信息的方法替换成空函数,让它吃进再多数据都吐不出来。

做法很简单。在你的应用入口文件,也就是JavaScript解析器最开始执行的那一行代码之前,写上这一句:console.log = ()=>{}。注意,必须放在所有模块加载之前、所有页面逻辑之前,保证它最先执行。这样,之后不管你的业务代码里写了多少次console.log,统统会撞上一堵什么都没有的墙,全部被吞掉。

但你别急着高兴。这招看着痛快,其实暗藏杀机。很多开发者一听到能全局禁用,第一反应就是把console上所有方法都覆盖掉,log、error、warn、debug一概设为空函数。这就从一个极端跳到了另一个极端。你把error也禁了,生产环境突然出了致命异常,浏览器里安安静静,监控系统也未必能抓到,线上报错直接变成黑洞,排查问题的人只能对着一块白屏干瞪眼。

所以,对待那个包含25个方法的console对象,你得学会差别对待。log可以关掉,因为那大多是你临时写的调试信息;table、time、debug这些也可以视情况全部或部分禁用。但error一定要留着,它是线上事故的最后一条安全带,是程序在濒死时发出的最后一声呼救。

搞清楚了该关什么、该留什么,下一个问题就是:只在生产环境里关。开发阶段你还得靠着console来摸数据结构和排查接口,那才是它该在的地方。怎么判断当前是开发还是生产?这里有两个最常用的通路。

第一个是Node.js项目。运行环境里通常都会注入NODE_ENV这个变量,你只需要判断它是不是"production"。写法就一行:if (process.env.NODE_ENV === "production") { console.log = ()=>{}; }。把这段放在入口文件的最顶部,打包编译后一旦部署到线上,所有log立刻闭嘴,本地开发时完全不受影响。

第二个方式更适合非Node环境,或者你不太信任环境变量,想靠自己掌握的域名来做判断。原理也很直白,利用浏览器提供的location.origin,检查当前域名是不是你公司的正式地址。比如:if (location.origin.includes("mydomain")) { console.log = ()=>{}; }。只要域名匹配到你的主站,控制台输出就会被掐掉。这样一来,测试环境、本地环境、预发布环境即使代码一样,也照样能继续打log,只有真正面向用户的那套页面会静默。

不过现实常常比这更脏。很多人接手的项目根本没有这么清晰的部署流程,线上代码和本地代码完全一样,甚至没有构建过程,纯手写的HTML、CSS、JS直接上传。这种时候,你还得提防那个在页面初始化之前就插进来的统计脚本、聊天插件或者第三方客服组件,它们自己也会悄悄调用console.log。如果是这种情况,光在入口覆盖一次也许不够,你得确认猴补代码跑在所有外部资源之前,甚至是放在最靠前的inline脚本里。

更要命的是console对象里的25个方法并不都是IE时代的老古董,有很多新方法是现代前端调试的心头好。比如console.table能把数组或对象用表格形式打印出来,看后端返回的列表数据一目了然;console.group和groupCollapsed能折叠日志层级;console.time和timeEnd能计算代码片段的执行时长,在做性能优化的时候特别有用。你的代码里很可能不是只用了log一个方法,而是log、table、time满天飞。如果没有全覆盖,上线后其他方法照旧会在控制台打印,等于白干。

那是否要把所有25个方法全部逐个覆盖呢?理论上是,但实际操作时记得专门把error挑出来留着。一个比较稳妥的策略是:直接在入口处重写一个干净的console对象,把除了error之外的方法全部设为空函数。这样可以一次性封堵所有可能的泄漏路径,也不用担心未来谁突然用了console.info或者console.trace。然而这种做法也带来新的问题,毕竟直接替换整个对象可能和你依赖的某个第三方监控工具冲突,人家也许会往console上挂自定义方法,或是依赖console的其他内部引用。所以最安全的选择仍然是明确列出你要禁用的那几个高频方法,log、info、debug、warn都封掉,table和time按需处理,其余尽量不动。

说一千道一万,console开关这件事明明三行代码就能搞定,为什么大把的项目还是裸奔上线?根源在于开发流程里缺少“生产环境代码检查”这一环。编译打包的时候没人去扫描console语句,代码审查也不会专门留意console.log有没有被删掉,测试阶段更是几乎不会去检查线上环境会不会暴露敏感数据。最终的结果就是,团队在办公室里对着控制台修改逻辑,上线后用户对着控制台看你们的数据,只有黑客在暗处笑出声。

如果今天你要着手解决这个问题,我建议不要只加一行猴子补丁就了事,而要顺手把这件事写进团队的开发规范里。第一,明确要求console只用于本地调试,提交代码前必须移除或通过构建工具自动去除;第二,给ESLint加上no-console规则,让它直接卡住所有带console的提交;第三,在CI/CD流程中加入扫描,发现生产构建产物里仍然存在console语句就中止部署。这三步走完,console泄漏才算是真正上了锁。

当然,再严格的规范也有管不到的地方,比如紧急修复时直接在线改代码、运营活动页临时加的埋点输出等等。正因如此,猴子补丁那个空函数覆盖才更像最后一道兜底的保险丝。它能保证就算代码里不小心留下了log,也绝不会出现在用户的屏幕上。

最后回到那个被很多人踩过的坑:千万别动console.error。它不仅不能关,你甚至应该保证它的输出渠道永远畅通。因为无论你的线上监控体系有多完善,总有一些异常会在SDK初始化之前就发生,或者在监控脚本加载失败时直接陷入真空。此时唯一能抓住那一束错误信号的,就是原生的console.error。它可能是唯一一个向这个世界发出“我不行了”的窗口,你嫌它吵把它封死了,那就只能祈祷用户好心给你截张图。

所以,下次再看到有人想都不想就把整个console全部覆盖掉,赶紧拦住他。一个25个方法的对象,关掉20个你还有救,关掉error那就真的聋了。