【若川视野 x 源码共读】第36期 | omit.js 剔除对象中的属性


theme: juejin

前言

本文参加了由点击了解详情一起参与。

本篇是源码共读第36期 | 可能是历史上最简单的一期 omit.js 剔除对象中的属性,点击了解本期详情

准备

  1. 找到代码仓库,并clone下来
git clone https://github.com/benjycui/omit.js.git
  1. 先看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,一个是 toEqualtoBe 是表示全等,就是 ===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

就会看到下面的结果

image.png

我们会看到两个测试用例没有通过的报错,接下来我们要完善我们的代码,让测试用例通过。

4.写逻辑代码

第一步写浅拷贝,源码仓库里用的是 Object.assign 方法,这里用更简洁的语法,扩展运算符来写,编译的时候其实还是会编译成 Object.assign

// src/index.ts

export default function omit(obj, props) {
  let shadowClone = { ...obj };

  return shadowClone;
}

保存过后,就会看到通关了一个测试用例了。

image.png

第二步,跟源码仓库一样,写一个循环逻辑,然后把指定的字段删除掉,不过,我们这里希望当它是单个字段的时候可以接收字符串参数,所以需要在之前做一下处理,把单个的字符串也转成数组。

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,参数传错的话,还是会内部报错的。边界情况,这里就不做处理了。

保存完,发现两个测试用例都通过了。

image.png

不过,这里还差一步,我们还没有写我们的入参和出参的类型,这里完善一下。

第一个参数是一个对象,第二个参数是字符串或者字符串数组,所以刚开始可能会这样写

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.

image.png

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的源码地址

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容