在 SAP 生态里混过几年的前端,大概没人敢说没用过 Log.js。不管是调试一个 XMLTemplateProcessor 的解析问题,还是排查某个 Model 更新顺序的诡异 bug,第一反应基本都是打开控制台看 sap/base/Log 输出的日志。但这个每天被调用无数次的模块,内部到底是怎么设计的,可能很多人没细究过。
我最近正好把 Log.js 的源码从头到尾过了一遍,越看越觉得这东西比想象中有意思。它表面上只是个日志工具,实际上却是一个集状态管理、缓存策略、订阅发布、多端适配于一体的基础设施模块。这篇就当是我的源码阅读笔记,结合我自己在实际项目里踩过的坑,把 Log.js 的核心机制、关键 API 实现思路,以及一个很常见的 log.js:72 [echarts] can't get dom width or height 报错背后的排查逻辑,一次讲清楚。
1. 先搞清楚一件事:Log.js 到底在管什么
1.1 日志不是 console.log 的简单 wrapper
很多前端开发者对日志模块的理解就是“给 console.log 套个壳”,加个时间戳、加个级别前缀,完事。但 UI5 的 Log.js 显然不是这个思路。它更像是一个 日志数据中心,除了把日志输出到控制台,它还维护了一个内存缓冲区,供框架内部在运行时随时检索。比如有些问题在页面加载早期就发生了,等你打开控制台想复现时可能已经看不到完整链路,UI5 之所以能通过 Log.getLogEntries() 把历史日志捞回来,靠的就是这个缓冲区。
这也解释了为什么 Log.js 被归类在 sap/base/ 目录下——它是整个 UI5 框架的基础设施,几乎所有模块都会依赖它,所以它的设计必须足够轻量、稳定、无副作用,不能反向依赖任何 UI 组件。
1.2 两种模块形态:老版本与新版本
如果你看老一些的 UI5 项目,可能会看到 jQuery.sap.log 这种调用方式。这是 UI5 1.x 早期基于 jQuery 封装的日志接口,在 1.58 左右开始逐步被新的 sap/base/Log 替代。新模块是 ES2015 风格的纯 JavaScript 模块,不再强依赖 jQuery,这也是 UI5 走向现代前端架构的关键一步。
从源码阅读的角度,我建议你直接看 sap/base/Log.js,也就是新版实现。它更纯粹,没有 jQuery 兼容的历史包袱,而且模块内部大量的细节处理(比如 console 对象缺失、浏览器兼容性、回调函数异常隔离)都非常值得学习。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 源码骨架:闭包里的那些私货
2.1 模块结构与私有状态
打开 sap/base/Log.js,第一眼会看到一个标准的 IIFE(立即执行函数表达式),外层函数返回一个 Log 对象。这样做的目的很单纯:所有核心状态都锁在闭包里,外部只能通过暴露的 API 间接操作。这也是一种非常经典的“单体模式”实现,UI5 框架内大量模块都是这种风格。
模块内部的私有变量大致如下:
javascript复制var iLevel = 3; // 当前日志级别,默认 ERROR
var aLog = []; // 日志缓冲区,存所有历史条目
var bLogSupport = false; // 浏览器是否支持 console
var fOnLog = null; // 日志监听回调
var sDefaultComponent = ""; // 默认组件标识
var iMaxEntries = 500; // 缓冲区最大条数
这里有个细节值得注意:iLevel 的默认值是 3(即 ERROR),也就是说在不显式调用 Log.setLevel() 的情况下,INFO、WARNING、DEBUG 这些级别的日志是不会出现在输出里的。很多新手在调 bug 时发现 console 一片干净,以为是代码没执行,其实只是日志级别挡住了。
2.2 日志级别的业务语义
UI5 的日志级别不是一个简单的枚举,它背后有明确的业务语义:
| 级别 | 数值 | 语义 |
|---|---|---|
| FATAL | 0 | 致命错误,程序无法继续运行 |
| ERROR | 1 | 一般错误,某个功能不可用 |
| WARNING | 2 | 警告,可能引发问题但不影响主流程 |
| INFO | 3 | 信息,记录业务流转 |
| DEBUG | 4 | 调试信息,开发者用 |
| TRACE | 5 | 跟踪,最详细的方法级记录 |
| ALL | 6 | 全部输出 |
| NONE | 7 | 关闭所有日志 |
这里面最容易被忽略的是 FATAL 和 TRACE 的边界。我自己的习惯是:系统初始化失败、依赖缺失这类直接导致模块无法工作的场景用 FATAL,而 TRACE 通常只在分析复杂数据流时临时打开,平时开着性能损耗太大。
2.3 日志缓冲区 Entry 结构
aLog 数组中存的对象,结构大致是:
javascript复制{
time: 1635308800000, // 时间戳
level: 3, // 日志级别
message: "Something happened",
component: "Some.Component",
details: null // 附加详情
}
在 Log.js 里,details 往往不是必须的,但一旦框架内部调用时传入了 details 参数,它会作为日志条目的补充信息存下来,不会直接拼进 message。这个设计非常有价值,因为拼进 message 会把结构化信息变成纯字符串,后续检索、分析就很麻烦。
3. 核心 API 逐个拆
3.1 打日志的入口:log() 与便捷方法
Log.js 最核心的方法是 log,它的签名是:
javascript复制Log.log(level, message, details, component)
而 info()、warning()、error()、debug()、trace() 这些便捷方法本质上都是对 log 的封装。举个例子:
javascript复制Log.error("Something failed", "Error details", "My.Component");
内部它会校验 level 是否小于等于当前的 iLevel(这里是指数值,注意 UI5 的级别数值越小越严重,所以判断是 level <= iLevel),只有通过校验才会真正写入缓冲区以及输出到 console。
这里有个很重要的性能考量:先把 level 比较放在前面,再决定是否需要拼接最终字符串。如果你在 log 入口处直接 String(message),就算日志被过滤掉也会产生一次无谓的字符串拼接。日志频繁调用时,这会影响性能。这也是为什么 UI5 内部调用 Log.error 时经常传一个函数而不是字符串:
javascript复制Log.error(function() {
return "Computed message: " + complexComputation();
});
当级别被过滤时,这个函数根本不会执行,从而避免昂贵的字符串构造。这是一个非常实用的小技巧。
3.2 级别控制:setLevel / getLevel
setLevel 用于动态控制全局日志级别:
javascript复制Log.setLevel(Log.Level.INFO);
源码实现中,它同时维护了两个状态:一个是当前生效级别 iLevel,另一个是调用 isLogLevelAtLeast 可能用到的辅助状态。实际上 isLogLevelAtLeast 就是简单比较 level >= iLevel,但因为 UI5 的级别数值方向和直觉相反(数值越大越啰嗦),这个方法的语义值得仔细读一遍。
我在实际项目中遇到过一种坑:某组件在 onInit 里调用了 Log.setLevel(Log.Level.ERROR),这会导致全局日志级别被改掉,之后想看另一个组件的 DEBUG 日志怎么都看不出来。后来排查才发现是这种“全局写状态”的副作用。正确做法是尽量不用 setLevel 改全局级别,而是用 sap/ui/core/library 里的调试配置,或者临时在控制台手动设置。
3.3 组件标签:setComponent
setComponent(component) 设置默认组件标签。当调用 Log.info() 等方法时如果没有显式传入第四参 component,就会使用 sDefaultComponent。
这个组件参数在大型项目里非常重要。一个 UI5 应用往往包含十几个模块,如果日志没有组件标识,搜索日志时只能靠猜。我在项目里通常约定:组件名用命名空间+模块名,比如 eshop.cart.controller.CartController,这样在 getLogEntries() 里按组件过滤时,能精准定位问题模块。
3.4 订阅能力:addLogListener
老版本的 jQuery.sap.log 有 addLogListener,新版的 sap/base/Log 也有对应的 Log.addLogListener 和 Log.removeLogListener。注册的监听函数会在每次日志写入时被调用,参数包含日志级别、消息、组件、详情等。
这个机制常见用途有两个:
- 自定义日志上报:把前端日志聚合发送到后端采集系统。
- 在测试中拦截日志断言:比如单元测试里注册一个 listener,断言某个特定错误确实被记录过。
源码里有一个细节:当 fOnLog 存在时,它并不是简单地同步调用,而是用 try { ... } catch (e) { /* ignore */ } 把回调异常吞掉。也就是说监听函数的异常不会影响主日志流程。这是一种非常稳健的设计——日志模块本身不应该因为监听者的 bug 而崩溃。
3.5 缓冲区管理:getLogEntries / setMaxLogEntries
Log.getLogEntries() 返回整个缓冲区,setMaxLogEntries 控制最大容量。默认 500 条,超过后最老的数据会被丢弃。
这里有一个容易被忽略的点:默认 500 条是 UI5 官方权衡内存与调试便利后给出的值。如果日志量巨大,又不希望缓冲区占用太多内存,可以调小;反过来,在测试环境里临时调到 5000 条,能拿到更长的运行轨迹。
在看源码时,我会关注它内部的“覆盖”策略:
javascript复制if (aLog.length > iMaxEntries) {
aLog.splice(0, aLog.length - iMaxEntries);
}
注意这里不是像队列那样先 shift() 再 push(),而是批量 splice 裁剪头部。在最多 500 条的数据规模下这样没问题,但大量条目时它的效率其实是 O(n)。这就是个典型的“够用就好”例子。
3.6 时间戳生成:getTimeStamp
Log.getTimeStamp() 返回当前格式化时间字符串,格式是 yyyy/MM/dd HH:mm:ss.SSS。源码里没有直接依赖 Date.toISOString,而是用 pad 之类的方式手动补零。
UI5 之所以要自己格式化,是因为 toISOString 返回的是 UTC 时间,而大多数业务场景需要本地时间。此外 .SSS 毫秒用于精确排布事件顺序,这在分析异步任务时序时特别有用。我在排查竞态问题时,经常把 getLogEntries 的 JSON 导出,然后按时间戳逐条对照,往往能发现某个异步回调出现在预期之前的诡异场景。
4. 控制台兼容性:老版本浏览器的那些坑
4.1 如何安全地调用 console
Log.js 源码里有一段关于 console 对象的探测逻辑。它检查 window.console 是否存在,以及 console.info、console.debug 等方法是否可用。在老 IE 里,console 对象在没有打开开发者工具时根本不存在,直接调用会抛 ReferenceError。
解决方案是先把 console 方法缓存在局部变量里:
javascript复制var fnInfo = null;
if (bLogSupport) {
fnInfo = (console.info || console.log).bind(console);
}
注意这里用了 bind(console),因为有些浏览器的 console 方法必须有正确的 this 指向,否则会报 IllegalArgumentException(Safari 老版本有这个问题)。
也因为这个原因,Log.log 输出的日志并不是统一走 console.log 的,而是按级别对应到 console.error、console.warn、console.info、console.debug。这样做的直接好处是浏览器控制台可以按级别高亮、过滤,而不需要 log 内容里前缀一堆 [ERROR] 文字。
4.2 为什么不直接用 console
有一个问题很值得思考:既然最后都是输出到 console,为什么不直接在业务代码里调用 console 方法?
原因至少有三点:
- 可检索性:
console输出是即时的、不可回溯的,而Log.js的缓冲区可以随时用getLogEntries捞回历史记录。 - 可控制性:你可以用
setLevel全局关闭或开启某一级别的日志,普通的 console 做不到按级别统一开关。 - 可扩展性:通过
addLogListener,你能把日志接入自定义上报、测试断言,而这些在不改动业务代码的情况下完成。
这也是“框架日志模块”和“裸 console”之间本质的区别。Log.js 本质上是一个“日志总线”,console 只是其中一个输出端。
5. 延伸排查:log.js:72 与 ECharts 的“容器没宽高”
5.1 这个 log.js 和 UI5 的 Log.js 是两回事
最近在不少问答平台上反复看到一条报错:log.js:72 [echarts] can't get dom width or height. please check dom.clientWidth。
很多刚接触 UI5 加 ECharts 组合的开发者会误以为这是 UI5 的 Log.js 在报错,急着去翻 sap/base/Log.js,结果发现风马牛不相及。实际上这里的 log.js:72 是 ECharts 源码内部的日志模块输出的,文件位于 node_modules/echarts/lib/util/log.js 附近,第 72 行就是它的 warn 函数实现。说白了,这是 ECharts 的日志模块,不是 UI5 的日志模块,只是文件名字都叫 log.js,容易让人混淆。
这个报错意味着 ECharts 在初始化时,目标容器拿到的宽度或高度是 0。对 ECharts 来说,它需要一个有实际尺寸的 DOM 容器才能计算画布大小,如果拿不到尺寸,图表就无法正确渲染。
5.2 什么场景最容易触发这个报错
根据我的实际排查经验,最常见的场景是以下这几类:
- 容器处于
display: none状态时执行了echarts.init()。 - 容器在
FlexBox或HBox中还没有完成布局计算(布局尚未 ready),clientWidth仍为 0。 - 在页面导航到某个视图的
onInit里立即创建图表,但视图还没插入到 DOM,或者插入后还没触发渲染。 - 容器虽然有宽度,但高度设为
100%且父元素高度为auto,导致最终高度塌陷为 0。
5.3 实际排查步骤与代码
根本思路是:确保容器在初始化图表前已经有非零宽高。我的建议排查流程如下:
-
打开控制台,在报错位置点进去看一下当前容器的
clientWidth和clientHeight:javascript复制document.getElementById("chartContainer").clientWidth document.getElementById("chartContainer").clientHeight如果两个都是 0,说明 DOM 布局没完成或容器隐藏。
-
如果是布局未完成,把
echarts.init放到 UI5 的onAfterRendering里,或者用requestAnimationFrame延迟一次:javascript复制onAfterRendering: function() { this._initChart(); } -
如果容器在
Dialog、Popover或FlexBox中,要等这些控件弹出/渲染完成后初始化,必要时用setTimeout或者监听afterOpen事件。 -
检查
.css里容器的高度设定。如果是height: 100%,父元素必须有不小于容器的高度;如果父元素是auto,子元素 100% 高度无效。 -
如果图表已经初始化但后来容器尺寸变化(比如左侧栏折叠),需要调用
chart.resize()让 ECharts 重新计算:javascript复制window.addEventListener("resize", function() { chart.resize(); });
要说最省心的做法,我比较推荐封装一个小组件:初始化和 resize 都放在一个方法里,onAfterRendering 时调用,onBeforeRendering 时销毁或延迟重建,这样能有效避开大部分“容器没有宽高”的问题。
6. 怎么读 Log.js 这类源码才不亏
6.1 善用断点和条件断点
纯读源码容易停留在语法理解层面,我建议你在自己的 demo 项目里引用一个 Log.error 调用,然后在浏览器 Sources 面板里打开 sap/base/Log.js,对 log 方法设置断点。断点命中后,你可以逐步观察 iLevel、aLog、sDefaultComponent 的实时值,比空想源码逻辑直观得多。
如果你只关心某个特定组件产生的日志,可以在断点上加条件,比如 component === "My.Component",这样日志干扰会小很多,调试体验更接近真实项目。
6.2 在控制台直接操作 Log 对象
Log.js 暴露的 API 在全局 namespace 下是可以直接访问的。你在控制台输入 sap.base.Log.getLogEntries() 就能看到当前缓冲区里所有日志条目;输入 sap.base.Log.setLevel(sap.base.Log.Level.DEBUG) 可以直接动态开 DEBUG 日志,随时复现“日志不显示”的场景。
这里顺便说一个调 UI5 应用日志的经验:如果页面已经跑起来但又想从零开始记录日志,可以调用 sap.base.Log.clear()(如果存在)来清空缓冲区,或者直接 sap.base.Log.setMaxLogEntries(2000) 把大数据量记录能力打开,再复现 bug,最后导出日志分析。
6.3 读源码的顺序建议
如果你是第一次尝试读 UI5 源码,Log.js 确实是个很好的入门选择。它的体量小、依赖少、行为可预测。读完它之后,你可以按这个顺序继续:
sap/base/util/uid.js:看一下唯一 ID 生成,跟Log.js一样是零依赖小模块。sap/base/util/ObjectPath.js:了解 namespace 管理。sap/ui/core/Component.js:这时候再读组件生命周期,会更有全局观。
读完几个基础模块再去看 UI5 的渲染机制、模型绑定,会明显感觉轻松很多。因为框架设计里大量复用同样的模式和思想,比如闭包状态、模块级单例、事件订阅、兼容性垫片,你在 Log.js 里已经见过了,再遇到就不会觉得陌生。
我在实际项目中还有个小习惯:凡是像 Log.js 这种基础模块,我都会在本地建一个带注释的“源码笔记”副本,把自己对每个 API 的理解和踩坑记录贴进去,等遇到类似问题时直接翻自己的笔记,比再读一遍源码快得多。这个方法也推荐给你试试,读源码不追求一次全记住,而是把理解沉淀成自己的资产,后面每一次翻阅都会更高效。
