# package.json解析

# 常见配置字段

# name

项目的名称,该字段决定了你发布的包在 npm 的名字,也就是安装库的包名 npm i 包名

该字段也是有命名规范的,如下:

  • 名称的长度必须小于或等于 214 个字符,不能以._开头。不要使用空格<>[]{}|\^% 等。
  • 不能使用大写字母命名
  • 如果要发布到 npm 上,名称不能和社区已有的重复,可以使用npm view命令查询,或直接上npm查。
npm view test-utils

# 报 404,证明该名称可用
npm ERR! code E404
1
2
3
4

除了常规命名,我们还会见到社区有@开头的命名,格式如:@[scope]/[name]

例子:@ant-design/icons@babel/preset-env,这代表一个组织下的库。

如果你也想使用这样的结构,比如用自己名字@开头的话,需要在自己 npm 上建立组织(Add Organization),不这么做的话包名带@则发布时会不通过。

比如我创建了个组织 @asasugar-use,包名是custom-json2excel,则我的这个包和name值就命名为 @@asasugar-use/custom-json2excel

"name": "@asasugar-use/custom-json2excel"
1

# version

项目版本号,当发布 npm 包时nameversion这两个字段是必须的,两个共同构成唯一的项目标识,通过更改 version 来对你的 npm 包进行发布更新。

"version": "0.0.1"
1

通常推荐使用 semver 版本控制策略,开源项目也通常遵循这个语义化规范

版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

  1. 主版本号 Major,当你做了不兼容的 API 修改,通常在涉及重大功能更新,产生了破坏性变更时会更新此版本号
  2. 次版本号 Minor,引入了新功能,但未产生破坏性变更,依然可以向下兼容时会更新此版本号,可理解为 Feature 版本
  3. 修订号 Patch,修复了一些 bug,也未产生破坏性变更时会更新此版本号,可理解为 Bug Fix

当要发布大版本或者核心的功能时,无法确定该版本功能稳定,可能无法满足预期的兼容性需求,这个时候就需要通过发布先行版本。先行版本通过会加在版本号的后面,通过-号连接以.分隔的标识符和版本编译信息:

  • 内部版本(alpha):通常该版本软件的 Bug 较多,需要持续修改
  • 公测版本(beta):相对于 alpha 版已有很大改进,会继续加新特性,修复了较严重的错误,但依然存在一些缺陷
  • 候选版本(rc,即 release candiate):正式版本的候选版本,该版本一般可以说错误较少了,基本是修复而不是再加新特性了
react:16.3.0-alpha.0
vue:3.2.34-beta.1、3.0.0-rc.13
1
2

比如想查看 vue 的历史 Tag 版本,可看到 vue 是比较严格遵循 semver 版本规范的:

20240712093734

# 查看 vue 历史版本
npm view vue versions
# 查看 vue 最新版本
npm view vue version
1
2
3
4

一些问题解答:

在库初始开发阶段,该如何进行版本控制?

最简单的做法是以 0.1.0 作为你的初始化开发版本,并在后续的每次发行时递增次版本号。当最终完成用于正式线上环境了,这时就可以改为 1.0.0,之后就都按照 Semver 规范走了

公司一些较小的公共库版本,怎么维护?

这个可视乎库规模和使用人数多少决定,像一般公司大部分私库测试环境都可直接发布 beta 版本,等 UAT/线上就是直接走的正式版本

# description

项目的描述,信息会直接展示在 npm 官网,通过它能让别人能快速你的项目。

vue库的:

"description": "The progressive JavaScript framework for building modern web UI."
1

20240712094017

# keywords

项目关键词。关键词填得准,可方便在 npm 上更好地检索

如 Ant Design 的 keywords

"keywords": [
    "ant",
    "component",
    "components",
    "design",
    "framework",
    "frontend",
    "react",
    "react-component",
    "ui"
],
1
2
3
4
5
6
7
8
9
10
11

# repository

项目的仓库地址以及版本控制信息,可通过该字段找到代码托管地址

// vue-router
"repository": {
    "type": "git",
    "url": "git+https://github.com/vuejs/router.git"
}
1
2
3
4
5

# license

