package.json 中的 main、types、module、exports 字段
# 0.前言
在库开发中,最头疼的就是导出模块的模块语法规范问题。由于现在对 AMD
的需求越来越少了, UMD
格式也逐渐推出历史舞台。因此现在项目组一般要准备两份代码模块,分别是 commonJS
规范以及 esm
规范。
在 package.json
中涉及导出的字段很多,比如说 main
、types
、module
、exports
。本篇文章就对这块内容好好做一个梳理工作。
其中还需要解答一个双打包模块的问题,即根据情况,合理去导入 commonjs
语法以及 esm
语法。
# main
字段与 types
字段
这部分主要参考的是:
Typescript
在Publishing
章节官方的说明,见具体网址 (opens new window) 。这一部分主要涉及到对
.lib.ts
文件的导出。
在这篇文章中说明,有两种方式可以导出声明文件:
- 将声明文件与要导出的
npm
包一起导出。 - 将声明文件导出给
npm
仓库中的@types
组织。
虽然 ts
官方推荐第二种方案,但是在项目中显然第一种方案用的更多些。
假设在你的 package
中有一个主入口.js
文件,就可以直接显示在 package.json
中指明对应的主声明文件。将 types
属性设置为指向需要捆绑的声明文件,举例如下:
// package.json
{
"name": "awesome",
"author": "Vandelay Industries",
"version": "1.0.0",
"main": "./lib/main.js",
"types": "./lib/main.d.ts"
}
2
3
4
5
6
7
8
其中,typings
字段等价于 types
,同样也可以被使用。
额外的有,当工程中主文件为 index.js
并且声明文件为 index.d.ts
时,上述声明文件无需显式配置,当然显式声明是推荐的选择。
# module
字段
关于 modules
字段的使用,在stackoverflow
上讨论非常多:
- 【What is the "module" package.json field for?】 (opens new window)
- 【How to choose 'module' instead of 'main' file in package.json】 (opens new window)
结论如下:
The
module
field is not officially defined by Node.js and support is not planned. Instead, the Node.js community settled on package exports (opens new window) which they believe is more versatile.For practical reasons JavaScript bundlers will continue to support the
module
field. The esbuild docs explain (opens new window) when to usemodule
as well as related fieldsmain
andbrowser
.
module
字段一直没有被 Node
官方认可,这也是在 Node document
文档里始终无法查询的原因。module
字段的提出实际上是来源于一个提案 (opens new window),但是实际上官方已经有相应标识 standard module
的字段了,也就是 type: "module"
。 module
只是被打包工具等构建工具使用作为 ES
模块的入口。
# exports
字段
Node
官方 api
使用说明:https://nodejs.org/api/packages.html#exports
但是这里推荐看两篇文章:
Node
官方写的:Conditional exports (opens new window),里面提出了一个dual package hazard
的概念Esbuild
官方,在看了Node
官方的文章后,写一篇总结 (opens new window)。
Conditional exports
提供了一种根据特定条件映射到不同路径的方法,同时支持 commonJs
语法以及 ESM
语法。
例如,一个包想要为 require()
和 import
提供不同的 ES
模块导出可以这样写:
// package.json
{
"exports": {
"import": "./index-module.js",
"require": "./index-require.cjs"
},
"type": "module"
}
2
3
4
5
6
7
8
除了 import
和 require
可以设置外,还可以设置 default
,node
,browser
字段。甚至还可以对子包的模块语法进一步划分,官方案例如下:
{
"exports": {
".": "./index.js",
"./feature.js": {
"node": "./feature-node.js",
"default": "./feature.js"
}
}
}
2
3
4
5
6
7
8
9
当使用 require('pkg/feature.js')
或者 import 'pkg/feature.js'
时,使用 default
来处理位置的 js
环境,当明确为 node
环境时,则导入 ./feature-node.js
文件。
# 在提案中 module
字段的解析次序如下:
提案地址:https://github.com/dherman/defense-of-dot-js/blob/master/proposal.md
To be more specific about how Node.js will decide how to process a given file, once the require algorithm has found it:
- If a package does not have a
"module"
key and has a"main"
key, Node.js module resolution for entry points beginning with that package name are unchanged, and Node.js evaluates all files in that package as CommonJS modules. - If a package has no
"main"
key and has a"module"
key, Node.js evaluates all files in that package as standard modules. - If a package has a
"modules.root"
key, Node.js resolves any requires nested under that package ("lodash/array"
inlodash
) relative to the"modules.root"
, and Node.js evaluates all files in that package as standard modules. - If a package has both a
"main"
key and a"module"
key, it can enumerate a list of files ("app/index.js"
) or directories ("app/routes/"
) using the"modules"
key. Node.js module resolution remains unchanged, but Node.js will evaluate any enumerated files, as well as files inside of enumerated directories, as standard modules. - If a package does not have a
"main"
or"module"
key:- If it has a
module.js
in the root, it is identical in all respects to the presence of a"module": "module.js"
in thepackage.json
. - Otherwise, if it has an
index.js
in the root, it is identical in all respects to"main": "index.js"
in thepackage.json
(the current behavior).
- If it has a
- Both
require
andimport
use only these rules to decide whether to evaluate a file as a standard module or a CommonJS module.
# 打包器(webpack)中对模块化的处理
在 webpack
中还可以还通过配置 resolve.mainFields
来进行模块化匹配。
如果不主动设置,这个参数实际上是跟着 target
走的。
当
target
为webworkder
、web
时:module.exports = { //... resolve: { mainFields: ["browser", "module", "main"], // browser > module > main }, };
1
2
3
4
5
6当
target
设置为node
环境时,则默认为:module.exports = { //... resolve: { mainFields: ["module", "main"], // moudule > main 字段 }, };
1
2
3
4
5
6
例如,考虑任意一个名为 upstream
的类库 package.json
包含以下字段:
{
"browser": "build/upstream.js",
"module": "index"
}
2
3
4
当我们 import * as Upstream from 'upstream'
时,这实际上会从 browser
属性解析文件。在这里 browser
属性是最优先选择的,因为它是 mainFields
的第一项。同时,由 webpack
打包的 Node.js
应用程序首先会尝试从 module
字段中解析文件。
简单来说:打包的时候从
module
字段为入口文件,导入时看browser
字段。 --哭了,好复杂啊。