package.json 是前端每个项目都有的 json 文件,位于项目的根目录。package.json 里面有许许多多的配置,与项目息息相关,了解它们有助于了解项目,提效开发,规范代码。
 
今天主要介绍一些常见配置,我把它们分为了 7 大类:
  • 描述配置
  • 文件配置
  • 脚本配置
  • 依赖配置
  • 发布配置
  • 系统配置
  • 第三方配置

1. 描述配置

主要是项目的基本信息,包括名称,版本,描述,仓库,作者等,部分会展示在 npm 官网上。
notion image

name

项目的名称,如果是第三方包的话,其他人可以通过该名称使用 npm install 进行安装。

version

项目的版本号,开源项目的版本号通常遵循 semver 语义化规范,具体规则如下图所示:
notion image
简单介绍一下:
  • 1 代表主版本号 Major,通常在涉及重大功能更新,产生了破坏性变更时会更新此版本号
  • 2 代表次版本号 Minor,在引入了新功能,但未产生破坏性变更,依然向下兼容时会更新此版本号
  • 3 代表修订号 Patch,在修复了一些问题,也未产生破坏性变更时会更新此版本号
除了 X.Y.Z 这样的标准版本号,还有 Pre-release 和 Metadata 来描述项目的测试版本
回到 package.json 的 version 字段,name + version 能共同构成一个完全唯一的项目标识符,所以它两是最重要的两个字段。
最优版本会在版本前多了一个^符号,这个符号其实是有特殊意义的。同理这个符号还有可能是~符号。
那这两个符号有什么区别呢?

^ 兼容某个大版本 (^锁主要版本)

^意味着下载的包可能会是更高的次版本号或者修订版本号。(也就是主版本不能变,次版本、修订版本可以随意变)。

~ 兼容某个次版本

~意味着有可能会有更高的修订版本号。(也就是主版本、次版本不能变,修订版本可以随意变)。
看了上面版本号的指定后,我们可以知道,当我们使用了 ^ 或者 ~ 来控制依赖包版本号的时候 ,多人开发,就有可能存在大家安装的依赖包版本不一样的情况,就会存在项目运行的结果不一样。
 

repository

项目的仓库地址以及版本控制信息。

description

项目的描述,会展示在 npm 官网,让别人能快速了解该项目。

keywords

一组项目的技术关键词,比如 Ant Design 组件库的 keywords 如下:
好的关键词可以帮助别人在 npm 官网上更好地检索到此项目,增加曝光率。

homepage

项目主页的链接,通常是项目 github 链接,项目官网或文档首页。

bugs

项目 bug 反馈地址,通常是 github issue 页面的链接。

license

项目的开源许可证。项目的版权拥有人可以使用开源许可证来限制源码的使用、复制、修改和再发布等行为。常见的开源许可证有 BSD、MIT、Apache 等,它们的区别可以参考:如何选择开源许可证?

author

项目作者。

2. 文件配置

包括项目所包含的文件,以及入口等信息。

files

项目在进行 npm 发布时,可以通过 files 指定需要跟随一起发布的内容来控制 npm 包的大小,避免安装时间太长。
发布时默认会包括 package.json,license,README 和main 字段里指定的文件。忽略 node_modules,lockfile 等文件。
在此基础上,我们可以指定更多需要一起发布的内容。可以是单独的文件,整个文件夹,或者使用通配符匹配到的文件。
一般情况下,files 里会指定构建出来的产物以及类型文件,而 src,test 等目录下的文件不需要跟随发布。

type

在 node 支持 ES 模块后,要求 ES 模块采用 .mjs 后缀文件名。只要遇到 .mjs 文件,就认为它是 ES 模块。如果不想修改文件后缀,就可以在 package.json文件中,指定 type 字段为 module。
这样所有 .js 后缀的文件,node 都会用 ES 模块解释。
如果还要使用 CommonJS 模块规范,那么需要将 CommonJS 脚本的后缀名都改成.cjs,不过两种模块规范最好不要混用,会产生异常报错。

main

项目发布时,默认会包括 package.json,license,README 和main 字段里指定的文件,因为 main 字段里指定的是项目的入口文件,在 browser 和 Node 环境中都可以使用。
如果不设置 main 字段,那么入口文件就是根目录下的 index.js
比如 packageA 的 main 字段指定为 index.js。
我们引入 packageA 时,实际上引入的就是 node_modules/packageA/index.js
这是早期只有 CommonJS 模块规范时,指定项目入口的唯一属性。

browser

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

module

