普通视图

发现新文章,点击刷新页面。
昨天 — 2026年3月26日首页

npm 包入口指南:package.json 中的 main、module、exports

2026年3月26日 10:54

你有没有遇到过这些问题:

  • 明明装了包,import 就报错,换成 require 又好了?
  • TypeScript 提示找不到类型声明,但包里明明有 .d.ts 文件?
  • 发布了一个 npm 包,别人用的时候打包体积巨大,Tree Shaking 不生效?
  • mainmoduleexportsbrowsertypes 写了一堆,到底谁在生效?

如果你也被这些问题折磨过,这篇文章就是为你写的。


一、先搞清一件事:模块系统的历史包袱

在讲入口字段之前,你必须理解一个前提 —— JavaScript 有两套模块系统,而且它们互不兼容

CommonJS(CJS)

// 导出
module.exports = { add, subtract }
// 或
exports.add = function() {}

// 导入
const { add } = require('lodash')
  • Node.js 原生支持(从诞生起就有)
  • 同步加载,不适合浏览器
  • 文件后缀:.js(在 type: "commonjs" 下)或 .cjs

ES Module(ESM)

// 导出
export function add() {}
export default subtract

// 导入
import { add } from 'lodash-es'
  • ECMAScript 官方标准
  • 静态分析,支持 Tree Shaking
  • Node.js 12+ 开始支持
  • 文件后缀:.js(在 type: "module" 下)或 .mjs

矛盾的根源

一个 npm 包的使用者可能是:

使用场景 期望的模块格式
Node.js 老项目(require CJS
Node.js 新项目(import ESM
Webpack / Vite 前端项目 ESM(优先)或 CJS
浏览器直接 <script type="module"> ESM
SSR(Nuxt / Next.js) CJS 或 ESM

一个包要服务这么多场景,只用一个入口文件显然不够。 这就是为什么 package.json 需要这么多入口字段。


二、入口字段逐个击破

2.1 main — 最古老的入口

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

历史地位: 这是 package.json 中最早的入口字段,Node.js 从一开始就读它。

行为: 当别人写 require('your-package')import 'your-package' 时,Node.js 会去找 main 字段指向的文件。

注意:

  • 如果不写 main,Node.js 默认找包根目录下的 index.js
  • main 指向的文件格式应该和 type 字段一致(后面会讲)
  • 在有 exports 字段的情况下,main 只是作为兜底存在

一句话: main 是给 require() 用的,通常指向 CJS 格式的文件。


2.2 module — 打包工具的"私下约定"

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

重要:这不是 Node.js 官方标准。 它是 Rollup 在 2015 年提出的一个社区约定,后来 Webpack 也支持了。

为什么需要它?

假设你写了一个工具库,你想同时提供 CJS 和 ESM 两种格式:

{
  "main": "dist/index.cjs.js",
  "module": "dist/index.esm.js"
}

打包工具(Webpack、Rollup、Vite)看到 module 字段就会优先使用 ESM 版本,因为 ESM 支持静态分析Tree Shaking。而 Node.js 直接运行时会忽略 module,走 main 拿到 CJS 版本。

一句话: module 是给 Webpack / Rollup / Vite 这些打包工具看的 ESM 入口。


2.3 browser — 浏览器专用入口

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

使用场景: 你的包在 Node.js 和浏览器中需要不同的实现。

典型例子:

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

比如一个 HTTP 请求库,Node 端用 http 模块,浏览器端用 fetchXMLHttpRequestaxios 就是这么干的。

高级用法 —— 模块替换:

{
  "browser": {
    "./lib/ws.js": "./lib/ws-browser.js",
    "fs": false,
    "path": false
  }
}
  • "./lib/ws.js": "./lib/ws-browser.js" → 替换特定文件
  • "fs": false → 在浏览器端将 fs 模块替换为空对象

Webpack 在构建 target: 'web' 时会读取这个字段。

一句话: browser 是给浏览器环境用的入口,解决 Node vs 浏览器 API 差异。


2.4 types / typings — TypeScript 类型入口

{
  "types": "dist/index.d.ts"
}

作用: 告诉 TypeScript 编译器去哪里找类型声明文件。

没有这个字段会怎样?

TypeScript 会尝试找 main 字段指向的文件,把 .js 替换为 .d.ts。比如 main: "dist/index.js" → 找 dist/index.d.ts。找不到就报那个烦人的错误:

Could not find a declaration file for module 'xxx'.

types vs typings 完全等价,推荐用 types(更简短)。


2.5 type — 模块系统的"开关"

{
  "type": "module"
}

这个字段不是入口,而是一个全局开关,决定了 Node.js 怎么理解 .js 文件:

type 的值 .js 文件被视为 .cjs 文件 .mjs 文件
"commonjs"(默认) CommonJS CommonJS ESModule
"module" ESModule CommonJS ESModule

关键点:

  • .cjs 永远是 CommonJS,不管 type 怎么设
  • .mjs 永远是 ESModule,不管 type 怎么设
  • .js 的身份取决于 type 字段

一个容易踩的坑:

你在 package.json 里写了 "type": "module",然后你的 .eslintrc.js 配置文件用了 module.exports = {},Node.js 就会报错:

SyntaxError: Unexpected token 'export'

因为 Node.js 把 .js 当 ESM 处理了,但 module.exports 是 CJS 语法。解决办法:把配置文件改名为 .eslintrc.cjs


2.6 exports — 终极解决方案(重点!)

如果你只想记住一个字段,那就记住 exports

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

exports 是 Node.js 12.11 引入的官方方案,一个字段解决了 mainmodulebrowsertypes 四个字段干的事

能力一:条件导出

根据不同的使用方式,返回不同的文件:

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

当使用者写 import pkg from 'your-package' → 走 import 条件,拿到 ESM 文件
当使用者写 const pkg = require('your-package') → 走 require 条件,拿到 CJS 文件

支持的条件关键字:

条件 含义 谁在用
types TypeScript 类型声明 TypeScript 编译器
import ESM import 方式引入 Node.js、打包工具
require CJS require() 方式引入 Node.js、打包工具
node Node.js 环境 Node.js
browser 浏览器环境 打包工具
development 开发环境 部分打包工具
production 生产环境 部分打包工具
default 兜底条件 所有

条件匹配规则:从上到下,命中第一个就停。 所以顺序很重要:

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",    // ← 必须第一个!
      "node": {
        "import": "./dist/node.mjs",
        "require": "./dist/node.cjs"
      },
      "browser": "./dist/browser.mjs",
      "default": "./dist/index.mjs"     // ← 兜底放最后
    }
  }
}

