- 作者:老汪软件技巧
- 发表时间:2024-12-31 10:05
- 浏览量:
领导被我的花式console.log吸引了!直接写入公司公共库!- 掘金
5分钟教你使用 console.log 输出五彩斑斓的黑console.log - 掘金
不过我这里不需要这么多花里胡哨的,毕竟如果控制台全是这种五颜六色的说实话还不如之前的了,我们可以内置几个类似组件库标签的样式供用户使用,起到与其他日志区分的作用即可:
不过 CSS 编写方式确实有点鸡肋,因为我们的项目都是在 React 下使用,所以让用户按正常样式编写,我们内部处理成字符串即可,至于 API 我们有以下三种方式:
前缀函数封装
因为前缀这部分配置都在 log 前,所以我们先来解决这部分
React.CSSProperties 里的样式编写都是小驼峰,所以我们需要写一个工具函数将其进行转换,交给 AI 就行:
养成工具函数写 jsDoc 注释的好习惯,哪怕不标注参数只写一行描述都比不写强
现在写起来就方便多了:
下面我们先来封装一个简单的 FsLogger 类,先简单来实现一下 prefix 和 prefixStyle:
用户可以在实例化时进行前缀配置,也可以实例调用对应的方法进行配置,当然优先级肯定是实例调用,所以内部我们设置了相关私有属性,这在最终 log 时会进行合并
需要注意的是这里严格意义上来讲并不完全是链式调用,是有一定调用顺序要求的,不过这里的 prefix 和 prefixStyle 倒是不用考虑顺序
所以我们不能无脑返回 this,要考虑到 this 指向问题,具体细节看上面标注的 bind 使用
这两个方法实现后我们来实现 log 函数,它会收敛我们一开始 API 设计的三种调用方式,并保存用户要打印的内容:
前缀配置优先级:log 配置 > 函数调用配置 > 实例化配置,最终我们收敛在了 opt 中,真正执行打印操作的逻辑我们使用私有方法 __log:
这里面只需要根据用户前缀配置 options 的情况进行逻辑处理打印即可,别忘了打印结束后把保存的私有变量全部置空
现在我们的 console 就可以这样用了,符合预期:
实现 expect、toBe
下面来到我们最开始提的需求,其实很简单无非是把 if 语句给放到里面,只不过我们在 API 设计中考虑到要链式调用,我们依旧以 log 作为最终收口,添加额外的 __isLog 变量来判断是否需要打印:
关于 toBe 我们提取了 typeof 返回的基本数据类型,而回调的参数类型设置为 unknown,因为毕竟是用户传进来的,也无法在一开始借助泛型推导了,所以直接把决定权给用户,想确定就直接 as 即可
突然想到之前实现 log 里的泛型好像也没什么用,还是给删了吧...
现在我们可以这样用了:
主题色拓展
让用户自己手搓一些样式还是不太合适的,我们考虑内置类似标签主题色方便使用,关键在于这里的 API 又该怎么设计呢?
一开始我们想到无脑 logger.primary、logger.wran 但是这又与 console 上携带的那几个方法有些冲突,我们这里的的样式主要作用于前缀上,所以命名还是更具体一些比较好
先来写几个内置的主题样式:
我们不再针对于这几个主题都写一个独立的方法了,可以统一使用 usePrefixStyle 来设置主题,它的作用其实很 prefixStyle 一样,只不过会使用我们内置的样式:
当然上小节忘记把 expect 与 prefix、prefixStyle 联动了,在这里顺手补上
由于 usePrefixStyle 和 prefixStyle 功能上基本是一致的,所以就不互相引用了,下面直接看效果:
既然已经内置主题了,假如用户还想自定义主题呢?当然可以,无非就是再加一个配置项的事:
部分细节问题
以上就是我们实现的全部内容了,其实还真不少,但缺陷也不少,我们简单来盘一下
第一个问题:toBe 的调用顺序问题
我们在实现 prefix 的时候就有提到实际上我们的链式调用是有强调顺序的,像 toBe 本身就需要与 expect 强绑定,也就是说它必须紧跟在 expect 后面
而我们目前实现只保证了 expect 后面是 toBe,没有保证 toBe 的前面一定是 expect,所以你可以这样用:
logger.toBe('string').log();
很显然这是一种错误用法,怎么避免这种调用呢?很简单,应该把它作为私有属性:
但实际上这种私有属性只是 TS 编译期间的限制,想直接无视肯定也可以:
我们这里就不再延申了,对 JS 私有属性感兴趣的可以看光哥的这篇文章:
第二个问题:上下文参数混淆
链式调用有一个很大的问题在于我们无法知道用户到底停在哪里,我们的实现如果要想有打印功能是必须要以 log 方法调用结尾的,但耐不住用户这样错误的使用:
const logger = new FsLogger({ prefix: "check", prefixStyle: { color: "red" } });
logger.expect(1).toBe("number");
logger.prefix("hello").prefixStyle({ color: "blue" });
logger.log();
这样我们最底部的打印结果就会受上面 toBe、prefix、prefixStyle 的污染
这个问题要解决还是比较困难的,因为我们的实现不像 JQuery 那样直接 $(selector) 0 帧起手获取 DOM,而是混到了中间调用且位置也不固定
所以这块只能要求用户正确的以 log 结尾使用,其次我们可以暴露出一个 clearContext 的方法,万一真有这个场景出现上述类似问题,允许用户先手动进行清除:
这样至少能够解决污染问题:
const logger = new FsLogger({ prefix: "check", prefixStyle: { color: "red" } });
logger.expect(1).toBe("number");
logger.prefix("hello").prefixStyle({ color: "blue" });
logger.clearContext().log("hello world");
第三个问题:打印结果右侧定位链接定位不准确问题
这个问题几乎无解,因为 console 结果位置就是按照调用位置来的,我们做了一层封装后肯定就在自己的工具库里打印,所以肯定无法直接定位到原来业务代码位置的
虽然也可以另辟蹊径,比如在非严格模式下的 caller 能够拿到函数的调用者,但是光一个非严格模式就直接卡死了
其次也可以借助 error 对象的 stack 属性来获取路径,比如我们可以让用户这样传入,然后内部提取:
但是这里 stack 路径在项目开发时也不一定会准确,毕竟我们现在的前端资源都会进行打包,其次在我们公司的业务中经常会有代理前端资源映射的操作,这里的 stack 是肯定无法回显到业务代码中的
不过话又说回来,我之所以去封装 console.log 的业务场景也并不是去拿右侧定位信息,恰恰相反,实际上我是知道在业务代码中哪个位置进行打印,只是想第一时间找到打印的结果,所以这里不实现倒也没什么
End
最后源码奉上,大部分时间都是白天抽空敲的,部分实现细节也没有细究,就是无聊时图一乐罢了,现在看看还挺长的:
/** 驼峰转为中横线 */
function camelToKebab(str: string) {
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
}
/** 将样式对象转为字符串 */
function transformStyleToStr(style: React.CSSProperties) {
let str = "";
for (const key in style) {
str += `${camelToKebab(key)}:${style[key as keyof React.CSSProperties]};`;
}
return str;
}
type Theme = "primary" | "success" | "info" | "error";
const themePublicStyle = { padding: "2px 4px", borderRadius: "4px" };
const themesPrefixStyle: Record<Theme, React.CSSProperties> = {
primary: {
color: "#548bfb",
background: "#e9f3ff",
...themePublicStyle,
},
success: {
color: "#d4380d",
background: "#fff2e8",
...themePublicStyle,
},
info: {
color: "#c41d7f",
background: "#fff0f6",
...themePublicStyle,
},
error: {
color: "#ff4d4f",
background: "#fff2f0",
...themePublicStyle,
},
};

