源码共读之 arrify 包

911 阅读5分钟

我正在参与掘金会员专属活动-源码共读第一期,点击参与


从包名 arrify 我们就能知道这个包能够将各种类型的输入值转换为数组,当我们想要确保传递的值是一个数组时,可以使用 arrify 函数将其转换为数组,防止抛出异常。

用法

arrify(null);      // []
arrify(undefined); // []
arrify(1);         // [1]
arrify([1]);       // [1]
arrify('foo');     // ['foo']
arrify(new Set([1, 2, 3]));  // [1, 2, 3]

接下来阅读下它的源码,只有 10 几行。

源码分析

从仓库的 package.json 文件的 exports 字段可以看到入口文件为 index.js

function arrify(value) {
  // 如果输入值 value 为 null 或 undefined,返回空数组
  if (value === null || value === undefined) {
    return [];
  }
  // 使用 isArray 方法检查输入值 value 是否为数组,是则直接返回 value
  if (Array.isArray(value)) {
    return value;
  }
  // 使用 typeof 操作符来检查输入值是否为字符串。是则将 value 包装在数组中并返回
  if (typeof value === 'string') {
    return [value];
  }
  // 检查输入值是否具有迭代器方法,是则使用扩展运算符 ... 将可迭代对象转换为数组并返回
  if (typeof value[Symbol.iterator] === 'function') {
    return [...value];
  }
  // 其他类型则返回
  return [value];
}

源码里面利用 Array.isArray 方法判断输入值 value 是否为数组,如果为数组的话就没必要再往下执行了,直接返回 value。

如果为字符串类型则将 value 包装在数组中并返回。为什么要做这一步判断呢?因为字符串类型其实也是可迭代的,如果不做判断的话,将会去调用字符串的迭代器方法(即 Symbol.iterator 属性),那么输出就不符合我们预期了;例如:arrify(’foo’) 将会返回 ['f', 'o', 'o'] 而不是 ['foo'] ,显然这不是我们想要的结果。

扩展了解

Symbol.iterator

Symbol.iterator 是 ES6 引入的一个类型为 Symbol 的特殊值,用于指定对象的迭代器方法。迭代器方法是一个特殊的函数,执行迭代器方法会返回一个迭代器对象,可以使用 for...of 来遍历迭代器对象。

在 JavaScript 中,有许多内置对象是可迭代的(String、Array、TypedArray、Map 和 Set),这些对象都默认部署了迭代器方法(即 Symbol.iterator 方法),用于遍历它们的值。

Untitled.png

默认会调用 Iterator 接口(即Symbol.iterator方法)的场合:

  • for … of
  • 解构赋值
  • 扩展运算符
  • yield*
  • 任何接受数组作为参数的场合,都调用了遍历器接口,因为数组的遍历会调用遍历器接口;例如Set()Promise.all()

了解更多:

包入口配置:exports 字段

在 arrify 包的 package.json 文件中的入口定义使用的 exports 而不是 main 字段,那就来了解下这个字段吧。

package.json 文件中,mainexports 字段都可以用于指定 ESM 或 CommonJS 模块的入口,但是 main 能力有限,只能定义一个主入口,因此 node v12.7.0 引入了 exports 字段来替代 main,在支持 exports 字段的 Node.js 版本中,exports 的优先级要高于 main

exports 作为 main 的替代方案,有以下优势:

  • 可以定义多入口;
  • 具有封装性;所有入口都需要在 exports 中显示定义,否则在导入未定义的入口时将报错 ERR_PACKAGE_PATH_NOT_EXPORTED
  • 支持条件导出;
  • 可以自定义条件导出。可以通过选项 --conditions 设置任意数量的自定义条件,例如 node --conditions=foo --conditions=bar index.js

多入口定义

例如有一个包 my-package,有三个文件 index.js , lib.js , feature.js ,它的 package.json 如下定义:

{
  "name": "my-package",
  "exports": {
    ".": "./index.js",
    "./lib": "./lib.js"
  }
}

那么可以这样导入

// CommonJS 方式引入模块
const mypkg = require("my-package");
const lib = require("my-package/lib")

由于我们未在 exports 中定义 feature.js 文件的入口,导入 feature 子模块将报错 ERR_PACKAGE_PATH_NOT_EXPORTED ,这样包所有对外暴露的模块都需要显示定义,使包的使用及维护升级更可靠。

// 报错:Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './feature' is not defined by "exports" ...
const feature = require("my-package/feature")

条件导出

exports 字段提供的条件导出功能,可以定义不同条件下对应不同模块的入口文件。

以 vue 为例

// package.json
{
  “name”: "vue",
  // 定义包的入口
  "exports": {
    // 主入口
    ".": {
      // 通过 ESM 方式,例如 import, import() ...
      "import": {
        // 在 node 环境中
        "node": "./index.mjs",
        // 默认入口作为回退方案,放在最后
        "default": "./dist/vue.runtime.esm-bundler.js"
      },
      // 通过 require() 方式
      "require": "./index.js",
      "types": "./dist/vue.d.ts" // 指定 ts 声明文件
    },
    // ...
  }
}

上面 exports 字段定义表示,如果是

  • ESM 方式导入模块:

    • 在node 环境中,则入口文件为 ./index.mjs
    • 其他 js 运行环境,则入口文件为 ./dist/vue.runtime.esm-bundler.js
  • CJS 方式导入模块,则入口文件为 ./index.js

其中 . 代表包导出的主入口,如果只定义一个入口的话,可以直接设为 exports 的值,例如本文 arrify 包的写法:

// package.json
{
  "name": "arrify",
  "exports": "./index.js"
  // 等同于
  // “exports”: {
  //   ".": "./index.js"
  // }
  // ...
}

自定义条件导出

除此之外,exports 还支持通过选项 --conditions 设置用户自定义条件;如下例子:

在 main.js 中导入一个包 my-package,执行 main.js 文件时指定 --conditions=foo ,即 node --conditions=foo main.js

// main.js
const myPkg = require('my-package')

在 my-package 的 package.json 文件中的 exports 字段定义一个自定义条件 foo

// package.json
{
   "name": "my-package",
   "exports": {
      "foo": "./index.js"
   }
}

那么包 my-package 的入口文件就是 ./index.js (即会匹配为 exports 字段中定义的 foo 条件对应的文件)。

优先级

如果 packege.json 中同时定义了 exportsmainexports 字段优先于 mainexports 定义的顺序也是有优先级的,越先定义被匹配的优先级越高。

例子:exports 字段如下定义,default 定义在 require 前面,那么就会先匹配到 default 条件。

{ 
    "name": "my-package", 
    "main": "./index.js", 
    "exports": { 
        "default": "./index.js", 
        "require": "./main.js" 
    }
}

了解更多 Node.js 文档 - Package entry points

总结

从 arrify 包的源码中,拓展了解了 Symbol.iterator ;通过 package.json 的入口文件配置( exports 字段),了解到如何声明一个模块的入口文件,感觉每个知识点都可以再单独展开写一篇文章,找时间再深入学习下。