你是否曾在TypeScript项目中因库的类型定义缺失而卡住?或在Node.js中反复遭遇require与import的兼容性陷阱?package.json的exports和imports字段是现代JS生态的“瑞士军刀”,能同时解决类型安全与运行时兼容问题。本文将手把手拆解其设计逻辑,让你的库/应用在多环境无缝运行——无需再为环境差异反复调试。


一、基本概念与定义

exports(导出条件):Node.js 12+ 引入,用于定义模块在不同环境(ESM/CJS)下的入口点。它取代了main、module等字段,实现条件化导出。

imports(内部导入):Node.js 12+ 支持,用于在代码中定义路径别名(如import { a } from "#/utils"),需配合package.json中的imports字段使用。

✅ 关键区别:

  • exports:导出模块的入口(供外部使用)
  • imports:导入内部模块的别名(供代码内使用)
    (Node.js 12.17+ 支持两者,浏览器不支持imports)

二、关键属性与最小可运行示例

1. exports 的条件结构

{
  "exports": {
    ".": {
      "import": "./dist/index.mjs",  // ESM 代码入口
      "require": "./dist/index.cjs",  // CJS 代码入口
      "types": "./dist/index.d.ts"    // TypeScript 类型定义
    },
    "./utils": {
      "import": "./dist/utils.mjs",
      "require": "./dist/utils.cjs"
    }
  }
}

输入/输出说明:

  • 当外部代码使用 import * as lib from 'my-lib' → Node.js 优先匹配 import 字段 → 加载 ./dist/index.mjs
  • 当使用 const lib = require('my-lib') → 匹配 require 字段 → 加载 ./dist/index.cjs
  • TypeScript 编译时 → 通过 types 字段找到 ./dist/index.d.ts

💡 边界情况:若未指定 types,TypeScript 会报错“无法解析模块”;若未指定 import/require,Node.js 会回退到 main(不推荐)。

2. imports 的别名使用

{
  "imports": {
    "#/utils": "./dist/utils.mjs"  // 定义内部别名
  }
}

代码示例:

// 代码中使用别名
import { fetchData } from '#/utils'; // 实际解析为 ./dist/utils.mjs

⚠️ 注意:imports 仅在Node.js中生效,浏览器需用module标签或构建工具处理。


三、工作原理:Node.js 模块解析流程

Node.js 解析模块的优先级顺序(从高到低):

  1. 检查 package.json 中的 exports 条件
  2. 根据环境匹配字段:
    • ESM 代码 → 优先 import
    • CJS 代码 → 优先 require
  3. 若未匹配 → 回退到 main(若存在)
  4. 若仍失败 → 报错 Cannot find module

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
(简化版:环境 → 匹配条件 → 加载文件)

📌 关键点:exports 的字段顺序决定优先级。例如:

"exports": {
  ".": {
    "require": "./cjs",  // 优先级高
    "import": "./esm"    // 优先级低
  }
}

此配置下,CJS 会加载 ./cjs,ESM 会加载 ./esm。


四、实战示例:正确用法 vs 错误用法

场景1:库开发(正确写法)

目标:确保TypeScript类型与Node.js运行时一致。

package.json:

{
  "name": "my-lib",
  "version": "1.0.0",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "types": "./dist/index.d.ts"
    }
  },
  "scripts": {
    "build": "tsc --outDir dist"
  }
}

TypeScript 代码(src/index.ts):

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

构建流程:

  1. npm run build → 生成 dist/index.mjs(ESM)、dist/index.cjs(CJS)、dist/index.d.ts
  2. TypeScript 使用 types 字段 → 自动识别类型
  3. Node.js ESM 代码 → 从 dist/index.mjs 加载
  4. Node.js CJS 代码 → 从 dist/index.cjs 加载

✅ 效果:无类型错误,运行时兼容。


场景2:常见错误(类型不匹配)

错误 package.json:

{
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}

问题:

  • TypeScript 编译时找不到类型定义 → 报错 Could not find a declaration file for module 'my-lib'
  • 修复:必须添加 "types": "./dist/index.d.ts"

错误 package.json(条件顺序问题):

{
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}

✖️ 问题:若外部代码使用 import 但Node.js版本<12,会回退到 main(可能不存在),导致 Cannot find module。


五、应用场景与最佳实践

场景推荐方案为什么?
新库开发仅用 exports(弃用 main/module)避免环境冲突,TypeScript/Node.js统一支持
类型安全要求高必须指定 types 字段TypeScript 依赖此字段解析类型
浏览器兼容用 exports 的 browser 条件如 "browser": "./dist/index.browser.mjs"
内部模块别名用 imports 定义路径别名简化代码,避免相对路径冗余

✅ 最佳实践:

  1. 为新库直接使用 exports,不保留 main/module。
  2. 所有类型定义必须放在 types 字段。
  3. 用 browser 条件处理浏览器环境(如React库)。

六、常见坑与排错建议

坑点排错方法
TypeScript 类型缺失检查 package.json 是否有 types 字段
Node.js 未匹配 import用 node -p "require('my-lib').default" 测试
条件顺序错误按优先级排序:import → require → default
imports 别名未生效确认Node.js版本≥12.17(node -v)

💡 防御性写法:

"exports": {
  ".": {
    "import": "./dist/index.mjs",
    "require": "./dist/index.cjs",
    "types": "./dist/index.d.ts",
    "default": "./dist/index.cjs"  // 作为回退
  }
}

七、性能与安全注意

  • 性能:exports 仅影响模块解析阶段,无额外运行时开销。
    (Node.js 会缓存解析结果,无需担心多次加载)

  • 安全:

    • exports 本身无安全风险(路径解析在沙箱内)。
    • 避免在 exports 中使用 ../ 或 * 通配符(可能引发路径遍历),例如:
      "exports": {
        ".": {
          "import": "./dist/*.mjs"  // ❌ 不安全!
        }
      }
      

八、与相关概念的对比

概念用途适用场景选型建议
exports定义模块导出条件新库开发、多环境兼容优先使用
main/module旧版入口点(单环境)遗留项目迁移逐步替换为 exports
imports代码内路径别名(Node.js专属)内部模块简化导入按需使用
browser 条件为浏览器环境指定单独入口浏览器库(如React、Vue)在 exports 中添加

✅ 选型结论:

  • 新项目:直接用 exports + types,无需 main/module。
  • 浏览器支持:在 exports 中添加 "browser" 条件。

结语:掌握核心,进阶路径

exports 和 imports 是Node.js 12+ 生态的基石,正确使用能彻底解决类型与运行时的双重矛盾。核心要点:

  1. 用 exports 替代 main/module,指定 import/require/types。
  2. 用 imports 简化内部导入,但仅限Node.js。
  3. 必须为TypeScript提供 types 字段。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