前端包管理工具对比

前端包管理工具总结

1 包管理工具

1.1 npm

作用

npm 是随同 NodeJS 一起安装的包管理工具,允许用户下载或上传包或命令行程序供开发者使用;

npm install 流程

执行命令后,首先会构建依赖树,然后针对每个节点下的包,会经历下面四个步骤:

  1. 将依赖包的版本区间解析为某个具体的版本号
  2. 下载对应版本依赖的 tar 包到本地离线镜像
  3. 将依赖从离线镜像解压到本地缓存
  4. 将依赖从缓存拷贝到当前目录的 node_modules 目录

常用命令

npm init -y 直接生成默认配置

npm -v 版本查看

npm list 查看已安装包 带上[–depth 0] 不深入到包的支点

npm info jquery 查看版本

npm install 包名@版本号

npm install -g cnpm –registry=https://registry.npm.taobao.org 安装淘宝镜像

npm search 搜索词 -g 用于搜索 npm 仓库,它后面可以跟字符串,也可以跟正则表达式

npm update [-g] 更新模块

npm run 执行脚本

npm publish 发布模块

npm dedupe 合并重复依赖,减少重复

问题(3 版本之前)

  1. 依赖层级太深,会导致文件路径过长的问题,尤其在 window 系统下,会造成安装失败或删除 node_modules 失败。
  2. 大量重复的包被安装,node_moudles 体积超级大。比如跟 foo 同级目录下有一个 baz,两者都依赖于同一个版本的 lodash,那么 lodash 会分别在两者的 node_modules 中被安装,也就是重复安装。
  3. 模块实例不能共享。比如 React 有一些内部变量,在两个不同包引入的 React 不是同一个模块实例,因此无法共享内部变量,导致一些不可预知的 bug。

PS:3 版本及其后的和 yarn 的问题一致;

包调试方案

npm link 用于连接本地项目和本地 npm 模块,使得可以在本地进行模块测试;

具体用法

  1. 项目和模块在同一个目录下,可以使用相对路径:npm link ../module
  1. 项目和模块不在同一个目录下
    1. cd 到模块目录,npm link,进行全局 link
    2. cd 到项目目录,npm link 模块名(package.json 中的 name)
  1. 解除 link:npm unlink 模块名

原理:在全局包路径中创建一个软链(Symlinked)指向对应的 npm 包,然后在项目中通过软链将全局的软链指向到 node_modules 的对应包中;

此方案缺点

  1. 影响 node_modules 中原本的依赖包;
  2. 软链接和文件系统引发的其他各种奇怪的问题;
  3. webpack 在进行编译的时候无法编译软链接的依赖库。

相对路径或者绝对路径使用

// import { Button } from ‘good-ui’ // 为了调试,强行改成了绝对或者相对路径 import { Button } from ‘C:/codes/good-ui/dist’

此方案缺点:需要频繁改业务代码,这既麻烦又危险(路径有可能进行修改,在 git 提交代码的时候,引用路径忘记修正回来则其他开发者无法正常使用)。

yalc

模拟 npm 发布,并将包缓存本地,下载时也是模拟 npm install,所以不会存在相关依赖库丢失,只是模拟发布和下载不会真的推包;

使用方法

  1. 需要发包的项目中执行:yarn build && yalc publish
  2. 使用包的项目中执行:yalc add 包名

其他概念

dependencies 与 devDependencies 之间的区别:dependencie 配置当前程序所依赖的其他包。devDependencie 配置当前程序所依赖的其他包,只会下载模块,而不下载这些模块的测试和文档框架

包版本概念:^表示第一位版本号不变,后面两位取最新的;~表示前两位不变,最后一个取最新;*表示全部取最新

lock 文件:npm5 通过添加 lock 文件来记录依赖树信息,进行依赖锁定,从而唯一确定 node_modules 的结构,这样处理可以保证团队成员使用同一份 node_modules 依赖结构。

1.2 yarn

