包管理工具
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 publishnpm 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 lintnpm 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 --forcenpm cache clear --forcenode_modules 的结构问题
npm v2时期
嵌套结构
package-a
|--lodash@4.17.4
package-b
|--lodash@4.17.4package-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.4package-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 件事情
借助软链接的方式,解决了重复依赖安装的问题,节省了单个项目的体积
借助硬链接的方式,节省了多个项目重复依赖的体积