关于包管理工具

228 阅读11分钟

npm

我们一般用npm来进行包管理:通过package.json进行依赖项的管理

image.png

package.json 中有非常多的属性,其中必须填写的只有两个: nameversion这两个属性组成一个 npm 模块的唯一标识

npm包命名规则

name 即模块名称,其命名时需要遵循官方的一些规范和建议:

  • 包名会成为模块url、命令行中的一个参数或者一个文件夹名称,任何非url安全的字符在包名中都不能使用,可以使用 validate-npm-package-name 包来检测包名是否合法;
  • 语义化包名,可以帮助开发者更快的找到需要的包,并且避免意外获取错误的包;
  • 若包名称中存在一些符号,将符号去除后不得与现有包名重复;
    例如:由于 react-native 已经存在, react.nativereactnative 都不可以再创建。
  • 如果你的包名与现有的包名太相近导致你不能发布这个包,那么推荐将这个包发布到你的作用域下。
    例如:用户名 conard ,那么作用域为 @conard ,发布的包可以是 @conard/react

查看包名是否被占用

name 是一个包的唯一标识,不得和其他包名重复,我们可以执行 npm view packageName 查看包是否被占用,并可以查看它的一些基本信息;
若包名称从未被使用过,则会抛出 404 错误。

包版本管理

npm 具有优秀的依赖管理系统
我们可以执行 npm view package version 查看某个 package最新版本。执行 npm view xianzao versions 查看某个 package 在npm服务器上所有发布过的版本。

SemVer规范

npm包 中的模块版本都需要遵循 SemVer规范——由 Github 起草的一个具有指导意义的,统一的版本号表示规则。实际上就是 Semantic Version(语义化版本)的缩写。

SemVer规范官网:semver.org

标准版本

SemVer规范的标准版本号采用 X.Y.Z 的格式,其中 X、Y 和 Z 为非负的整数,且禁止在数字前方补零。X 是主版本号、Y 是次版本号、而 Z 为修订号。每个元素必须以数值来递增。

  • 主版本号( major ):当你做了不兼容的API 修改;
  • 次版本号( minor ):当你做了向下兼容的功能性新增;
  • 修订版本号( patch ):当你做了向下兼容的问题修正(修改了bug);

例如:1.9.1 -> 1.10.0 -> 1.11.0

先行版本

当某个版本改动比较大、并非稳定而且可能无法满足预期的兼容性需求时,你可能要先发布一个先行版本。

先行版本号可以加到“主版本号.次版本号.修订号”的后面,先加上一个连接号再加上一连串以句点分隔的标识符和版本编译信息。

  • 内部版本(alpha);
  • 公测版本(beta);
  • 正式版本的候选版本rc: 即 Release candiate;

发布版本

在修改 npm 包某些功能后通常需要发布一个新的版本,我们通常的做法是直接去修改 package.json 到指定版本。如果操作失误,很容易造成版本号混乱,我们可以借助符合 Semver 规范的命令来完成这一操作:

  • npm version patch : 升级修订版本号
  • npm version minor : 升级次版本号
  • npm version major : 升级主版本号

版本工具使用

在开发中肯定少不了对一些版本号的操作,如果这些版本号符合 SemVer 规范 ,我们可以借助 用于操作版本的npm包 semver 来帮助我们进行比较版本大小、提取版本信息等操作。
Npm 也使用了该工具来处理版本相关的工作。

npm install semver

比较版本号大小

semver.gt('1.2.3','9.8.7') // false
semver.lt('1.2.3','9.8.7') // true

判断版本号是否符合规范,返回解析后符合规范的版本号

semver.valid('1.2.3') // '1.2.3'
semver.valid('a.b.c') // null

将其他版本号强制转换成semver版本号

semver.valid(semver.coerce('v2')) // 2.0.0
semver.valid(semver.coerce('42.6.7.9.3-alpha')) // '42.6.7'

一些其他用法

semver.clean(' =v1.2.3 ') // '1.2.3'
semver.satisfies('1.2.3', '1.x || >=2.5.0 || 5.0.0 - 7.2.3') // true
semver.minVersion('>=1.0.0') // '1.0.0'

依赖版本管理

我们经常看到,在 package.json 中各种依赖的不同写法:

"dependencies": { 
  "signale": "1.4.0",  // 固定版本号
  "figlet": "*",  // 任意版本(>=0.0.0"react": "16.x", //匹配主要版本(>=16.0.0 <17.0.0)
  // "react": "16.3.x": 匹配主要版本和次要版本(>=16.3.0 <16.4.0"table": "~5.4.6", 
  "yargs": "^14.0.0"
}
  • ~: 当安装依赖时获取到有新版本时,安装到 x.y.z 中 z 的最新的版本
    例如:~1.2.3 -> >=1.2.3 && <1.3.0;~0.1.2 -> >=0.1.2 && <0.2.0;~0.0.1 -> >=0.0.1 && <0.1.0
  • ^: 当安装依赖时获取到有新版本时,不会改变最左侧非0的数字
    例如:^1.0.0 -> >=1.0.1 && <2.0.0;^0.1.0 -> >=0.1.1 && <0.2.0

package.json 文件中最常见的应该是 "yargs": "^14.0.0" 这种格式的 依赖, 因为我们在使用 npm install package 安装包时,npm 默认安装当前最新版本,然后在所安装的版本号前加 ^ 号。

当主版本号为 0 的情况,会被认为是一个不稳定版本

锁定依赖版本

lock文件

实际开发中,经常会因为各种依赖不一致而产生奇怪的问题,或者在某些场景下,我们不希望依赖被更新,可以在开发中使用 package-lock.json

锁定依赖版本意味着在我们不手动执行更新的情况下,每次安装依赖都会安装固定版本。保证整个团队使用版本号一致的依赖。
每次安装固定版本,无需计算依赖版本范围,大部分场景下能大大加速依赖安装时间。

定期更新依赖

目的是保证团队中使用的依赖一致或者稳定,而不是永远不去更新这些依赖。实际开发场景下,我们虽然不需要每次都去安装新的版本,仍然需要定时去升级依赖版本,来让我们享受依赖包升级带来的问题修复、性能提升、新特性更新。使用 npm outdated 可以帮助我们列出有哪些还没有升级到最新版本的依赖

  • 黄色表示不符合我们指定的语意化版本范围——不需要升级
  • 红色表示符合指定的语意化版本范围——需要升级

执行 npm update升级所有的红色依赖

npm install 原理

嵌套结构

执行 npm install 后,依赖包被安装到了 node_modules
在 npm 的早期版本, npm 处理依赖的方式简单粗暴,以递归的形式,严格按照 package.json 结构以及子依赖包的 package.json 结构将依赖安装到他们各自的 node_modules 中。直到有子依赖包不再依赖其他模块。

举个例子,我们的模块 my-app 现在依赖了两个模块: bufferignore

{ 
  "name": "my-app", 
  "dependencies": { 
    "buffer": "^5.4.3", 
    "ignore": "^5.1.4", 
  }
}

ignore 是一个纯 JS 模块,不依赖任何其他模块,而 buffer 又依赖了下面两个模块: base64-jsieee754

{ 
  "name": "buffer", 
  "dependencies": { 
    "base64-js": "^1.0.2",
    "ieee754": "^1.1.4" 
  }
}

那么,执行 npm install 后,得到的 node_modules 中模块目录结构就是下面这样的:

image.png

这样的方式优点很明显, node_modules 的结构和 package.json 结构一一对应,层级结构明显,并且保证了每次安装目录结构都是相同的。

但是,如果你依赖的模块非常之多,你的 node_modules 将非常庞大,嵌套层级非常之深:

image.png

扁平结构

为了解决以上问题,NPM 在 3.x 版本做了一次较大更新。其将早期的嵌套结构改为扁平结构:

  • 安装模块时,不管其是直接依赖还是子依赖的依赖,优先将其安装在 node_modules 根目录。

还是上面的依赖结构,我们在执行 npm install 后将得到

image.png

但npm 3.x 版本并未完全解决老版本的模块冗余问题,甚至还会带来新的问题。

Lock文件

为了解决 npm install 的不确定性问题,在 npm 5.x 版本新增了 package-lock.json 文件,而安装方式还沿用了 npm 3.x 的扁平化的方式。

package-lock.json 的作用是锁定依赖结构,即只要你目录下有 package-lock.json 文件,那么你每次执行 npm install 后生成的 node_modules 目录结构一定是完全相同的。

缓存

在执行 npm installnpm update 命令下载依赖后,除了将依赖包安装在 node_modules目录下外,还会在本地的缓存目录缓存一份。通过 npm config get cache 命令可以查询到。

npm 在执行安装时,可以根据 package-lock.json 中存储的 integrity、version、name 生成一个唯一的 key 对应到 index-v5 目录下的缓存记录,从而找到 tar包的 hash,然后根据 hash 再去找缓存的 tar包直接使用。

npm 提供了几个命令来管理缓存数据

  • npm cache add:官方解释说这个命令主要是 npm 内部使用,但是也可以用来手动给一个指定的package 添加缓存;
  • npm cache clean:删除缓存目录下的所有数据,为了保证缓存数据的完整性,需要加上 --force参数;
  • npm cache verify:验证缓存数据的有效性和完整性,清理垃圾数据;

基于缓存数据,npm 提供了离线安装模式,分别有以下几种:

  • --prefer-offline: 优先使用缓存数据,如果没有匹配的缓存数据,则从远程仓库下载;
  • --prefer-online: 优先使用网络数据,如果网络数据请求失败,再去请求缓存数据,这种模式可以及时获取最新的模块;
  • --offline: 不请求网络,直接使用缓存数据,一旦缓存数据不存在,则安装失败;