TypeScript 的 types 条件必须放在最前面! 否则 TS 可能匹配到其他条件就停了,导致找不到类型。

能力二:子路径导出

不需要暴露整个包,可以精确控制哪些路径可以被外部引用:

{
  "exports": {
    ".": "./dist/index.mjs",
    "./utils": "./dist/utils.mjs",
    "./hooks": "./dist/hooks.mjs",
    "./styles": "./dist/styles.css"
  }
}

使用方式:

import { debounce } from 'your-package/utils'
import { useAuth } from 'your-package/hooks'
import 'your-package/styles'

通配符导出:

{
  "exports": {
    ".": "./dist/index.mjs",
    "./components/*": "./dist/components/*/index.mjs",
    "./icons/*": "./dist/icons/*.mjs"
  }
}
import Button from 'your-package/components/Button'
import StarIcon from 'your-package/icons/Star'

能力三:封装隔离

一旦声明了 exports未列出的路径就无法被外部访问

{
  "exports": {
    ".": "./dist/index.mjs",
    "./utils": "./dist/utils.mjs"
  }
}
// ✅ 可以用
import pkg from 'your-package'
import { foo } from 'your-package/utils'

// ❌ 报错!未在 exports 中声明
import internal from 'your-package/dist/internal.mjs'
import helper from 'your-package/src/helper.js'

这是一个非常重要的特性 —— 保护内部实现细节,防止使用者依赖你的私有 API


三、到底什么时候需要打包?什么时候不需要?

这可能是最让人困惑的问题了。同样是写 npm 包,有的包 dist/ 目录里放着打包好的文件,有的包直接发布源码。到底怎么选?

场景一:纯 Node.js 工具包(CLI / 服务端)

my-cli/
├── src/
│   ├── index.js
│   └── utils.js
├── package.json
└── README.md

不需要打包。

原因:

  • Node.js 直接运行 JS 文件,不需要打包
  • 没有浏览器兼容性问题
  • 不需要 Tree Shaking(Node.js 用不到)
  • 发布源码即可
{
  "main": "src/index.js",
  "type": "module",
  "files": ["src"]
}

但如果用了 TypeScript,需要编译(不是打包):

{
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": ["dist"],
  "scripts": {
    "build": "tsc"
  }
}

这里用 tsc 只是把 .ts.js一对一转换,不是打包。

场景二:前端 UI 组件库

my-ui/
├── src/
│   ├── Button/
│   ├── Modal/
│   └── index.ts
├── dist/
│   ├── index.mjs      ← ESM
│   ├── index.cjs       ← CJS
│   ├── index.d.ts      ← 类型
│   └── style.css       ← 样式
└── package.json

需要打包。

原因:

  • 使用者的打包工具需要 ESM 格式做 Tree Shaking
  • 需要编译 TypeScript / JSX / Vue SFC
  • 需要处理 CSS / Less / Sass
  • 可能需要同时提供 CJS 和 ESM
{
  "main": "dist/index.cjs",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./style.css": "./dist/style.css"
  },
  "sideEffects": ["*.css"],
  "files": ["dist"]
}

场景三:工具函数库(lodash 那种)

需要打包,而且最好提供多种格式。

{
  "main": "dist/index.cjs",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  },
  "sideEffects": false,
  "files": ["dist"]
}

sideEffects: false 至关重要 —— 它告诉打包工具"这个包里所有模块都没有副作用,可以放心 Tree Shaking"。

场景四:全栈框架的插件/中间件

{
  "main": "dist/index.cjs",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "node": {
        "import": "./dist/node.mjs",
        "require": "./dist/node.cjs"
      },
      "browser": "./dist/browser.mjs",
      "default": "./dist/index.mjs"
    }
  }
}

Node 端和浏览器端实现不同,需要条件导出区分。

场景五:只发布类型声明(纯 .d.ts 包)

比如 @types/node@types/lodash

不需要打包。

{
  "types": "index.d.ts",
  "files": ["*.d.ts", "**/*.d.ts"]
}

决策速查表

问题 是 → 否 →
用了 TypeScript? 至少需要 tsc 编译 可以直接发布源码
用了 JSX / Vue SFC / Sass? 需要打包/编译
需要 Tree Shaking? 必须提供 ESM 格式 只提供 CJS 也行
Node 和浏览器行为不同? 需要多入口(exports 条件导出) 单入口即可
需要同时支持 requireimport 提供 CJS + ESM 双格式 只提供一种

四、不同工具的解析优先级

你写了一堆入口字段,但最终谁在生效?这取决于"谁在消费你的包"。

Node.js(>= 16)

exports  →  main  →  index.js
  • 如果有 exports完全忽略 mainmodulebrowser
  • 如果没有 exports,读 main
  • 如果没有 main,找 index.js

Webpack 5

exports  →  browser  →  module  →  main
  • 优先 exports
  • 然后看 browser(如果 target 是 web)
  • 再看 module(ESM 优先)
  • 最后 main

Vite / Rollup

exportsmodule  →  main
  • Vite 基于 Rollup,天然偏好 ESM
  • 不读 browser 字段(通过 Vite 自己的 resolve.conditions 处理)

TypeScript

exports["types"]  →  types  →  typings  →  main 对应的 .d.ts

需要 tsconfig.json 配合:

{
  "compilerOptions": {
    "moduleResolution": "bundler"    // 或 "node16" / "nodenext"
  }
}

注意: 如果 moduleResolution 还是 "node"(旧模式),TypeScript 不会读 exports 字段!这是很多人类型丢失的根本原因。

优先级总览图

               Node.js         Webpack 5        Vite/Rollup      TypeScript
               ───────         ─────────        ──────────       ──────────
最高优先级 →    exports         exports          exports          exports.types
               │               │                │                │
               │               browser          module           types/typings
               │               │                │                │
               main            module           main             main→.d.ts
               │               │
               index.js        main

五、Dual Package 的陷阱(CJS + ESM 双格式)

同时提供 CJS 和 ESM 是好事,但有一个隐藏的大坑:Dual Package Hazard(双包风险)

问题是什么?

假设你的包导出了一个单例:

// 你的包
let count = 0
export function increment() { count++ }
export function getCount() { return count }

如果使用者的项目中同时通过 importrequire 引用了你的包(这在复杂项目中很常见),Node.js 会加载两份代码 —— ESM 一份,CJS 一份。两份代码各自维护自己的 count,状态不共享,产生诡异的 bug。

解决方案一:ESM Wrapper

只打包一份 CJS,ESM 入口只是一个转发:

// dist/index.cjs  ← 真正的实现
module.exports = { increment, getCount }

// dist/index.mjs  ← 只是一个 wrapper
import cjs from './index.cjs'
export const { increment, getCount } = cjs
{
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}

这样 ESM 和 CJS 用的是同一份代码,状态一致。

解决方案二:无状态设计

如果你的包本身是纯函数、无状态的(大部分工具函数库都是),那就不用担心,直接双格式打包即可。


六、实战配置模板

模板一:TypeScript 工具函数库

打包工具推荐 tsup(基于 esbuild,零配置):

{
  "name": "my-utils",
  "version": "1.0.0",
  "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"
    }
  },
  "files": ["dist"],
  "sideEffects": false,
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts --clean",
    "dev": "tsup src/index.ts --format cjs,esm --dts --watch"
  },
  "devDependencies": {
    "tsup": "^8.0.0",
    "typescript": "^5.0.0"
  }
}

模板二:Vue 组件库

打包工具推荐 Vite Library Mode

{
  "name": "my-components",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.umd.js",
  "module": "dist/index.es.js",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.es.js",
      "require": "./dist/index.umd.js"
    },
    "./style.css": "./dist/style.css"
  },
  "files": ["dist"],
  "sideEffects": ["*.css"],
  "peerDependencies": {
    "vue": "^3.3.0"
  },
  "scripts": {
    "build": "vite build"
  }
}

模板三:React 组件库

{
  "name": "my-react-ui",
  "version": "1.0.0",
  "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"
    },
    "./styles": "./dist/styles.css"
  },
  "files": ["dist"],
  "sideEffects": ["*.css"],
  "peerDependencies": {
    "react": "^18.0.0 || ^19.0.0",
    "react-dom": "^18.0.0 || ^19.0.0"
  },
  "peerDependenciesMeta": {
    "react-dom": { "optional": true }
  }
}

模板四:纯 Node.js 包(不打包)

{
  "name": "my-server-lib",
  "version": "1.0.0",
  "type": "module",
  "main": "src/index.js",
  "types": "src/index.d.ts",
  "exports": {
    ".": {
      "types": "./src/index.d.ts",
      "default": "./src/index.js"
    },
    "./middleware": {
      "types": "./src/middleware.d.ts",
      "default": "./src/middleware.js"
    }
  },
  "files": ["src"],
  "engines": {
    "node": ">=18.0.0"
  }
}

模板五:CLI 工具

{
  "name": "my-cli",
  "version": "1.0.0",
  "type": "module",
  "bin": {
    "mycli": "./bin/cli.js"
  },
  "files": ["bin", "src"],
  "engines": {
    "node": ">=18.0.0"
  }
}

CLI 工具通常不需要别人 import,所以连 main 都不需要写。


七、常见报错排查指南

报错 1:ERR_REQUIRE_ESM

Error [ERR_REQUIRE_ESM]: require() of ES Module not supported

原因: 你用 require() 引入了一个 "type": "module" 的包。

解决:

  • 改用 import(推荐)
  • 或者用 await import('the-package')(动态导入)
  • 或者在你的项目中也设置 "type": "module"

报错 2:ERR_PACKAGE_PATH_NOT_EXPORTED

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './lib/foo' is not defined by "exports"

原因: 包设置了 exports,但你访问的路径不在 exports 的声明里。

解决:

  • 只使用包 exports 中声明的路径
  • 如果你是包作者,把遗漏的路径加到 exports

报错 3:Could not find a declaration file for module

Could not find a declaration file for module 'xxx'.
'xxx' implicitly has an 'any' type.

原因: TypeScript 找不到类型声明。

排查步骤:

  1. 包有 types 字段吗?指向的 .d.ts 文件存在吗?
  2. 包有 exports 吗?exports 里有 types 条件吗?
  3. 你的 tsconfig.jsonmoduleResolution 是什么?如果是 "node"(旧模式),改为 "bundler""node16"
  4. 如果都没问题,安装 @types/xxx

报错 4:Tree Shaking 不生效,打包体积大

排查步骤:

  1. 包有 moduleexports.import 入口吗?(必须是 ESM 格式)
  2. 包设置了 "sideEffects": false 吗?
  3. 你是用 import { specific } from 'pkg' 而不是 import * as pkg from 'pkg' 吗?
  4. 检查是否有 barrel file(index.tsexport * from 一大堆)导致的连锁引入

八、总结

一张决策流程图帮你选择正确的配置:

你的包是什么类型?
│
├── CLI 工具
│   └── 只需要 bin,不需要 main
│
├── 纯 Node.js 库
│   ├── 用 JS 写的 → 不需要打包,直接发布源码
│   └── 用 TS 写的 → tsc 编译,发布 dist
│
├── 前端组件库
│   └── 需要打包(Vite / tsup / Rollup)
│       ├── 提供 CJS + ESM 双格式
│       ├── 设置 exports 条件导出
│       ├── 设置 sideEffects
│       └── peerDependencies 声明框架依赖
│
└── 工具函数库
    └── 需要打包
        ├── 提供 CJS + ESM 双格式
        ├── sideEffects: false(关键!)
        └── exports 条件导出

无论哪种类型,如今的最佳实践是:
✅ 始终写 exports(现代标准)
✅ 保留 main + module 做向后兼容
✅ types 条件放在 exports 的第一个
✅ moduleResolution 用 "bundler""node16"

如果这篇文章帮你解开了心中的疑惑,点个赞让更多人看到吧。有问题欢迎在评论区讨论!

昨天以前首页

前端工程化基石:package.json 40+ 字段逐一拆解

2026年3月25日 17:30

每个前端项目的根目录下几乎都有一个 package.json,但你真的了解它的每个字段吗?本文将从基础字段高级配置,逐一拆解 package.json 中的所有字段,帮你彻底搞懂它。


一、必填字段

1.1 name — 包名

{
  "name": "@packageName/sdk"
}

规则:

  • 长度不超过 214 个字符
  • 不能以 ._ 开头
  • 不能包含大写字母
  • 不能包含 URL 不安全字符(如空格、~ 等)
  • 支持 scope(作用域),格式为 @scope/name,常用于组织级别的包管理,例如 @vue/cli@babel/core

作用:
name 是包的唯一标识符。当你执行 npm install xxx 时,xxx 就是这个字段的值。配合 version,它们共同构成了包的"身份证"。


1.2 version — 版本号

{
  "version": "1.6.7"
}

必须遵循 Semantic Versioning(语义化版本) 规范,格式为 MAJOR.MINOR.PATCH

含义 示例场景
MAJOR 不兼容的 API 变更 重构了核心 API
MINOR 向下兼容的功能新增 新增了一个工具函数
PATCH 向下兼容的问题修复 修复了一个边界 Bug

还支持预发布标签:1.0.0-alpha.11.0.0-beta.21.0.0-rc.1


二、描述信息字段

2.1 description — 包描述

{
  "description": "packageDescription"
}

简短描述包的功能,会展示在 npm search 的搜索结果中,也是 npm 官网搜索排序的权重因子之一。

2.2 keywords — 关键词

{
  "keywords": ["cloud", "sdk", "vue", "plugin", "micro-frontend"]
}

字符串数组,用于 npm 官网的搜索优化(SEO),帮助其他开发者更快找到你的包。

2.3 homepage — 项目主页

{
  "homepage": "https://github.com/user/project#readme"
}

项目官网或文档地址,会展示在 npm 包详情页的侧边栏。

2.4 bugs — Bug 反馈地址

{
  "bugs": {
    "url": "https://github.com/user/project/issues",
    "email": "bugs@example.com"
  }
}

也可以简写为字符串:"bugs": "https://github.com/user/project/issues"

2.5 license — 开源协议

{
  "license": "MIT"
}

常见协议:

协议 特点
MIT 极其宽松,几乎无限制
Apache-2.0 允许商用,需保留版权,提供专利许可
GPL-3.0 传染性协议,衍生作品也需开源
ISC 类似 MIT,更简洁
UNLICENSED 私有包,不允许他人使用

2.6 author — 作者

{
  "author": {
    "name": "张三",
    "email": "zhangsan@example.com",
    "url": "https://zhangsan.dev"
  }
}

也支持简写形式:"author": "张三 <zhangsan@example.com> (https://zhangsan.dev)"

2.7 contributors — 贡献者

{
  "contributors": [
    { "name": "李四", "email": "lisi@example.com" },
    "王五 <wangwu@example.com>"
  ]
}

格式同 author,是一个数组。

2.8 funding — 赞助信息

{
  "funding": {
    "type": "opencollective",
    "url": "https://opencollective.com/project"
  }
}

也支持数组形式,用于声明多个赞助渠道。执行 npm fund 可查看项目的赞助信息。


三、入口文件字段

这是 package.json 中最核心也最容易混淆的一组字段,直接决定了别人引用你的包时,加载的是哪个文件。

3.1 main — CommonJS 入口

{
  "main": "dist/cloud-sdk.umd.js"
}

作用:
Node.js 和旧版打包工具默认读取的入口。当执行 require('your-package') 时,实际加载的就是 main 指向的文件。

3.2 module — ESModule 入口

{
  "module": "dist/cloud-sdk.esm.js"
}

作用:
这不是 Node.js 官方字段,而是由打包工具(Webpack、Rollup、Vite)约定的。当打包工具发现 module 字段时,会优先使用它,因为 ESM 格式支持 Tree Shaking,能有效减小打包体积。

3.3 browser — 浏览器入口

{
  "browser": "dist/cloud-sdk.browser.js"
}

当包需要在浏览器中运行,且浏览器版本与 Node 版本实现不同时使用。打包工具在构建浏览器端代码时会优先读取此字段。

也支持对象形式,用于替换特定模块:

{
  "browser": {
    "./lib/server-utils.js": "./lib/browser-utils.js",
    "fs": false
  }
}

3.4 types / typings — TypeScript 类型入口

{
  "types": "dist/index.d.ts"
}

指定 TypeScript 类型声明文件的入口路径。typestypings 等价,推荐用 types

3.5 exports — 条件导出(重点!)

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/cloud-sdk.esm.js",
      "require": "./dist/cloud-sdk.umd.js"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.esm.js",
      "require": "./dist/utils.cjs.js"
    }
  }
}

这是 Node.js 12.11+ 引入的现代模块解析方案,是 mainmodulebrowser 的"终极替代方案"。

核心能力:

特性 说明
条件导出 根据环境(import / require / node / browser / default)返回不同文件
子路径导出 允许 import { foo } from 'pkg/utils' 形式的子路径引用
封装隔离 未在 exports 中声明的路径,外部无法访问,保护内部实现

条件匹配的优先级(从上到下):

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "node": {
        "import": "./dist/node.mjs",
        "require": "./dist/node.cjs"
      },
      "browser": "./dist/browser.js",
      "default": "./dist/index.js"
    }
  }
}

注意: types 条件必须放在最前面,否则 TypeScript 可能无法正确解析类型。

3.6 type — 模块系统声明

{
  "type": "module"
}
含义
"module" .js 文件默认作为 ESModule 处理
"commonjs"(默认值) .js 文件默认作为 CommonJS 处理

设置为 "module" 后:

  • .js → ESM
  • .cjs → CommonJS(强制)
  • .mjs → ESM(强制)

四、文件管控字段

4.1 files — 发布包含的文件

{
  "files": ["dist", "README.md", "LICENSE"]
}

白名单机制,指定 npm publish 时需要包含的文件和目录。类似 .gitignore 的反向操作。

始终包含的文件(无法排除):

  • package.json
  • README(任何大小写和扩展名)
  • LICENSE / LICENCE
  • CHANGELOG
  • main 字段指向的文件

始终排除的文件(无法包含):

  • .git
  • node_modules
  • .npmrc
  • package-lock.json

技巧: 也可以用 .npmignore 做黑名单控制,但 files 字段优先级更高,两者同时存在时以 files 为准。

4.2 directories — 项目目录结构

{
  "directories": {
    "lib": "src/lib",
    "bin": "bin",
    "man": "man",
    "doc": "docs",
    "example": "examples",
    "test": "test"
  }
}

声明项目的目录结构。实际使用较少,主要是一种语义化描述。


五、脚本与命令字段

5.1 scripts — NPM 脚本

{
  "scripts": {
    "dev": "vite build --watch",
    "build": "vite build",
    "lint": "eslint src",
    "lint:fix": "eslint src --fix",
    "format": "prettier --write src",
    "prepare": "husky install",
    "preinstall": "npx only-allow pnpm"
  }
}

通过 npm run <script-name> 执行。部分脚本名有特殊含义:

生命周期脚本:

脚本名 触发时机
preinstall 安装依赖之前执行
install 安装依赖时执行
postinstall 安装依赖之后执行
prepare npm install 之后、npm publish 之前执行
prepublishOnly 仅在 npm publish 之前执行
prepack 打 tarball 之前(npm pack / npm publish
postpack 打 tarball 之后

pre/post 钩子:

任何自定义脚本都可以加 pre / post 前缀:

{
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "vite build",
    "postbuild": "echo 构建完成"
  }
}

执行 npm run build 会依次执行:prebuildbuildpostbuild

注意: pnpm 和 yarn 现代版本默认不会自动执行 pre/post 钩子,需手动配置开启。

5.2 bin — 可执行文件

{
  "bin": {
    "create-uver": "./bin/create.js"
  }
}

当用户全局安装(npm install -g)或通过 npx 执行时,系统会创建软链接到 bin 指定的文件。

如果只有一个可执行文件,可以简写为:

{
  "name": "create-uver",
  "bin": "./bin/create.js"
}

此时命令名就是 name 字段的值。

5.3 man — 帮助手册

{
  "man": ["./man/doc.1", "./man/doc.2"]
}

指定 man 命令的文档文件路径,文件必须以数字结尾或以 .gz 压缩。


六、依赖管理字段

6.1 dependencies — 生产依赖

{
  "dependencies": {
    "lodash-es": "^4.17.21",
    "vue": "^3.4.0",
    "vue-router": "^4.5.0"
  }
}

项目运行时必须的依赖,npm install 默认安装,最终会被打包进产物中。

6.2 devDependencies — 开发依赖

{
  "devDependencies": {
    "typescript": "^5.3.3",
    "vite": "^6.3.5",
    "eslint": "^9.3.4",
    "prettier": "^3.2.5"
  }
}

仅开发阶段需要的依赖(构建工具、Linter、测试框架等)。其他项目安装你的包时不会安装 devDependencies

6.3 peerDependencies — 宿主依赖

{
  "peerDependencies": {
    "vue": "^3.0.0",
    "react": "^18.0.0"
  }
}

声明"我需要宿主环境提供这个依赖",而不是自己安装一份。最经典的场景是 UI 组件库 —— element-plus 声明 peerDependencies: { "vue": "^3.0.0" },因为它不应该自带一份 Vue。

npm 版本 行为
npm 3-6 仅发出警告
npm 7+ 自动安装 peerDependencies

6.4 peerDependenciesMeta — 宿主依赖元信息

{
  "peerDependencies": {
    "vue": "^3.0.0",
    "react": "^18.0.0"
  },
  "peerDependenciesMeta": {
    "react": {
      "optional": true
    }
  }
}

标记某个 peerDependency 为可选,未安装时不会报警告。

6.5 optionalDependencies — 可选依赖

{
  "optionalDependencies": {
    "fsevents": "^2.3.0"
  }
}

安装失败时不会导致整个 npm install 失败。典型场景:fsevents 仅在 macOS 下可用。

6.6 bundleDependencies / bundledDependencies — 捆绑依赖

{
  "bundleDependencies": ["lodash", "chalk"]
}

npm pack 时会将这些依赖打包进 tarball。适用于需要确保特定版本依赖的场景,或内网环境发布。

6.7 overrides(npm)/ resolutions(yarn)— 依赖覆盖

npm(overrides):

{
  "overrides": {
    "source-map": "^0.7.4"
  }
}

yarn(resolutions):

{
  "resolutions": {
    "source-map": "^0.7.4"
  }
}

pnpm(pnpm.overrides):

{
  "pnpm": {
    "overrides": {
      "source-map": "^0.7.4"
    }
  }
}

强制将依赖树中所有匹配的包替换为指定版本。常用于修复深层依赖的安全漏洞或兼容性问题。

版本号范围速查

符号 含义 示例 匹配范围
^ 兼容版本 ^1.2.3 >=1.2.3 <2.0.0
~ 近似版本 ~1.2.3 >=1.2.3 <1.3.0
>= 大于等于 >=1.2.3 >=1.2.3
* 任意版本 * 所有版本
无符号 精确版本 1.2.3 1.2.3
` ` ^1.0.0 || ^2.0.0 满足任一条件