同样,项目也可以指定 ES 模块的入口文件,这就是 module 字段的作用。
当一个项目同时定义了 main,browser 和 module,像 webpack,rollup 等构建工具会感知这些字段,并会根据环境以及不同的模块规范来进行不同的入口文件查找。
比如 webpack 构建项目时默认的 target 为 'web',也就是 Web 构建。它的 resolve.mainFeilds 字段默认为 ['browser', 'module', 'main']。
此时会按照 browser -> module -> main 的顺序来查找入口文件。

exports

node 在 14.13 支持在 package.json 里定义 exports 字段,拥有了条件导出的功能。
exports 字段可以配置不同环境对应的模块入口文件,并且当它存在时,它的优先级最高。
比如使用 require 和 import 字段根据模块规范分别定义入口:
这样的配置在使用 import 'xxx' 和 require('xxx') 时会从不同的入口引入文件,exports 也支持使用 browser 和 node 字段定义 browser 和 Node 环境中的入口。
上方的写法其实等同于:
为什么要加一个层级,把 require 和 import 放在 "." 下面呢?
因为 exports 除了支持配置包的默认导出,还支持配置包的子路径。
比如一些第三方 UI 包需要引入对应的样式文件才能正常使用。
我们可以使用 exports 来封装文件路径:
用户引入时只需:
除了对导出的文件路径进行封装,exports 还限制了使用者不可以访问未在 "exports" 中定义的任何其他路径。
比如发布的 dist 文件里有一些内部模块 dist/internal/module ,被用户单独引入使用的话可能会导致主模块不可用。为了限制外部的使用,我们可以不在 exports 定义这些模块的路径,这样外部引入 packageA/dist/internal/module 模块的话就会报错。
结合上面入口文件配置的知识,再来看看下方 vite 官网推荐的第三方库入口文件的定义,就很容易理解了。
notion image

workspaces

项目的工作区配置,用于在本地的根目录下管理多个子项目。可以自动地在 npm install 时将 workspaces 下面的包,软链到根目录的 node_modules 中,不用手动执行 npm link 操作。
workspaces 字段接收一个数组,数组里可以是文件夹名称或者通配符。比如:
表示在 workspace-a 目录下还有一个项目,它也有自己的 package.json。
通常子项目都会平铺管理在 packages 目录下,所以根目录下 workspaces 通常配置为:

3. 脚本配置

scripts

指定项目的一些内置脚本命令,这些命令可以通过 npm run 来执行。通常包含项目开发,构建 等 CI 命令,比如:
我们可以使用命令 npm run build / yarn build 来执行项目构建。
除了指定基础命令,还可以配合 pre 和 post 完成命令的前置和后续操作,比如:
当执行 npm run build 命令时,会按照 prebuild -> build -> postbuild 的顺序依次执行上方的命令。
但是这样的隐式逻辑很可能会造成执行工作流的混乱,所以 pnpm 和 yarn2 都已经废弃掉了这种 pre/post 自动执行的逻辑,参考 pnpm issue 讨论 github.com/pnpm/pnpm/i…
如果需要手动开启,pnpm 项目可以设置 .npmrc enable-pre-post-scripts=true。

config

config 用于设置 scripts 里的脚本在运行时的参数。比如设置 port 为 3001:
在执行脚本时,我们可以通过 npm_package_config_port 这个变量访问到 3001。

4. 依赖配置

项目可能会依赖其他包,需要在 package.json 里配置这些依赖的信息。

dependencies

dependencies表示项目依赖,这些依赖都会成为你的线上生产环境中的代码组成的部分。当它关联到 npm包被下载的时候,dependencies下的模块也会作为依赖,一起被下载。
运行依赖,也就是项目生产环境下需要用到的依赖。比如 react,vue,状态管理库以及组件库等。
使用 npm install xxx 或则 npm install xxx --save 时,会被自动插入到该字段中。

devDependencies

devDependencies表示开发依赖, 不会被自动下载的。项目开发环境需要用到而运行时不需要的依赖,用于辅助开发,通常包括项目工程化工具。比如说我们用到的 Webpackviteeslint,预处理器 babel-loaderscss-loader,测试工具E2E等, 这些都相当于是辅助的工具包, 无需在生产环境被使用到的。
使用 npm install xxx -D 或者 npm install xxx --save-dev 时,会被自动插入到该字段中。

peerDependencies

同版本的依赖,同伴依赖,一种特殊的依赖,不会被自动安装,通常用于表示与另一个包的依赖与兼容性关系来警示使用者。
比如我们安装 A,A 的正常使用依赖 B@2.x 版本,那么 B@2.x 就应该被列在 A 的 peerDependencies 下,表示“如果你使用我,那么你也需要安装 B,并且至少是 2.x 版本”。
比如 React 组件库 Ant Design,它的 package.json 里 peerDependencies 为
表示如果你使用 Ant Design,那么你的项目也应该安装 react 和 react-dom,并且版本需要大于等于 16.9.0。

optionalDependencies 可选依赖

依赖是可选的,它不会阻塞主功能的使用,安装或者引入失败也无妨。这类依赖如果安装失败,那么 npm 的整个安装过程也是成功的。不建议大家去使用, 可能会增加项目的不确定性和复杂性。  了解即可。
比如我们使用 colors 这个包来对 console.log 打印的信息进行着色来增强和区分提示,但它并不是必需的,所以可以将其加入到 optionalDependencies,并且在运行时处理引入失败的逻辑。
使用 npm install xxx -O 或者 npm install xxx --save-optional 时,依赖会被自动插入到该字段中。

bundledDependencies 捆绑依赖

它是一个数组,里面定义了捆绑依赖。
当我们此时执行 npm pack的时候, 就会生成一个 test-1.0.0.tgz的压缩包, 在该压缩包中还包含了 bundleD1和 bundleD2 两个安装包。
实际使用到这个压缩包的时候,npm install test-1.0.0.tgz 的命令时, bundleD1和 bundleD2 也会被安装的。
这样做的意义是什么呢?
如果bundleD1bundleD2包在npm上有,能下载到固然没意义。但是当bundleD1bundleD2是自己临时开发的,并且当前项目又依赖这两个包,并且这两个包在npm上没有,下载不到,这下就有意义了。相当于跟主包一起捆绑发布。
这里需要注意的是: 在 bundledDependencies 中指定的依赖包, 必须先在dependencies 和 devDependencies 声明过, 否则 npm pack 阶段是会报错的。
 

peerDependenciesMeta

同伴依赖也可以使用 peerDependenciesMeta 将其指定为可选的。

bundleDependencies

打包依赖。它的值是一个数组,在发布包时,bundleDependencies 里面的依赖都会被一起打包。
比如指定 react 和 react-dom 为打包依赖:
在执行 npm pack 打包生成 tgz 压缩包中,将出现 node_modules 并包含 react 和 react-dom。
需要注意的是,这个字段数组中的值必须是在 dependencies,devDependencies 两个里面声明过的依赖才行。
普通依赖通常从 npm registry 安装,但当你想用一个不在 npm registry 里的包,或者一个被修改过的第三方包时,打包依赖会比普通依赖更好用。

overrides

overrides 可以重写项目依赖的依赖,及其依赖树下某个依赖的版本号,进行包的替换。
比如某个依赖 A,由于一些原因它依赖的包 foo@1.0.0 需要替换,我们可以使用 overrides 修改 foo 的版本号:
当然这样会更改整个依赖树里的 foo,我们可以只对 A 下的 foo 进行版本号重写:
overrides 支持任意深度的嵌套。
如果在 yarn 里也想复写依赖版本号,需要使用 resolution 字段,而在 pnpm 里复写版本号需要使用 pnpm.overrides 字段。

5. 发布配置

主要是和项目发布相关的配置。

private

如果是私有项目,不希望发布到公共 npm 仓库上,可以将 private 设为 true。

publishConfig

顾名思义,publishConfig 就是 npm 包发布时使用的配置。
比如在安装依赖时指定了 registry 为 taobao 镜像源,但发布时希望在公网发布,就可以指定 publishConfig.registry。

6. 系统配置

和项目关联的系统配置,比如 node 版本或操作系统兼容性之类。这些要求只会起到提示警告的作用,即使用户的环境不符合要求,也不影响安装依赖包。

engines

一些项目由于兼容性问题会对 node 或者包管理器有特定的版本号要求,比如:
要求 node 版本大于等于 14 且小于 16,同时 pnpm 版本号需要大于 7。需要注意,engines只是起一个说明的作用,即使用户安装的版本不符合要求,也不影响依赖包的安装。

os

在 linux 上能正常运行的项目可能在 windows 上会出现异常,使用 os 字段可以指定项目对操作系统的兼容性要求。

cpu

指定项目只能在特定的 CPU 体系上运行。

7. 第三方配置

一些第三方库或应用在进行某些内部处理时会依赖这些字段,使用它们时需要安装对应的第三方库。

types 或者 typings

指定 TypeScript 的类型定义的入口文件

unpkg

可以让 npm 上所有的文件都开启 CDN 服务。
比如 vue package.json 的 unpkg 定义为 dist/vue.global.js
当我们想通过 CDN 的方式使用链接引入 vue 时。
访问 https://unpkg.com/vue 会重定向到 https://unpkg.com/vue@3.2.37/dist/vue.global.js,其中 3.2.27 是 Vue 的最新版本。