前端打包工具对比

Yarn

Yarn,或 Yet Another Resource Navigator ,是 Facebook 开发的一个相对较新的包管理器。它的开发是为了提供 NPM 当时缺乏的更高级的功能(例如版本锁定,但后续的功能npm都补充上了),同时也使其更安全、更可靠和更高效。

Yarn 现在更像是 NPM 的替代品,由于 Yarn 没有预装 Node.js,因此需要显式安装:

npm install yarn -g

全局安装后,您可以通过在项目中设置版本来在每个项目的基础上使用它,如下所示:

yarn set version <version-name>

yarn的特点:

  1. 即插即用:从 Yarn 版本 2 开始,不再使用 node_modules 文件夹。相反,它会生成一个映射项目依赖关系的 .pnp.cjs 文件。这会导致更优化的依赖树和更快的项目启动和包安装;
  2. 零安装:此功能与 Plug'n'Play 结合使用,后者使用 .pnp.cjs 文件来映射离线缓存中的包。这使您可以快速检索和安装已保存的软件包;
  3. 检查器:Yarn 带有一个内置的检查器,用于下载和安装包;

Yarn 和 NPM 之间的相似之处

  1. Yarn 和 NPM 都会自动生成一个版本锁定文件,用于跟踪项目使用的依赖项的确切列表;
  2. Yarn 和 NPM 都提供了将依赖项保存在离线缓存中的选项,即使您处于离线状态,您也可以安装依赖项;
  3. Yarn 和 NPM 都支持工作空间,允许您从单个存储库管理多个项目的依赖关系;
  4. 使用 NPM 中的 npx 命令和 Yarn 中的 yarn dlx 命令,您可以在两个管理器中远程运行脚本;

Yarn 和 NPM 的区别

依赖管理

YarnNPM
使用 yarn add 命令来安装依赖项使用 npm install 命令安装依赖项
使用并行安装依赖项按顺序安装依赖项
使用 yarn.lock 作为锁定文件使用 package-lock.json 作为锁定文件
支持即插即用功能,它会生成一个 .pnp.cjs 文件,其中包含项目的依赖关系图不支持

Yarn 也支持 NPM 创建的 package-lock.json 文件,方便将版本数据从 NPM 迁移到 Yarn

性能和速度

YarnNPM
使用并行安装依赖项按顺序安装依赖项
在安装大文件时更快安装大文件时更慢
支持零安装功能,允许您离线安装依赖项,几乎没有延迟不支持

安全

YarnNPM
在下载包时,它会利用包的许可信息在后台运行安全检查,以避免下载危险的脚本或导致依赖问题在 NPM 的早期版本中,安全性是一个主要问题。从版本 6 开始,每次安装包时,NPM 都会进行安全审计以避免漏洞并确保没有不兼容的依赖项
校验和验证包使用存储在 package-lock.json 文件中的 SHA-512进行验证

使用 NPM,还可以执行手动审核以查找任何漏洞并解决它。要查找漏洞,可以使用 npm audit

指令

CommandNPMYarn
初始化项目npm inityarn init
运行脚本npm runyarn run
运行测试用例npm testyarn test
安装依赖npm installyarn
安装指定包npm install yarn add
删除指定包npm uninstall <packagename>yarn remove <packagename>
全局安装包npm install -g <packagename>yarn global add <packagename>
全局删除包npm uninstall -g <packagename>yarn global remove
更新包npm update <packagename>yarn upgrade <packagename>
执行依赖更新npm run upgrade-interactiveyarn upgrade-interactive
检查过时的包npm outdatedyarn outdated
管理本地缓存npm cache cleanyarn cache clean
登录/退出npm login/logoutyarn login/logout
发布包npm publishyarn publish
更新包管理器npm updateyarn upgrade
远端运行包Not Supported (but npx)yarn dlx
检查许可证Not Supportedyarn licenses ls

如何选择

yarn

优点

  1. 支持并行安装、即插即用和零安装等功能,从而获得更好的性能;
  2. 更安全;
  3. 庞大的活跃用户社区;

缺点

  1. 不适用于旧版本的 Node.js(低于版本 5);
  2. 安装本机模块的问题;

NPM

优点

  1. 易于使用,特别适用于习惯工作流旧版本的开发人员;
  2. 优化本地包安装,节省硬盘空间;

缺点

  1. 需要网络访问才能从在线注册表安装软件包;
  2. 安全漏洞仍然存在;

PNPM

pnpm,即performant npm,高性能的npm。相比起目前主流的包管理器,pnpm是速度快、节省磁盘空间的包管理器。
与目前主流的包管理器npm、yarn相比,无论有无 cache 、有无 lockfile 、有无 node_modules ,pnpm的安装速度都有明显的优势