七、发布配置字段

7.1 private — 私有包

{
  "private": true
}

设置为 true 后,npm publish 会直接拒绝发布。用于防止 monorepo 根目录或内部项目被意外发布到公共 npm。

7.2 publishConfig — 发布配置

{
  "publishConfig": {
    "registry": "http://jfrog.gdu-tech.com/artifactory/api/npm/gdu-npm-package/",
    "access": "public",
    "tag": "latest"
  }
}
字段 说明
registry 发布到指定 npm 仓库(私有源)
access "public""restricted",scope 包默认 restricted
tag 发布时的 dist-tag,默认 latest

7.3 repository — 仓库信息

{
  "repository": {
    "type": "git",
    "url": "https://github.com/user/project.git",
    "directory": "packages/cloud-sdk"
  }
}

directory 字段在 monorepo 中非常有用,指明包在仓库中的具体位置。

npm 官网会根据此字段在包详情页展示源码链接。


八、环境约束字段

8.1 engines — 运行环境要求

{
  "engines": {
    "node": ">=18.0.0",
    "pnpm": ">=9.15.0",
    "npm": ">=8.0.0"
  }
}

声明项目所需的 Node.js 和包管理器版本。默认仅作为建议,如需强制校验:

  • npm:.npmrc 中设置 engine-strict=true
  • yarn: 自动强制检查
  • pnpm: 自动强制检查

8.2 os — 操作系统限制

{
  "os": ["darwin", "linux", "!win32"]
}

限制包可运行的操作系统。! 前缀表示排除。

8.3 cpu — CPU 架构限制

{
  "cpu": ["x64", "arm64", "!ia32"]
}

限制包可运行的 CPU 架构。

8.4 packageManager — 指定包管理器

{
  "packageManager": "pnpm@9.15.0"
}

Node.js 16.9+ 引入的 Corepack 特性。声明项目使用的包管理器及精确版本,搭配 corepack enable,其他包管理器会被拦截。


九、Monorepo 相关字段

9.1 workspaces — 工作空间

npm/yarn:

{
  "workspaces": [
    "packages/*",
    "business/*"
  ]
}

pnpm 使用独立的 pnpm-workspace.yaml

packages:
  - 'packages/*'
  - 'business/*'

工作空间允许在一个仓库中管理多个包,共享 node_modules,实现包之间的互相引用。

9.2 pnpm — pnpm 专有配置

{
  "pnpm": {
    "overrides": {
      "source-map": "^0.7.4"
    },
    "peerDependencyRules": {
      "ignoreMissing": ["@babel/*"],
      "allowedVersions": {
        "vue": "3"
      }
    },
    "neverBuiltDependencies": ["fsevents"],
    "patchedDependencies": {
      "express@4.18.2": "patches/express@4.18.2.patch"
    }
  }
}

pnpm 的专属扩展配置项,功能非常丰富:

字段 说明
overrides 强制覆盖依赖版本
peerDependencyRules 控制 peerDep 检查行为
neverBuiltDependencies 跳过某些包的 postinstall 脚本
patchedDependencies 声明补丁文件,搭配 pnpm patch 使用

十、工具链配置字段

许多工具支持直接在 package.json 中配置,免去创建额外配置文件。

10.1 lint-staged

{
  "lint-staged": {
    "*.{js,ts,vue}": ["eslint --fix", "prettier --write"],
    "*.{json,md,yaml,yml}": ["prettier --write"]
  }
}

配合 husky 在 git commit 前对暂存文件执行 lint 和格式化。

10.2 browserslist

{
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead",
    "not ie 11"
  ]
}

声明目标浏览器范围,影响 Babel、PostCSS Autoprefixer、SWC 等工具的编译输出。

10.3 sideEffects

{
  "sideEffects": false
}

告知打包工具(Webpack/Rollup/Vite)该包的所有模块都没有副作用,可以安全 Tree Shaking。

也可以指定有副作用的文件:

{
  "sideEffects": ["*.css", "*.scss", "./src/polyfill.js"]
}

这是优化打包体积最关键的字段之一。如果你的库设置了 "sideEffects": false,使用者只 import 了一个函数,打包工具就敢放心地把其余代码全部删掉。

10.4 config

{
  "config": {
    "port": "8080"
  }
}

可以在 npm scripts 中通过 npm_package_config_port 环境变量读取,用户可以用 npm config set project:port 3000 覆盖。

10.5 其他工具内联配置

以下工具都支持在 package.json 中直接配置:

工具 字段名 说明
ESLint(旧版) eslintConfig ESLint 配置
Prettier prettier 代码格式化配置
Babel babel 编译器配置
Jest jest 测试框架配置
Stylelint stylelint CSS Lint 配置
commitlint commitlint Commit 消息规范
unplugin-auto-import auto-import 自动导入配置

十一、不常见但有用的字段

11.1 flat — 扁平化依赖(yarn)

{
  "flat": true
}

强制 yarn 安装依赖时使用扁平结构,如果有版本冲突会提示用户选择。

