package.json 之 Node.js 相关字段

October 17, 2020

前言

这阵子在工作中参与了业务的抽象,计划打造通用组件并发布成 npm 包便于后续项目的共建,自然而然,学习到了一些关于 package.json 之前没有接触到的知识点。

在此,介绍一些在 package.json 中被 Node.js 所使用的字段,以下所使用的 Nodejs 版本均为 v14.14.0.

  • name - 定义了以 package 形式导入时的名称,同时它决定了它在 npm 源上的唯一名称。
  • type - 规定 package 下的 .js 文件被 Node.js 以 CommonJS Modules 或 ECMAScript Modules 加载。
  • exports - 设置 package 的导出。
  • main - 规定加载 package 时的默认入口文件。

name

json
{
"name": "package-name"
}
json
{
"name": "package-name"
}

"name" 字段定义你的 package 名称,当你将 package 发布到 npm 源上,牢记 name 需要满足 规范

当你下载的 package 位于 node_modules 时,你就可以使用以下方式导入:

js
const pkg = require("package-name");
// or
import pkg from "package-name";
js
const pkg = require("package-name");
// or
import pkg from "package-name";

type

在 js 的世界中,存在着两种影响力最大的模块规范,它们是 CommonJS Modules 以及 ECMAScript Modules,好在 Nodejs 自从 v12 起就全部支持了,.js .cjs 文件默认以 CommonJS Modules 执行,.mjs 则默认以 ECMAScript Modules 执行。

"type" 字段的出现让我们更好得决定 .js 文件被哪种模块规范执行,它的值有两个,分别是 "module""commonjs".

json
{
"type": "commonjs"
}
json
{
"type": "commonjs"
}

当你设置为 "commonjs" 时,那些以该 package.json 作为最近的父级配置文件的 .js .cjs 文件默认以 CommonJS Modules 执行,如果你想执行 ECMAScript Modules,就必须将后缀名改为 .mjs.

json
{
"type": "module"
}
json
{
"type": "module"
}

同理,当你设置为 "module" 时,那些以该 package.json 作为最近的父级配置文件的 .js .mjs 文件默认以 ECMAScript Modules 执行,如果你想执行 CommonJS Modules,就必须将后缀名改为 .cjs.

exports

"exports" 字段允许你通过引用自己的 package name(Self-referencing a package using its name)来定义 package 的入口文件,举个例子:

json
{
"name": "pkg",
"exports": {
".": "./main.mjs",
"./foo": "./foo.js"
}
}
json
{
"name": "pkg",
"exports": {
".": "./main.mjs",
"./foo": "./foo.js"
}
}

以上可以被解读为:

json
{
"exports": {
"pkg": "pkg/main.mjs",
"pkg/foo": "pkg/foo.js"
}
}
json
{
"exports": {
"pkg": "pkg/main.mjs",
"pkg/foo": "pkg/foo.js"
}
}
js
import { something } from "pkg"; // from "pkg/main.mjs"
js
import { something } from "pkg"; // from "pkg/main.mjs"
js
const { something } = require("pkg/foo"); // require("pkg/foo.js")
js
const { something } = require("pkg/foo"); // require("pkg/foo.js")

它从 Node.js v12 开始被支持,并作为 "main"(具体介绍请看下一节) 字段的替代方案。

他最大的一个特性就是 条件导出(Conditional Exports),当该 package 被导入时,能够判断被导入时的模块环境,从而执行不同的文件,简而言之就是,我们如果使用 import 命令,入口会加载 ECMAScript Modules 文件,如果使用 require 命令,入口则加载 CommonJS Modules 文件。

我们来一探究竟,具体目录结构如下:

bash
├── mod
│ ├── mod.js
│ ├── mod.cjs
│ ├── package.json
│── app.js
└── app.mjs
bash
├── mod
│ ├── mod.js
│ ├── mod.cjs
│ ├── package.json
│── app.js
└── app.mjs

mod 作为一个本地的 package,它的 package.json 定义如下:

json
{
"name": "mod",
"main": "index.js",
"type": "module",
"exports": {
"require": "./mod.cjs",
"import": "./mod.js"
}
}
json
{
"name": "mod",
"main": "index.js",
"type": "module",
"exports": {
"require": "./mod.cjs",
"import": "./mod.js"
}
}

并且他提供了实现相同功能的两个脚本文件,以应对不同的模块环境:

js
// mod.cjs
exports.name = "cjs";
js
// mod.cjs
exports.name = "cjs";
js
// mod.js
export const name = "mjs";
js
// mod.js
export const name = "mjs";

调用该模块的文件如下:

js
// app.js
const { name } = require("mod");
console.log(name);
js
// app.js
const { name } = require("mod");
console.log(name);
js
// app.mjs
import { name } from "mod";
console.log(name);
js
// app.mjs
import { name } from "mod";
console.log(name);

这一切还不够,因为 mod 现在作为一个本地 package,它既不是从 npm 上 download 下来,也不处于 node_modules 目录中,无法通过 package name 的形式导入。

办法总是有的,借助于 package link,我们可以实现本地 package 的关联,而不用正式发布到 npm 源上。

先在 ./mod 目录下执行 yarn link

bash
success Registered "mod".
info You can now run `yarn link "mod"` in the projects where you want to use this package and it will be used instead.
bash
success Registered "mod".
info You can now run `yarn link "mod"` in the projects where you want to use this package and it will be used instead.

然后在项目根目录下,执行 yarn link "mod"

bash
success Using linked package for "mod".
bash
success Using linked package for "mod".

最后,验证我们的 "exports" 字段是否起作用了:

bash
$ node app.js
$ cjs
$ node app.mjs
$ mjs
bash
$ node app.js
$ cjs
$ node app.mjs
$ mjs

结果证实,在不同的模块环境中,执行了不同的脚本文件。

注意,对于所有在 "exports" 中定义的路径都必须是绝对路径。即 ./ 的形式。

main

json
{
"main": "./main.js"
}
json
{
"main": "./main.js"
}

"main" 字段定义了导入 package 目录时使用的入口文件,举个例子:

js
const pkg = require("package-name");
js
const pkg = require("package-name");

由于 node_modules 的查找机制,会被解析成以下路径:

js
const pkg = require("./node_modules/package-name");
js
const pkg = require("./node_modules/package-name");

这里只导入了一个目录,他不是一个具体模块,但是借助 main 即入口文件,文件路径最终被解析为:

js
const pkg = require("./node_modules/package-name/main.js");
js
const pkg = require("./node_modules/package-name/main.js");

注意,当一个 package 同时拥有 "exports""main" 字段时,在被以 package name 方式导入时,exports 的优先级较高。

Reference


B2D1 (包邦东)

Written by B2D1 (包邦东)