theme: juejin
前言
本文参加了由点击了解详情一起参与。
本篇是源码共读第36期 | 可能是历史上最简单的一期 omit.js 剔除对象中的属性,点击了解本期详情
准备
- 找到代码仓库,并clone下来
git clone https://github.com/benjycui/omit.js.git
- 先看readme,了解这个库的一些介绍和使用说明
Utility function to create a shallow copy of an object which had dropped some fields.
这是一个函数,浅拷贝一个对象并丢弃一些字段。
所以有两个核心功能点
- 浅拷贝
- 忽略指定的字段
使用方法为
var omit = require('omit.js');
omit({ name: 'Benjy', age: 18 }, [ 'name' ]); // => { age: 18 }
上面是在commonjs规范中使用的方法,一般也就是nodejs环境中使用的方法。
如果是ESM环境中,则为
import omit from 'omit.js'
omit({ name: 'Benjy', age: 18 }, [ 'name' ]); // => { age: 18 }
3.找到入口文件
一般情况下,项目的入口文件是在 src/index.js
中,但看到这个项目的根目录下有一个 index.js
,所以看一下内容
import omit from './src';
export default omit;
其实也是从 scr/index.js
中导入进来的
所以我们还是转到 src/index.js
中
开始
接下来开始我们的源码阅读
function omit(obj, fields) {
// eslint-disable-next-line prefer-object-spread
const shallowCopy = Object.assign({}, obj);
for (let i = 0; i < fields.length; i += 1) {
const key = fields[i];
delete shallowCopy[key];
}
return shallowCopy;
}
export default omit;
总共核心的代码阅读就以上不到10行代码
第一句 const shallowCopy = Object.assign({}, obj)
是做了一个对象的浅拷贝。
对于对象的浅拷贝,其实方法有很多,这里用的是 Object.assign
,当然也可以用扩展运算符。
下面的几句就是对传进来的字段做一个遍历,然后从浅拷贝出来的对象中用 delete
方法删除,仅此而已。
所以若川大大说这可能是史上最简单的一期,是真的简单,简单都不足以写一篇文章来记录了。也是冲着这个简单先开始的😀,可能后面都不会有这么简单的源码阅读了。
既然这么简单,那不如自己来实现一个,增强一下自己后面阅读源码的信心。
自己实现
本来想写一个mini版的,但这已经简单到没办法再迷你了😊
1.创建一个文件夹,用 pnpm
先初始化一个项目
# /omit.js/
pnpm init
2.安装依赖
pnpm add vitest typescript -D -E
vitest
用来做单元测试,替换原来的 assert
,因为 vitest
开箱即用,足够简单,并且无需配置就可以对 typescript
支持,我是一个懒人,能不配置就尽量不配置,能少配置就少配置😄。
typescript
用来做类型校验,有时候觉得写类型会觉得很麻烦,浪费时间,但作为一个工具库,提供给第三方用,是一定需要类型支持的。类型即文档,有的时候看你类型定义就可以了,不需要看文档。源码库是用的js,然后在根目录单独写了一个 index.d.ts
的类型支持,这里打算直接用 typesript
来写,然后编译的时候生成类型文件。
3.先写测试
这里准备用 TDD
的方式来写,先写测试,然后再写代码。
在根目录创建一个 __tests__
文件夹,在文件夹下创建一个 index.test.ts
文件。 vitest
会默认扫瞄你根目录下的所有以 .test.ts
结尾的文件,作为测试文件,所以这里也不需要配置。
在看源码的时候,我们理清了,omit
方法它主要是做了两件事情
- 浅拷贝
- 过滤指定的字段
所以,测试的话,我们也针对这两个方面去写。
关于 vitest
的使用,详尽的可以看 vitest官网。它语法是兼容 jest
的,跟源码库用到 asset
的几个方法,使用也差不多。
- describe: 我这里简单地把它理解为测试组
- test: 单个测试用例
- expect: 就是期望执行的内容,后面会跟一些链式调用的判断语法,这里我们会用到两个,一个是
toBe
,一个是toEqual
。toBe
是表示全等,就是===
,toEqual
是比较值,会递归地遍历每个原始类型的值是不是相等,不判断引用。
好了,差不多可以开始写测试代码了。
// __tests__/index.test.ts
import { describe, expect, test } from 'vitest';
import omit from '../src';
describe('test omit method', () => {
// 测试是否会过滤指定的字段
test('should drop fields which are passed in', () => {
const benjy = { name: 'Benjy', age: 18 };
// 这里希望支持单个字段的时候可以传字符串
expect(omit(benjy, 'age')).toEqual({ name: 'Benjy' });
expect(omit(benjy, ['age', 'name'])).toEqual({});
});
// 测试是不是浅拷贝
test('should shadow copy', () => {
const benjy = { name: 'Benjy', age: 18, hobby: ['reading', 'coding'] };
const shadowFilteredValue = omit(benjy, []);
expect(shadowFilteredValue).not.toBe(benjy);
const newName = 'xiaoyao';
shadowFilteredValue.name = newName;
expect(shadowFilteredValue.name).toBe(newName);
expect(benjy).not.toBe(newName);
shadowFilteredValue.hobby.push('cooking');
expect(shadowFilteredValue.hobby).toEqual(['reading', 'coding', 'cooking']);
expect(benjy.hobby).toEqual(['reading', 'coding', 'cooking']);
});
});
以上就是我们要写的两个功能点的测试代码,然后我们跑起来看一下。
先在package.json
中的 scripts
字段中添加一条脚本
scripts: {
"test": "vitest"
}
然后运行 pnpm test
就会看到下面的结果
我们会看到两个测试用例没有通过的报错,接下来我们要完善我们的代码,让测试用例通过。
4.写逻辑代码
第一步写浅拷贝,源码仓库里用的是 Object.assign
方法,这里用更简洁的语法,扩展运算符来写,编译的时候其实还是会编译成 Object.assign
。
// src/index.ts
export default function omit(obj, props) {
let shadowClone = { ...obj };
return shadowClone;
}
保存过后,就会看到通关了一个测试用例了。
第二步,跟源码仓库一样,写一个循环逻辑,然后把指定的字段删除掉,不过,我们这里希望当它是单个字段的时候可以接收字符串参数,所以需要在之前做一下处理,把单个的字符串也转成数组。
export default function omit(obj, props) {
let shadowClone = { ...obj };
let arrayProps = Array.isArray(props) ? props : [props];
for (let i = 0; i < arrayProps.length; i++) {
delete shadowClone[arrayProps[i]];
}
return shadowClone;
}
这里只是做了一下简单的是不是数组的判断,如果要更严谨一些,其实还要判断其他的情况,比如不传或者传的是非字符串和非字符串数组的情况。因为用了 typescript
,我们就 ts 类型帮我们报错传值类型不对的情况。不过,如果使用者没有用 ts,参数传错的话,还是会内部报错的。边界情况,这里就不做处理了。
保存完,发现两个测试用例都通过了。
不过,这里还差一步,我们还没有写我们的入参和出参的类型,这里完善一下。
第一个参数是一个对象,第二个参数是字符串或者字符串数组,所以刚开始可能会这样写
export default function omit(obj: Record<string, any>, props: string | string[]): Record<string, any> {
...
}
但这样有几个问题,一个是props的string不一定是前面的obj的key,另外返回值也是一个对象,跟前面的obj值没有什么关系。
我们可以看一下源码库中的 index.d.ts
的类型定义
declare function Omit<T, K extends keyof T>(
obj: T,
keys: Array<K>
): Omit<T, K>;
export default Omit;
这样 K
就能保证是对象 T
的key了,另外用了 typecript 的工具函数 Omit
,在原对象上忽略掉 K 字段,这样就比较完美了。
那回到我们自己的函数上
export default function omit<T, K extends keyof T>(obj: T, props: K | K[]): Omit<T, K> {
...
}
这样代码部分就全部完成了。
vitest 还提供了输出测试覆盖率,执行 vitest run --coverage
.
5.构建
由于 typescript
自身带了 tsc
的编译,所以就用tsc完成编译,源仓库用的是 father
,正如它官网介绍的,是一款 NPM 包研发工具,有兴趣的可以深入了解
初始的配置文件是通过,tsc init
生成的,由于懒,还是遵循尽可能少的配置或者修改配置。初始化之后,修改的几个地方:
- rootDir: 改成了”./src”
- declaration: true
- declarationDir: 改成了 “./”
- include: [“./src/**/*”]
- exclude: [“*.test.ts”]
好了,差不多可以满足构建的需求了。
还有一点,我们开发的工具包,有可能会被用在commonjs的环境,也就是nodejs环境,有可能被用在esm的环境,一般就是前端应用开发环境(这么说,可能不太准确,现在较新版本的nodejs环境也可以支持esm),所以打包的时候,也需要打包两种格式。一般有两种做法,一个是都打包在一个文件夹中,一个是打包在不同的文件夹下,这里我们保持跟源码库一致,打包在不同的文件夹下。
然后我们拆分一下配置文件,在 typescript
中,module
字段表示的就是编译的模块规范,所以我们有一个tsconfig.base.json
,主要是两种模块规范通用的打包编译配置。
{
"compilerOptions": {
/* Language and Environment */
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"rootDir": "./src" /* Specify the root folder within your source files. */,
/* Emit */
"declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
"declarationDir": "./" /* Specify the output directory for generated declaration files. */,
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": ["./src/**/*"],
"exclude": ["*.test.ts"]
}
另外针对不同的模块规范,在配置不同的配置文件
一个是 tsconfig.esm.json
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "es6",
"outDir": "es"
}
}
一个是 tsconfig.cjs.json
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "lib"
}
}
这样打包编译的配置文件就好了,再就是添加脚本。
在 package.json
配置文件中添加
scripts: {
...,
"build:cjs": "tsc --project ./tsconfig.cjs.json",
"build:esm": "tsc --project ./tsconfig.esm.json",
"build": "pnpm build:cjs & pnpm build:esm",
}
执行 pnpm build
就可以完成打包了。
6.发布
添加一个脚本
scripts: {
...,
"push": "npm publish --access public --registry=https://registry.npmjs.org"
}
--access publish
是在发布以 @xxx/xxx
的包的时候需要用到,后面指定registry是有可能我们本地用的是其他源,比如taobao源,所以指定发布的仓库。
配置完了之后,执行 pnpm push
就可以发包了。
总结
这是第一次完整地看完源码,并输出文章,迈出第一步很重要,不管它有多简单,这是增加信心的过程。之前一直想看写博客,看源码,这算是一个开始吧。
关于源码库本身其实比较简单,就是可以关注到一些工程化相关的东西,比如单元测试、文档、打包等,有了这个开始,可以帮你构建以后开发工具库的思路和模板。
最后,贴上github的源码地址
暂无评论内容