fh-csl
Version:
Beautify console logs
400 lines (374 loc) • 9.81 kB
JavaScript
// 初始化日志对象
const csl = { ...console }
// 日志队列
const logsQuene = []
// 队列是否正在执行
let logsQueueExecuting = false
// 日志索引
let logIndex = 1
// 配置项
const options = {
// subject、action、target 的分割符
split: '>',
// 主体颜色值
subjectColors: {
req: '#aaa',
res: '#00aa00',
soc: '#ff34b3',
bus: '##9932cc',
app: '#7fff00',
non: '#f00',
sys: '#666',
},
// 默认主体颜色值
defaultSubjectColor: '#67cdfe',
// 日志等级颜色值
levelColors: {
log: '#000',
info: 'cyan',
debug: '#4169E1',
warn: 'orange',
error: 'red',
},
// 忽略规则
ignores: [
]
}
// 默认占用 console 对象
setGrab(true)
/**
* 设置是否占用 console 对象
* @param {Boolean} grab 是否占用 console 对象
*/
function setGrab(grab) {
Object.keys(options.levelColors).forEach(level => {
console[level] = grab ? (function () {
print(level, {
subject: 'non',
action: 'log',
target: 'unknown',
remark: '未归组日志',
extra: [...arguments]
})
}) : csl[level]
})
}
/**
* 设置忽略规则
* @param {Array} ignores
*/
function setIgnores(ignores) {
ignores = ignores.map(ig => {
const arr = ig.split('>')
return {
level: arr[0],
subject: arr[1],
action: arr[2],
target: arr[3]
}
})
options.ignores = ignores
}
/**
* 取消忽略规则
*/
function clearIgnores() {
options.ignores = []
}
/**
* 检查是否符合忽略规则
* @param {Object} data
*/
function isIgnored(data) {
const { level, subject, action, target } = data
const { ignores } = options
return ignores.some(ig =>
['*', level].includes(ig.level) &&
['*', subject].includes(ig.subject) &&
['*', action].includes(ig.action) &&
['*', target].includes(ig.target)
)
}
/**
* 添加主体
* @param {Object} colors 主体颜色
*/
function setSubjectColors(colors) {
Object.assign(options.subjectColors, colors)
}
/**
* 清除主体
*/
function clearSubjectColors() {
options.subjectColors = {}
}
/**
* 设置默认主体颜色值
* @param {String} color 颜色值
*/
function setDefaultSubjectColor(color) {
options.defaultSubjectColor = color
}
/**
* 获取日志主体打印颜色
* @param {string} subject
*/
function getSubjectColor(subject) {
return options.subjectColors[subject] || options.defaultSubjectColor
}
/**
* 打印日志
* @param {Object} data
* @param {String} data.level 日志等级,与 console 的等级一致
* @param {String} data.subject 触发者
* @param {String} data.action 行为
* @param {String} data.target 作用方
* @param {String} data.meta 行为数据
*/
function print() {
const data = checkParameter(...arguments)
if (!data) return
if (isIgnored(data)) return
let { level, subject, action, target, remark, extra } = data
let date = formatDate(new Date(), 'HH:mm:ss.fff')
extra = extra || {}
extra = extra instanceof Object ? extra : { extra }
logsQuene.push({
level, subject, action, target, remark, extra, date
})
setTimeout(queueExecutor)
}
/**
* 打印 log 日志
*/
function log() {
print('log', ...arguments)
}
/**
* 打印 debug 日志
*/
function debug() {
print('debug', ...arguments)
}
/**
* 打印 info 日志
*/
function info() {
print('info', ...arguments)
}
/**
* 打印 warn 日志
*/
function warn() {
print('warn', ...arguments)
}
/**
* 打印 error 日志
*/
function error() {
print('error', ...arguments)
}
/**
* 检查日志参数是否正确,并返回日志对象
* @returns 日志对象
*/
function checkParameter() {
const arg0 = arguments[0]
const arg1 = arguments[1]
const arg2 = arguments[2]
const arg3 = arguments[3]
const arg4 = arguments[4]
const arg5 = arguments[5]
let data = null
try {
const splitArr = arg1?.toString().split(options.split)
switch (arguments.length) {
case 2:
if (Object.prototype.toString.apply(arg1) === '[object String]') {
if (splitArr.length === 3) {
data = {
level: arg0,
subject: splitArr[0],
action: splitArr[1],
target: splitArr[2]
}
}
} else {
data = {
level: arg0,
...arg1
}
}
break
case 3:
if (splitArr.length === 3) {
data = {
level: arg0,
subject: splitArr[0],
action: splitArr[1],
target: splitArr[2],
remark: arg2
}
}
break
case 4:
if (splitArr.length === 3) {
data = {
level: arg0,
subject: splitArr[0],
action: splitArr[1],
target: splitArr[2],
remark: arg2,
extra: arg3
}
} else if (splitArr.length === 1) {
data = {
level: arg0,
subject: arg1,
action: arg2,
target: arg3,
}
}
break
case 5:
data = {
level: arg0,
subject: arg1,
action: arg2,
target: arg3,
remark: arg4
}
break
case 6:
data = {
level: arg0,
subject: arg1,
action: arg2,
target: arg3,
remark: arg4,
extra: arg5
}
break
}
} catch { }
if (!data) return invalidParameter(arguments)
if (Object.prototype.toString.apply(data) !== '[object Object]') return invalidParameter(arguments)
if (!Object.keys(options.levelColors).includes(data.level)) return invalidParameter(arguments)
if (!data.subject || Object.prototype.toString.apply(data.subject) !== '[object String]') return invalidParameter(arguments)
if (!data.action || Object.prototype.toString.apply(data.action) !== '[object String]') return invalidParameter(arguments)
if (!data.target || Object.prototype.toString.apply(data.target) !== '[object String]') return invalidParameter(arguments)
return data
}
/**
* 日志参数错误时的提示
*/
function invalidParameter(args) {
error('csl>error>print', 'csl日志参数错误', Array.from(arguments[0]).slice(1))
}
/**
* 队列执行
*/
function queueExecutor() {
if (logsQueueExecuting || !logsQuene.length) return
logsQueueExecuting = true
let [{ level, subject, action, target, remark = '', extra = null, date }] = logsQuene
if (isDevTools()) {
const baseStyle = 'border:1px solid #000;color:#aaa;background:#242424;padding: 2px 4px;border-radius: 4px;'
const styles = [
`${baseStyle}margin: 0 2px 0;`,
`border:1px solid ${options.levelColors[level]};color:${getSubjectColor(subject)};background:#111;padding: 2px 4px;border-radius: 4px;`,
`${baseStyle}margin: 2px`,
]
csl.groupCollapsed(`%c${subject}%c${action}%c${target}`, ...styles)
csl.groupCollapsed(`%cstack trace%c${date}%c${zeroify(logIndex++, 4)}%c${remark.replace(/\s/g, '')}`,
`${baseStyle}margin: 0 2px 0;'`,
`${baseStyle}margin: 0 2px 0 0;'`,
`${baseStyle}'`,
`${baseStyle}margin: 2px;`)
csl.trace()
csl.groupEnd()
csl.log(JSON.stringify(extra, null, 4))
csl.groupEnd()
} else {
extra = extra instanceof Error ? { extra } : extra
const styles = [
`display:inline-block;padding:0 3px;background:${{ error: 'red', warn: 'gold' }[level] || '#aaa'};color:#fff;border-radius:5px;margin:5px 0 0`,
'word-break:break-all;white-space:pre-wrap;',
]
csl['log'].apply(csl, [`%c ${date} %c ${subject}>${action}>${target} ${remark ? ' ' + remark : ''}`, ...styles, extra])
}
logsQuene.shift()
logsQueueExecuting = false
setTimeout(queueExecutor)
}
/**
* 将 Date 转化为指定格式的String
*/
function formatDate(date, fmt) {
date = date || new Date()
fmt = fmt || 'yyyy-MM-dd HH:mm:ss.fff'
// 正则与值的对应关系
let regmap = {
'y+': date.getFullYear(), // 年份
'M+': date.getMonth() + 1, //月份
'd+': date.getDate(), //日
'H+': date.getHours(), //小时
'm+': date.getMinutes(), //分
's+': date.getSeconds(), //秒
'f+': date.getMilliseconds(), // 毫秒
}
// 循环替换正则为实际的值
for (let reg in regmap) {
if (new RegExp(`(${reg})`).test(fmt)) {
fmt = fmt.replace(RegExp.$1, zeroify(regmap[reg], RegExp.$1.length))
}
}
return fmt
}
/**
* 字符串前补0
* @param {String} str 字符串
* @param {Number} max 最大长度
* @param {Boolean} cut 原字符串超出max时是否裁剪
*/
function zeroify(str, max, cut = false) {
str = str ? str.toString() : ''
if (max < str.length) {
if (cut) {
str = str.substring(0, max)
}
} else {
while (max - str.length > 0) {
str = `0${str}`
}
}
return str
}
let _isDevTools
/**
* 当前环境是否是开发环境
*/
function isDevTools() {
if (_isDevTools === undefined) {
if (wx) {
let { brand } = wx.getSystemInfoSync()
_isDevTools = brand === 'devtools'
} else {
_isDevTools = true
}
}
return _isDevTools
}
module.exports = {
log,
info,
debug,
warn,
error,
setGrab,
setIgnores,
clearIgnores,
setSubjectColors,
clearSubjectColors,
setDefaultSubjectColor
}