发布一个 ESM 和 CJS 并存的 package

发布一个 ESM 和 CJS 并存的 package.png

目录

  • 前言
  • Node 对 CJS 和 ESM 的支持
      1. 我们可以通过后缀来解决
      1. 通过 type 字段解决
      1. --input-type 标志
    • 疑问
  • module 字段的牛掰
  • main 字段的缺点
  • 王者 exports
      1. 作用域包
      1. 子路径的模式
      1. 支持条件导出
  • 一式两份
  • 总结
  • 参考

前言

Node 有一个非常核心的知识点——模块,在前端模块化还未真正到来的时代,Node 给出的解决方案是 CommonJS 简称 CJS。

后来 ECMAScript 通过了 JS 的模块化系统,由此开辟了 CJS 和 ESM 同存的局面,在如今模块化流行的今天,你有没有想过大多数 package 为什么既能通过 CJS 使用也能通过 ESM 使用。

我们来研究下这个原理。

ESM 和 CJS

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 字段的缺点

  1. main 字段首要的缺点就是不同时支持双格式。
  2. package 内部的文件无法隔离起来,可以随意引用,比如 我引用 chalk 的 package.json 文件可以 import 这个相对路径 node_modules/chalk/package.json

新增的 exports 和打包工具支持的 module 有异曲同工之妙,但是 exports 获得了 Node 的原生支持而且还更强大。

王者 exports

exports 最重要的有三个作用:

  1. 作用域包。
  2. 子路径模式
  3. 支持条件导出

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

image.png

顺便多 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.jsdist/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 顺道完善了自身的功能。

目前还未看到有开源项目使用这个功能,但是我相信不就得未来你就能在各大开源项目看到它。

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容