debug插件

187 阅读5分钟

通过不同的实例打印,查看输出

## node.js


// https://github.com/visionmedia/debug

// util.format: 根据使用第一个参数返回格式化的字符串。格式化的字符串包含零个或多个格式说明符
// util.inspect: 根据第二个参数将对象转换 为字符串的方法
// process.stderr.writ: 控制台打印字符串

/**
 * 1. 把 'init', 'log', 'formatArgs', 'save', 'load', 'useColors', 'colors', 'inspectOpts' 挂到exports中,导入'common.js'默认的导出方法'setup'并且调用把exports对象全部当参数传递进去
		1. init: 初始化“调试”实例,从环境变量构建默认的' inspectOpts '对象给debug
			1. showHidden 是一个可选参数,如果值为 true,将会输出更多隐藏信息
			2. colors 值为 true,输出格式将会以 ANSI 颜色编码
			3. depth 表示最大递归的层数,如果对象很复杂,你可以指定层数以控制输出信息的多 少。如果不指定depth,默认会递归 2 层
		2. log: 打印日志
		3. formatArgs:添加ANSI颜色转义码
		4. save: 有namespaces就保存到process.env.DEBUG,没有就删除
		5. useColors: 判断inspectOpts有没有colors,有就返回false,没有就是true
		6. colors: 当前实例的颜色
		7. inspectOpts: 获取debug_开头的process.env环境变量,取debug_后面的变量并且转换为驼峰,然后值进行true,false,null等转换后存到debug.inspectOpts中
 * 2. setup被调用后给createDebug对象挂载'debug', 'default', 'coerce', 'disable', 'enable', 'enabled', 'humanize', 'inspectOpts', 'selectColor'并且通过循环把exports挂载的属性全部挂载到setup中
		1. debug: 默认到处的方法给开发者使用
		2. default: createDebug函数本身
		3. coerce: 如果传递的参数是报错,转换输出为报错信息,msg返回
		4. disable: 禁用调试输出
		5. enable: 动态控制调试的启动和关闭
		6. enabled: 判断实例是否被开启
		7. humanize: ms模块,用于时间转换
		8. inspectOpts: 实例的配置,showHidden,colors,depth
		9. selectColor: 为这个名称的调试选择颜色
 * 3. 走createDebug.enable(createDebug.load()), createDebug.load()获取process.env.DEBUG的值,
    	1. 调用createDebug.save判断createDebug.load()有没有数据,有数据就存到process.env.DEBUG,没有就删除process.env.DEBUG
		2. 如果createDebug.load()不是字符串的话就进行切割并且把*都替换成.*?, 如果第一个字符串是-就删除掉记录到skips中不是的话就记录到names中,names和skips和namespaces挂载到createDebug中
		3. return createDebug给开发者调用
 * 4. 当开发者const debug = require('debug')('detect-port')后导入了createDebug并且调用传递一个名称'detect-port'
		1. debug挂载 ’namespace‘,’useColors‘,’color‘,’extend‘
			1. namespace: 当前实例的名称,用来区分每一个debug
	  		2. useColors: createDebug.useColors(),根据inspectOpts判断有没有colors,有就说false没有就算true
			3. color: createDebug.selectColor(namespace),为这个名称的调试选择颜色
			4. extend: 基于这个实力扩展另外一个debug实例,实例名称
    	2. 使用Object.defineProperty控制debug的enabled属性,调用debug.enabled的时候自动判断namespaces和缓存的namespaces不一样并且属于createDebug.skips就开启调试,不然就无法开启调试
		3. 调用createDebug.init(debug),环境的初始化逻辑
		4. return debug提供开发者使用
 * 5. debug('detect free port between [%o, %s)', 3000, 65535);
 */


/**
 * 1. 可以设置一些环境变量来改变调试日志的行为,以DEBUG_开头的环境变量最终被转换为一个Options对象
		1. DEBUG: 启用/禁用特定调试名称空间
		2. DEBUG_HIDE_DATE: 从调试输出中隐藏日期(非tty)。
		3. DEBUG_COLORS: 是否在调试输出中使用颜色。
		4. DEBUG_DEPTH: 对象检查深度。
		5. DEBUG_SHOW_HIDDEN: 显示检查对象的隐藏属性。
 * 2. 通过%设置参数的替换方法,可以createDebug.formatters.h扩展一个%h的方法
		1. %O:多行打印一个对象
		2. %o:将一个对象全部打印在一行上
		3. %s:打印字符串
		4. %d:数字(整数和浮点数)。
		5. %j:JSON。如果参数包含循环引用,则用字符串'[Circular]'替换
		6. %%:单百分比符号('%')。这不会消耗一个参数。
 * 3. 通过extend扩展debug实例
		1. const log = require('debug')('auth');
           const logSign = log.extend('sign');
           const logLogin = log.extend('login');
           log('hello'); // auth hello
           logSign('hello'); //auth:sign hello
           logLogin('hello'); //auth:login hello
 * 4. 调用enable()方法动态地启用调试
		1. require('debug').enabled('test')
 * 5. 将禁用所有名称空间。这些函数返回当前启用(并跳过)的名称空间。如果您想在不知道启用了什么的情况下暂时禁用调试,那么这将非常有用
		1. require('debug')('auth').disable()
 * 6. 在你创建了一个调试实例之后,你可以通过检查enabled属性来确定它是否被启用,您还可以手动切换此属性,以强制启用或禁用调试实例。
		1. require('debug')('http').enabled
 * 7. 
 */	

/**
 * 模块依赖
 */

// 终端模块
const tty = require('tty');

// 实用工具
const util = require('util');

/**
 * node环境
 */

exports.init = init;
exports.log = log;
exports.formatArgs = formatArgs;
exports.save = save;
exports.load = load;
exports.useColors = useColors;

/**
 * Colors.
 */

exports.colors = [6, 2, 3, 4, 5, 1];

try {
	// Optional dependency (as in, doesn't need to be installed, NOT like optionalDependencies in package.json)
	// eslint-disable-next-line import/no-extraneous-dependencies
	const supportsColor = require('supports-color');

	if (supportsColor && (supportsColor.stderr || supportsColor).level >= 2) {
		exports.colors = [
			20,
			21,
			26,
			27,
			32,
			33,
			38,
			39,
			40,
			41,
			42,
			43,
			44,
			45,
			56,
			57,
			62,
			63,
			68,
			69,
			74,
			75,
			76,
			77,
			78,
			79,
			80,
			81,
			92,
			93,
			98,
			99,
			112,
			113,
			128,
			129,
			134,
			135,
			148,
			149,
			160,
			161,
			162,
			163,
			164,
			165,
			166,
			167,
			168,
			169,
			170,
			171,
			172,
			173,
			178,
			179,
			184,
			185,
			196,
			197,
			198,
			199,
			200,
			201,
			202,
			203,
			204,
			205,
			206,
			207,
			208,
			209,
			214,
			215,
			220,
			221
		];
	}
} catch (error) {
	// Swallow - we only care if `supports-color` is available; it doesn't have to be.
}

/**
 * 从环境变量构建默认的' inspectOpts '对象。
 *
 *   $ DEBUG_COLORS=no DEBUG_DEPTH=10 DEBUG_SHOW_HIDDEN=enabled node script.js
 * showHidden 是一个可选参数,如果值为 true,将会输出更多隐藏信息
 * colors 值为 true,输出格式将会以 ANSI 颜色编码
 * depth 表示最大递归的层数,如果对象很复杂,你可以指定层数以控制输出信息的多 少。如果不指定depth,默认会递归 2 层
 */

exports.inspectOpts = Object.keys(process.env).filter(key => {
	// 过滤找process里面的环境变量debug_开头的并且部分大小写
	return /^debug_/i.test(key);
}).reduce((obj, key) => {
	// Camel-case
	const prop = key
		.substring(6) // 去除debug_
		.toLowerCase() // 转换为小写
		.replace(/_([a-z])/g, (_, k) => { // 匹配下划线,全部切换为大写
			return k.toUpperCase();
		});

	// 强制将字符串值转换为JS值
	let val = process.env[key];
	if (/^(yes|on|true|enabled)$/i.test(val)) {
		val = true;
	} else if (/^(no|off|false|disabled)$/i.test(val)) {
		val = false;
	} else if (val === 'null') {
		val = null;
	} else {
		val = Number(val);
	}

	obj[prop] = val;

	return obj;
}, {});



/**
 * 如果启用,则添加ANSI颜色转义码。
 *
 * @api public
 */

function formatArgs(args) {
	const {namespace: name, useColors} = this;

	if (useColors) {
		const c = this.color;
		const colorCode = '\u001B[3' + (c < 8 ? c : '8;5;' + c);
		const prefix = `  ${colorCode};1m${name} \u001B[0m`;

		args[0] = prefix + args[0].split('\n').join('\n' + prefix);
		args.push(colorCode + 'm+' + module.exports.humanize(this.diff) + '\u001B[0m');
	} else {
		args[0] = getDate() + name + ' ' + args[0];
	}
}

function getDate() {
	if (exports.inspectOpts.hideDate) {
		return '';
	}
	return new Date().toISOString() + ' ';
}

/**
 * 使用指定的参数调用' util.format() '并写入stderr。
 * process.stderr.write: 终端输出log
 */


function log(...args) {
	return process.stderr.write(util.format(...args) + '\n');
}

/**
 * 保存 `namespaces`.
 *
 * @param {String} namespaces
 * @api private
 */
function save(namespaces) {
	// 有namespaces设置process.env.DEBUG
	if (namespaces) process.env.DEBUG = namespaces;
	else delete process.env.DEBUG; //如果你设置了一个进程。如果Env字段为null或未定义,它将被转换为 //字符串'null'或'undefined'。只是删除
}

/**
 * 加载 `namespaces`.
 *
 * @return {String} 返回先前持久化的调试模式
 * @api private
 */

function load() {
	return process.env.DEBUG;
}

/**
 * 初始化“调试”实例的逻辑。
 *
 *  创建一个新的' inspectOpts '对象,以防' useColors '被设置 *对于特定的' debug '实例是不同的。
 */

function init(debug) {
	debug.inspectOpts = {};

	// 从环境变量构建默认的' inspectOpts '对象给debug
	const keys = Object.keys(exports.inspectOpts);
	for (let i = 0; i < keys.length; i++) {
		debug.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]];
	}
}

module.exports = require('./common')(exports);

const {formatters} = module.exports;

/**
 * 挂载到formatters的方法,用来做调用,有%o的时候就把%o替换为方法返回的值,参数从调用debug第二次参数对应%的index
 */

formatters.o = function (v) {
	// 有colors属性就返回false, 没有就返回true
	this.inspectOpts.colors = this.useColors;

	// 将任意对象转换 为字符串的方法
	return util.inspect(v, this.inspectOpts)
		.split('\n')
		.map(str => str.trim())
		.join(' ');
};

/**
 * Map %O to `util.inspect()`, allowing multiple lines if needed.
 */

formatters.O = function (v) {
	this.inspectOpts.colors = this.useColors;
	return util.inspect(v, this.inspectOpts);
};

/**
 * stdout是TTY吗?当' true '时启用彩色输出。
 * 有colors属性就返回false, 没有就返回true
 */

 function useColors() {
	return 'colors' in exports.inspectOpts ? // 判断从环境中找的默认配置有没有这个属性
		Boolean(exports.inspectOpts.colors) : // 有color就变为false
		tty.isatty(process.stderr.fd); // process.stderr.fd固定为2, 然后返回true
}

