Open lmk123 opened 1 year ago
最近打算写几个 npm 包,但是在了解过现在的模块导出方式之后,我发现选择太多了,于是在经过一番调查后,将结论整理如下。
最受支持的模块格式当然是 CommonJS 了。只需要在 pacakge.json 里定义一个 main 字段即可导出 CommonJS:
main
{ "main": "./dist/index.js" }
但是,随着前端也开始使用 npm 作为模块发布载体,CommonJS 已经不够用了。这是因为,CommonJS 会导入一个 npm 模块中的所有代码,即使我本身可能只用到了一小部分。举个例子,如果你使用了 const _ = require('lodash'),那么这会将 lodash 里的所有工具函数代码都打包进最终代码里,即使我们可能只用到了其中几个工具函数。
const _ = require('lodash')
也因此,前端开始使用 ESM 作为主流的模块导出方式。作为一个 npm 模块的开发者,当我们会像 lodash 那样提供多个导出项时,我们就应该提供 ESM 的导出方式,帮助我们的使用者减少最终生成的代码体积。
于是,Webpack 和 Rollup 等打包工具开始支持 package.json 里的 module 字段,而流行的做法就是——同时提供 CommonJS 和 ESM 的代码。一个典型的 package.json 文件会是这样子:
module
{ "main": "./dist/lib.common.js", "module": "./dist/lib.esm.js" }
但是这仍然不够。这是因为,部分 npm 模块既支持在浏览器里使用、也支持在 Node.js 里使用,比如 axios,它会在 Node.js 里使用 https 模块、在浏览器里使用 XMLHttpRequest 来发起网络请求,所以 axios 需要为不同的使用环境导出不同的代码,于是,package.json 里又新增了一个 browser 字段用于这种场景。
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
前面介绍了 main、module 和 browser 这三个字段的使用,不过现在出现了一个新的字段 exports 用于替代前面这几种导出方式。
exports 的使用方式非常多,不仅支持针对引用形式(import 或者 require())导出不同的文件,还支持针对使用目标(Node.js、浏览器甚至 Electron 等)导出不同的文件。
import
require()
具体的介绍太过复杂,这里提供两个链接:
这里列举一些使用 exports 替代以前的导出方式的例子,不过我没有真的使用过,不知道会不会有问题,如果有,欢迎指正:
以前是 { "main": "index.js" },现在是 { "exports": "./index.js"}
{ "main": "index.js" }
{ "exports": "./index.js"}
以前是 { "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,后面会介绍。
.cjs
type: module
以前是 { "main": "./dist/lib.common.js", "browser": "./dist/lib.borowser.js" },现在是
{ "main": "./dist/lib.common.js", "browser": "./dist/lib.borowser.js" }
{ "exports": { "browser": "./dist/lib.browser.js", "default": "./dist/lib.js" } }
我觉得,这要视情况而定,具体分析如下:
import { a, b, c } from 'another-module'
相关链接:
最近打算写几个 npm 包,但是在了解过现在的模块导出方式之后,我发现选择太多了,于是在经过一番调查后,将结论整理如下。
导出为 CommonJS
最受支持的模块格式当然是 CommonJS 了。只需要在 pacakge.json 里定义一个
main
字段即可导出 CommonJS:但是,随着前端也开始使用 npm 作为模块发布载体,CommonJS 已经不够用了。这是因为,CommonJS 会导入一个 npm 模块中的所有代码,即使我本身可能只用到了一小部分。举个例子,如果你使用了
const _ = require('lodash')
,那么这会将 lodash 里的所有工具函数代码都打包进最终代码里,即使我们可能只用到了其中几个工具函数。也因此,前端开始使用 ESM 作为主流的模块导出方式。作为一个 npm 模块的开发者,当我们会像 lodash 那样提供多个导出项时,我们就应该提供 ESM 的导出方式,帮助我们的使用者减少最终生成的代码体积。
同时导出 CommonJS 与 ESM
于是,Webpack 和 Rollup 等打包工具开始支持 package.json 里的
module
字段,而流行的做法就是——同时提供 CommonJS 和 ESM 的代码。一个典型的 package.json 文件会是这样子:同时导出针对 Node.js 和浏览器的代码
但是这仍然不够。这是因为,部分 npm 模块既支持在浏览器里使用、也支持在 Node.js 里使用,比如 axios,它会在 Node.js 里使用 https 模块、在浏览器里使用 XMLHttpRequest 来发起网络请求,所以 axios 需要为不同的使用环境导出不同的代码,于是,package.json 里又新增了一个
browser
字段用于这种场景。举例来说,axios v0.27.2 的 package.json 是这么写的:
当在 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
前面介绍了
main
、module
和browser
这三个字段的使用,不过现在出现了一个新的字段exports
用于替代前面这几种导出方式。exports
的使用方式非常多,不仅支持针对引用形式(import
或者require()
)导出不同的文件,还支持针对使用目标(Node.js、浏览器甚至 Electron 等)导出不同的文件。具体的介绍太过复杂,这里提供两个链接:
exports
的描述:https://webpack.js.org/guides/package-exports/exports
的描述:https://nodejs.org/api/packages.html#package-entry-points这里列举一些使用 exports 替代以前的导出方式的例子,不过我没有真的使用过,不知道会不会有问题,如果有,欢迎指正:
只导出 CommonJS 模块
以前是
{ "main": "index.js" }
,现在是{ "exports": "./index.js"}
同时导出 CommonJS 与 ESM
以前是
{ "main": "./dist/lib.common.js", "module": "./dist/lib.esm.js" }
,现在是注意,这里出现了两个前文没介绍过的内容:
.cjs
扩展名和type: module
,后面会介绍。同时导出针对 Node.js 和浏览器的代码
以前是
{ "main": "./dist/lib.common.js", "browser": "./dist/lib.borowser.js" }
,现在是那么我的模块该提供哪些形式的导出?
我觉得,这要视情况而定,具体分析如下:
import { a, b, c } from 'another-module'
),那么应该提供 ESM 来减少最终打包出来的代码体积。反过来说,如果你的模块本身只导出一个项目且没有部分导入其它模块,那么我觉得只提供 CommonJS 即可。exports
,比如针对生产环境和开发环境提供不同的代码、针对浏览器和 Node.js 提供不同的代码等。相关链接: