理解package.json中的exports, main, module, type

在package.json中与模块化相关的,包括:

  • main
  • browser
  • module
  • exports
  • types或typings

main

对应commonjs引入方式的程序入口文件

{
  "main": "dist/index.js"
}

webpack 等打包工具会根据 `package.json` 文件中的 `main` 字段的值来确定模块的入口文件。默认情况下,Webpack 会将 `main` 指定的文件作为应用程序的入口点。如果 `main` 字段未定义,Webpack 会查找默认入口文件,通常是 `index.js`

browser

但是我们发布的包可能会被用于浏览器环境,里面可能依赖了浏览器特有的功能或 API, 单纯一个 main 无法满足我们的需求, 这时候就需要用到 browser 字段了。

{
  "main": "dist/index.js",
  "browser": "dist/index.browser.js"
}

module

module 字段允许包作者明确指定在使用 ES module 时应该加载的文件。如果没有指定 module 字段,那么在使用 ES module 时,会默认使用 main 字段指定的文件。

{
  "module": "dist/module.esm.js"
}

Webpack 在构建项目时,默认的 target 是 ‘web’,即用于构建在浏览器环境中运行的代码, 在这种情况下,如果一个包中同时存在 browser, module 和 main 字段, 那么解析的优先级分别是: browser > module > main。

types或typings

在 `package.json` 中,`types` 和 `typings` 都用于指定 TypeScript 类型定义文件的位置,但它们之间有些区别。

  • types指定 TypeScript 的类型定义文件(`.d.ts`)的位置,是 TypeScript 2.0 及以后版本推荐的字段。
  • typings是早期的字段,用于指定类型定义的路径,在 TypeScript 2.0 后被 `types` 替代,但仍被支持以保持向后兼容。

如果同时存在 `types` 和 `typings` 字段,TypeScript 优先使用 `types`。 

exports

exports 字段是在 Node.js 14 及更新版本中引入的,用于定义模块的导出方式。它提供了一种更灵活和强大的方法来指定模块的导出方式,可以定义多个入口文件和条件导出。

路径封装

exports 字段可以对于包中导出的路径进行封装。如下:

{
   "exports": {
     ".": {
       "require": "./dist/index.js",
       "import": "./dist/index.esm.js"
     },
     "./feature": "./dist/feature.js"
   }

}

要使用 `exports` 中定义的 `feature`,你可以按照以下方式在你的代码中导入它:

import feature from 'your-package-name/feature';

条件导出

通常我们编写的 NPM 包支持被 ESM 和 CJS 两种方式同时引入,需要根据不同的引入方式来寻找不同的入口文件。

{
  "exports": {
    // ESM 引入时的入口文件
    "import": "./index-module.js",
    // CJS 方式引入时寻找的路径
    "require": "./index-require.cjs"
  },
}

可以看到 exports 关键字中定义的 key 为 import 和 require 分别表示两种不同的模块引入方式使用该包时引入的不同文件路径。

如果引入的 Npm 包中定义了 exports 关键字来定义对应的入口文件导出,package.json中的 module、main 字段都是无效。此时自然 webpack 中的 resolve.mainFields 字段也会失去它的效果,需要通过 resolve.conditionNames 字段来定义对应的环境。

也就是说,在引入的 Npm 包的 package.json 中如果存在 exports 关键字时,构建配置的 resolve.mainFields 是无效的。如果未设置 resolve.conditionNames 字段,那么默认 webpack 会按照你当前的运行环境以及引入方式从而去 npm 包中的 exports 字段查找对应匹配的文件。

type和exports/main/module的关系

首先我们需要理解type字段的含义:

当设置为“module”时,所在项目中(不包含node_modules)所有.js文件将被视为EsModule类型文件。
如果省略“type”字段或设置为“commonjs”,则项目中(不包含node_modules)所有.js文件都被视为CommonJS类型文件。

type: "module"

此时.js文件将被视为esmodule,我们需要将commonjs文件显示声明为.cjs。

改造配置如下:

{
  ...,
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./unstyled": {
      "types": "./dist/unstyled.d.ts", // 可以省略,但不建议
      "import": "./dist/unstyled.js",
      "require": "./dist/unstyled.cjs"
    },
    "./*": "./*"
  },
  ...
}

type: "commonjs" 或不设置

此时.js将被视为commonjs,并且我们需要将esmodule文件显示声明为.mjs/.esm.js(实际上你声明成.xxx.js也可以,甚至.xxx也行,但不建议)。

改造配置如下:

{
  ...,
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.js"
    },
    "./unstyled": {
      "types": "./dist/unstyled.d.ts", // 可以省略,但不建议
      "import": "./dist/unstyled.mjs",
      "require": "./dist/unstyled.js"
    },
    "./*": "./*"
  },
  ...
}

main/module 和 exports的关系

exports省略场景

如果没有子路径,比如没有my-package/xxx,只是简单的my-package, 那配置可以简化为:

{
  ...,
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  ...
}

exports不可省略场景

存在子路径,此时需要添加exports进行路径映射,并且export中的"."配置会具有较高优先级,所以"."对应的路径必须是真实存在的。

package.json子模块(子路径导出)

在 `package.json` 中,子模块指的是一个包内的特定功能或文件,通常位于包的某个子目录下。

主要用于以下几个目的:

  • 模块组织:可以将功能划分为多个文件,便于管理和维护。
  • 精确导出:允许用户仅导入他们需要的部分,而不是整个模块,从而减少包的大小和提高性能。
  • 版本控制:可以为不同的子模块指定不同的版本或构建方式,提供更灵活的升级路径。
  • 兼容性:使得在不同环境(如 CommonJS 和 ES Modules)中都能有效使用相同的包结构。
  • 清晰的API:通过明确定义哪些部分是公共 API,提升用户的使用体验。

假设有一个 npm 包,结构如下:

my-package/
├── package.json
├── dist/
│   └── styles.css
└── index.js

package.json内容如下

{
  "name": "my-package",
  "version": "1.0.0",
  "exports": {
    "./styles.css": "./dist/styles.css"
  }
}

在另一个项目中,可以这样引入样式:

import styles from 'my-package/styles.css';

这将会加载 `my-package/dist/styles.css` 文件。

如果有人试图直接引用 `dist/styles.css`,例如:

import styles from 'my-package/dist/styles.css';

这将会失败,因为 `exports` 没有公开这个路径。

通过这种方式,你可以确保其他开发者只能通过定义的接口访问你的模块,从而提高代码的安全性和可维护性。

posted @ 2024-11-09 23:01  李小菜丶  阅读(374)  评论(0)    收藏  举报