快速、可靠、安全的依赖管理工具。

安装

npm install –global yarn

作用

  1. 速度快:Yarn 缓存了每个下载过的包,所以再次使用时无需重复下载。 同时利用并行下载以最大化资源利用率,因此安装速度更快。如果你以前安装过某个包,再次安装时可以在没有任何互联网连接的情况下进行。
  2. 超级安全:在执行代码之前,Yarn 会通过算法校验每个安装包的完整性。
  3. 超级可靠:使用详细、简洁的锁文件格式和明确的安装算法,Yarn 能够保证在不同系统上无差异的工作。
  4. 确定性:不管安装顺序如何,相同的依赖关系将在每台机器上以相同的方式安装。
  5. 网络性能:Yarn 有效地对请求进行排队处理,避免发起的请求如瀑布般倾泻,以便最大限度地利用网络资源。
  6. 相同的软件包:从 npm 安装软件包并保持相同的包管理流程。
  7. 网络弹性:重试机制确保单个请求失败并不会导致整个安装失败。
  8. 扁平模式:将依赖包的不同版本归结为单个版本,以避免创建多个副本。所有的依赖都被拍平到 node_modules 目录下,不再有很深层次的嵌套关系。这样在安装新的包时,根据 node require 机制,会不停往上级的 node_modules 当中去找,如果找到相同版本的包就不会重新安装,解决了大量包重复安装的问题,而且依赖层级也不会太深。

问题

  1. NPM 分身:依赖结构的不确定性。只提升 package.json 里面排在前面的包的重复引用包,其他版本的不提升;
  2. 扁平化算法本身的复杂性很高,耗时较长。
  3. 幽灵依赖或幻影依赖:项目中仍然可以非法访问 package.json 没有声明过依赖的包,因为部分包被提升了;

常用命令

命令 慕课释义
yarn add 添加依赖
yarn audit 对已安装的软件包执行漏洞审核
yarn autoclean 从程序包依赖项中清除并删除不必要的文件
yarn bin 显示依赖 bin 文件夹的位置
yarn cache 管理用户目录中的依赖缓存
yarn check 验证当前项目中程序包依赖项
yarn config 管理依赖配置文件
yarn create 创建 Yarn 工程
yarn dedupe 删除重复的依赖
yarn generate-lock-entry 生成 Yarn 锁文件
yarn global 在全局安装依赖
yarn help 显示 Yarn 的帮助信息
yarn import 迁移当前依赖的项目 package-lock.json
yarn info 显示有关依赖的信息
yarn init 初始化工程并创建 package.json 文件
yarn install 用于安装项目的所有依赖项
yarn licenses 列出已安装依赖的许可证及源码 url
yarn link 链接依赖文件夹
yarn list 列出已安装的依赖
yarn login 存储您在 registry 上的用户名和 email
yarn logout 清除你在 registry 上用户名和 email
yarn outdated 列出所有依赖项的版本信息
yarn owner 展示依赖作者
yarn pack 创建依赖项的压缩 gzip
yarn policies 规定整个项目中执行 Yarn 的版本
yarn publish 将依赖发布到 npm 注册表
yarn remove 删除依赖
yarn run 运行定义的程序脚本命令
yarn tag 在依赖上添加,删除或列出标签
yarn team 管理组织中的团队,并更改团队成员身份
yarn test 运行程序的 test 命令
yarn upgrade 将指定依赖升级为最新版本
yarn upgrade-interactive 更新过期依赖的简便方法
yarn version 展示依赖版本信息
yarn versions 展示所有依赖项版本信息
yarn why 显示有关为什么安装依赖的信息
yarn workspace Yarn 的工作区信息
yarn workspaces Yarn 的所有工作区信息

1.3 pnpm

快速的,节省磁盘空间的包管理工具;

安装

npm install –global pnpm

