包管理工具
sermer
Semantic Versioning
语义化版本的缩写,由[major, minor, patch]
3 部分构成。
major
: 当你发了一个含有 Breaking Change 的 API minor
: 当你新增了一个向后兼容的功能时 patch
: 当你修复了一个向后兼容的 Bug 时
在发包前,npm version 的相关命令可以自动更新版本
# 增加一个修复版本号: 1.0.1 -> 1.0.2 (自动更改 package.json 中的 version 字段)
$ npm version patch
# 增加一个小的版本号: 1.0.1 -> 1.1.0 (自动更改 package.json 中的 version 字段)
$ npm version minor
# 将更新后的包发布到 npm 中
$ npm publish
# 增加一个修复版本号: 1.0.1 -> 1.0.2 (自动更改 package.json 中的 version 字段)
$ npm version patch
# 增加一个小的版本号: 1.0.1 -> 1.1.0 (自动更改 package.json 中的 version 字段)
$ npm version minor
# 将更新后的包发布到 npm 中
$ npm publish
~
和^
的范围
对于
~1.2.3
而言,它的版本号范围是>=1.2.3 <1.3.0
对于
^1.2.3
而言,它的版本号范围是>=1.2.3 <2.0.0
package-lock 的工作流程
当
package-lock.json
该package
锁死的版本号符合package.json
中的版本号范围时,将以package-lock.json
锁死版本号为主。当
package-lock.json
该package
锁死的版本号不符合package.json
中的版本号范围时,将会安装该package
符合package.json
版本号范围的最新版本号,并重写package-lock.json
package.json 中的一些关键字段
main
{
"module": "./dist/index.js"
}
{
"module": "./dist/index.js"
}
指定了入口文件,是 CommonJS 时代的产物。
module
示例:
{
"module": "./es/index.mjs"
}
{
"module": "./es/index.mjs"
}
指定了 ESM 模块的入口文件,如果使用了 import 进行导入,文件系统会先查找 module 指定的文件,如果未找到则使用 main 指定的文件。
随着 ESM 的发展,许多 package 会打包成多种模块化格式,如antd
既支持ESM
又支持umd
,分别打包出es
和dist
目录。
如果代码只分发ESM
方案,则直接使用main
指定即可。
exports
{
"exports": {
".": "./dist/index.js",
"get": "./dist/get.js"
}
}
{
"exports": {
".": "./dist/index.js",
"get": "./dist/get.js"
}
}
可以控制子目录的路径,如指定了此项,那么不在 exports 字段中的模块则无法引用。
exports
还可以根据环境变量和运行环境导入不同的入口文件。
{
"type": "module",
"exports": {
"electron": {
"node": {
"development": {
"module": "./index-electron-node-with-devtools.js",
"import": "./wrapper-electron-node-with-devtools.js",
"require": "./index-electron-node-with-devtools.cjs"
},
"production": {
"module": "./index-electron-node-optimized.js",
"import": "./wrapper-electron-node-optimized.js",
"require": "./index-electron-node-optimized.cjs"
},
"default": "./wrapper-electron-node-process-env.cjs"
},
"development": "./index-electron-with-devtools.js",
"production": "./index-electron-optimized.js",
"default": "./index-electron-optimized.js"
},
"node": {
"development": {
"module": "./index-node-with-devtools.js",
"import": "./wrapper-node-with-devtools.js",
"require": "./index-node-with-devtools.cjs"
},
"production": {
"module": "./index-node-optimized.js",
"import": "./wrapper-node-optimized.js",
"require": "./index-node-optimized.cjs"
},
"default": "./wrapper-node-process-env.cjs"
},
"development": "./index-with-devtools.js",
"production": "./index-optimized.js",
"default": "./index-optimized.js"
}
}
{
"type": "module",
"exports": {
"electron": {
"node": {
"development": {
"module": "./index-electron-node-with-devtools.js",
"import": "./wrapper-electron-node-with-devtools.js",
"require": "./index-electron-node-with-devtools.cjs"
},
"production": {
"module": "./index-electron-node-optimized.js",
"import": "./wrapper-electron-node-optimized.js",
"require": "./index-electron-node-optimized.cjs"
},
"default": "./wrapper-electron-node-process-env.cjs"
},
"development": "./index-electron-with-devtools.js",
"production": "./index-electron-optimized.js",
"default": "./index-electron-optimized.js"
},
"node": {
"development": {
"module": "./index-node-with-devtools.js",
"import": "./wrapper-node-with-devtools.js",
"require": "./index-node-with-devtools.cjs"
},
"production": {
"module": "./index-node-optimized.js",
"import": "./wrapper-node-optimized.js",
"require": "./index-node-optimized.cjs"
},
"default": "./wrapper-node-process-env.cjs"
},
"development": "./index-with-devtools.js",
"production": "./index-optimized.js",
"default": "./index-optimized.js"
}
}
dependencies 与 devDependencies
当进行业务开发时,严格区分 dependencies
与 devDependencies
并无必要,实际上,大部分业务对二者也并无严格区别。
但是对于库 (Package
) 开发而言,是有严格区分的。
当在项目中安装一个依赖的 Package
时,该依赖的 dependencies
也会安装到项目中,即被下载到 node_modules
目录中。但是 devDependencies
不会。
engines
可以用于指定一个项目所需的 node 最小版本。
{
"engines": {
"node": ">=14.0.0"
}
}
{
"engines": {
"node": ">=14.0.0"
}
}
如果对于版本不匹配的情况,npm 会抛出警告,而 yarn 会直接报错。
files
控制实际发包内容,通常只需要发构建后的资源,源代码目录可发可不发
{
"files": ["dist"]
}
{
"files": ["dist"]
}
npm scripts
npm 自带 script
npm install
npm test
npm publish
npm install
npm test
npm publish
自定义工具链
{
"start": "serve ./dist",
"build": "webpack",
"lint": "eslint"
}
{
"start": "serve ./dist",
"build": "webpack",
"lint": "eslint"
}
npm run start
npm run build
npm run lint
npm run start
npm run build
npm run lint
生命周期
npm scripts 的生命周期常用来解决 2 大问题
在某个 npm 包安装完毕后,执行自动操作
npm 包在发布前需要执行自动打包
当我们执行任意npm run
脚本时,将会自动触发pre
和post
的生命周期。
pre
发生在执行前,post
发生在执行后
例如:
{
"scripts": {
"postinstall": "patch-package"
}
}
{
"scripts": {
"postinstall": "patch-package"
}
}
那么在执行完npm install
后将会自动执行npm run postintall
而发包涉及到的声明周期则更为复杂,当执行npm publish
时,将自动执行以下脚本
prepublishOnly: 如果发包之前需要构建,可以放在这里执行
prepack
prepare: npm install 后和 npm publish 前都会执行
postpack
publish
postpublish
如果涉及到类型检查、测试、构建,最常用的是 prepublishOnly
{
"scripts": {
"prepublishOnly": "npm run test && npm run build"
}
}
{
"scripts": {
"prepublishOnly": "npm run test && npm run build"
}
}
而prepare
,会在npm install
之后执行,在npm publish
之前执行。例如需要安装 husky
{
prepare: "husky install";
}
{
prepare: "husky install";
}
npm scripts
也存在一定的风险,例如被攻击后注入了 npm postinstall 自动执行一些事,例如挖矿。
npm cache
npm
会把所有下载的包,保存在用户文件夹下面。
下次 npm install
时,会根据 package-lock.json
里面保存的 sha1
值去文件夹里面寻找包文件,如果找到就不用从新下载安装了。
可以通过以下命令清空缓存
npm cache clear --force
npm cache clear --force
node_modules 的结构问题
npm v2
时期
嵌套结构
package-a
|--lodash@4.17.4
package-b
|--lodash@4.17.4
package-a
|--lodash@4.17.4
package-b
|--lodash@4.17.4
存在 2 大问题
嵌套过深
体积过大
npm v3
之后
平铺结构
package-a
package-b
|--lodash@4.16.1
lodash@4.17.4
package-a
package-b
|--lodash@4.16.1
lodash@4.17.4
如果a
和b
分别依赖了不同的lodash
版本,那么会有一个版本被放到子目录中,产生分身
。
幻影依赖
当项目中使用了一个没有在package.json
中定义的包时,便产生了幻影依赖。
{
"name": "my-library",
"version": "1.0.0",
"main": "lib/index.js",
"dependencies": {
"minimatch": "^3.0.4"
},
"devDependencies": {
"rimraf": "^2.6.2"
}
}
{
"name": "my-library",
"version": "1.0.0",
"main": "lib/index.js",
"dependencies": {
"minimatch": "^3.0.4"
},
"devDependencies": {
"rimraf": "^2.6.2"
}
}
var minimatch = require('minimatch')
var expand = require('brace-expansion') // ???
var glob = require('glob') // ???
var minimatch = require('minimatch')
var expand = require('brace-expansion') // ???
var glob = require('glob') // ???
这是因为 node_modules
目录的平铺结构,并且 node
的 esm
不需要考虑 package.json
,所以它找到了这些库。
这就是 NPM
的 node_modules
树的特性,是必然的,是由其设计导致,无法避免。
分身的结果
小项目内很少遇到分身,但是在大型的 monorepo 中很常见,这会导致一些问题。
更慢的安装时间
增大包体积
非单一的全局变量
多重类型
语义上的分身
破坏单例模式,破坏缓存,如
postcss
的许多插件将postcss
扔进dependencies
,重复的版本将导致解析 AST 多次
包管理工具
npm
: 它是当今最广泛的 JavaScript 包管理工具,它开创了包管理标准,其开发者还维护了世界上最多人使用的分布式开源 JavaScript 包管理网站 npmjs.com
yarn
: 它重新实现了 NPM, 与之相比,Yarn 具有相同的管理方式,但是安装速度更快(多线程),稳定性更好,而且提供了一些新特性(例如 Yarn workspaces),用于大型开发。
pnpm
: 它提供了一个全新的包管理模式,该模式解决了“幻影依赖”和“ NPM 分身”的问题,同时符号链接使之与 NodeJS
模块解析标准保持 100% 兼容。
pnpm 详解
软链接和硬链接
$ ln -s hello hello-soft
$ ln hello hello-hard
$ ls -lh
total 768
45459612 -rw-r--r-- 2 xiange staff 153K 11 19 17:56 hello
45459612 -rw-r--r-- 2 xiange staff 153K 11 19 17:56 hello-hard
45463415 lrwxr-xr-x 1 xiange staff 5B 11 19 19:40 hello-soft -> hello
$ ln -s hello hello-soft
$ ln hello hello-hard
$ ls -lh
total 768
45459612 -rw-r--r-- 2 xiange staff 153K 11 19 17:56 hello
45459612 -rw-r--r-- 2 xiange staff 153K 11 19 17:56 hello-hard
45463415 lrwxr-xr-x 1 xiange staff 5B 11 19 19:40 hello-soft -> hello
他们的区别有以下几点:
1.软链接可理解为指向源文件的指针,它是单独的一个文件,仅仅只有几个字节,它拥有独立的 inode
2.硬链接与源文件同时指向一个物理地址,它与源文件共享存储数据,它俩拥有相同的 inode
pnpm 为何省空间
它解决了 npm/yarn
平铺 node_modules
带来的依赖项重复的问题 (doppelgangers
)
生成的 node_modules
结构如图所示
./node_modules/package-a -> .pnpm/package-a@1.0.0/node_modules/package-a
./node_modules/package-b -> .pnpm/package-b@1.0.0/node_modules/package-b
./node_modules/package-c -> .pnpm/package-c@1.0.0/node_modules/package-c
./node_modules/package-d -> .pnpm/package-d@1.0.0/node_modules/package-d
./node_modules/.pnpm/lodash@3.0.0
./node_modules/.pnpm/lodash@4.0.0
./node_modules/.pnpm/package-a@1.0.0
./node_modules/.pnpm/package-a@1.0.0/node_modules/package-a
./node_modules/.pnpm/package-a@1.0.0/node_modules/lodash -> .pnpm/package-a@1.0.0/node_modules/lodash@4.0.0
./node_modules/.pnpm/package-b@1.0.0
./node_modules/.pnpm/package-b@1.0.0/node_modules/package-b
./node_modules/.pnpm/package-b@1.0.0/node_modules/lodash -> .pnpm/package-b@1.0.0/node_modules/lodash@4.0.0
./node_modules/.pnpm/package-c@1.0.0
./node_modules/.pnpm/package-c@1.0.0/node_modules/package-c
./node_modules/.pnpm/package-c@1.0.0/node_modules/lodash -> .pnpm/package-c@1.0.0/node_modules/lodash@3.0.0
./node_modules/.pnpm/package-d@1.0.0
./node_modules/.pnpm/package-d@1.0.0/node_modules/package-d
./node_modules/.pnpm/package-d@1.0.0/node_modules/lodash -> .pnpm/package-d@1.0.0/node_modules/lodash@3.0.0
./node_modules/package-a -> .pnpm/package-a@1.0.0/node_modules/package-a
./node_modules/package-b -> .pnpm/package-b@1.0.0/node_modules/package-b
./node_modules/package-c -> .pnpm/package-c@1.0.0/node_modules/package-c
./node_modules/package-d -> .pnpm/package-d@1.0.0/node_modules/package-d
./node_modules/.pnpm/lodash@3.0.0
./node_modules/.pnpm/lodash@4.0.0
./node_modules/.pnpm/package-a@1.0.0
./node_modules/.pnpm/package-a@1.0.0/node_modules/package-a
./node_modules/.pnpm/package-a@1.0.0/node_modules/lodash -> .pnpm/package-a@1.0.0/node_modules/lodash@4.0.0
./node_modules/.pnpm/package-b@1.0.0
./node_modules/.pnpm/package-b@1.0.0/node_modules/package-b
./node_modules/.pnpm/package-b@1.0.0/node_modules/lodash -> .pnpm/package-b@1.0.0/node_modules/lodash@4.0.0
./node_modules/.pnpm/package-c@1.0.0
./node_modules/.pnpm/package-c@1.0.0/node_modules/package-c
./node_modules/.pnpm/package-c@1.0.0/node_modules/lodash -> .pnpm/package-c@1.0.0/node_modules/lodash@3.0.0
./node_modules/.pnpm/package-d@1.0.0
./node_modules/.pnpm/package-d@1.0.0/node_modules/package-d
./node_modules/.pnpm/package-d@1.0.0/node_modules/lodash -> .pnpm/package-d@1.0.0/node_modules/lodash@3.0.0
它做了 2 件事情
借助软链接的方式,解决了重复依赖安装的问题,节省了单个项目的体积
借助硬链接的方式,节省了多个项目重复依赖的体积