项目的开源许可证,说明你的库使用哪个许可证,用户保护你自己和贡献者。

没有 License 的内容是默认会被版权保护,如果你允许社区内开发者可基于你的项目二次改造等,需要选择合适的许可证才能赋予任何人放心使用(改造、分享等)。

目前 Github 我们熟知的大部分项目都是 MIT 许可证

 "license": "MIT"
1

MIT 基本就是没有任何限制,表示任何人都可以售卖我的软件,甚至可以用作者的名字促销。 项目的版权拥有人可以使用开源许可证来限制源码的使用、复制、修改和再发布等行为,

通常开源项目都会在根目录下新建 LICENSE 文件,并将许可证的文本复制到这里。如看下 Vue

对于公司不开源的项目,这个配置项一般可忽略。

了解详细可阅读:

# private

设置为私有防止意外发布。如果你不希望把项目发布到 npm 仓库上,可以将 private 设为 true。

"private": true
1

当设置为 true 时,执行npm publish会报错,npm 会拒绝发布这个项目。

像公司的非开源项目就可以设置为 true,防止被无意间发布出去

# publishConfig

npm 包发布使用的配置,通常publishConfig会配合private使用;如果只想让模块发布到特定 npm 仓库,就可以这样来配置

"publishConfig": {
    "registry": "https://registry.npmjs.org", // 私源地址配置
    "tag": "beta", // 如果没有指定tag,默认是 latest
    "access": "public"
}
1
2
3
4
5
  • registry:npm 私源地址
  • tag:指定当前版本对应的 tag
  • access:包的访问级别。如果是 scoped 包(如 @babel/xxx,@ant-design/xxx),一定需要设置为 public(除非加入付费计划)

# bugs

bug 反馈地址,通过该链接向你的仓库反馈 bug,github 上的一般都是 github issue 页的链接

"bugs": "https://github.com/vuejs/router/issues",
1

# homepage

项目主页地址,如:仓库 Github 链接、官网地址、文档地址等

// vue-router
"homepage": "https://github.com/vuejs/router#readme"
1
2

homepage字段会展示在 npm 的这个地方:

20240712094555

homepage 还可设置应用的根路径,详细可见【小技巧】package.json 中 homepage 属性的作用

# author

项目的作者(信息)

  • 字符串形式
"author": "asasugar"
1
  • 对象形式:包含必选的 name 属性和可选的 url、email 属性
"author": {
    "name": "asasugar",
    "email": "asasugar@gmail.com",
    "url": "https://github.com/asasugar"
}
1
2
3
4
5

也可以用下面的字符串简写,npm 会帮我们解析:

"author": "asasugar <asasugar@gmail.com> (https://github.com/asasugar)"
1

# contributors

贡献者信息。对象格式数组,对象的内容和author一样

"contributors": [{
    "name": "asasugar",
    "email": "asasugar@gmail.com",
    "url": "https://github.com/asasugar"
}]
1
2
3
4
5

通常对应的,Github 开源项目通常会有 /.github/CONTRIBUTING.md贡献指南这个文件,里面也可能有列出一些贡献者简要信息等,如 Vue 的 CONTRIBUTING.md

# scripts

项目内置的脚本命令,这一字段下的东西是我比较感兴趣的,通常可以看到项目的启动方式、prettier/eslint 运行脚本,是否用了单元测试等等...

找到项目的/node_modules/.bin目录,项目 npm run xx的底层调用命令在这里都能找到

npm 脚本的原理:每当执行 npm run,就会自动新建一个 Shell,在这个 Shell 里面执行指定的脚本命令。因此,只要是 Shell(一般是 Bash)可以运行的命令,就可以写在 npm 脚本里面

比如我们安装了 eslint,scripts里面配置:

"eslint": "eslint --fix --ext .ts,.tsx ."
1

而不必写成路径的方式

"eslint": "./node_modules/.bin/eslint --fix --ext .ts,.tsx .",
1

npm scripts 钩子

npm 脚本有 pre 和 post 两个钩子,如 运行 npm run install 的时候,分 3 个阶段:

  1. 检查 scripts 对象中是否存在 preinstall 命令,如果有,先执行该命令;
  2. 检查是否有 install 命令,有的话运行 install 命令,没有的话报错;
  3. 检查是否存在 postinstall 命令,如果有,执行 postinstall 命令

