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


一、基本概念与定义

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

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 依赖此字段解析类型
浏览器兼容 exportsbrowser 条件 "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" 测试
条件顺序错误 按优先级排序:importrequiredefault
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" 条件。

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

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

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

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

更多推荐