#跟着若城学鸿蒙# Node.js的debug模块源码分析及在harmonyOS平台移植
Debug库 是一个小巧但功能强大的 JavaScript 调试工具库,可以帮助开发人员更轻松地进行调试,以便更快地发现和修复问题。它的主要特点是可以轻松地添加调试日志语句,同时在不需要调试时可以轻松地禁用它们,以避免在生产环境中对性能产生影响。我们在一些有名的三方库如socket.io,就能看到debug库的身影,说明它确实很常用。
Debug 库介绍
一个模仿Node.js核心调试技术的小型JavaScript调试实用程序。适用于Node.js和web浏览器。Debug库 是一个小巧但功能强大的 JavaScript 调试工具库,可以帮助开发人员更轻松地进行调试,以便更快地发现和修复问题。它的主要特点是可以轻松地添加调试日志语句,同时在不需要调试时可以轻松地禁用它们,以避免在生产环境中对性能产生影响。
它提供了一种比console.log()更方便的打印调试信息的方式。Debug 库包含了一个全局函数 debug(),通过调用这个函数可以创建一个具有指定名称的调试器对象。调试器对象提供了一些方法,可以用于在控制台中输出调试信息。同时,可以使用环境变量来控制调试器对象是否启用。
我移植发布的鸿蒙的debug库:
参见鸿蒙三方中心库:OpenHarmony三方库中心仓
简单使用
下面是一个 Debug 库的代码示例:
let debug = require('debug')('myapp');
function myFunction() {
debug('This is a debug message');
debug('This is a debug message:%d',2);
}
myFunction();
首先使用 require() 函数将 Debug 库引入到我们的代码中,并调用 debug() 函数创建一个名为 "myapp" 的调试器对象。然后,我们定义一个名为 myFunction() 的函数,在这个函数中调用调试器对象的 debug() 方法输出一条调试信息。
如果我们想要在控制台中看到这些调试信息,我们需要设置环境变量 DEBUG 的值为 "myapp",这可以在终端中使用命令行来完成:
$ DEBUG=myapp node myscript.js
这会启动名为 "myapp" 的调试器对象,并在控制台中输出所有与该名称相关联的调试信息。如果我们要禁用调试信息,只需要将 DEBUG 环境变量的值设置为空即可:
$ DEBUG= node myscript.js
这将禁用所有调试信息,避免在生产环境中对性能产生影响。
此外,js-debug库还支持设置不同的命名空间来过滤特定的调试信息。例如:
const debug1 = require('debug')('app:debug1')
const debug2 = require('debug')('app:debug2')
然后在终端输入DEBUG=app:* node app.js
,则会输出形如app:debug1 This is debug1 message!
的信息。
上述示例是基于nodejs的命令行环境下的。
鸿蒙下的使用举例:
import debugModule from "@yyz116/debug" // debug()
const debug = debugModule("MyApp:client");
debug.enable('') //为空则是禁用日志输出
debug.enable('MyApp:client'); //开启日志。注意,默认日志输出是关闭的,开启调试日志需要代码里调用enbale接口。不同于原库的环境变量方式
debug('booting %s', 'Hello MyTest');
debug("this is a %d test", 5);
debug("hello");
源码简析
源码很简单,代码量也不多,很容易看懂。首先看 index.js:
if (typeof process !== 'undefined' && process.type === 'renderer') {
module.exports = require('./browser.js');
} else {
module.exports = require('./node.js');
}
说明它同时支持浏览器环境和nodejs环境,通过if判断process对象是否存在来决定是使用哪个文件的导出。
由于我们是要移植到鸿蒙系统中的,于是node的那版就不看了,直接看browser.js源码:
/* eslint-env browser */
/**
* This is the web browser implementation of `debug()`.
*/
exports.formatArgs = formatArgs;
exports.save = save;
exports.load = load;
exports.useColors = useColors;
exports.storage = localstorage();
exports.destroy = (() => {
let warned = false;
return () => {
if (!warned) {
warned = true;
console.warn('Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.');
}
};
})();
/**
* Colors.
*/
exports.colors = [
'#0000CC',
'#0000FF',
......
'#FFCC33'
];
/**
* Currently only WebKit-based Web Inspectors, Firefox >= v31,
* and the Firebug extension (any Firefox version) are known
* to support "%c" CSS customizations.
*
* TODO: add a `localStorage` variable to explicitly enable/disable colors
*/
// eslint-disable-next-line complexity
function useColors() {
// NB: In an Electron preload script, document will be defined but not fully
// initialized. Since we know we're in Chrome, we'll just detect this case
// explicitly
if (typeof window !== 'undefined' && window.process && (window.process.type === 'renderer' || window.process.__nwjs)) {
return true;
}
// Internet Explorer and Edge do not support colors.
if (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/)) {
return false;
}
// Is webkit? http://stackoverflow.com/a/16459606/376773
// document is undefined in react-native: https://github.com/facebook/react-native/pull/1632
return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) ||
// Is firebug? http://stackoverflow.com/a/398120/376773
(typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) ||
// Is firefox >= v31?
// https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages
(typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31) ||
// Double check webkit in userAgent just in case we are in a worker
(typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/));
}
/**
* Colorize log arguments if enabled.
*
* @api public
*/
function formatArgs(args) {
args[0] = (this.useColors ? '%c' : '') +
this.namespace +
(this.useColors ? ' %c' : ' ') +
args[0] +
(this.useColors ? '%c ' : ' ') +
'+' + module.exports.humanize(this.diff);
if (!this.useColors) {
return;
}
const c = 'color: ' + this.color;
args.splice(1, 0, c, 'color: inherit');
// The final "%c" is somewhat tricky, because there could be other
// arguments passed either before or after the %c, so we need to
// figure out the correct index to insert the CSS into
let index = 0;
let lastC = 0;
args[0].replace(/%[a-zA-Z%]/g, match => {
if (match === '%%') {
return;
}
index++;
if (match === '%c') {
// We only are interested in the *last* %c
// (the user may have provided their own)
lastC = index;
}
});
args.splice(lastC, 0, c);
}
/**
* Invokes `console.debug()` when available.
* No-op when `console.debug` is not a "function".
* If `console.debug` is not available, falls back
* to `console.log`.
*
* @api public
*/
exports.log = console.debug || console.log || (() => {});
/**
* Save `namespaces`.
*
* @param {String} namespaces
* @api private
*/
function save(namespaces) {
try {
if (namespaces) {
exports.storage.setItem('debug', namespaces);
} else {
exports.storage.removeItem('debug');
}
} catch (error) {
// Swallow
// XXX (@Qix-) should we be logging these?
}
}
/**
* Load `namespaces`.
*
* @return {String} returns the previously persisted debug modes
* @api private
*/
function load() {
let r;
try {
r = exports.storage.getItem('debug');
} catch (error) {
// Swallow
// XXX (@Qix-) should we be logging these?
}
// If debug isn't set in LS, and we're in Electron, try to load $DEBUG
if (!r && typeof process !== 'undefined' && 'env' in process) {
r = process.env.DEBUG;
}
return r;
}
/**
* Localstorage attempts to return the localstorage.
*
* This is necessary because safari throws
* when a user disables cookies/localstorage
* and you attempt to access it.
*
* @return {LocalStorage}
* @api private
*/
function localstorage() {
try {
// TVMLKit (Apple TV JS Runtime) does not have a window object, just localStorage in the global context
// The Browser also has localStorage in the global context.
return localStorage;
} catch (error) {
// Swallow
// XXX (@Qix-) should we be logging these?
}
}
module.exports = require('./common')(exports);
const {formatters} = module.exports;
/**
* Map %j to `JSON.stringify()`, since no Web Inspectors do that by default.
*/
formatters.j = function (v) {
try {
return JSON.stringify(v);
} catch (error) {
return '[UnexpectedJSONParseError]: ' + error.message;
}
};
代码量很少,注定移植是很简单的事情。这个文件中就定义了一系列的函数,挂载到了node的exports对象上面。至于功能是干什么的,简单的不用再说了。直接看这里:
module.exports = require('./common')(exports);
const {formatters} = module.exports;
这里才是关键,核心的类和对象的实现是在common.js文件里的。
//common.js
/**
* This is the common logic for both the Node.js and web browser
* implementations of `debug()`.
*/
function setup(env) {
createDebug.debug = createDebug;
createDebug.default = createDebug;
createDebug.coerce = coerce;
createDebug.disable = disable;
createDebug.enable = enable;
createDebug.enabled = enabled;
createDebug.humanize = require('ms');
createDebug.destroy = destroy;
Object.keys(env).forEach(key => {
createDebug[key] = env[key];
});
/**
* The currently active debug mode names, and names to skip.
*/
createDebug.names = [];
createDebug.skips = [];
/**
* Map of special "%n" handling functions, for the debug "format" argument.
*
* Valid key names are a single, lower or upper-case letter, i.e. "n" and "N".
*/
createDebug.formatters = {};
/**
* Selects a color for a debug namespace
* @param {String} namespace The namespace string for the debug instance to be colored
* @return {Number|String} An ANSI color code for the given namespace
* @api private
*/
function selectColor(namespace) {
let hash = 0;
for (let i = 0; i < namespace.length; i++) {
hash = ((hash << 5) - hash) + namespace.charCodeAt(i);
hash |= 0; // Convert to 32bit integer
}
return createDebug.colors[Math.abs(hash) % createDebug.colors.length];
}
createDebug.selectColor = selectColor;
/**
* Create a debugger with the given `namespace`.
*
* @param {String} namespace
* @return {Function}
* @api public
*/
function createDebug(namespace) {
let prevTime;
let enableOverride = null;
let namespacesCache;
let enabledCache;
function debug(...args) {
// Disabled?
if (!debug.enabled) {
return;
}
const self = debug;
// Set `diff` timestamp
const curr = Number(new Date());
const ms = curr - (prevTime || curr);
self.diff = ms;
self.prev = prevTime;
self.curr = curr;
prevTime = curr;
args[0] = createDebug.coerce(args[0]);
if (typeof args[0] !== 'string') {
// Anything else let's inspect with %O
args.unshift('%O');
}
// Apply any `formatters` transformations
let index = 0;
args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format) => {
// If we encounter an escaped % then don't increase the array index
if (match === '%%') {
return '%';
}
index++;
const formatter = createDebug.formatters[format];
if (typeof formatter === 'function') {
const val = args[index];
match = formatter.call(self, val);
// Now we need to remove `args[index]` since it's inlined in the `format`
args.splice(index, 1);
index--;
}
return match;
});
// Apply env-specific formatting (colors, etc.)
createDebug.formatArgs.call(self, args);
const logFn = self.log || createDebug.log;
logFn.apply(self, args);
}
debug.namespace = namespace;
debug.useColors = createDebug.useColors();
debug.color = createDebug.selectColor(namespace);
debug.extend = extend;
debug.destroy = createDebug.destroy; // XXX Temporary. Will be removed in the next major release.
Object.defineProperty(debug, 'enabled', {
enumerable: true,
configurable: false,
get: () => {
if (enableOverride !== null) {
return enableOverride;
}
if (namespacesCache !== createDebug.namespaces) {
namespacesCache = createDebug.namespaces;
enabledCache = createDebug.enabled(namespace);
}
return enabledCache;
},
set: v => {
enableOverride = v;
}
});
// Env-specific initialization logic for debug instances
if (typeof createDebug.init === 'function') {
createDebug.init(debug);
}
return debug;
}
function extend(namespace, delimiter) {
const newDebug = createDebug(this.namespace + (typeof delimiter === 'undefined' ? ':' : delimiter) + namespace);
newDebug.log = this.log;
return newDebug;
}
/**
* Enables a debug mode by namespaces. This can include modes
* separated by a colon and wildcards.
*
* @param {String} namespaces
* @api public
*/
function enable(namespaces) {
createDebug.save(namespaces);
createDebug.namespaces = namespaces;
createDebug.names = [];
createDebug.skips = [];
let i;
const split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/);
const len = split.length;
for (i = 0; i < len; i++) {
if (!split[i]) {
// ignore empty strings
continue;
}
namespaces = split[i].replace(/\*/g, '.*?');
if (namespaces[0] === '-') {
createDebug.skips.push(new RegExp('^' + namespaces.slice(1) + '$'));
} else {
createDebug.names.push(new RegExp('^' + namespaces + '$'));
}
}
}
/**
* Disable debug output.
*
* @return {String} namespaces
* @api public
*/
function disable() {
const namespaces = [
...createDebug.names.map(toNamespace),
...createDebug.skips.map(toNamespace).map(namespace => '-' + namespace)
].join(',');
createDebug.enable('');
return namespaces;
}
/**
* Returns true if the given mode name is enabled, false otherwise.
*
* @param {String} name
* @return {Boolean}
* @api public
*/
function enabled(name) {
if (name[name.length - 1] === '*') {
return true;
}
let i;
let len;
for (i = 0, len = createDebug.skips.length; i < len; i++) {
if (createDebug.skips[i].test(name)) {
return false;
}
}
for (i = 0, len = createDebug.names.length; i < len; i++) {
if (createDebug.names[i].test(name)) {
return true;
}
}
return false;
}
/**
* Convert regexp to namespace
*
* @param {RegExp} regxep
* @return {String} namespace
* @api private
*/
function toNamespace(regexp) {
return regexp.toString()
.substring(2, regexp.toString().length - 2)
.replace(/\.\*\?$/, '*');
}
/**
* Coerce `val`.
*
* @param {Mixed} val
* @return {Mixed}
* @api private
*/
function coerce(val) {
if (val instanceof Error) {
return val.stack || val.message;
}
return val;
}
/**
* XXX DO NOT USE. This is a temporary stub function.
* XXX It WILL be removed in the next major release.
*/
function destroy() {
console.warn('Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.');
}
createDebug.enable(createDebug.load());
return createDebug;
}
module.exports = setup;
代码也比较简单。主要看三个函数,setup, createDebug函数和其内部的debug函数。
function setup(env) {
createDebug.debug = createDebug;
createDebug.default = createDebug;
createDebug.coerce = coerce;
createDebug.disable = disable;
createDebug.enable = enable;
createDebug.enabled = enabled;
createDebug.humanize = require('ms');
createDebug.destroy = destroy;
Object.keys(env).forEach(key => {
createDebug[key] = env[key];
});
/**
* The currently active debug mode names, and names to skip.
*/
createDebug.names = [];
createDebug.skips = [];
.....
}
JavaScript中的函数是一种特殊的对象,因此可以像普通对象一样被扩展属性和方法。这种行为是由JavaScript中的函数对象的特性所决定的。
在JavaScript中,函数也是一种对象,也可以拥有属性和方法。当我们定义一个函数时,实际上是在内存中创建了一个函数对象,并且该函数对象可以拥有自己的属性和方法。这意味着我们可以在函数外部对其进行扩展,添加新的属性和方法,或者修改已有的属性和方法。上述代码是给createDebug
的函数扩展了一些属性和方法。这些属性会成为函数对象的一部分,并且可以被访问和使用。
JavaScript中函数对象的这种特性为我们提供了一定的灵活性,使得可以在函数对象上添加额外的信息或者功能,这在某些场景下是非常有用。
需要注意的是,尽管函数对象可以被扩展属性和方法,但这并不意味着推荐在实际开发中频繁地对函数对象进行扩展。通常情况下,函数应当专注于其原本的用途和功能,并遵循单一职责原则。
另外需要注意的是,createDebug
是一个函数而不是一个类。在JavaScript中,函数和类都可以用来创建对象,但它们的语法和用法有所不同。createDebug
函数被用作一个工厂函数,用于创建调试器对象。它返回一个包含内部状态和方法的对象,而不是一个类的实例。通过调用createDebug
函数,我们可以得到一个具有特定功能和状态的调试器对象。在这种情况下,并没有使用JavaScript中的类(class)关键字来定义createDebug
,而是直接定义了一个函数来实现相同的功能。
js在es6规范之前没有class的定义,但可以通过函数来实现类对象的行为,这种方式通常称为构造函数和原型继承。通过构造函数和原型继承的方式,模拟类和实现类对象的行为。如下面这一小段代码:
// 通过构造函数定义类
function Person(name, age) {
this.name = name;
this.age = age;
}
// 在原型上定义方法
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
};
// 创建类的实例
let person1 = new Person('Alice', 25);
let person2 = new Person('Bob', 30);
// 调用实例方法
person1.sayHello(); // 输出:Hello, my name is Alice
person2.sayHello(); // 输出:Hello, my name is Bob
setup函数分析
在给定环境env
下,通过调用setup(env)
来设置和返回一个名为createDebug
的函数。createDebug
函数内部定义了一个名为debug
的内部函数,以及一系列与debug
相关的属性和方法。这种将函数作为返回值的写法是一种高阶函数的应用,通过内部函数的闭包特性,我们可以在外部函数中定义私有的属性和方法,并返回一个可供外部使用的函数。
接下来,让我们解释一下debug
函数。debug
是在createDebug
函数内部定义的一个函数,用于创建一个带有给定namespace
的调试器。它还包含一些内部状态和逻辑,用于控制调试器的输出行为。在这个代码中,通过闭包的方式实现了一些私有状态的存储和访问。
这种嵌套定义的写法充分利用了JavaScript中闭包的特性,可以有效地封装内部状态,并提供一个对外的接口。这种写法为代码的模块化和可维护性提供了一定的帮助。
简单来说,createDebug
函数是用于设置调试功能的工厂函数,而debug
函数是用于创建特定命名空间的调试器的函数。整个代码的结构是基于函数式编程和闭包的思想,通过高阶函数的方式来组织代码和管理状态,使得逻辑清晰,灵活性较高。
d.ts声明文件
为了便于在TypeScript 中能够使用js的代码,d.ts声明文件必不可少。以便在开发过程中进行类型检查、自动补全、跳转到定义等操作。
.d.ts
声明文件(Declaration Files)是用来描述一个 JavaScript 模块、库或者框架的类型信息以及代码结构的文件。它的作用是为了在开发阶段能够让 TypeScript 或者其他支持类型检查的工具能够了解 JavaScript 代码中的类型信息和结构。
为什么需要.d.ts
声明文件?主要是因为 JavaScript 是一种动态类型语言,而 TypeScript 是一种静态类型的超集。为了在 TypeScript 中能够对 JavaScript 模块进行类型检查和更好的代码提示,需要声明文件来提供类型信息。很多优秀的 JavaScript 模块和库都提供了官方或者社区维护的声明文件,以便让 TypeScript 或其他支持类型检查的工具能够更好地理解和协助开发者使用这些模块。
//index.d.ts
declare var debug: debug.Debug & { debug: debug.Debug; default: debug.Debug };
export = debug;
declare namespace debug {
interface Debug {
(namespace: string): Debugger;
coerce: (val: any) => any;
disable: () => string;
enable: (namespaces: string) => void;
enabled: (namespaces: string) => boolean;
formatArgs: (this: Debugger, args: any[]) => void;
log: (...args: any[]) => any;
selectColor: (namespace: string) => string | number;
names: RegExp[];
skips: RegExp[];
formatters: Formatters;
inspectOpts?: {
hideDate?: boolean | number | null;
colors?: boolean | number | null;
depth?: boolean | number | null;
showHidden?: boolean | number | null;
};
}
type IDebug = Debug;
interface Formatters {
[formatter: string]: (v: any) => string;
}
type IDebugger = Debugger;
interface Debugger {
(formatter: any, ...args: any[]): void;
color: string;
diff: number;
enabled: boolean;
enable: (namespaces: string) => void;
log: (...args: any[]) => any;
namespace: string;
destroy: () => boolean;
extend: (namespace: string, delimiter?: string) => Debugger;
}
}
interface Debug是对应 createDebug函数的接口,interface Debugger是对应内部的debug函数的接口声明。因为
interface Debug 接口中包括了coerce
、disable
、enable
、enabled
等方法,这些方法与 createDebug
函数的行为和特性相符。
harmonyOS平台移植
搞懂了源码,移植就简单了。下面分享下在harmonyOS平台下的移植。
新建一harmony.js的文件,仿照实现以下内容:
/* eslint-env harmonyos */
/**
* This is the harmonyos implementation of `debug()`.
* author:yangyongzhen
* blog: blog.scdn.net/qq8864
*/
let exp = {}
exp.useColors = useColors;
exp.formatArgs = formatArgs;
exp.save = save;
exp.load = load;
exp.storage = (function () {
var data = {};
return {
setItem: function (key, item) { data[key] = item; },
getItem: function (key) { return data[key]; },
removeItem: function (key) { delete data[key]; },
};
})();
exp.destroy = (() => {
let warned = false;
return () => {
if (!warned) {
warned = true;
console.warn('Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.');
}
};
})();
/**
* Invokes `console.debug()` when available.
* No-op when `console.debug` is not a "function".
* If `console.debug` is not available, falls back
* to `console.log`.
*
* @api public
*/
exp.log = console.log || (() => {});
/**
* Colors.
*/
exp.colors = [
'#0000CC',
'#0000FF',
......
'#FFCC33'
];
/**
* Currently only WebKit-based Web Inspectors, Firefox >= v31,
* and the Firebug extension (any Firefox version) are known
* to support "%c" CSS customizations.
*
* TODO: add a `localStorage` variable to explicitly enable/disable colors
*/
// eslint-disable-next-line complexity
function useColors() {
// NB: In an Electron preload script, document will be defined but not fully
// initialized. Since we know we're in Chrome, we'll just detect this case
// explicitly
return false;
}
/**
* Colorize log arguments if enabled.
*
* @api public
*/
function formatArgs(args) {
args[0] = (this.useColors ? '%c' : '') +
this.namespace +
(this.useColors ? ' %c' : ' ') +
args[0] +
(this.useColors ? '%c ' : ' ');
if (!this.useColors) {
return;
}
const c = 'color: ' + this.color;
args.splice(1, 0, c, 'color: inherit');
// The final "%c" is somewhat tricky, because there could be other
// arguments passed either before or after the %c, so we need to
// figure out the correct index to insert the CSS into
let index = 0;
let lastC = 0;
args[0].replace(/%[a-zA-Z%]/g, match => {
if (match === '%%') {
return;
}
index++;
if (match === '%c') {
// We only are interested in the *last* %c
// (the user may have provided their own)
lastC = index;
}
});
args.splice(lastC, 0, c);
}
/**
* Save `namespaces`.
*
* @param {String} namespaces
* @api private
*/
function save(namespaces) {
try {
if (namespaces) {
exp.storage.setItem('debug', namespaces);
} else {
exp.storage.removeItem('debug');
}
} catch (error) {
// Swallow
// XXX (@Qix-) should we be logging these?
}
}
/**
* Load `namespaces`.
*
* @return {String} returns the previously persisted debug modes
* @api private
*/
function load() {
let r;
try {
r = exp.storage.getItem('debug');
} catch (error) {
// Swallow
// XXX (@Qix-) should we be logging these?
}
// If debug isn't set in LS, and we're in Electron, try to load $DEBUG
if (!r && typeof process !== 'undefined' && 'env' in process) {
r = process.env.DEBUG;
}
return r;
}
import {setup} from './common'
var outs = setup(exp);
const {formatters} = outs;
/**
* Map %j to `JSON.stringify()`, since no Web Inspectors do that by default.
*/
formatters.j = function (v) {
try {
return JSON.stringify(v);
} catch (error) {
return '[UnexpectedJSONParseError]: ' + error.message;
}
};
export {outs};
发布到中心仓
写完单元测试用例,测试没问题,发布的过程很简单。参见:HarmonyOS 鸿蒙应用开发(九、还是蓝海,如何贡献第三方库)_鸿蒙开发第三方库社区-CSDN博客
其他资源
本文作者 https://blog.csdn.net/yyz_1987,如若转载,请注明出处。
更多推荐
所有评论(0)