npm 默认提供下面这些钩子。

prepublish,postpublish
preinstall,postinstall
preuninstall,postuninstall
preversion,postversion
pretest,posttest
prestop,poststop
prestart,poststart
prerestart,postrestart
1
2
3
4
5
6
7
8

另外,我们自定义的脚本命令也可以加上 pre 和 post 钩子。

下面举例我工作中使用到 npm 钩子的场景:

  1. 所有项目都由 npm 迁移 pnpm,但有的人没注意或忘了,拿到项目还是使用 npm 安装,也产生了 package-lock.json 文件

解决方案:在 npm install 之前限制住只能使用 pnpm 安装。.gitignore 添加 npm 和 yarn 的 lock 文件

"scripts": {
    "preinstall": "npx only-allow pnpm"
}
1
2
3

.gitignore

yarn.lock
package-lock.json
1
2

了解更多 npm scripts 可参考学习:npm scripts 使用指南


建议 script 有约定俗成的规范脚本命令,提高可读性与降低维护成本

来看下一些开源项目的 scripts 配置:

Vue

 "scripts": {
    "dev": "rollup -w -c scripts/config.js --environment TARGET:full-dev",
    "dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:runtime-cjs-dev",
    "dev:esm": "rollup -w -c scripts/config.js --environment TARGET:runtime-esm",
    "dev:ssr": "rollup -w -c scripts/config.js --environment TARGET:server-renderer",
    "dev:compiler": "rollup -w -c scripts/config.js --environment TARGET:compiler ",
    "build": "node scripts/build.js",
    "build:ssr": "npm run build -- runtime-cjs,server-renderer",
    "build:types": "rimraf temp && tsc --declaration --emitDeclarationOnly --outDir temp && api-extractor run && api-extractor run -c packages/compiler-sfc/api-extractor.json",
    "test": "npm run ts-check && npm run test:types && npm run test:unit && npm run test:e2e && npm run test:ssr && npm run test:sfc",
    "test:unit": "vitest run test/unit",
    "test:ssr": "npm run build:ssr && vitest run server-renderer",
    "test:sfc": "vitest run compiler-sfc",
    "test:e2e": "npm run build -- full-prod,server-renderer-basic && vitest run test/e2e",
    "test:transition": "karma start test/transition/karma.conf.js",
    "test:types": "npm run build:types && tsc -p ./types/tsconfig.json",
    "format": "prettier --write --parser typescript \"(src|test|packages|types)/**/*.ts\"",
    "ts-check": "tsc -p tsconfig.json --noEmit",
    "ts-check:test": "tsc -p test/tsconfig.json --noEmit",
    "bench:ssr": "npm run build:ssr && node benchmarks/ssr/renderToString.js && node benchmarks/ssr/renderToStream.js",
    "release": "node scripts/release.js",
    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
  },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# config

用于添加命令行的环境变量,即我们脚本在运行时的参数。

"config" : { "port" : "8080" },
"scripts" : { "start" : "node server.js" }
1
2

在 server.js 脚本就可以引用 config 字段的值

http
  .createServer(...)
  .listen(process.env.npm_package_config_port) // 8080
1
2
3

执行 npm run start 命令时,这个脚本就可以得到值。

用户可以改变这个值

npm config set foo:port 80
1

这个字段没用过,实战中这些配置一般不会直接写在 package.json 文件里面

例子引用自 config 字段

# engines

声明项目需要在怎样的 node 环境下运行,曾遇过比较老旧的项目,甚至可能要降级 node 版本才能跑起来。这时可以给项目添加字段,说明哪个版本下运行能跑起来项目,当然你现在正维护开发的项目也推荐补充下。

"engines": {
    "node": ">=16.17.0",
    "pnpm": ">=6 <7"
},
1
2
3
4

engines只是起一个说明的作用,即使版本不符合要求也丝毫不影响安装使用。

但如果真的遇上对 node 版本要求比较严格的项目,就可以在 .npmrc文件设置

engine-strict=true
1

这时本地 node 版本不匹配的话就会报错 Unsupported engine,要求你必须切换engines配置的对应版本才能正常安装

# workspaces

monorepo 项目的工作区配置,用于在本地的根目录下管理多个子项目,可以自动地在 npm install 时将 workspaces 下面的包,软链到根目录的 node_modules 中,而不用手动执行 npm link 操作

workspaces 字段接收一个数组,数组里可以是文件夹名称或者通配符,通常子项目都会管理在 packages 目录下。比如:

  "workspaces": [
    "packages/*"
  ]
1
2
3

Babel 的配置:

  "workspaces": [
    "codemods/*",
    "eslint/*",
    "packages/*",
    "test/esm",
    "test/runtime-integration/*",
    "benchmark"
  ],
1
2
3
4
5
6
7
8

# files

项目发布时包含的文件,配置格式为数组,可以指定单独的文件或整个文件夹。

如 Vue:

"files": [
    "src",
    "dist/*.js",
    "dist/*.mjs",
    "types/*.d.ts",
    "compiler-sfc",
    "packages/compiler-sfc"
]
1
2
3
4
5
6
7
8

声明files字段后,当该 npm 包被安装时,安装的是files字段指定的内容,从而做到精准控制发布内容,也控制了 npm 包大小。

如果有不想提交的文件,可以在项目根目录中新建一个.npmignore文件进行配置,声明不需要提交的文件;写在这里的文件即使 files 声明了也不会被提交,这是为了防止垃圾无用文件被推到 npm 上

.vscode
1

# sideEffects

声明设置哪些模块具有副作用,让打包工具知道你的模块是否是“纯”的以此更好的 Tree Shaking。

首先要知道什么会让一个模块有副作用?

例如修改一个全局变量,发送 API 请求,或导出 CSS,是否重写了原生对象方法,总结起来就是函数可能对外部产生影响的行为

window.foo = 'global foo'
1

Tree Shaking 怎么优化的

Webpack 的 Tree Shaking 机制由 optimization.usedExportssideEffects 共同承担

  1. 通过设置 usedExports 为 true,表示模块只导出被使用的成员,配合 terser 删除项目所有模块中未被引用的导出变量
module.exports = {
  optimization: {
    usedExports: true, // 作用于代码语句层面,只导出(export)有使用的变量/方法
  },
}
1
2
3
4
5

当然这个 webpack 打包生产环境默认开启的

  1. 通过 package.json 配置的 sideEffects,用于标记整个模块的副作用。

sideEffects 设为 false,表示没有任何模块具有副作用,所有模块都是"纯"的,即打包的时候不管它是否有没有副作用,只要它没有被引用,整个模块/包都会被完整的移除

"sideEffects": false
1

也可以使用字符串数组来列出哪些文件具有副作用

如 Ant Design 的sideEffects声明如下 ,告知这些文件有副作用,引入后不能被删除

"sideEffects": [
    "dist/*",
    "es/**/style/*",
    "lib/**/style/*",
    "*.less"
],
1
2
3
4
5
6

# type

定义 package.json 文件和及其所在目录根目录中.js文件和没有拓展名文件的处理方式

默认值为 commonjs

"type": "commonjs"
1

比如我们在项目根目录下新建两个文件:

// test.js
export const foo = 10

// index.js
import { foo } from './test.js'

console.log(foo)
1
2
3
4
5
6
7

然后在控制台运行node index.js,这时会报错

SyntaxError: Cannot use import statement outside a module
1

因为 Node 的模块化方案采用的是 CommonJS,而代码是 ES 语法,所以报错;

而 Node 在 v13.2.0 已开始正式支持 ES Modules 特性,我们可在 package.json 设置:

"type": "module"
1

这样子 Node 就会用 ES 规范进行解析,重新运行就不会报错了

需要注意的是,无论package.json中的type字段设置为何值,只要文件后缀是 .mjs的文件都依然按照 ES 模块规范来处理, .cjs的文件都按照 CommonJS 模块规范来处理,不会受 type影响

# types/typings

对外暴露相关的类型,指定 TypeScript 的类型定义的入口文件,前提是项目是用 TypeScript 写的,用于方便 IDE 识别与智能提示。

"typings": "types/index.d.ts"
1

使用 typestypings都可以,作用相同

# main

mainbrowsermodule三个字段都是用于 npm 包的,如果项目不是作为 npm 包发布,这三个字段不需要写。

main 字段指定加载的入口文件,指向一个兼容 CommonJS 格式的产出,这个文件名是项目作为 npm 包被打包时配置的,在 browser 和 node 环境中都可以使用。如使用require的方式导入该 npm 包时,返回的就是main字段指定文件的module.exports属性。

当不设置该字段时,默认值是根目录下的index.js 文件

"main": "./index.js"
1
// Vue
"main": "dist/vue.runtime.common.js"
1
2

# browser

指向支持 UMD 模块的入口文件,这个字段也会被一些公共 CDN 使用,比如 unpkgjsdelivr。 也可以直接通过声明 unpkgjsdelivr 字段来配置入口文件,下面再细说这两个字段。

UMD:兼容 CommonJS 和 AMD 的模块,既可以在 node 环境中被 require 引用,也可以在浏览器中直接用 CDN 被script标签 引入

unpkg 和 jsDelivr 是开源 CDN 服务

main 字段里指定的入口文件在 browser 和 node 环境中都可以使用。如果只想在 web 端使用,禁止在 server 端使用,可以通过 browser 字段指定入口。

"browser": "./browser/index.js"
1

# module

  • module 指向支持 ESM 模块的入口文件,browser 环境和 node 环境均可使用
"module": "./index.mjs"
1

如果一个项目同时定义了 mainbrowsermodule,Webpack 等构建工具打包的时候会根据环境以及不同的模块规范来进行不同的入口文件查找。

// Vue:
"main": "dist/vue.runtime.common.js",
"module": "dist/vue.runtime.esm.js",
1
2
3

总结 npm 包 mainmodulebrowser

  • 导出包只在 web 端使用,并且禁止在 server 端使用,使用 browser
  • 导出包只在 server 端使用,使用 main
  • 导出 ESM 规范的包,使用 module
  • 导出包在 web 端和 server 端都允许使用,使用 modulemain

还有其他具体情况这里就不展开了

# exports

如果打包工具支持 exports 字段,则该字段优先级最高,会忽略 main/browser/module的配置。

比如同时使用 requireimport 字段定义模块规范入口:

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

