当前位置: 首页 > news >正文

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

目录

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

术语

什么是exports领域?

exports好处

保护内部文件

多格式包

将子路径映射到dist目录

子路径导出

单一入口点

多个入口点

公开软件包文件的子集

有条件出口

设置使用条件

默认条件

句法

针对 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.tsresolve: {conditions: ['custom'],},

 

 

默认条件

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

  • Node.js:node,,,import require​ default
  • Vite:import,,,,,,或require​  default module​​browser production development
  • esbuild:import,,,,,,require default​ browser node​ module

句法

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

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

条件键的顺序很重要

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

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

针对 Node.js 和浏览器

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

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

{"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 应用打包工具能够识别的条件。

http://www.lryc.cn/news/590513.html

相关文章:

  • 多人协作游戏中,团队共同获取的装备如何确定按份共有或共同共有
  • 软路由 + 代理 IP 实现多手机不同公网 IP 分配教程
  • 从0开始学习R语言--Day48--Calibration Curves 评估模型
  • JobSet:Kubernetes 分布式任务编排的统一解决方案
  • 【add vs commit】Git 中的 add 和 commit 之间的区别
  • PLUS模型+生态系统服务多情景模拟预测实践技术
  • 大语言模型幻觉检测:语义熵揭秘
  • Reddit Karma是什么?Post Karma和Comment Karma的提升指南
  • 精彩代码分析-1
  • 光伏项目快速获取地址,三种地图赋能设计
  • 倪海厦全套下载,八纲辨证,人纪,天纪,针灸,电子版
  • vue3中高阶使用与性能优化
  • Day04_C语言网络编程20250716
  • Nginx,MD5和Knife4j
  • PHP面向对象编程:类与对象的基础概念与实践
  • Uniapp中双弹窗为什么无法显示?
  • Coze工作流无法更新问题处理
  • React+Next.js+Tailwind CSS 电商 SEO 优化
  • 2_概要设计编写提示词_AI编程专用简化版
  • 正确选择光伏方案设计软件:人力成本优化的关键一步
  • 【技术追踪】基于检测器引导的对抗性扩散攻击器实现定向假阳性合成——提升息肉检测的鲁棒性(MICCAI-2025)
  • 第五届计算机科学与区块链国际学术会议(CCSB 2025)
  • Java大厂面试实录:从电商场景到AI应用的深度技术考察
  • 【计算机网络】数据通讯第二章 - 应用层
  • CentOS网络配置与LAMP环境搭建指南
  • 【后端】.NET Core API框架搭建(6) --配置使用MongoDB
  • 用Amazon Q Developer助力Python快捷软件开发
  • nextjs+react项目如何代理本地请求解决跨域
  • LiFePO4电池的安全详解
  • 从缓存 CAS 看Kimi K2使用的MuonClip优化器