Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

npm 包导出模块的选择 #112

Open
lmk123 opened this issue Jul 14, 2022 · 0 comments
Open

npm 包导出模块的选择 #112

lmk123 opened this issue Jul 14, 2022 · 0 comments

Comments

@lmk123
Copy link
Owner

lmk123 commented Jul 14, 2022

最近打算写几个 npm 包,但是在了解过现在的模块导出方式之后,我发现选择太多了,于是在经过一番调查后,将结论整理如下。

导出为 CommonJS

最受支持的模块格式当然是 CommonJS 了。只需要在 pacakge.json 里定义一个 main 字段即可导出 CommonJS:

{
  "main": "./dist/index.js"
}

但是,随着前端也开始使用 npm 作为模块发布载体,CommonJS 已经不够用了。这是因为,CommonJS 会导入一个 npm 模块中的所有代码,即使我本身可能只用到了一小部分。举个例子,如果你使用了 const _ = require('lodash'),那么这会将 lodash 里的所有工具函数代码都打包进最终代码里,即使我们可能只用到了其中几个工具函数。

也因此,前端开始使用 ESM 作为主流的模块导出方式。作为一个 npm 模块的开发者,当我们会像 lodash 那样提供多个导出项时,我们就应该提供 ESM 的导出方式,帮助我们的使用者减少最终生成的代码体积。

同时导出 CommonJS 与 ESM

于是,Webpack 和 Rollup 等打包工具开始支持 package.json 里的 module 字段,而流行的做法就是——同时提供 CommonJS 和 ESM 的代码。一个典型的 package.json 文件会是这样子:

{
  "main": "./dist/lib.common.js",
  "module": "./dist/lib.esm.js"
}

同时导出针对 Node.js 和浏览器的代码

但是这仍然不够。这是因为,部分 npm 模块既支持在浏览器里使用、也支持在 Node.js 里使用,比如 axios,它会在 Node.js 里使用 https 模块、在浏览器里使用 XMLHttpRequest 来发起网络请求,所以 axios 需要为不同的使用环境导出不同的代码,于是,package.json 里又新增了一个 browser 字段用于这种场景。

举例来说,axios v0.27.2 的 package.json 是这么写的:

{
  "main": "index.js",
  "browser": {
    "./lib/adapters/http.js": "./lib/adapters/xhr.js",
    "./lib/defaults/env/FormData.js": "./lib/helpers/null.js"
  }
}

当在 Node.js 中引用 axios 时,Node.js 会忽略 browser 字段,按照正常的 CommonJS 方式处理 index.js;而代码打包工具(Webpack 或 Rollup)在遇到引用 axios 的情况时,会进行判断:如果打包的目标是浏览器,那么它们会在解析 "./lib/adapters/http.js" 这个文件时,将它替换为 "./lib/adapters/xhr.js" 的内容打包进最终的代码里,这样一来就能在浏览器里使用 axios 了。

顺带一提,axios 是给 browser 字段提供了一个对象来替换了部分模块,不过 browser 字段也支持提供一个字符串,作为整个模块的替代。

最新的模块导出方式:exports

前面介绍了 mainmodulebrowser 这三个字段的使用,不过现在出现了一个新的字段 exports 用于替代前面这几种导出方式。

exports 的使用方式非常多,不仅支持针对引用形式(import 或者 require())导出不同的文件,还支持针对使用目标(Node.js、浏览器甚至 Electron 等)导出不同的文件。

具体的介绍太过复杂,这里提供两个链接:

这里列举一些使用 exports 替代以前的导出方式的例子,不过我没有真的使用过,不知道会不会有问题,如果有,欢迎指正:

只导出 CommonJS 模块

以前是 { "main": "index.js" },现在是 { "exports": "./index.js"}

同时导出 CommonJS 与 ESM

以前是 { "main": "./dist/lib.common.js", "module": "./dist/lib.esm.js" },现在是

{
  "type": "module",
  "exports": {
    "require": "./dist/lib.common.cjs",
    "import": "./dist/lib.esm.js"
  }
}

注意,这里出现了两个前文没介绍过的内容:.cjs 扩展名和 type: module,后面会介绍。

同时导出针对 Node.js 和浏览器的代码

以前是 { "main": "./dist/lib.common.js", "browser": "./dist/lib.borowser.js" },现在是

{
  "exports": {
    "browser": "./dist/lib.browser.js",
    "default": "./dist/lib.js"
  }
}

那么我的模块该提供哪些形式的导出?

我觉得,这要视情况而定,具体分析如下:

  1. 任何情况下,模块都应该有一个 CommonJS 的导出。很多工具对 ESM 的支持不是很好,或者需要一些额外的配置才支持 ESM,举例来说,jest 目前对 ESM 只有实验性质的支持,且需要用到 babel 转为 CommonJS。
  2. 如果模块提供多个导出项目、或者模块本身引用了另一个模块的少数项目,那么则应该提供 EMS。举例来说,如果你的模块提供了多个函数、或者你的模块导入了另一个模块的部分导出项目(import { a, b, c } from 'another-module'),那么应该提供 ESM 来减少最终打包出来的代码体积。反过来说,如果你的模块本身只导出一个项目且没有部分导入其它模块,那么我觉得只提供 CommonJS 即可。
  3. 最后,就根据你的需要来配置 exports,比如针对生产环境和开发环境提供不同的代码、针对浏览器和 Node.js 提供不同的代码等。

相关链接:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant