package.json 用来描述项目及项目所依赖的模块信息。
全文以npm为例

package.json 与 package-lock.json 的关系

版本指定

~ 会匹配最近的小版本依赖包,比如 ~1.2.3 会匹配所有 1.2.x 版本,但是不包括 1.3.0
^ 会匹配最新的大版本依赖包,比如 ^1.2.3 会匹配所有 1.x.x 的包,包括 1.3.0,但是不包括 2.0.0
* 安装最新版本的依赖包,比如 *1.2.3 会匹配 x.x.x
  • 为了保证不同人电脑安装的所有依赖版本都是一致的,确保项目代码在安装所执行的运行结果都一样,这时 package-lock.json 就应运而生了。
  • package-lock.json 是在 npm(^5.x.x.x)后才有。
  • package-lock.json 它会npm 更改 node_modules 目录树 或者 package.json 时自动生成的 ,它准确的描述了当前项目npm包的依赖树,并且在随后的安装中会根据 package-lock.json 来安装,保证是相同的一个依赖树。
注意,使用cnpm install时候,并不会生成 package-lock.json 文件,也不会根据 package-lock.json 来安装依赖包,还是会使用 package.json 来安装。

生成逻辑

如果我们现在有三个 package,在项目 test中,安装依赖A,A项目里面有B,B项目里面有C:
// package test
{ "name": "test", "dependencies": { "A": "^1.0.0" }}
// package A
{ "name": "A", "version": "1.0.0", "dependencies": { "B": "^1.0.0" }}
// package B
{ "name": "B", "version": "1.0.0", "dependencies": { "C": "^1.0.0" }}
// package C
{ "name": "C", "version": "1.0.0" }
在这种情况下 package-lock.json, 会生成类似下面铺平的结构:
// package-lock.json
{ 
    "name": "lock-test",  
    "version": "1.0.0",  
    "dependencies": {    
        "A": { "version": "1.0.0" },
        "B": { "version": "1.0.0" },
        "C": { "version": "1.0.0" }  
    }
}
  • 如果后续无论是直接依赖的 A 发版,或者间接依赖的B, C 发版,只要我们不动 package.json, package-lock.json 都不会重新生成。
  • 我们可以手动运行npm i A@1.1.0来实现升级。因为 1.1.0 package-lock.json 里记录的 A@1.0.0是不一致的,因此会更新 package-lock.json 里的 A 的版本为 1.1.0。
  • 如果后面,B 发布了新版本 1.1.0, 此刻如果我们不做操作是不会自动升级 B 的版本的。但如果我们项目里升级 A@1.1.1,此时 package-lock.json 里会把 B 直接升到 1.1.0
经过这些操作后,项目的 package-lock.json变成:
{  
    "name": "test",  
    "version": "1.0.0",
    "dependencies": {  
        "A": { "version": "1.1.0" },
        "B": { "version": "1.1.0" },
        "C": { "version": "1.0.0" }
    }
}

这个时候我们将 B 加入项目的依赖, B@^1.0.0,package.json如下:
{ "dependencies": { "A": "^1.1.0", "B": "^1.0.0" }}
  • 我们执行这个操作后,package-lock.json 并没有被改变,因为现在 package-lock.json里 B@1.1.0 满足 ^1.0.0 的要求。

但是如果我们将 B 的版本固定到 2.x 版本, package-lock.json 就会发生改变:
{ "dependencies": { "A": "^1.1.0", "B": "^2.0.0" }}
  • 因为存在了两个冲突的B版本,package-lock.json 文件会变成如下形式:
{  
    "name": "test",
    "version": "1.0.0",  
    "dependencies": {    
        "A": {      
            "version": "1.1.0",      
            "dependencies": {        
                "B": { "version": "1.1.0" }      
            }    
        },    
        "B": { "version": "2.0.0" },    
        "C": { "version": "1.0.0" }  
    }
}
 

可能被意外更改的原因

  1. package.json 文件修改了
  1. 挪动了包的位置
将部分包的位置从 dependencies 移动到 devDependencies 这种操作,虽然包未变,但是也会影响 package-lock.json
  1. registry(镜像)的影响
如果我们 node_modules 文件夹下的包中下载时,就算版本一样,安装源 registry 不同,执行 npm i 时也会修改 package-lock.json。
 

package.json依赖

package.json中跟依赖相关的配置属性包含了dependencies、devDependencies、peerDependencies、peerDependenciesMeta、optionalDependencies和bundledDependencies等。

(1)dependencies

  • dependencies是项目的依赖,确保应用能正常运行。
npm install 依赖 --save
npm install 依赖 -S

(2)devDependencies

  • devDependencies是开发所需要的模块,所以我们可以在开发过程中需要的安装上去,来提高我们的开发效率。
  • 形如webpack、babel、打包相关、ESLint相关、Loader相关等等等是开发依赖,而不是项目本身的依赖,要放在devDependencies中。
npm install 依赖 --save-dev
npm install 依赖 -D
npm install:安装dependencies和devDependencies的依赖
npm install --production : 只安装dependencies的依赖
  • 我们开发时的执行命令是npm install,其实我们的依赖包安装在dependencies还是devDependencies没有任何区别,都会下载
  • 在别人引用我们包的时候,放在 devDependencies 的包,不会被 npm 下载。

(3)peerDependencies

  • 具有peerDependencies的项目通常不是最终应用,这个项目会被宿主应用作为一个插件或第三方库消费。
  • peerDependencies的存在,主要是期望宿主应用安装这些依赖,让相同依赖不会在宿主应用和库中被重复安装。
  • 同时为了减少库或插件的大小的目的,将一些依赖统一放到宿主应用安装。
比如"react-redux": "^7.2.9"中,peerDependencies如下:
"peerDependencies": {
  "react": "^16.8.3 || ^17 || ^18"
},
如果本地没有react安装,安装react-redux报警告:
npm WARN react-redux@7.2.9 requires a peer of react@^16.8.3 || ^17 || ^18 but none is installed. You must install peer dependencies yourself.
npm 7中可以自动安装peerDependencies
在npm的之前版本(4-6)中,peerDependencies冲突会有版本不兼容的警告,但仍会安装依赖并不会抛出错误。
在npm 7中,如果存在无法自动解决的依赖冲突,将会阻止安装。

(4)peerDependenciesMeta

  • “Meta”就有元数据的意思,peerDependenciesMeta就是详细修饰了peerDependicies
比如在react-redux这个npm包中的package.json中有这么一段:
"peerDependencies": {
  "react": "^16.8.3 || ^17 || ^18"
},
"peerDependenciesMeta": {
  "react-dom": {
    "optional": true
  },
  "react-native": {
    "optional": true
  }
},
这里指定了"react-dom","react-native"在peerDependenciesMeta中,且为可选项,因此如果项目中检测没有安装"react-dom"和"react-native"都不会报错。

(5)optionalDependencies

  • optionalDependencies定义可选的dependencies,npm在安装dependencies过程中出错会退出安装,但对optionalDependencies来说,即使一些依赖安装失败,也不影响最终应用运行,但还要做好相应模块容错处理。
  • 另外optionalDependencies会覆盖dependencies中的同名依赖包,所以不要在两个地方都写。

(6)bundledDependencies / bundleDependencies

  • bundledDependencies 期望一些依赖包能出现在最终打包的包里,是一个包含依赖包名的数组。
{
  "name": "myReact",
  "version": "1.0.0",
  "bundledDependencies": [
    "react", "react-dom"
  ],
}
 

bin和scripts

  • bin 项用来指定各个内部命令对应的可执行文件的位置。
  • 比如我们"someTool"包的bin文件如下
"bin": {
  "someTool": "./bin/someTool.js"
},
  • 上面代码指定,someTool 命令对应的可执行文件为 bin 子目录下的 someTool.js。
  • npm会寻找这个文件,在node_modules/.bin/目录下建立符号链接。
  • someTool.js会建立符号链接node_modules/.bin/someTool。由于node_modules/.bin/目录会在运行时加入系统的PATH变量,因此在运行npm时,就可以不带路径,直接通过命令来调用这些脚本。

  • 在npm中使用script标签来定义脚本。
  • 当前目录的node_modules/.bin子目录里面的所有脚本,都可以直接用脚本名调用,而不必加上路径。
"scripts": {
  "dev": "someTool build"
 }
 // 等价于:
 "scripts": {
  "dev": "./node_modules/bin/someTool.js build"
 }
 

workspaces

  • 在项目过大的时候,最近越来越流行monorepo。提到monorepo就绕不开workspaces
  • npm 7开始支持workspaces。
  • workspaces解决了本地文件系统中如何在一个顶层root package下管理多个子packages的问题,在workspaces声明目录下的package会软链到最上层root package的node_modules中。

最上层的名为my-project的root包,有packages/a子包。
+-- my-project
+-- package.json
`-- packages
   +-- a
   |  `-- package.json
在顶层package.json中存在workspaces配置:
{
  "name": "my-project",
  "workspaces": [
    "packages/a"
  ]
}
在顶层root package安包,node_modules中存在软链,指向的是package/a.
+-- node_modules
|  `-- packages/a -> ../packages/a
+-- package-lock.json
+-- package.json
`-- packages
   +-- a
   |   `-- package.json
 

main & module & browser

npm 包其实分为:
  • 只允许在客户端使用的,
  • 只允许在服务端使用的,
  • 浏览器/服务端都可以使用。
  • package.json中有main,module和browser 3个字段来定义npm包的入口文件
  • main : 定义了 npm 包的入口文件,browser 环境和 node 环境均可使用
  • module : 定义 npm 包的 ESM 规范的入口文件,browser 环境和 node 环境均可使用
  • browser : 定义 npm 包在 browser 环境下的入口文件

文件优先级

  • 由于我们使用的模块规范有 ESM 和 commonJS 两种,为了能在 node 环境下原生执行 ESM 规范的脚本文件,.mjs 文件就应运而生。
  • 当存在 index.mjsindex.js 这种同名不同后缀的文件时,import './index' 或者 require('./index') 是会优先加载 index.mjs 文件的。
  • 也就是说,优先级 mjs > js

使用场景与优先级

首先,我们假定 npm 包 有以下目录结构
----- lib
   |-- index.browser.js
   |-- index.browser.mjs
   |-- index.js
   |-- index.mjs
其中 *.js 文件是使用 commonJS 规范的语法(require('xxx')),*.mjs 是用 ESM 规范的语法(import 'xxx')
其 package.json 文件:
"main": "lib/index.js",  // main
"module": "lib/index.mjs", // module

// browser 可定义成和 main/module 字段一一对应的映射对象,也可以直接定义为字符串
"browser": {
  "./lib/index.js": "./lib/index.browser.js", // browser+cjs
  "./lib/index.mjs": "./lib/index.browser.mjs"  // browser+mjs
},

//  "browser": "./lib/index.browser.js" // browser
 
根据上述配置,那么其实我们的 package.json 指定的入口可以有
  • main
  • module
  • browser
  • browser+cjs
  • browser+mjs
这 5 种情况。

(1)webpack + web + ESM

  • 这是我们最常见的使用场景,通过 webpack 打包构建我们的 web 应用,模块语法使用 ESM
  • 实际上的加载优先级是 browser = browser+mjs > module > browser+cjs > main 也就是说 webpack 会根据这个顺序去寻找字段指定的文件,直到找到为止。

(2)webpack + web + commonJS

  • 构建 web 应用时,使用 ESM 或者 commonJS 模块规范对于加载优先级并没有任何影响
  • 优先级依然是 browser = browser+mjs > module > browser+cjs > main

(3)webpack + node + ESM/commonJS

  • 优先级是: module > main

(4)node + commonJS

  • 只有 main 字段有效。

总结

  • 如果 包导出的是 ESM 规范的包,使用 module
  • 如果 包只在 web 端使用,并且严禁在 server 端使用,使用 browser。
  • 如果 包只在 server 端使用,使用 main
  • 如果 包在 web 端和 server 端都允许使用,使用 browser 和 main
 

exports

  • 如果在package.json中定义了exports字段,那么这个字段所定义的内容就是该npm包的真实和全部的导出。
  • 如果存在exports属性,exports属性不仅优先级高于main,同时也高于modulebrowser字段。

用法

(1)子目录别名

package.json文件的exports字段可以指定脚本或子目录的别名。
// ./node_modules/es-module-package/package.json
"exports": {
    "./submodule": "./src/submodule.js"
  }
上面的代码指定src/submodule.js别名为submodule,然后就可以从别名加载这个文件。
import submodule from "es-module-package/submodule";
// 加载 ./node_modules/es-module-package/src/submodule.js

(2)main 的别名

exports字段的别名如果是.,就代表模块的主入口,优先级高于main字段,并且可以直接简写成exports字段的值。
{
    "exports": {".": "./main.js"}
}

// 等同于
{
    "exports": "./main.js"
}

(3)条件加载

  • 我们可以根据不同的引用方式或者模块化类型,来指定包引用不同的入口文件。
"exports": {
    "require": "./main-require.cjs",
    "import": "./main-module.js"
  },
上述的例子中,如果我们通过:
const p = require("pkg")
引用的就是"./main-require.cjs"
如果通过:
import p from "pkg"
引用的就是"./main-module.js"
  • 除了像上面使用直接映射方式之外,还可以使用嵌套条件的方式来定义 "exports"
"exports": {
    "node": {
      "import": "./feature-node.mjs",
      "require": "./feature-node.cjs"
    },
    "default": "./feature.mjs",
  }
"node" 用于匹配任何 Node.js 环境,可以是 CommonJS 或者是 ES Module 文件。
"default" 用于在不满足上面任何条件情况下的导出。
 

第三方配置

  • package.json 文件还可以承载命令特有的配置,例如 Babel、ESLint 等。

(1) typings

  • typings字段用来指定TypeScript的入口文件:
"typings": "types/index.d.ts"

(2)eslintConfig

  • eslint的配置可以写在单独的配置文件.eslintrc.json 中,也可以写在package.json文件的eslintConfig配置项中。
"eslintConfig": {
  "root": true,
  "env": {
    "node": true
  },
  "extends": [
    "plugin:@byted-lint/eslint-plugin-meta/react",
    "plugin:@byted-lint/eslint-plugin-meta/typescript",
  ],
  "rules": {},
  "parserOptions": {
    "parser": "babel-eslint"
  }
}

(3) babel

  • babel用来指定Babel的编译配置,代码如下:
"babel": {
        "presets": ["@babel/preset-env"],
        "plugins": [...]
}

(4) unpkg

  • 使用该字段可以让包上所有的文件都开启 cdn 服务,该cdn服务由unpkg提供:
"unpkg": "dist/index.js"

(5) lint-staged

  • lint-staged是一个在Git暂存文件上运行的工具,会将所有暂存文件的列表传递给任务,通常配合gitHooks/husky一起使用。
"lint-staged": {
  "*.{js,ts,tsx}": "eslint --fix"
}
使用lint-staged时,每次提交代码只会检查当前改动的文件。

(6)gitHooks

  • gitHooks用来定义一个钩子,在提交(commit)之前执行ESlint检查。
  • 这里就是配合上面的lint-staged来进行代码的检查操作。
  • 在执行lint命令后,会自动修复暂存区的文件。修复之后的文件并不会存储在暂存区,所以需要重新加入暂存区。
  • 在执行pre-commit命令之后,如果没有错误,就会执行git commit命令。
"gitHooks": {
     "pre-commit": "lint-staged"
}

(7)browserslist

  • 用来告知支持哪些浏览器及版本
"browserslist": [
  "> 5%", //浏览器市场份额至少在全球5%以上
  "not ie", //不能是ie
  "not dead"//浏览器官方更新没有停滞24个月以上
]