作用

  1. 快速:pnpm 比其他包管理器快 2 倍;
  2. 高效:node_modules 中的文件为复制或链接自特定的内容寻址存储库;
  3. 支持 monorepos:pnpm 内置支持单仓多包;
  4. 严格:pnpm 默认创建了一个非平铺的 node_modules,因此代码无法访问任意包;

原理

node_modules 并不是扁平化结构,而是目录树结构,同时还有个.pnpm 目录,.pnpm 以平铺的形式存储着所有的包,并以组织名(若无会省略)+包名@版本号/node_modules/名称(项目名称) 结构存储;由于它只会根据项目中的依赖生成,并不存在提升,所以它不会存在之前提到的幻影依赖问题;

pnpm 资源在磁盘上的存储位置为.pnpm-store 的文件夹中,Mac/linux 中默认会设置到{home dir}>/.pnpm-store/v3;windows 下会设置到当前盘的根目录下,比如 C(C/.pnpm-store/v3)、D 盘(D/.pnpm-store/v3)。由于每个磁盘有自己的存储方式,所以 Store 会根据磁盘来划分。 如果磁盘上存在主目录,存储则会被创建在 /.pnpm-store;如果磁盘上没有主目录,那么将在文件系统的根目录中创建该存储。 例如,如果安装发生在挂载在 /mnt 的文件系统上,那么存储将在 /mnt/.pnpm-store 处创建。 Windows 系统上也是如此。可以在不同的磁盘上设置同一个存储,但在这种情况下,pnpm 将复制包而不是硬链接它们,因为硬链接只能发生在同一文件系统同一分区上。如图可以看到在使用 pnpm 对项目安装依赖的时候,如果某个依赖在 sotre 目录中存在了话,那么就会直接从 store 目录里面去 hard-link,避免了二次安装带来的时间消耗,如果依赖在 store 目录里面不存在的话,就会去下载一次。

常用命令

  1. pnpm store prune 删除不被引用的包
  2. pnpm add xxx 添加包
    1. –save-prod, -P:安装到dependencies
    2. –save-dev, -D:安装到devDependencies
    3. –save-optional, -O:安装到optionalDependencies
    4. –save-peer:安装到peerDependenciesdevDependencies
    5. –global:安装全局依赖。
    6. –workspace:仅添加在 workspace 找到的依赖项。
  1. pnpm remove xxx 删除某个包
  2. pnpm install 安装所有依赖
  3. pnpm list 以一个树形结构输出所有的已安装 package 的版本及其依赖。添加参数–json 后会输出 JSON 格式的日志。
  4. pnpm run xxx 跑脚本;

其他概念

inode :是描述文件/目录属性的数据库,例如元数据和硬盘上的物理位置. 它们本质上是完整地址的数字等价物。使用 inode,操作系统可以检索有关文件的信息,例如权限和数据在硬盘驱动器上的物理位置,以访问文件。如果文件从一个文件夹移动到另一个文件夹,该文件将被移动到硬盘驱动器上的不同位置,其 inode 值将随之自动更改。

硬连接:硬连接指通过索引节点来进行连接。在 Linux 的文件系统中,保存在磁盘分区中的文件不管是什么类型都给它分配一个编号,称为索引节点号(Inode Index)。在 Linux 中,多个文件名指向同一索引节点是存在的。一般这种连接就是硬连接。硬连接的作用是允许一个文件拥有多个有效路径名,这样用户就可以建立硬连接到重要文件,以防止“误删”的功能。其原因如上所述,因为对应该目录的索引节点有一个以上的连接。只删除一个连接并不影响索引节点本身和其它的连接,只有当最后一个连接被删除后,文件的数据块及目录的连接才会被释放。也就是说,文件真正删除的条件是与之相关的所有硬连接文件均被删除。hark link 只能用于文件不能用于目录

软连接:另外一种连接称之为符号连接(Symbolic Link),也叫软连接。软链接文件有类似于 Windows 的快捷方式。它实际上是一个特殊的文件。在符号连接中,文件实际上是一个文本文件,其中包含的有另一文件的位置信息。目录使用软连接

