基础前端

发布一个 ESM 和 CJS 并存的 package

2021-12-03  本文已影响0人  CondorHero
发布一个 ESM 和 CJS 并存的 package.png

目录

前言

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

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

我们来研究下这个原理。

ESM 和 CJS

Node 对 CJS 和 ESM 的支持

Node 默认支持 CJS,这我们都知道,后来支持了 ESM 所以 Node 做了怎么调整呢。

1. 我们可以通过后缀来解决

2. 通过 type 字段解决

3. --input-type 标志

疑问

但是无论如何,正常情况下,一个 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 顺道完善了自身的功能。

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

参考

上一篇 下一篇

猜你喜欢

热点阅读