exports使用 package.json字段控制如何访问你的 npm 包

目录

[想象一下你正在开发一个 npm 包......](#想象一下你正在开发一个 npm 包……)

术语

什么是exports领域?

exports好处

保护内部文件

多格式包

将子路径映射到dist目录

子路径导出

单一入口点

多个入口点

公开软件包文件的子集

有条件出口

设置使用条件

默认条件

句法

[针对 Node.js 和浏览器](#针对 Node.js 和浏览器)


想象一下你正在开发一个 npm 包......

您希望提供多个入口点,但同时限制对内部文件的访问 。您需要同时支持 CJS 和 ESM,包含类型定义,甚至可能还要确保浏览器兼容性。您如何管理所有这些需求?

在早期版本的 Node.js 中,包使用main字段 in package.json来定义单个入口点**。**这种方法虽然简单,但存在局限性:它只允许一个入口点,并且包中的所有文件都可访问,无法保护内部文件。随着生态系统的发展(尤其是 ESM 的兴起和对多格式包的需求),这种方法很快就显得力不从心。

术语

  • ECMAScript 模块(ESM) :JavaScript 使用原生import&export语法的标准化模块格式。

  • CommonJS (CJS) :Node.js 的遗留模块格式,用于require()导入和module.exports导出。

  • 包入口点 :访问包的入口路径(例如pkg-apkg-a/file)。

  • 包子路径 :包名称后面的路径(例如,/this/is/subpath.jspkg-a/this/is/subpath.js)。

什么是exports领域?

该字段在Node.js v12.7.0(2019 年 7 月)中引入,通过两个核心功能满足了这些需求:exports package.json

  1. 子路径导出 :包可以定义多个入口点,只允许公开特定文件,同时阻止对包内部的访问。

  2. 条件导出:包可以切换入口点,以针对不同的环境(例如,Node.js 与浏览器)和模块类型(例如,CJS 与 ESM)解析不同的文件。

从那时起,exports它得到了主要 JavaScript 工具和构建系统的广泛支持,例如 TypeScript、Deno、Vite、Webpack、esbuild 等。

exports好处

保护内部文件

以前,用户可以导入软件包中的任何文件,甚至是内部文件。这导致软件包维护者难以更新或重构软件包,因为他们无法判断用户是否依赖这些内部文件。现在exports,维护者可以明确定义哪些文件可以访问,从而建立清晰的公共 API,并防止意外导入内部文件。这有助于维护者管理更新,而不会给用户带来损坏的风险。


您可以使用子路径模式 ( ) 使软件包中的所有文件均可访问*。此模式会捕获子路径(包括嵌套路径)中的任何字符串,并将其映射到目标文件路径。使用此设置,用户可以通过引用路径来导入软件包中的任何文件。

* 匹配一切

*字符的行为与 glob 语法不同。它会捕获嵌套路径,并可能暴露比您预期更多的文件**。**

复制代码
{
    "name": "pkg-a",
    "exports": {
        "./*": "./*" // 公开所有文件,包括嵌套路径
    }
}

尽可能避免暴露所有文件

允许用户导入任何文件意味着即使对界面进行微小的更改(包括您不希望用户访问的文件(例如,捆绑块))也会成为重大更改,并且需要进行重大的 semver 更新。

复制代码
import foo from 'pkg-a' // 🚫 已阻止(入口点未定义)
import { name } from 'pkg-a/package.json' // ✅

多格式包

如今,软件包经常面临支持多种环境的挑战------Node.js、浏览器、ESM、CJS 和 TypeScript 定义。exports中的字段package.json允许您为每个环境和模块格式指定不同的文件。这确保了兼容性,并通过仅包含与每个目标相关的内容来优化导入。


为了让您的包同时支持 ESM 和 CommonJS 使用者,您可以根据包的导入方式指定需要加载的文件。这样,Node.js 和 TypeScript 就能在各自的上下文中解析正确的代码 ( import vs require) 和合适的类型定义 ( .d.mtsvs .d.cts)。

复制代码
{
    "name": "pkg-a",
    "exports": {
        "require": {
            "types": "./dist/index.d.cts",     // Types for require('pkg-a')
            "default": "./dist/index.cjs"      // Code for require('pkg-a')
        },
        "import": {
            "types": "./dist/index.d.mts",     // Types for import 'pkg-a'
            "default": "./dist/index.mjs"      // Code for import 'pkg-a'
        }
    }
}

将其与子路径导出相结合,您可以为每个入口点导出不同的类型,同时仍然支持 ESM 和 CommonJS 消费者:

复制代码
{
    "name": "pkg-a",
    "exports": {
        ".": {
            "require": {
                "types": "./dist/index.d.cts",    // Types for require('pkg-a')
                "default": "./dist/index.cjs"     // Code for require('pkg-a')
            },
            "import": {
                "types": "./dist/index.d.mts",    // Types for import 'pkg-a'
                "default": "./dist/index.mjs"     // Code for import 'pkg-a'
            }
        },
        "./feature": {
            "require": {
                "types": "./dist/feature.d.cts",  // Types for require('pkg-a/feature')
                "default": "./dist/feature.cjs"   // Code for require('pkg-a/feature')
            },
            "import": {
                "types": "./dist/feature.d.mts",  // Types for import 'pkg-a/feature'
                "default": "./dist/feature.mjs"   // Code for import 'pkg-a/feature'
            }
        }
    }
}

常问问题

  • 我是否需要为require和提供单独的类型文件import

    是的。TypeScript 使用文件的扩展名.d.ts来推断其描述的模块格式。一个.d.cts文件代表一个 CommonJS.cjs文件,一个.d.mts文件代表一个 ESM.mjs文件。如果将两个文件放在同一个.d.ts文件里,TypeScript 会错误地解释模块格式,并可能导致代码在运行时失败。

    请参阅 Andrew Branch (TypeScript 核心团队) 在类型错误吗?中解释这种不匹配→ 🎭 伪装成 CJS

  • 每个条件块内的键的顺序重要吗?

    是的。该types字段必须放在前面default,TypeScript 才能正确识别。如果放在后面,TypeScript 会忽略它。

  • 消费者需要哪些 TypeScript 设置?

    它们必须在其 中设置 moduleResolution Node16NodeNext或。这些模式启用条件导出解析和正确的模块格式检测。Bundler,``tsconfig.json

要了解更多信息,请参阅TypeScript 文档。其中深入介绍了配置exportsTypeScript 的其他方法(例如,跨 TypeScript 版本导出不同类型)。

将子路径映射到dist目录

JavaScript 项目经常将目录中的代码编译src到 中dist,从而产生类似 的导入import foo from 'pkg-a/dist/util'。包作者可能不希望dist在导入路径中包含 ,以获得更简单的 API,但将文件输出到包根目录需要复杂的发布步骤,这可能会污染开发环境。

通过该exports字段,包子路径可以直接映射到dist目录内部,从而允许消费者使用更清洁的导入,而import foo from 'pkg-a/util'无需为维护者提供复杂的发布脚本。


该exports字段的 subpaths 对象允许您将任意子路径定义为映射到包中文件路径的键。这使您可以使用更简单、更短的子路径来公开深度嵌套的路径。

复制代码
{
    "name": "pkg-a",
    "exports": {
        "./deep-file": "./dist/deep/deep/file.js", // 直接映射到文件
        "./*": "./dist/*" // 在根级别公开 dist 中的所有内容
    }
}

import foo from 'pkg-a' // 🚫 已阻止(入口点未定义)
import bar from 'pkg-a/deep-file' // ✅ - 解析为 dist/deep/deep/file.js
import baz from 'pkg-a/file.js' // ✅ - 解析为 dist/file.js

子路径导出

子路径导出允许您定义包的入口点并将它们映射到包内的文件路径。


要定义多个入口点,exports可以将该字段设置为子路径对象 ,其中每个键都以 开头..键表示主包入口,子路径以 开头./。键可以映射到包内的文件路径,也可以映射到条件对象(我们稍后会讨论)。

单一入口点

该字段最简单的用法exports是指向包入口文件的字符串。虽然它与 字段类似main,但有一个显著的区别:一旦使用exports它就会将您的包黑框起来。这意味着除非明确指定,否则默认情况下任何子路径(甚至package.json)都无法访问。

复制代码
{
    "name": "pkg-a",
    "exports": "./index.js" // Package entry point
}

import foo from 'pkg-a' // ✅ Resolves to pkg-a/index.js
import { name } from 'pkg-a/package.json' // 🚫 Blocked

多个入口点

要定义多个入口点,请设置exports为一个子路径对象 ------该对象的每个键都以 开头.,值是包内某个文件的相对路径。如上所述,.键表示主包入口,子路径以 开头./

复制代码
{
    "name": "pkg-a",
    "exports": {
        ".": "./index.js", // Package entry point
        "./package.json": "./package.json" // Allow importing pkg-a/package.json
    }
}

import foo from 'pkg-a' // ✅
import { name } from 'pkg-a/package.json' // ✅

公开软件包文件的子集

要仅公开特定目录,请将子路径模式放置在该子目录中。此方法允许使用者仅从指定目录导入文件。此外,您还可以通过将子路径映射到 来阻止对子路径的访问null

复制代码
{
    "name": "pkg-a",
    "exports": {
        "./dist/*": "./dist/*", // Only expose the dist directory
        "./dist/internal/*": null // Blocks access to dist/internal
    }
}

import foo from 'pkg-a' // 🚫 Blocked (entry point not defined)
import bar from 'pkg-a/dist/file.js' // ✅
import baz from 'pkg-a/dist/dir/file.js' // ✅
import qux from 'pkg-a/dist/internal/file.js' // 🚫 Blocked
import quux from 'pkg-a/dist/internal/dir/file.js' // 🚫 Blocked

有条件出口

条件导出是一个非常强大的功能。它使你的包能够根据使用者提供的条件动态加载不同的文件。利用此功能,你可以针对各种环境优化你的包。

举个简单的例子,假设你希望你的包入口点在两个不同的文件之间切换。为此,请在你的 字段中设置一个条件导出对象 :exports package.json

复制代码
{
    "name": "pkg-a",
    "exports": {
        // Ordered by priority
        "condition-a": "./file-a.js",
        "condition-b": "./file-b.js"
    }
}

导入此包时,加载的文件取决于运行时提供的条件:

复制代码
import foo from 'pkg-a' // ❓ 根据提供的条件可以是file-a.js或file-b.js

设置使用条件

node

现在你已经为你的包设置了条件和入口点,那么如何在用户端切换它们呢?这取决于谁在解析导入。

  • 如果您使用的是 Node.js,则可以使用标志指定条件。例如,这将加载,因为我们指定了:--conditions, -Cfile-a.js ``condition-a

    $ node --conditions=condition-a ./load-pkg-a.js

  • 如果您使用捆绑器,则可以在配置中传入条件。例如,使用 Vite 时,您可以传入条件(下面列出了支持条件的工具的文档)。resolve.conditions

  • 如果没有提供条件,则将无法解决并引发错误,因为没有default定义条件。

vite

包的情景模式

复制代码
// package.json
  "exports": {
    ".": {
      "custom": "./index.custom.js",
      "import": "./index.mjs",
      "require": "./index.cjs"
    }
  }

配置

复制代码
// vite.config.ts
  resolve: {
    conditions: ['custom'],
  },

默认条件

每个运行时/解析器通常设置自己的默认条件(这些不是按顺序排列的):

  • Node.jsnode,,,import ``require default
  • Viteimport,,,,,,或require default ``module``browser ``production ``development
  • esbuildimport,,,,,,require ``default browser ``node module

句法

与子路径对象相反,条件导出对象 是exports字段内的任何对象,其键并非全部以 开头.。

条件(对象键)按优先级排序,并解析为第一个匹配的条目。(这可能感觉不直观,因为 JavaScript 中的对象在技术上是无序的。)对象也可以嵌套,以指定解析文件所需的条件组合。

条件键的顺序很重要

由于解析器具有默认条件,并且返回其匹配的第一个条件,因此应始终首先指定您的自定义条件(例如,无法达到require、import、以下的任何内容)。default

复制代码
    "exports": {
        "import": "./prod.mjs",
        "require": "./prod.cjs",

        // 这将永远不会匹配,因为它低于默认条件
        "this-will-never-match": "./dev.ts"
    }
}

针对 Node.js 和浏览器

该exports字段可以定义一个适应 Node.js 或浏览器环境的入口点。在 Node.js 运行时中,用于解析的默认条件包括node、default和导入类型(import对于 ESM 为 ,require对于 CJS 为 )。

条件的优先级取决于包的条件导出对象中的关键顺序。

nord 复制代码
{
    "name": "pkg-a",
    "exports": {
        "node": "./dist/for-node.js", // Resolved by Node.js
        "default": "./dist/for-browsers.js" // Resolved by other environments
    }
}

default条件适用于非 Node 环境。或者,您可以使用browser Vite、Webpack 和 Parcel 等 Web 应用打包工具能够识别的条件。

相关推荐
ui设计兰亭妙微1 分钟前
# 信息架构如何决定搜索效率?
前端
1024小神28 分钟前
Cocos游戏中UI跟随模型移动,例如人物头上的血条、昵称条等
前端·javascript
Mapmost35 分钟前
告别多平台!Mapmost Studio将制图、发布、数据管理通通搞定!
前端
LaoZhangAI37 分钟前
GPT-4o mini API限制完全指南:令牌配额、访问限制及优化策略【2025最新】
前端·后端
前端的日常39 分钟前
ts中的type和interface的区别
前端
LaoZhangAI1 小时前
FLUX.1 API图像尺寸设置全指南:优化生成效果与成本
前端·后端
哑巴语天雨1 小时前
Cesium初探-CallbackProperty
开发语言·前端·javascript·3d
JosieBook1 小时前
【前端】Vue 3 页面开发标准框架解析:基于实战案例的完整指南
前端·javascript·vue.js
liwei_fang1 小时前
node.js 调度 --- 事件循环
前端