peerDependencies的目的是提示宿主环境去安装满足插件 peerDependencies 所指定依赖的包,然后在插件 import 或者 require 所依赖的包的时候,永远都是引用宿主环境统一安装的 npm 包,最终解决插件与所依赖包不一致的问题。

pnpm v3 链接图:

2 Monorepo

monorepo 就是把多个工程放到一个 git 仓库中进行管理,因此他们可以共享同一套构建流程(更改代码、发包)、代码规范也可以做到统一,特别是如果存在模块间的相互引用的情况,查看代码、修改 bug、调试等会更加方便。

3 基于 lerna 搭建 monorepo

lerna 是一个管理工具,用于管理包含多个软件包(package)的 js 项目,优化了使用 git 和 npm 管理多包存储库的工作流。

3.1 工作的两种模式

lerna 默认使用的是集中版本,所有的 package 共用一个 version。如果希望不同的 package 拥有自己的版本,可以使用Independent模式

Fixed/Locked mode (default)

vue,babel 都是用这种,在 publish 的时候,会在 lerna.json 文件里面”version”: “0.1.5”,依据这个号,进行增加,只选择一次,其他有改动的包自动更新版本号。

Independent mode

lerna init –independent 初始化项目。 lerna.json 文件里面”version”: “independent”,

每次 publish 时,您都将得到一个提示符,提示每个已更改的包,以指定是补丁、次要更改、主要更改还是自定义更改。

3.2 解决了哪些问题?

规范问题和简化流程;

  1. 自动解决 packages 之间的依赖关系
  2. 可采用 Independent 模式,通过 git 检测文件改动,自动发布,;
  3. 根据 git 提交记录,自动生成 CHANGELOG
  4. 统一整个工程化,比如 eslint 规则检查、prettier 自动格式化代码、提交代码,代码检查 hook、遵循 semver 版本规范

3.3 指令总览

指令 解释 链接(英文)
lerna publish 在当前项目中发布包注意: Lerna 永远不会发布标记为 private 的包(package.json 中的”private“: true) 前往
lerna version 更改自上次发布以来的包版本号 前往
lerna bootstrap 将本地包链接在一起并安装剩余的包依赖项 前往
lerna list 列出本地包 前往
lerna changed 列出自上次标记发布以来发生变化的本地包 前往
lerna diff 自上次发布以来的所有包或单个包的区别 前往
lerna exec 在每个包中执行任意命令 前往
lerna run 在包含该脚本中的每个包中运行 npm 脚本 前往
lerna init 创建一个新的 Lerna 仓库或将现有的仓库升级到 Lerna 的当前版本 前往
lerna add 向匹配的包添加依赖关系 前往
lerna clean 从所有包中删除 node_modules 目录 前往
lerna import 将一个包导入到带有提交历史记录的 monorepo 中 前往
lerna link 将所有相互依赖的包符号链接在一起 前往
lerna create 创建一个新的由 lerna 管理的包 前往
lerna info 打印本地环境信息 前往

3.4 搭建 lerna 项目

  1. 安装 lerna:npm install –global lerna
  2. 初始化项目:git init lerna-repo && cd lerna-repo
  3. 初始化 lerna:lerna init;得到文件夹 lerna-repo/ packages/ package.json lerna.json
  4. 创建子应用:lernam create 子应用名称;

4 基于 pnpm 搭建 monorepo

  1. 调整目录结构如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# app
├── packages
│   ├── pkg1
│   │   ├── package.json
│   │   └── pnpm-lock.yaml
│   ├── pkg2
│   │   ├── package.json
│   │   └── pnpm-lock.yaml
│   ├── pkg3
│   │   ├── package.json
│   │   └── pnpm-lock.yaml
│   └── app
│   ├── package.json
│   └── pnpm-lock.yaml
├── package.json
├── pnpm-lock.yaml
└── pnpm-workspace.yaml
  1. 配置 pnpm-workspace.yaml,让 pnpm 知道都有哪些 workspace。
