欢迎关注我的微信公众号『前端我废了』,查看更多文章!!!
从包名 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
方法),用于遍历它们的值。
默认会调用 Iterator 接口(即Symbol.iterator
方法)的场合:
- for … of
- 解构赋值
- 扩展运算符
- yield*
- 任何接受数组作为参数的场合,都调用了遍历器接口,因为数组的遍历会调用遍历器接口;例如
Set()
、Promise.all()
等
了解更多:
包入口配置:exports
字段
在 arrify 包的 package.json 文件中的入口定义使用的 exports
而不是 main
字段,那就来了解下这个字段吧。
在 package.json
文件中,main
和 exports
字段都可以用于指定 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
- 在node 环境中,则入口文件为
- 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 中同时定义了 exports
和 main
,exports
字段优先于 main
。 exports
定义的内容顺序也是有优先级的,越先定义被匹配的优先级越高。
例子:下面 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
字段),了解到如何声明一个模块的入口文件,感觉每个知识点都可以再单独展开写一篇文章,找时间再深入学习下。