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
modulefield 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
modulefield. The esbuild docs explain (opens new window) when to usemoduleas well as related fieldsmainandbrowser.
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.jsin 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.jsin the root, it is identical in all respects to"main": "index.js"in thepackage.json(the current behavior).
- If it has a
- Both
requireandimportuse 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字段。 --哭了,好复杂啊。