高级前端必须掌握的 package.json 字段知识:包的入口点

448 阅读3分钟

在 npm 包的 package.json 文件中,有两个字段可以定义包的入口点:mainexports。这两个字段既可以定义 ES 模块的入口点,也可以定义 CommonJS 模块的入口点。

main 字段在所有版本的 Node.js 中都得到支持,但其功能是有限的:它仅定义了包的主要入口点。

有关 main 字段的用法详见 package.json 中的 main 字段的作用是什么?

exports 字段为 main 提供了一种更现代的替代方案,它允许定义多个入口点,支持在不同环境之间进行条件入口解析,并且除了 exports 中定义的入口点之外,阻止其他任何入口点。这种封装方式使模块作者能够清晰地定义其包对外的接口。

对于面向当前新版本的 Node.js 的 npm 包。建议使用 exports 字段定义包的入口。对于面向 Node.js 10 或更低版本的 Node.js 包,main 字段是必需的。如果同时定义了 exportsmain,在受支持的 Node.js 版本中,exports 字段优先于 main 字段。

可以在 exports 中使用条件导出,以根据不同的环境定义包的不同入口点,包括包是通过 require 还是通过 import 引用的。

已有的 npm 包如果引入了 exports 字段,将阻止 npm 包的使用者使用任何未定义的入口点,包括 package.json (例如 require('your-package/package.json'))。这可能会是该包的破坏性变更。

具体看看案例1,该例子的目录结构如下:

85.png

为了模仿实际项目中使用 npm 包的场景,就创建了 node_modules 文件夹,node_modules/common/package.json 的内容如下,使用 exports 导出了 node_modules/common/index.js 导出的所有 API:

{
  "name": "common",
  "exports": {
    ".": "./index.js"
  }
}

node_modules/common/index.js 文件内容如下:

function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

module.exports = {
  add: add,
  subtract: subtract,
};

app.js 文件的内容如下:

const util = require("common")
const pkg = require('common/package.json')

const total = util.add(1, 2)

console.log('res  ',  {
  pkg,
  total
})

然后在命令行终端使用 Node.js 运行 app.js 文件代码,会发现报错了,是由于 package.json 文件中的 exports 字段没有导出 package.json 所致:

86.png

下面看看案例2

此案例在 node_modules/common/package.json 文件中导出了 package.json 中的内容:

{
  "name": "common",
  "exports": {
    ".": "./index.js",
    "./package.json": "./package.json"
  }
}

其他地方与案例1相同,然后在命令行终端运行 app.js 文件,发现没有报错了:

87.png

为了使 package.json 中 exports 的引入不造成破坏性变更,需确保每个之前支持的入口点都被导出。最好明确指定入口点,以便包的对外开放的 API 得到良好定义。例如,一个之前导出了 mainlibfeaturepackage.json 的项目可以使用如下 package.exports

{
  "name": "my-package",
  "exports": {
    ".": "./lib/index.js",
    "./lib": "./lib/index.js",
    "./lib/index": "./lib/index.js",
    "./lib/index.js": "./lib/index.js",
    "./feature": "./feature/index.js",
    "./feature/index": "./feature/index.js",
    "./feature/index.js": "./feature/index.js",
    "./package.json": "./package.json"
  }
}

或者,一个 npm 包可以选择使用导出模式,导出带扩展名和不带扩展名子路径的整个文件夹:

{
  "name": "my-package",
  "exports": {
    ".": "./lib/index.js",
    "./lib": "./lib/index.js",
    "./lib/*": "./lib/*.js",
    "./lib/*.js": "./lib/*.js",
    "./feature": "./feature/index.js",
    "./feature/*": "./feature/*.js",
    "./feature/*.js": "./feature/*.js",
    "./package.json": "./package.json"
  }
}

通过上述方式为任何小版本的 npm 包提供向后兼容性,那么该 npm 包未来的主版本更新就可以适当地将导出内容限制为仅公开特定的功能导出:

{
  "name": "my-package",
  "exports": {
    ".": "./lib/index.js",
    "./feature/*.js": "./feature/*.js",
    "./feature/internal/*": null
  }
}

👆 不会导出 feature/internal 文件下的所有内容

总结

在 npm 包的 package.json 文件中,有两个字段可以定义包的入口点:mainexports

main 字段定义包的主要入口点,而 export 字段是 main 字段的现代替代方案,可以定义多个入口点,支持在不同环境之间进行条件入口解析,并且除了 exports 中定义的入口点之外,阻止其他任何入口点。

对于面向 Node.js 10 或更低版本的 Node.js 包,main 字段是必需的。如果同时定义了 exportsmain,在受支持的 Node.js 版本中,exports 字段优先于 main 字段。