1
2
3
4
5
6
7
8
# ./pnpm-workspace.yaml
packages:
# root directory
- "."
# all packages in subdirs of packages/
- "packages/**"
# exclude packages that are inside test/ directories
- "!**/test/**" # '!' means exclude
  1. 配置执行脚本,如执行某个包的 dev 指令,pnpm run –filter @package/app dev
  2. 批量执行命令,如对所有的包进行 lint:pnpm run –filter="@app/*" lint
  3. 复用同仓库下的代码:假设 app 依赖于pkg1@1.5.0pkg2@1.5.0,而后两者均依赖于pkg3@1.5.0。常规的做法是直接使用 npm 上的版本。但是如果想直接用当前正在开发中的pkg3@1.5.1,而又还没有发布到 npm 上, 就很难办了。总不能 import xxx from “../../pacakge/pkc3/xxx”吧。这个时候 workspace 就派上了用场,可以这样给 pkg1 写依赖;在设置依赖版本的时候推荐用 workspace: *,就可以保持依赖的版本是工作空间里最新版本,不需要每次手动更新依赖版本。
1
2
3
4
5
6
// packages/pkg1/package.json
{
"dependencies": {
"@laffery/pkg3": "workspace:1.5.1",
}
}

5 Turborepo

Turborepo 是一个为 monorepo 而生的极快的构建系统。目的是为了解决大型 monorepo 项目构建速度缓慢的一大痛点。turbo 的核心是永远不会重新构建已经构建过的内容。turbo 会把每次构建的产物与日志缓存起来,下次构建时只有文件发生变动的部分才会重新构建,没有变动的直接命中缓存并重现日志。turbo 拥有更智能的任务调度程序,充分利用空闲 CPU,使得整体构建速度更快。另外,turbo 还具有远程缓存功能,可以与团队和 CI/CD 共享构建缓存。

优势

  • 增量构建:缓存构建内容,并跳过已经计算过的内容,通过增量构建来提高构建速度
  • 内容 hash:通过文件内容计算出来的 hash 来判断文件是否需要进行构建
  • 云缓存:可以和团队成员共享 CI/CD 的云构建缓存,来实现更快的构建
  • 并行执行:在不浪费空闲 CPU 的情况下,以最大并行数量来进行构建
  • 任务管道:通过定义任务之间的关系,让 Turborepo 优化构建的内容和时间
  • 约定式配置:通过约定来降低配置的复杂度,只需要几行简单的 JSON 就能完成配置

turbo 通过「智能缓存」与「任务调度」,极大的提升了构建速度,节省了计算资源。并且 turbo 配置非常简单,侵入性小,可以渐进式的采用。相信未来 turbo 会成为 monorepo 工具链上的重要一环。

搭建项目

  1. 将 Turborepo 添加到项目最外层的 devDependecies 中,npm install turbo -D
  2. 在 package.json 中增加 Turborepo 的配置项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// package.json 将想要"涡轮增压"的命令添加到管道中 管道定义了 npm 包中 scripts 的依赖关系,
// 并且为这些命令开启了缓存。这些命令的依赖关系和缓存设置会应用到 monorepo 中的各个包中
{
"turbo": {
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**"]
},
"test": {
"dependsOn": ["^build"],
"outputs": []
},
"lint": {
"outputs": []
},
"dev": {
"cache": false
}
}
}
}

build 和 test 这两个任务具有依赖性,必须要等他们的依赖项对应的任务完成后才能执行,所以这里用^来表示。 对于每个包中 package.json 中的 script 命令,如果没有配置覆盖项,那么 Turborepo 将缓存默认输出到 dist/** 和 build/**文件夹中。可以通过 outputs 数组来设置缓存的输出目录,示例中将缓存保存到.next/**文件夹中。Turborep 会自动将没个 script 的控制台 log 缓存到.turbo/turbo-