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 [email protected]
来实现升级。因为 1.1.0package-lock.json
里记录的 [email protected]是不一致的,因此会更新package-lock.json
里的 A 的版本为 1.1.0。
- 如果后面,B 发布了新版本 1.1.0, 此刻如果我们不做操作是不会自动升级 B 的版本的。但如果我们项目里升级 [email protected],此时
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
里 [email protected] 满足 ^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" }
}
}
可能被意外更改的原因
- package.json 文件修改了
- 挪动了包的位置
dependencies
移动到 devDependencies
这种操作,虽然包未变,但是也会影响 package-lock.json
。
- registry(镜像)的影响
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
的存在,主要是期望宿主应用安装这些依赖,让相同依赖不会在宿主应用和库中被重复安装。
- 同时为了减少库或插件的大小的目的,将一些依赖统一放到宿主应用安装。
"peerDependencies": {
"react": "^16.8.3 || ^17 || ^18"
},
如果本地没有react安装,安装react-redux报警告:
npm WARN [email protected] 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
。
"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.mjs
和index.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
(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
,同时也高于module
和browser
字段。
用法
(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个月以上
]
标签:npm,package,js,json,main,浅析,browser
From: https://www.cnblogs.com/gg-qq/p/16836685.html