本文正在参加「金石计划」
前两天摸鱼写了个 console.log 样式增强库,主要是给 console.log 增加一些 Arco-desgin、Material ui 的按钮样式,效果如主图,属实是小火了一把,掘友们真的很热情,一个小玩具库周下载都有好几十次:
但也有很多同学提了建议和想法。咱优点不多,就是听劝,这就把掘友们的提议来说道说道,给咱这小玩具库优化优化。顺带给大家讲一讲发布 npm 包时,tsconfig.json 及 package.json 里的一些字段的实操作用。
导航至上一篇文章:闲来无事,摸鱼时让 chatgpt 帮忙,写了一个 console 样式增强库并发布 npm
掘友们的建议
- 掘友1:建议开发成 vscode 插件,这样使用更加方便,而不是还需要单独下载一个库
- 掘友2:打印出来的 log,不能追溯使用者源码,而是指向了库的内部
- 掘友3:库虽然使用了 ts,但是连个类型声明文件都没有,类型提示不够友好,希望像下面一样,有参数和类型提示
针对掘友的提议,我们来看看到底如何解决这些问题。
回答掘友1的提议
首先 vscode 的插件市场里已经有不少类似的插件了,功能是一键插入各种奇奇怪怪的 log 信息,比如:🚀 + 文件名 + 行信息。虽然在样式上存在一定的区别,但是这并不是一个好点子。首先我们的功能并不是很复杂,就是给 log 加点样式而已,代码片段完全可以胜任,而不必下载插件。我在写库之前,已经用了几天的代码片段了,有比较严重的问题,那就是代码冗余,每次 log 都需要书写重复的样式,每次都在 5 行代码以上,这会导致产生很多多余的 log 样式代码,代码行数会爆增,并不值得。即使开发成插件,也仍然是往代码里插入现成的样式代码,和代码片段没有区别,且代码冗余的问题仍然没有解决。所以这点建议,我并没有采纳。
回答掘友2的疑问
一开始确实没有注意到这个问题,我自己试了一下,确实是个蛮影响开发体验的问题,对排查问题不够友好。但是一直没有好的解决方案,加上最近工作忙,这事也就搁置了。今天想起来,我们不必把 console.log 由库调用,而是交给使用者来调用,而库本身只提供生成的样式参数给 console.log,也一样可以实现样式。这样,此问题得以解决。
回答掘友3的疑问
一开始也确实没想到文章会有这么多人关注,纯粹是中午没事干赶出来的一个玩具,但是呢,开发就要有开发的态度,看着下载的朋友变得越来越多,也确实存在这种问题,这必须安排上。
代码解决掘友2的问题
作为一个库的作者,如果我们前期已经有了一些用户,那么我们进行版本更新时就得注意兼容下旧的 API,要么是不改动旧代码,要么就是重写旧的 API,总之要考虑 API 变动带来的影响。我们这里就不改动旧的 API 代码了,尽管它存在一定的问题。
首先新建一个 types.ts 文件用于存放我们的类型定义,代码量上来后要拆分类型定义和逻辑代码,不能耦合在一起:
...
export type TMaterialLog = 'yellow' | 'orange' | 'red' | 'green' | 'cyan' | 'blue' | 'purple';
export interface IMaterialLogConfig {
logName: string;
type?: TMaterialLog;
isGradient?: boolean
}
其次,我们几个方法都写了重复的样式代码,这里封装两个函数。我们的 buttonLog 其实就是两种类型,一种是双按钮,一种是单按钮。针对两种按钮各编写一个生成样式的函数:
getDoubleButtonConfigs
该函数返回一个数组,数组里是 console.log 函数生成按钮样式所需要的模板参数:
export const getDoubleButtonConfigs = (
logBy: string,
logName: string,
preButtonColor: string,
nextButtonColor: string,
...logData: unknown[]
): unknown[] => {
const configs = [
`%c log-by-${logBy} %c ${logName} `,
`background: ${preButtonColor}; padding: 6px; border-radius: 1px 0 0 1px; color: #fff`,
`background: ${nextButtonColor}; padding: 6px; border-radius: 0 1px 1px 0; color: #fff`,
...logData
];
return configs;
};
它会生成如下样式的按钮:
getMaterialConfigs
功能也是返回 console.log 模板参数:
import { TMaterialLog } from './types';
const colorMap = new Map([
['yellow', '#FFC107'],
['orange', '#ff9800'],
['red', '#f44336'],
['green', '#4caf50'],
['cyan', '#00BCD4'],
['blue', '#2196f3'],
['purple', '#9C27B0'],
]);
const gradientColorMap = new Map([
['yellow', 'linear-gradient(to right, #FDB813, #FFAA00)'],
['orange', 'linear-gradient(to right, #FFA500, #FF6347)'],
['red', 'linear-gradient(to right, #FF416C, #FF4B2B)'],
['green', 'linear-gradient(to right, #00b09b, #96c93d)'],
['cyan', 'linear-gradient(to right, #1D976C, #93F9B9)'],
['blue', 'linear-gradient(to right, #2196F3, #4FC3F7)'],
['purple', 'linear-gradient(to right, #DA22FF, #9733EE)'],
]);
export const getMaterialConfigs = (
isGradient: boolean,
logName: string,
type: TMaterialLog,
...data: unknown[]
): unknown[] => {
const configs = [
`%c${logName}`,
`${isGradient ? 'background-image' : 'background'}: ${isGradient ? gradientColorMap.get(type) : colorMap.get(type)}; padding: 6px 12px; border-radius: 2px; font-size: 14px; color: #fff; font-weight: 600;`,
...data,
];
return configs;
};
它会生成 material ui 风格的按钮:
buttonLogUtils
我们需要一批工具函数,生成不同样式的模板参数,以便给使用者调用,他们都是返回样式模板数组,而不是直接调用 console.log。使用方法,我们希望类似于这样:
import { buttonLogUtils } from 'console-log-button';
console.log(...buttonLogUtils.blue('window'), window)
这样的话,打印的 log 仍然在使用者的代码中,而不会指向库的内部。
代码实现:
import { getDoubleButtonConfigs, getMaterialConfigs } from './common';
import { VUE_DEEP_CYAN, VUE_BLUE_GRAY } from '../style';
export const vueDevtool = (logBy: string, logName: string) => getDoubleButtonConfigs(logBy, logName, VUE_DEEP_CYAN, VUE_BLUE_GRAY);
export const red = (logName: string) => getMaterialConfigs(false, logName, 'red');
export const orange = (logName: string) => getMaterialConfigs(false, logName, 'orange');
export const yellow = (logName: string) => getMaterialConfigs(false, logName, 'yellow');
export const green = (logName: string) => getMaterialConfigs(false, logName, 'green');
export const cyan = (logName: string) => getMaterialConfigs(false, logName, 'cyan');
export const blue = (logName: string) => getMaterialConfigs(false, logName, 'blue');
export const purple = (logName: string) => getMaterialConfigs(false, logName, 'purple');
export const redLinearGradient = (logName: string) => getMaterialConfigs(true, logName, 'red');
export const orangeLinearGradient = (logName: string) => getMaterialConfigs(true, logName, 'orange');
export const yellowLinearGradient = (logName: string) => getMaterialConfigs(true, logName, 'yellow');
export const greenLinearGradient = (logName: string) => getMaterialConfigs(true, logName, 'green');
export const cyanLinearGradient = (logName: string) => getMaterialConfigs(true, logName, 'cyan');
export const blueLinearGradient = (logName: string) => getMaterialConfigs(true, logName, 'blue');
export const purpleLinearGradient = (logName: string) => getMaterialConfigs(true, logName, 'purple');
export const buttonLogUtils = {
vueDevtool,
red,
orange,
yellow,
green,
cyan,
blue,
purple,
redLinearGradient,
orangeLinearGradient,
yellowLinearGradient,
greenLinearGradient,
cyanLinearGradient,
blueLinearGradient,
purpleLinearGradient
};
我们这里不使用默认导出是为了更好地生成 ts 类型声明文件。
来看下,log 指向是否已经得到解决:
点击 log 的右侧进入源码,可以看到进入的是使用者的代码,而不再是库的内部了,到这里,我们已经解决了掘友2的问题。
代码解决掘友3的问题:生成 .d.ts 类型声明文件
不熟悉 ts 的同学可能不知道这是什么,我们先来了解下。
我们的代码虽然都是使用 ts 写的,内部编写时虽然没有问题,但是给外部调用时,用户可能使用 ts 也有可能使用 js,而且如果我们类型定义没有暴露,即使用户使用 ts 也没有良好的代码提示。这时候 .d.ts 文件的意义就是给我们写的方法及各类 API 提供类型指导,帮助使用者更好地获得代码提示。例如在上面,我们暴露了一个 buttonLogUtils 对象,如果没有 .d.ts,我们在 buttonLogUtils.xxx 的时候,就会 . 不出它上面的属性。
不过在讲解之前,还是先给大家讲讲一些基础,咱们先来认识下 package.json 里一些重要的字段。
package.json 重点字段解读
先来看我的配置,重点讲解在注释:
{
"name": "console-log-button", // 库的名字,不能带大写
"private": false, // 开源包这里必须是 false
"version": "0.0.5", // 库的版本,遵循 semver 规范,每次升级必须大于前一个版本
"type": "module", // 指定库的模块化类型
"main": "lib/index.js", // 库的入口文件,commonjs 规范入口
"module": "es/index.js", // 库的入口文件,esmodule 规范入口
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext '.js,.ts' --fix",
"precommit": "lint-staged",
"publish": "yarn build && npm publish"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
"eslint": "^8.36.0",
"husky": "^8.0.3",
"lint-staged": "^13.2.0",
"typescript": "^4.9.4",
"vite": "^4.0.4"
},
"files": [ // 指定最终上传成为 npm 包的文件,package.json 是默认上传的
"lib", // commonjs 规范的最终代码产物,名字根据你的 vite.config.ts 文件中的配置来决定
"es", // esmodule 规范的最终代码产物,名字来源同上
"README", // 库的说明文档
"LICENSE", // 库的开源协议
"index.d.ts" // 库的类型声明文件
],
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,ts}": [
"eslint --fix",
"git add"
]
},
"keywords": [
"console",
"log",
"button"
],
"types": "index.d.ts", // 指定库的类型声明从哪里引入
"repository": {
"type": "git",
"url": "https://github.com/Redstone-1/console-log-button"
},
"homepage": "https://www.npmjs.com/package/console-log-button",
"license": "MIT",
"publishConfig": {
"registry": "https://registry.npmjs.org/" // 发布 npm 时的地址,有些公司有自己的 npm 私仓,这里就会填写他们公司的仓库地址
}
}
唠叨下关于 main 与 module。很多人不太理解所谓的入口是什么意思。首先模块是有依赖关系的,类似于一个毛线团,从哪里开始引入模块,去建立各模块的依赖关系,必须理清这条线头,否则无法知道该从何处去引入依赖。
这里指定某个文件,作为入口,有几个注意点,如果你是本地开发,还没有发布上线,此时你没有 build 出 lib 与 es 两个最终产物包,那你的入口文件就是你本地库代码的 index.ts。因为你总不能每次改完代码都要 build 一遍最终的代码,再把它引入到测试工程里测试吧,这样效率太低,你肯定是直接引入你未 build 的代码进行测试。你 build 了,要发布了,main 与 module 要设置为你 build 后代码的 index.js。
file 字段也需要注意,既然指定了 index.d.ts 作为我们的类型声明文件,那么就应该将他上传成为 npm 包的一部分,因为在 package.json 里我们声明了"types": "index.d.ts",它会自动识别该文件,从中读取类型声明并给予我们代码提示。
自动生成代码声明文件
类型声明我们可以自己写,但是一个库那么多类型定义慢慢写要写到猴年马月,所以需要使用 ts 来自动完成这件事。需要在 tsconfig.json 做一下配置:
{
"compilerOptions": {
...
// "noEmit": true,
"emitDeclarationOnly": true, // 只输出声明文件(ts 产物)
"declaration": true, // 自动生成声明文件
"declarationDir": "dist", // 指定最终产物的输出目录
...
},
"include": ["src"]
}
我们看下最终的产物目录结构:
目录和代码目录是对应的,但是它会生成一些多余的类型声明,我们只需要将我们需要的复制到 index.d.ts 中就可以了。
对比下 buttonLogUtils 的代码和类型声明:
buttonLogUtils 代码
import { getDoubleButtonConfigs, getMaterialConfigs } from './common';
import { VUE_DEEP_CYAN, VUE_BLUE_GRAY } from '../style';
export const vueDevtool = (logBy: string, logName: string) => getDoubleButtonConfigs(logBy, logName, VUE_DEEP_CYAN, VUE_BLUE_GRAY);
export const red = (logName: string) => getMaterialConfigs(false, logName, 'red');
export const orange = (logName: string) => getMaterialConfigs(false, logName, 'orange');
export const yellow = (logName: string) => getMaterialConfigs(false, logName, 'yellow');
export const green = (logName: string) => getMaterialConfigs(false, logName, 'green');
export const cyan = (logName: string) => getMaterialConfigs(false, logName, 'cyan');
export const blue = (logName: string) => getMaterialConfigs(false, logName, 'blue');
export const purple = (logName: string) => getMaterialConfigs(false, logName, 'purple');
export const redLinearGradient = (logName: string) => getMaterialConfigs(true, logName, 'red');
export const orangeLinearGradient = (logName: string) => getMaterialConfigs(true, logName, 'orange');
export const yellowLinearGradient = (logName: string) => getMaterialConfigs(true, logName, 'yellow');
export const greenLinearGradient = (logName: string) => getMaterialConfigs(true, logName, 'green');
export const cyanLinearGradient = (logName: string) => getMaterialConfigs(true, logName, 'cyan');
export const blueLinearGradient = (logName: string) => getMaterialConfigs(true, logName, 'blue');
export const purpleLinearGradient = (logName: string) => getMaterialConfigs(true, logName, 'purple');
export const buttonLogUtils = {
vueDevtool,
red,
orange,
yellow,
green,
cyan,
blue,
purple,
redLinearGradient,
orangeLinearGradient,
yellowLinearGradient,
greenLinearGradient,
cyanLinearGradient,
blueLinearGradient,
purpleLinearGradient
};
buttonLogUtils 类型声明
export declare const buttonLogUtils: {
vueDevtool: (logBy: string, logName: string) => unknown[];
red: (logName: string) => unknown[];
orange: (logName: string) => unknown[];
yellow: (logName: string) => unknown[];
green: (logName: string) => unknown[];
cyan: (logName: string) => unknown[];
blue: (logName: string) => unknown[];
purple: (logName: string) => unknown[];
redLinearGradient: (logName: string) => unknown[];
orangeLinearGradient: (logName: string) => unknown[];
yellowLinearGradient: (logName: string) => unknown[];
greenLinearGradient: (logName: string) => unknown[];
cyanLinearGradient: (logName: string) => unknown[];
blueLinearGradient: (logName: string) => unknown[];
purpleLinearGradient: (logName: string) => unknown[];
};
那么,至此,掘友3的问题也得以解决,可以开心地引入并获得完整的代码提示了!
写在最后
小库小玩,很多东西就不往里做了,大家感兴趣自己 fork 一份去尝试下,还是很有收获的。
推荐我以往的文章:
【一年前端必知必会】了解 Blob,ArrayBuffer,Base64
正则表达式这篇文章点赞过 20 分享我 2 小时学会正则表达式使用的网站,跟着练习 2 小时候就能速通 js 正则。