jsdelivr

与 unpkg 类似,vue 通过如下的配置
访问 https://cdn.jsdelivr.net/npm/vue 实际上获取到的是 jsdelivr 字段里配置的文件地址。
notion image

browserslist

设置项目的浏览器兼容情况。babel 和 autoprefixer 等工具会使用该配置对代码进行转换。当然你也可以使用 .browserslistrc 单文件配置。

sideEffects

显示设置某些模块具有副作用,用于 webpack 的 tree-shaking 优化。
比如在项目中整体引入 Ant Design 组件库的 css 文件。
如果 Ant Design 的 package.json 里不设置 sideEffects,那么 webapck 构建打包时会认为这段代码只是引入了但并没有使用,可以 tree-shaking 剔除掉,最终导致产物缺少样式。
所以 Ant Design 在 package.json 里设置了如下的 sideEffects,来告知 webpack,这些文件具有副作用,引入后不能被删除。

lint-staged

lint-staged 是用于对 git 的暂存区的文件进行操作的工具,比如可以在代码提交前执行 lint 校验,类型检查,图片优化等操作。
lint-staged 通常配合 husky 这样的 git-hooks 工具一起使用。git-hooks 用来定义一个钩子,这些钩子方法会在 git 工作流程中比如 pre-commit,commit-msg 时触发,可以把 lint-staged 放到这些钩子方法中。

package-lock.json 详解

前面说了package.json里面的包版本不是一个具体的版本,而是一个最优版本。而package-lock.json里面定义的是某个包的具体版本,以及包之间的层叠关系。
一个 package-lock.json 里面的每个依赖主要是有以下的几部分组成的:
  • Version: 依赖包的版本号
  • Resolved: 依赖包的安装源(其实就是可以理解为下载地址)
  • Intergrity: 表明完整性的 Hash 值
  • Dev: 表示该模块是否为顶级模块的开发依赖或者是一个的传递依赖关系
  • requires: 依赖包所需要的所有依赖项,对应依赖包 package.json 里 dependencices 中的依赖项
  • dependencices: 依赖包 node_modeles 中依赖的包(特殊情况下才存在)。并不是所有的子依赖都有 dependencies 属性,只有子依赖的依赖和当前已安装在根目录的 node_modules 中的依赖冲突之后, 才会有这个属性。 这可能涉及嵌套情况的依赖管理。
假设我们的项目依赖A、B、C三个包,并且B包又依赖特定版本C包,所以B包就会有dependencices,那么我们项目的整个node_modules的目录结构如下

那什么情况下package.json和package-lock.json里面的版本号一致呢?

package.json里面不再使用最优版本,而是一个特定有效版本,也就是版本号前不带修饰符,这样package.jsonpackage-lock.json里面的版本号才是一致的。
或者我们npm install xxx@1.1.2 带上版本号,安装特定版本的包package.jsonpackage-lock.json里面的版本号也是一致的。
或者我们package.json里面定义的包版本就是最新的,我们安装的时候package.jsonpackage-lock.json里面的版本号也会是一致的。

package-lock.json什么时候会变

1、package-lock.jsonnpm install的时候会自动生成。
2、当我们修改依赖位置,比如将部分包的位置从 dependencies 移动到 devDependencies 这种操作,虽然包未变,但是也会影响 package-lock.json,会将部分包的 dev 字段设置为 true
3、还有如果我们安装源 registry 不同,执行 npm install 时也会修改 package-lock.json
因为他是会记录我们的依赖包地址的。
4、当我们使用npm install添加或npm uninstall移除包的时候,同时也会修改 package-lock.json
5、当我们更新某个包的版本的时候,同时也会修改 package-lock.json

如何查看安装的包版本

前面说了,package.json里面存储的包版本信息不是一个具体的版本,而是一个最优版本,也就是带^/~符号的,那我们如何知道我们安装的包具体是什么版本呢?
常用方法有三种
  1. 查看package-lock.json,这里面锁定的包就是具体的版本,但是文件内容多不好查找。
  1. 打开node_modules文件夹,找到需要看版本信息的包,然后找到它自身的package.json,里面的version就是它的版本号。当然这种方式也不太理想。
  1. 使用命令,npm list --depth 0查看本地安装的所有包信息,这种方式笔者感觉是最好的。
如果要查看全局安装的包版本我们带上-g参数就可以了。npm list -g --depth 0

怎么安装指定版本的包

我们知道,使用npm install xxx会安装某最新版本的包。
如果在根目录下直接运行npm install,那么npm会根据package.json下载所有的最新版本的包。
那么怎么安装指定版本的包呢?
我们只需要安装的时候带上版本号就可以啦。比如笔者上面element-ui默认安装的最新版本是2.15.9。我们来安装一个2.15.7的特定版本。
我们最新版本的2.15.9的版本包就会被替换。并且它会同步修改我们的package.jsonpackage-lock.json
 
怎么更新包的版本
我们知道,安装完生成package-lock.json之后包的版本就会被固定下来,后续不管你怎么npm install都不会再变化。

npm install指定版本

我们知道最新包的版本,直接按上面的安装指定包并带上版本号即可。原理和安装特定版本的包一致。
它会同步修改我们的package.jsonpackage-lock.json

npm update

不知道最新包的版本,我们运行npm update xxx 即可。
它会将指定包更新成最新版本,并且同步修改我们的package.jsonpackage-lock.json
如果想更新项目下所有的包,直接运行 npm update 即可。
请注意,更新包它是会遵循我们的最优版本号控制的。也就是说当我们的版本是被^修饰时,再怎么更新它只会更新次版本和修订版本,并不会更新主版本。

当我们出现了需要升级依赖的需求的时候:

  1. 升级小版本的时候, 依靠 npm update
  1. 升级大版本的时候, 依靠 npm install xxx
  1. 当然我们也有一种方法, 直接去修改 package.json 中的版本号, 并去执行 npm install 去升级版本
  1. 当我们本地升级新版本后确认没有问题之后, 去提交新的 package.json 和 package-lock.json文件。
 
当然,如果你不想盲目更新,想确切知道那些包过期了,最新版本是什么,你可以先使用npm outdated命令查看过期的包有哪些,并且需要更新到什么版本。current表示当前项目安装的包版本,wanted表示遵循版本控制需要更新的包版本,latest表示该包目前最新的版本。当我们使用npm update的时候,会将包都更新成wanted的版本。同理查看/更新全局过期的包使用npm outdated -gnpm update -g命令。

当我们出现了需要降级依赖的需求的时候:

执行npm install @x.x.x 命令后,验证没有问题之后, 是需要提交新的 package.json 和 package-lock.json 文件。

当我们出现了需要删除依赖的需求的时候:

  1. 当我们执行 npm uninstall 命令后,需要去验证,提交新的 package.json 和 package-lock.json 文件。
  1. 或者是更加暴力一点, 直接操作 package.json, 删除对应的依赖, 执行 npm install 命令, 需要去验证,提交新的package.json 和 package-lock.json 文件。
当你把更新后的package.json 和 package-lock.json提交到代码仓库的时候, 需要通知你的团队成员, 保证其他的团队成员拉取代码之后,更新依赖可以有一个更友好的开发环境保障持续性的开发工作。
如果你的 package-lock.json 出现冲突或问题, 我的建议是将本地的 package-lock.json文件删掉, 然后去找远端没有冲突的 package.json 和 package-lock.json, 再去执行 npm install 命令。
 
 

删除 package-lock.json

如果想更新项目下所有的包我们还可以将package-lock.json删除,然后运行npm install,它会遵循最优版本号控制,安装所有的最新版本的包,并且会重新生成package-lock.json

package-lock.json 需要提交到仓库吗

至于我们要不要提交 lockfiles 到仓库中?这个就需要看我们具体的项目的定位了。
  1. 如果是开发一个应用, 我的理解是 package-lock.json文件提交到代码版本仓库.这样可以保证项目中成员、运维部署成员或者是 CI 系统, 在执行 npm install后, 保证在不同的节点能得到完全一致的依赖安装的内容,减少bug的出现。
  1. 如果你的目标是开发一个给外部环境用的库,那么就需要认真考虑一下了, 因为库文件一般都是被其他项目依赖的,在不使用 package-lock.json的情况下,就可以复用主项目已经加载过的包,减少依赖重复和体积
  1. 如果说我们开发的库依赖了一个精确版本号的模块,那么在我们去提交 lockfiles 到仓库中可能就会出现, 同一个依赖被不同版本都被下载的情况,这样加大了node_modules的体积。如果我们作为一个库的开发者, 其实如果真的使用到某个特定的版本依赖的需求, 那么定义peerDependencies 是一个更好的选择。
所以, 我个人比较推荐的一个做法是:把 package-lock.json一起提交到仓库中去, 不需要 ignore。但是在执行 npm publish 命令的时候,也就是发布一个库的时候, 它其实应该是被忽略的不应该被发布出去的。

可以不使用package-lock.json吗

在项目中如果没有锁版本的必要,可以不使用package-lock.json,在安装模块时指定--no-lock即可。

Loading...