interface Options {
prefix?: string;
prefixStyle?: React.CSSProperties;
extendPrefixThemes?: Record<string, React.CSSProperties>;
}
type DataType = "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function";
type ToBeParams = DataType | ((value: unknown) => boolean);
class FsLogger {
__value: any;
private __isLog: boolean;
private __prefix: Options["prefix"];
private __prefixStyle: Options["prefixStyle"];
private __prefixOptions: Options;
private __prefixThemes = themesPrefixStyle;
constructor(options: Options = {}) {
this.__prefixOptions = options;
this.__value = undefined;
this.__isLog = true;
}
private __composeOptions(options?: Options) {
let opt: Options = {};
opt = {
prefix: this.__prefix ? this.__prefix : this.__prefixOptions.prefix,
prefixStyle: this.__prefixStyle ? this.__prefixStyle : this.__prefixOptions.prefixStyle,
};
if (options && Object.keys(options).length) {
opt = Object.assign(opt, options);
}
return opt;
}
private __clear() {
this.__prefix = undefined;
this.__prefixStyle = undefined;
this.__value = undefined;
this.__isLog = true;
}
private __log(value: any, options: Options) {
const { prefix, prefixStyle } = options;
if (this.__isLog) {
if (prefix && prefixStyle) {
console.log(`%c${prefix}`, transformStyleToStr(prefixStyle), value);
} else if (prefix) {
console.log(prefix, value);
} else {
console.log(value);
}
}
this.__clear();
}
private __toBe(params: ToBeParams) {
if (typeof params === "string") {
this.__isLog = typeof this.__value === params;
} else {
this.__isLog = params(this.__value);
}
return {
log: this.log.bind(this),
};
}
clearContext() {
this.__clear();
return this;
}
expect(value: any) {
this.__value = value;
return {
toBe: this.__toBe.bind(this),
};
}
prefix(text: string) {
this.__prefix = text;
return {
prefixStyle: this.prefixStyle.bind(this),
usePrefixStyle: this.usePrefixStyle.bind(this),
useDefinePrefixStyle: this.useDefinePrefixStyle.bind(this),
expect: this.expect.bind(this),
log: this.log.bind(this),
};
}
prefixStyle(style: React.CSSProperties) {
this.__prefixStyle = style;
return {
prefix: this.prefix.bind(this),
expect: this.expect.bind(this),
log: this.log.bind(this),
};
}
usePrefixStyle(name: Theme) {
this.__prefixStyle = this.__prefixThemes[name];
return {
prefix: this.prefix.bind(this),
expect: this.expect.bind(this),
log: this.log.bind(this),
};
}
useDefinePrefixStyle(name: string) {
const theme = this.__prefixOptions.extendPrefixThemes?.[name];
if (theme) this.__prefixStyle = theme;
return {
prefix: this.prefix.bind(this),
expect: this.expect.bind(this),
log: this.log.bind(this),
};
}
log(value?: any, options: Options = {}) {
if (value) {
this.__value = value;
}
this.__log(this.__value, this.__composeOptions(options));
}
}