美文网首页基础前端
发布一个 ESM 和 CJS 并存的 package

发布一个 ESM 和 CJS 并存的 package

作者: CondorHero | 来源:发表于2021-12-03 16:16 被阅读0次
    发布一个 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 顺道完善了自身的功能。

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

    参考

    相关文章

      网友评论

        本文标题:发布一个 ESM 和 CJS 并存的 package

        本文链接:https://www.haomeiwen.com/subject/tmfmxrtx.html