• 作者:老汪软件技巧
  • 发表时间:2024-12-31 10:05
  • 浏览量:

领导被我的花式console.log吸引了!直接写入公司公共库!- 掘金

5分钟教你使用 console.log 输出五彩斑斓的黑console.log - 掘金

不过我这里不需要这么多花里胡哨的,毕竟如果控制台全是这种五颜六色的说实话还不如之前的了,我们可以内置几个类似组件库标签的样式供用户使用,起到与其他日志区分的作用即可:

image.png

不过 CSS 编写方式确实有点鸡肋,因为我们的项目都是在 React 下使用,所以让用户按正常样式编写,我们内部处理成字符串即可,至于 API 我们有以下三种方式:

前缀函数封装

因为前缀这部分配置都在 log 前,所以我们先来解决这部分

React.CSSProperties 里的样式编写都是小驼峰,所以我们需要写一个工具函数将其进行转换,交给 AI 就行:

养成工具函数写 jsDoc 注释的好习惯,哪怕不标注参数只写一行描述都比不写强

现在写起来就方便多了:

image.png

下面我们先来封装一个简单的 FsLogger 类,先简单来实现一下 prefix 和 prefixStyle:

用户可以在实例化时进行前缀配置,也可以实例调用对应的方法进行配置,当然优先级肯定是实例调用,所以内部我们设置了相关私有属性,这在最终 log 时会进行合并

需要注意的是这里严格意义上来讲并不完全是链式调用,是有一定调用顺序要求的,不过这里的 prefix 和 prefixStyle 倒是不用考虑顺序

所以我们不能无脑返回 this,要考虑到 this 指向问题,具体细节看上面标注的 bind 使用

这两个方法实现后我们来实现 log 函数,它会收敛我们一开始 API 设计的三种调用方式,并保存用户要打印的内容:

前缀配置优先级:log 配置 > 函数调用配置 > 实例化配置,最终我们收敛在了 opt 中,真正执行打印操作的逻辑我们使用私有方法 __log:

这里面只需要根据用户前缀配置 options 的情况进行逻辑处理打印即可,别忘了打印结束后把保存的私有变量全部置空

现在我们的 console 就可以这样用了,符合预期:

image.png

实现 expect、toBe

下面来到我们最开始提的需求,其实很简单无非是把 if 语句给放到里面,只不过我们在 API 设计中考虑到要链式调用,我们依旧以 log 作为最终收口,添加额外的 __isLog 变量来判断是否需要打印:

关于 toBe 我们提取了 typeof 返回的基本数据类型,而回调的参数类型设置为 unknown,因为毕竟是用户传进来的,也无法在一开始借助泛型推导了,所以直接把决定权给用户,想确定就直接 as 即可

突然想到之前实现 log 里的泛型好像也没什么用,还是给删了吧...

现在我们可以这样用了:

image.png

主题色拓展

让用户自己手搓一些样式还是不太合适的,我们考虑内置类似标签主题色方便使用,关键在于这里的 API 又该怎么设计呢?

一开始我们想到无脑 logger.primary、logger.wran 但是这又与 console 上携带的那几个方法有些冲突,我们这里的的样式主要作用于前缀上,所以命名还是更具体一些比较好

先来写几个内置的主题样式:

我们不再针对于这几个主题都写一个独立的方法了,可以统一使用 usePrefixStyle 来设置主题,它的作用其实很 prefixStyle 一样,只不过会使用我们内置的样式:

当然上小节忘记把 expect 与 prefix、prefixStyle 联动了,在这里顺手补上

由于 usePrefixStyle 和 prefixStyle 功能上基本是一致的,所以就不互相引用了,下面直接看效果:

image.png

既然已经内置主题了,假如用户还想自定义主题呢?当然可以,无非就是再加一个配置项的事:

image.png

部分细节问题

以上就是我们实现的全部内容了,其实还真不少,但缺陷也不少,我们简单来盘一下

第一个问题:toBe 的调用顺序问题

我们在实现 prefix 的时候就有提到实际上我们的链式调用是有强调顺序的,像 toBe 本身就需要与 expect 强绑定,也就是说它必须紧跟在 expect 后面

而我们目前实现只保证了 expect 后面是 toBe,没有保证 toBe 的前面一定是 expect,所以你可以这样用:

logger.toBe('string').log();

很显然这是一种错误用法,怎么避免这种调用呢?很简单,应该把它作为私有属性:

但实际上这种私有属性只是 TS 编译期间的限制,想直接无视肯定也可以:

image.png

image.png

我们这里就不再延申了,对 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 属性来获取路径,比如我们可以让用户这样传入,然后内部提取:

image.png

但是这里 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,
  },
};

趁着手头业务不忙,简单记一次封装 console.log 的奇葩经历__趁着手头业务不忙,简单记一次封装 console.log 的奇葩经历

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)); } }


上一条查看详情 +二分搜索--魔鬼藏在细节中
下一条 查看详情 +没有了