前言
-
本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
-
这是源码共读的第23期,链接:juejin.cn/post/708298… 。
清单
- 了解read-pkg包使用场景及源码实现
- 测试用例方式调试代码
- 从源码看如何实现ts声明
- 源码中哪些优秀代码和思想可以投入到自己的项目中
前提准备
- 先下载源代码
git clone https://github.com/sindresorhus/read-pkg.git
- 然后
cd read-pkg && yarn
下载相关包 - 打开
package.json
文件看启动方式
场景应用及源码实现
不先了解清楚包的来龙去脉就直接调试看源码细节,就会感觉分外生疏,有种不知尔尔的感觉,接下来就先说下这个源码包存在的来源~
场景应用
- 是什么?
主要用来读package.json文件 - 为什么要写专门的库来读取这个文件?
从上述代码中可以看到,我以正常的方式引入package.json文件,想读取里面的json数据,结果…
据阮一峰老师的文档所写:
所以有了这个代码库的出现,就是为了解决读取package.json文件的问题
- 怎么使用?
npm install read-pkg
import {readPackage} from 'read-pkg';
console.log(await readPackage());
//=> {name: 'read-pkg', …}
console.log(await readPackage({cwd: 'some-other-directory'}));
//=> {name: 'unicorn', …}
- 从API提供上看,只有三个api:readPackage、readPackageSync、parsePackage,分别表示异步读取文件、同步读取文件、解析文件
源码分析
从上面的场景应用中了解到了read-pkg库的存在原因,为了解决什么问题出现的,也了解到了怎么使用的,接下来就看下作者在给出解决方案的过程中是如何思考的?
接着上述的前提准备的步骤
,看到了package.json
的script中只有"test": "xo && tsd && cd test && ava"
,这样看来面向测试用例调试
是一种不错的方式,
- xo && tsd && cd test && ava了解
- xo: 自带js,ts默认的eslint规则,不需要再单独配置
- tsd:用来检查ts的类型,enn~,项目中给xxx.d.ts也写了测试用例
- ava:用nodejs写法的单元测试框架
- 这三个都是轻量级,不需要做太多配置,拿来即用
test.js
文件
上述代码中看出,要进入test文件夹,找到test.js文件,通过vscode断点的方式调试,如上图所示;
跟随断点,进入到index.js
文件,可以看到导出了三个方法readPackage
、readPackageSync
、parsePackage
,其中两个是读取方法,一个是解析方法,按照断点的路径,先看readPackage
这个函数中的方法实现:
readPackage函数中,通过fsPromises.readFile
函数读取到package.json文件,这里用了文件读取,返回promise数据;
接着,通过_readPackage
方法,来解析成json格式数据返回;
这里看到了parseJson
函数,哟~,以后不想自己封装json解析,也可以使用parse-json
包来直接解析string类型json数据;
然后又看到了normalizePackageData
函数,这个是干嘛的呢?
现在不知道,先看下测试用例
test('async', async t => {
const package_ = await readPackage();
t.is(package_.name, 'unicorn');
t.truthy(package_._id);
});
因为normalize参数默认是true,就走到了normalizePackageData
函数,从上面的这个测试用例可以看到package_._id
,_id?一般情况下,_xxx表示私有属性,这个在package.json中,不存在_id属性,带着这个疑问,找到/node_modules/normalize-package-data/lib/normalize.js
文件,如下图,看到了_id是什么
了解了readPackage
函数后,再去对比了解下readPackageSync
函数有什么区别?
export function readPackageSync({cwd, normalize = true} = {}) {
const packageFile = fs.readFileSync(getPackagePath(cwd), 'utf8');
return _readPackage(packageFile, normalize);
}
除了fs.readFileSync
函数用了文件读取同步的操作,其他都一样;
接着看下parsePackage
函数中如何实现呢?
export function parsePackage(packageFile, {normalize = true} = {}) {
const isObject = packageFile !== null && typeof packageFile === 'object' && !Array.isArray(packageFile);
const isString = typeof packageFile === 'string';
if (!isObject && !isString) {
throw new TypeError('`packageFile` should be either an `object` or a `string`.');
}
// Input should not be modified - if `structuredClone` is available, do a deep clone, shallow otherwise
// TODO: Remove shallow clone when targeting Node.js 18
const clonedPackageFile = isObject
? (globalThis.structuredClone === undefined
? {...packageFile}
: structuredClone(packageFile))
: packageFile;
return _readPackage(clonedPackageFile, normalize);
}
先不说中间发生了什么,就看最后return也是执行的_readPackage
函数,所以,parsePackage函数中间也就就是根据类型判断边界错误,以及对数据做了拷贝处理…
看了这个函数,最好奇structuredClone是什么?去查了MDN后,才知道:
- 是一个js(不支持nodejs使用)的API,用来做深拷贝;
- 相比较JSON.parse(JSON.stringfy(val)),其优势,保持数据结构,对于对象自身引用、数组等都可以拷贝;
到这里为止,源码的分析就结束了~,接下来看下项目的ts声明是如何实现的?
从js源码看其ts项目实践
现在周围各个项目和框架都在应用ts做开发,ts在项目中的实践就显得尤为重要,那如何提高ts的应用水平呢?个人觉得,先从别人的优秀源码中学习他人是如何使用的
从index.d.ts
文件入手,上面的源码实现如何写ts类型声明?
在看之前,自己思考如何写ts类型声明呢?
个人觉得先从源码js开始,根据export导出的函数为入手点,要导出函数声明,然后抱着这样的想法去看类型声明,咦?三个函数,导出了6个函数类型声明,为啥呢?第一天看没看懂,先放着吧,然后第二天再打开接着看,这次我从上往下看,先看参数的定义,
export type Options = {
readonly cwd?: URL | string;
readonly normalize?: boolean;
};
type _NormalizeOptions = {
readonly normalize?: true;
};
export type NormalizeOptions = _NormalizeOptions & Options;
前两个我也常写,看得懂,这第三个&是什么?为什么要这样写?查了一番资料:
/**
* 为什么合并? 合并后是什么样的?
* &:交叉运算
* 作用:合并对象 交集,交集冲突时,会自动转换为never
* interface也可以使用&
*
* 使用场景区分:
* - type: 定义单个字段活定义工具类(做类型体操)时用
* - interface:定义base类、模块对外提供功能,提供接口、模块
*/
看了这个后,&就是为了进一步约束参数类型和默认值,我懂了~
接下来,看到了一个熟悉的类型声明export type ParseOptions = Omit<Options, 'cwd'>;
,曾经写过一篇源码共读是关于omit
源码共读,omit的作用就是剔除对象上的某个属性,这里ts中也是类似的功能,提出某个类型声明类型,如下图:
同时,这也是ts自带的工具类函数~
最后再看下函数的类型声明为啥是两个?
export function readPackage(options?: NormalizeOptions): Promise<NormalizedPackageJson>;
export function readPackage(options: Options): Promise<PackageJson>;
readPackage
函数中有两个参数,其中normalize
决定了_readPackage
返回的类型是否经过了normalizePackageData
函数的处理,所以有了两中不同的ts类型声明(个人理解),好了ts的类型声明的分析到这里也结束了~
最后,我有一个疑问,小伙伴们知道的,可以在评论区分享下,关于ts类型声明中,对参数的声明为什么也要export?
总结
一句话总结,read-pkg
就是通过nodejs的读到package.json的数据,然后通过parseJson库解析成json,如果需要元数据化,normalize
参数也支持其功能。
通过对read-pkg
源码的阅读,以后用到package.json
的读取,就能想到用read-pkg
库,其次,以后进行深拷贝,也不用自己封装了,structuredClone
可以直接代替,最后,对于ts在项目中的应用,我也可以把Omit
、&
以及函数声明都应用于自己的实践项目中~