## 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.selectColor = selectColor;

	// 扩展方法
	Object.keys(env).forEach(key => {
		createDebug[key] = env[key];
	});

	/**
	* 当前激活的调试模式名称,以及要跳过的名称。
	*/

	createDebug.names = [];
	createDebug.skips = [];

	/**
	* 特殊"%n"处理函数的映射,用于调试"format"参数。
	*
	* 有效的键名是一个单一的小写或大写字母,即。“n”和“n”。
	*/
	createDebug.formatters = {};

	/**
	* 为调试名称空间选择颜色
	* @param {String} namespace  要着色的调试实例的名称空间字符串
	* @return {Number|String} 给定名称空间的ANSI颜色代码
	* @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];
	}
	

	/**
	*  用给定的命名空间创建一个调试器
	*
	* @param {String} namespace
	* @return {Function}
	* @api public
	*/
	function createDebug(namespace) {
		let prevTime;
		let enableOverride = null;
		let namespacesCache;
		let enabledCache;

		// args[0]举例:detect free port between [%s, %s)
		function debug(...args) {

			// 检查调试是否被启用?
			if (!debug.enabled) {
				return;
			}

			const self = debug;

			// 计算调试的时间
			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]);

			// 其他类型,就把%O拼到前面
			if (typeof args[0] !== 'string') {
				args.unshift('%O');
			}

			// 应用任何'格式化器'转换
			let index = 0;

			// %: 单纯的一个%符号
			// [a-zA-Z%]: 匹配a-z或者A-Z或者%任意一个字符都成立
			// g: 全局匹配
			// 过滤%开头到a-z A-z %中的字符串,注意是%后的第一个不是全部
			args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format) => {
				// 如果遇到了两个%%就替换为%
				if (match === '%%') {
					return '%';
				}

				index++;

				// 根据%后面的符号调用方法默认有o和O两个方法
				const formatter = createDebug.formatters[format];

				if (typeof formatter === 'function') {
					// 获取后面的参数进行处理
					const val = args[index];

					match = formatter.call(self, val);

					// 现在我们需要删除' args[index] ',因为它被内联在' format '中
					args.splice(index, 1);
					index--;
				}
				return match;
			});

			// 应用特定于环境的格式(颜色等)
			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.enabled的时候自动判断namespaces和缓存的namespaces不一样并且属于createDebug.skips就开启调试,检查调试是否被启用
		Object.defineProperty(debug, 'enabled', {
			enumerable: true, // 当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中
			configurable: false, // 为 true 时,该属性的描述符才能够被改变
			get: () => {
				if (enableOverride !== null) {
					return enableOverride;
				}
				if (namespacesCache !== createDebug.namespaces) {
					namespacesCache = createDebug.namespaces;
					enabledCache = createDebug.enabled(namespace);
				}

				return enabledCache;
			},
			set: v => {
				enableOverride = v;
			}
		});

		// 用于调试实例的特定于环境的初始化逻辑
		if (typeof createDebug.init === 'function') {
			createDebug.init(debug);
		}

		return debug;
	}

	// 基于之前的namespace扩展一个新的debug实例:之前namespace:传递进来的namespace
	function extend(namespace, delimiter) {
		const newDebug = createDebug(this.namespace + (typeof delimiter === 'undefined' ? ':' : delimiter) + namespace);
		newDebug.log = this.log;
		return newDebug;
	}

	/**
	* 按名称空间启用调试模式。这可以包括模式 *由冒号和通配符分隔
	*
	* @param {String} namespaces
	* @api public
	*/
	function enable(namespaces) {
		createDebug.save(namespaces);

		// 存namespaces
		createDebug.namespaces = namespaces;

		// 存第一个字符不是-开头的正则表达式
		createDebug.names = [];
		// 存第一个字符是-开头的正则表达式
		createDebug.skips = [];

		let i;

		// []: 匹配里面任意一个规则\s或者,都行,满足其中一个都行
		// \s: 匹配一个空字符串,空格,制表符,换页符换行符
		// ,: 就当成匹配一个逗号
		// +: 前面表达式一个或者多个
		// 如果字符串中有空白符号或者,一次或者多次,都进行切割
		const split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/);
		const len = split.length;

		for (i = 0; i < len; i++) {
			// 如果是空格,结束当前循环,进行下一个循环
			if (!split[i]) continue;

			// g: 全局匹配
			// /*: 匹配*号,不做正则符号翻译
			// 字符串中出现*都替换成.*?
			namespaces = split[i].replace(/\*/g, '.*?');

			// namespaces[0]: 字符串第一个符号 === ‘-’
			if (namespaces[0] === '-') {
				// 删除第一个字符串-并且生成一个正则表达式/^ *** $/
				createDebug.skips.push(new RegExp('^' + namespaces.substr(1) + '$'));
			} else {
				// 直接生成正则表达式 /^ *** $/
				createDebug.names.push(new RegExp('^' + namespaces + '$'));
			}
		}
	}

	/**
	* 禁用调试输出
	*
	* @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;
	}

	/**
	* 如果启用了给定的模式名,则返回true,否则返回false。
	*
	* @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(/\.\*\?$/, '*');
	}

	/**
	* 如果是报错,转换输出为报错信息
	*
	* @param {Mixed} val
	* @return {Mixed}
	* @api private
	*/
	function coerce(val) {
		if (val instanceof Error) {
			return val.stack || val.message;
		}
		return val;
	}

	// 根据rocess.env.DEBUG初始化数据
	// createDebug.load(): 获取process.env.DEBUG
	// createDebug.enable: 保存namespaces(rocess.env.DEBUG)以及过滤保存skips,names
	createDebug.enable(createDebug.load());

	return createDebug;
}

module.exports = setup;