// 上面写法还等同于
"exports": {
  // 这里路径声明在根目录下,因为还支持配置包的子路径
  ".": {
    "require": "./index.js",
    "import": "./index.mjs"
  }
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

更多 exports 文档可看 NodeJS 文档说明

// Vue 的配置:
"exports": {
    ".": {
      "import": {
        "node": "./dist/vue.runtime.mjs",
        "default": "./dist/vue.runtime.esm.js"
      },
      "require": "./dist/vue.runtime.common.js",
      "types": "./types/index.d.ts"
    },
    "./compiler-sfc": {
      "import": "./compiler-sfc/index.mjs",
      "require": "./compiler-sfc/index.js"
    },
    "./dist/*": "./dist/*",
    "./types/*": "./types/*",
    "./package.json": "./package.json"
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# unpkg/jsdelivr

让 npm 上所有的文件都开启 cdn 服务。注意文件需要是 UMD 模块规范格式

"unpkg": "dist/vue.js",
"jsdelivr": "dist/vue.js",
1
2

使用unpkg/jsdelivr字段可以让 npm 上所有的文件都开启 cdn 服务,比如当我们从 CDN 访问使用 Vue时( https://unpkg.com/vue),

会重定向到 https://unpkg.com/vue@2.7.16/dist/vue.js (取最新Vue版本)获取文件

# dependencies

运行依赖,项目线上环境下需要用到的依赖。 使用 npm install xxxnpm install xxx --save 时,npm 包就会自动插入到该字段下。

"dependencies": {
  "@vue/compiler-sfc": "workspace:*",
  "csstype": "^3.1.0"
}
1
2
3
4

在安装之前就可以判断 npm 包是否需要在线上运行,如不需要则不要放到该字段下。

在我经历过的来看,建议一些重要且稳定的库锁死版本,防止意外升级导致生产 bug,因为升级库的次版本,也有概率出现较严重 Bug。

"vue": "2.7.16",
1

如何管理 npm 版本号:语义化版本策略 SemVer

# devDependencies

开发依赖,项目开发环境需要用到而线上运行时不需要的依赖,用于辅助开发。

比如 viteeslintbabel、TS @types/xxx类型文件等等。

当打包上线时并不需要这些包,所以可以把这些依赖添加到 devDependencies 中,这些依赖依然会在本地指定 npm install 时被安装和管理,但是不会被安装到生产环境中。

使用 npm install xxx -D 或者 npm install xxx --save-dev 时,npm 包就会自动插入到该字段下。

# peerDependencies

同伴依赖,防止包避免重复安装,一般组件库比较常见。

如 Ant Design 的配置:表示我们项目使用 antd npm 包还需安装 reactreact-dom,且版本都要>=16.9.0

"peerDependencies": {
    "react": ">=16.9.0",
    "react-dom": ">=16.9.0"
},
1
2
3
4

# peerDependenciesMeta

可选的同伴依赖

举例:react-redux package.json

 "peerDependencies": {
    "@types/react": "^16.8 || ^17.0 || ^18.0",
    "@types/react-dom": "^16.8 || ^17.0 || ^18.0",
    "react": "^16.8 || ^17.0 || ^18.0",
    "react-dom": "^16.8 || ^17.0 || ^18.0",
    "react-native": ">=0.59",
    "redux": "^4"
  },
  "peerDependenciesMeta": {
    "@types/react": {
      "optional": true
    },
    "@types/react-dom": {
      "optional": true
    },
    "react-dom": {
      "optional": true
    },
    "react-native": {
      "optional": true
    },
    "redux": {
      "optional": true
    }
  },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

上述指定了 5 个 npm 包在peerDependenciesMeta中,表示都为可选项,所以只安装任意几个可能不会报错,当然也不需要全部安装,因为这里明显看出这是区分 native 和 web 两个环境的,使用的话就任选只在一种环境下。

# bin

指定各个内部命令对应的可执行文件的位置,指定了 bin 字段的 npm 包,如果被全局安装,就会被加载到全局环境中,可以通过别名来执行该文件。如带有工具性质的 npm 包。

// webpack
"bin": {
    "webpack": "bin/webpack.js"
},

// eslint
"bin": {
    "eslint": "./bin/eslint.js"
},
1
2
3
4
5
6
7
8
9

node_modules/.bin/目录下的命令,都可以用 npm run [命令] 的格式运行

拿 eslint 举例,eslint 命令对应的可执行文件为 bin 子目录下的 eslint.js。npm 会在node_modules/.bin/目录下建立符号链接,由于node_modules/.bin/目录会在运行时加入系统的 PATH 变量,所以在运行 npm 时,就可以不带路径,直接通过命令来调用这些脚本。

"eslint": "./node_modules/bin/eslint.js --fix --ext .ts,.tsx"

// 简写为
"eslint": "eslint --fix --ext .ts,.tsx"
1
2
3
4

# overrides

重写项目依赖的依赖,及其依赖树下某个依赖的版本号,进行包的替换,支持任意深度的嵌套。。

比如fs-extra库,我们在项目中已经锁死了版本,但是有次发现 lock 文件更新了,当然它还是保持原来版本不变,但是它的子依赖graceful-fs被升级了小版本导致 bug。

由于graceful-fs这个库的场景在我们项目只有一处地方,为了解决这个问题,就需要把graceful-fs版本锁死,恢复到之前没有 bug 的版本,所以可以用该字段

"overrides": {
  "graceful-fs": "4.2.0"
}
1
2
3

此时查找 lock 文件依赖树,会发现 lock 文件全部被安装为我们指定的版本graceful-fs@4.2.0,当然你也可以针对单独的库进行版本重写,如下:

"overrides": {
  "fs-extra": {
    "graceful-fs": "4.2.0"
  }
}
1
2
3
4
5
  • pnpm 需要使用字段:
"pnpm": {
    "overrides": {
    }
  },
1
2
3
4
  • yarn 需要使用 resolution 字段

更多可了解: 前端依赖版本重写指南