11.2 preferGlobal — 建议全局安装(已废弃)

{
  "preferGlobal": true
}

npm 5+ 已废弃此字段,但部分老项目可能还在使用。

11.3 deprecated — 废弃提示

不是在 package.json 中设置的字段,而是通过 npm deprecate 命令发布:

npm deprecate my-package@"<2.0.0" "请升级到 2.x 版本"

安装时会显示黄色警告。


十二、字段优先级总结

入口文件解析优先级

不同工具对入口字段的解析优先级不同:

Node.js(>=12.11):

exports > main

Webpack 5:

exports > browser > module > main

Vite / Rollup:

exports > module > main

TypeScript:

exports["."]["types"] > types > typings > main(.d.ts)

一张图看清全貌

package.json
├── 📋 基本信息
│   ├── name            # 包名
│   ├── version         # 版本号
│   ├── description     # 描述
│   ├── keywords        # 关键词
│   ├── license         # 协议
│   ├── author          # 作者
│   └── contributors    # 贡献者
│
├── 📦 入口文件
│   ├── main            # CJS 入口
│   ├── module          # ESM 入口
│   ├── browser         # 浏览器入口
│   ├── types           # TS 类型入口
│   ├── exports         # 条件导出(现代方案)
│   └── type            # 模块系统声明
│
├── 📁 文件管控
│   ├── files           # 发布白名单
│   └── directories     # 目录结构声明
│
├── ⚙️ 脚本与命令
│   ├── scripts         # NPM 脚本
│   ├── bin             # 可执行文件
│   └── man             # 帮助手册
│
├── 📚 依赖管理
│   ├── dependencies          # 生产依赖
│   ├── devDependencies       # 开发依赖
│   ├── peerDependencies      # 宿主依赖
│   ├── peerDependenciesMeta  # 宿主依赖元信息
│   ├── optionalDependencies  # 可选依赖
│   ├── bundleDependencies    # 捆绑依赖
│   └── overrides/resolutions # 依赖覆盖
│
├── 🚀 发布配置
│   ├── private         # 私有标记
│   ├── publishConfig   # 发布配置
│   └── repository      # 仓库信息
│
├── 🔒 环境约束
│   ├── engines         # Node/npm 版本要求
│   ├── os              # 操作系统限制
│   ├── cpu             # CPU 架构限制
│   └── packageManager  # 包管理器声明
│
├── 🏗️ Monorepo
│   ├── workspaces      # 工作空间
│   └── pnpm            # pnpm 专有配置
│
└── 🔧 工具链配置
    ├── lint-staged     # 暂存文件 lint
    ├── browserslist    # 目标浏览器
    ├── sideEffects     # 副作用声明
    └── config          # 自定义配置

十三、最佳实践

1. 库开发的标准 package.json 模板

{
  "name": "@scope/my-lib",
  "version": "1.0.0",
  "description": "A modern library",
  "type": "module",
  "main": "dist/index.cjs",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  },
  "files": ["dist"],
  "sideEffects": false,
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts",
    "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
    "lint": "eslint src",
    "test": "vitest"
  },
  "peerDependencies": {
    "vue": "^3.0.0"
  },
  "peerDependenciesMeta": {
    "vue": { "optional": true }
  },
  "engines": {
    "node": ">=18.0.0"
  },
  "publishConfig": {
    "access": "public"
  },
  "license": "MIT"
}

2. Monorepo 根目录模板

{
  "name": "my-monorepo",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "packageManager": "pnpm@9.15.0",
  "engines": {
    "node": ">=18.0.0"
  },
  "scripts": {
    "dev": "pnpm --filter app dev",
    "build": "pnpm -r build",
    "lint": "eslint .",
    "prepare": "husky install"
  },
  "devDependencies": {
    "eslint": "^9.0.0",
    "husky": "^9.0.0",
    "lint-staged": "^15.0.0",
    "prettier": "^3.0.0",
    "typescript": "^5.0.0"
  },
  "lint-staged": {
    "*.{js,ts,vue}": ["eslint --fix", "prettier --write"]
  }
}

3. 常见误区

误区 正解
vite/webpack 放到 dependencies 构建工具应放在 devDependencies
不设置 files 字段 会把整个项目(含源码)都发布上去
exportstypes 条件放在后面 TypeScript 要求 types 必须在第一个
不设置 sideEffects 使用者无法有效 Tree Shaking
不设置 engines 用户在低版本 Node 上可能出现诡异问题
不设置 private: true monorepo 根目录可能被意外 npm publish

结语

package.json 看似简单,实则承载了包的身份信息、入口解析、依赖管理、构建配置、发布流程等方方面面。理解每一个字段的含义和使用场景,不仅能帮你写出更规范的 npm 包,还能在排查 "模块找不到"、"类型丢失"、"打包体积过大" 等问题时快速定位根因。

希望这篇文章能成为你的 package.json 随身手册,收藏备用!


如果觉得有帮助,别忘了点个赞 👍 收藏一下,后续还会更新更多前端工程化干货。

❌
❌