目录
- 前言
- Node 对 CJS 和 ESM 的支持
- 我们可以通过后缀来解决
- 通过 type 字段解决
- --input-type 标志
- 疑问
- module 字段的牛掰
- main 字段的缺点
- 王者 exports
- 作用域包
- 子路径的模式
- 支持条件导出
- 一式两份
- 总结
- 参考
前言
Node 有一个非常核心的知识点——模块,在前端模块化还未真正到来的时代,Node 给出的解决方案是 CommonJS 简称 CJS。
后来 ECMAScript 通过了 JS 的模块化系统,由此开辟了 CJS 和 ESM 同存的局面,在如今模块化流行的今天,你有没有想过大多数 package 为什么既能通过 CJS 使用也能通过 ESM 使用。
我们来研究下这个原理。
ESM 和 CJS:
- ESM - ECMAScript 模块
- CJS - CommonJS
Node 对 CJS 和 ESM 的支持
Node 默认支持 CJS,这我们都知道,后来支持了 ESM 所以 Node 做了怎么调整呢。
1. 我们可以通过后缀来解决
- ESM 以
.mjs
结尾。 - CJS 以
.cjs
结尾。
2. 通过 type 字段解决
- CJS 以
.js
结尾的文件,且最近的父package.json
中顶层字段"type"
值为"commonjs"
。 - ESM 以
.js
结尾的文件,且最近的父package.json
的顶层"type"
值为"module"
。
3. --input-type
标志
将标志
--input-type=commonjs
作为--eval
或--print
的参数,或通过STDIN
传递到node
。将标志为
--input-type=module
的字串,作为--eval
的参数传入或通过STDIN
传入node
疑问
但是无论如何,正常情况下,一个 package 只能支持一种模块它要么是 ESM 要么是 CJS。
但你发现,大多数 package 都能通过 require 和 import 来使用,这是怎么回事呢?
module 字段的牛掰
原来我们借助 Node 原生支持 CJS 去支持 require 语法,借助 Webpack 等打包工具去识别 package.json 的 module 字段,从而支持 ESM,相对 require 还顺便做到了 tree-shaking。
main 字段的缺点
- main 字段首要的缺点就是不同时支持双格式。
- package 内部的文件无法隔离起来,可以随意引用,比如 我引用 chalk 的
package.json
文件可以 import 这个相对路径node_modules/chalk/package.json
。
新增的 exports 和打包工具支持的 module 有异曲同工之妙,但是 exports 获得了 Node 的原生支持而且还更强大。
王者 exports
exports 最重要的有三个作用:
- 作用域包。
- 子路径模式
- 支持条件导出
exports 还有其他功能但不是我们今天文章的重点,所以略过了。
1. 作用域包
exports 和 main 字段两者是相互排斥的,如果你同时定义了 "exports"
和 "main"
,在支持"exports"
的 Node(版本大于等于 v12.7.0) 中 "exports"
会覆盖 "main"
,否则 "main"
生效。
所以我们只需要简单的复制 main 字段,改成 exports 即可使用 exports 功能,就像这样:
{
"main": "./index.js",
"exports": "./index.js"
}
注意非常要注意,如果 exports 字段生效,package 中未导出的文件,你是不能引用的,这一点不像 main 字段,这就是作用域包。
我们通过之前的文章 热乎乎的 workspaces 替代 npm link 调试的新方式 里面讲解的 workspaces 字段,创建的 calculator 计算器 demo 来讲解下。
在 加法 minus 文件夹下面,新增一个测试随意导出文件 subpath.js
,内容为:export default (str) => str;
,现在的文件夹目录
.
├── packages
│ ├── divide
│ │ ├── index.js
│ │ └── package.json
│ ├── minus
│ │ ├── subpath.js
│ │ ├── index.ts
│ │ └── package.json
│ ├── plus
│ │ ├── index.js
│ │ └── package.json
│ └── times
│ ├── index.js
│ └── package.json
minus 的 package.json
文件夹现在长成这样:
{
"main": "index.js"
}
我们使用的时候,可以随意引用包里面的文件,现在在根目录 index.js 文件 引入 subpath.js :
import subpath from "minus/subpath.js";
console.log(subpath("Hi JavaScript"));
但是,我们使用 exports 导出就不行了:
{
"main": "index.js",
"exports": "./index.js"
}
当定义了 "exports"
字段,所有子路径都会被封闭,调试抛出错误 ERR_PACKAGE_PATH_NOT_EXPORTED。
顺便多 YY 已经,npm 默认安装 package 真应该像 pnpm 学习下,做下包封闭功能。
2. 子路径的模式
好了,子路径封闭模式固然不错,但是有时候我们只想要一个包的某个功能,比如 Lodash 提供我们按需导入的能力。
这个就需要子路径模式了,其实就是做个路径映射。
还以上面的例子为例,我们如下在 exports 模式下做路径映射来。
"exports": {
".": "./index.js",
"./subpath.js": "./subpath.js"
},
这样在调试代码,ERR_PACKAGE_PATH_NOT_EXPORTED 错误就没了。
3. 支持条件导出
重点来了,条件导出,非常简单,.
表示当前目录。
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.cjs"
}
},
当你使用这个 package 的时候 Node 将根据用户或下游包环境解析对应的模块规范。现在我可以在支持 import 环境的项目 import 它,也可以在支持 require 的项目 require 它。
一式两份
既然 package 需要支持两个模块化,那么问题来了,我们写代码不可能一份代码两份实现的,那必须的借助打包工具,Webapck 和 Rollup 都行,但它们的配置都太复杂了,等你搞完环境,写代码的灵感和心情估计都没了,今天我们来介绍一个比较小而美的工具——tsup
。
tsup
还有一点完美的就是零配置结合 Typescript 使用,用法如下:
$ tsup src/index.ts
然后在你的项目根目录下就有 dist/index.js
文件供您发布。
当然,我们的重点是双格式的 module,所以支持双格式,只需一个标志:
$ tsup src/index.ts --format cjs,esm
两个文件dist/index.js
,dist/index.mjs
一起生成,非常的 Nice。
这里有份 package.json
使用的首选模板 tsup
:
{
"name": "calculator",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
"watch": "npm run build -- --watch src",
"prepublishOnly": "npm run build"
}
}
完事了,我还强烈建议尝试一下速度惊人的 esbuild
。
总结
今天,回顾了模块化的发展,认识了如今 CJS 和 ESM 共存的局面,Node 也与时俱进跟进了双包的支持,为了弥补 package 未导出可能被滥用了的情况,Node 顺道完善了自身的功能。
目前还未看到有开源项目使用这个功能,但是我相信不就得未来你就能在各大开源项目看到它。