前端包管理工具总结
1 包管理工具
1.1 npm
作用
npm 是随同 NodeJS 一起安装的包管理工具,允许用户下载或上传包或命令行程序供开发者使用;
npm install 流程
执行命令后,首先会构建依赖树,然后针对每个节点下的包,会经历下面四个步骤:
- 将依赖包的版本区间解析为某个具体的版本号
- 下载对应版本依赖的 tar 包到本地离线镜像
- 将依赖从离线镜像解压到本地缓存
- 将依赖从缓存拷贝到当前目录的 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
npm run 执行脚本
npm publish 发布模块
npm dedupe 合并重复依赖,减少重复
问题(3 版本之前)
- 依赖层级太深,会导致文件路径过长的问题,尤其在 window 系统下,会造成安装失败或删除 node_modules 失败。
- 大量重复的包被安装,node_moudles 体积超级大。比如跟 foo 同级目录下有一个 baz,两者都依赖于同一个版本的 lodash,那么 lodash 会分别在两者的 node_modules 中被安装,也就是重复安装。
- 模块实例不能共享。比如 React 有一些内部变量,在两个不同包引入的 React 不是同一个模块实例,因此无法共享内部变量,导致一些不可预知的 bug。
PS:3 版本及其后的和 yarn 的问题一致;
包调试方案
npm/yarn link
npm link 用于连接本地项目和本地 npm 模块,使得可以在本地进行模块测试;
具体用法:
- 项目和模块在同一个目录下,可以使用相对路径:npm link ../module
- 项目和模块不在同一个目录下
- cd 到模块目录,npm link,进行全局 link
- cd 到项目目录,npm link 模块名(package.json 中的 name)
- 解除 link:npm unlink 模块名
原理:在全局包路径中创建一个软链(Symlinked)指向对应的 npm 包,然后在项目中通过软链将全局的软链指向到 node_modules 的对应包中;
此方案缺点:
- 影响 node_modules 中原本的依赖包;
- 软链接和文件系统引发的其他各种奇怪的问题;
- webpack 在进行编译的时候无法编译软链接的依赖库。
相对路径或者绝对路径使用
// import { Button } from ‘good-ui’ // 为了调试,强行改成了绝对或者相对路径 import { Button } from ‘C:/codes/good-ui/dist’
此方案缺点:需要频繁改业务代码,这既麻烦又危险(路径有可能进行修改,在 git 提交代码的时候,引用路径忘记修正回来则其他开发者无法正常使用)。
yalc
模拟 npm 发布,并将包缓存本地,下载时也是模拟 npm install,所以不会存在相关依赖库丢失,只是模拟发布和下载不会真的推包;
使用方法:
- 需要发包的项目中执行:yarn build && yalc publish
- 使用包的项目中执行:yalc add 包名
其他概念
dependencies 与 devDependencies 之间的区别:dependencie 配置当前程序所依赖的其他包。devDependencie 配置当前程序所依赖的其他包,只会下载模块,而不下载这些模块的测试和文档框架
包版本概念:^表示第一位版本号不变,后面两位取最新的;~表示前两位不变,最后一个取最新;*表示全部取最新
lock 文件:npm5 通过添加 lock 文件来记录依赖树信息,进行依赖锁定,从而唯一确定 node_modules 的结构,这样处理可以保证团队成员使用同一份 node_modules 依赖结构。
1.2 yarn
快速、可靠、安全的依赖管理工具。
安装
npm install –global yarn
作用
- 速度快:Yarn 缓存了每个下载过的包,所以再次使用时无需重复下载。 同时利用并行下载以最大化资源利用率,因此安装速度更快。如果你以前安装过某个包,再次安装时可以在没有任何互联网连接的情况下进行。
- 超级安全:在执行代码之前,Yarn 会通过算法校验每个安装包的完整性。
- 超级可靠:使用详细、简洁的锁文件格式和明确的安装算法,Yarn 能够保证在不同系统上无差异的工作。
- 确定性:不管安装顺序如何,相同的依赖关系将在每台机器上以相同的方式安装。
- 网络性能:Yarn 有效地对请求进行排队处理,避免发起的请求如瀑布般倾泻,以便最大限度地利用网络资源。
- 相同的软件包:从 npm 安装软件包并保持相同的包管理流程。
- 网络弹性:重试机制确保单个请求失败并不会导致整个安装失败。
- 扁平模式:将依赖包的不同版本归结为单个版本,以避免创建多个副本。所有的依赖都被拍平到 node_modules 目录下,不再有很深层次的嵌套关系。这样在安装新的包时,根据 node require 机制,会不停往上级的 node_modules 当中去找,如果找到相同版本的包就不会重新安装,解决了大量包重复安装的问题,而且依赖层级也不会太深。
问题
- NPM 分身:依赖结构的不确定性。只提升 package.json 里面排在前面的包的重复引用包,其他版本的不提升;
- 扁平化算法本身的复杂性很高,耗时较长。
- 幽灵依赖或幻影依赖:项目中仍然可以非法访问 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
作用
- 快速:pnpm 比其他包管理器快 2 倍;
- 高效:node_modules 中的文件为复制或链接自特定的内容寻址存储库;
- 支持 monorepos:pnpm 内置支持单仓多包;
- 严格: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 prune 删除不被引用的包
- pnpm add xxx 添加包
- –save-prod, -P:安装到dependencies
- –save-dev, -D:安装到devDependencies
- –save-optional, -O:安装到optionalDependencies
- –save-peer:安装到peerDependencies和devDependencies中
- –global:安装全局依赖。
- –workspace:仅添加在 workspace 找到的依赖项。
- pnpm remove xxx 删除某个包
- pnpm install 安装所有依赖
- pnpm list 以一个树形结构输出所有的已安装 package 的版本及其依赖。添加参数–json 后会输出 JSON 格式的日志。
- 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 解决了哪些问题?
规范问题和简化流程;
- 自动解决 packages 之间的依赖关系
- 可采用 Independent 模式,通过 git 检测文件改动,自动发布,;
- 根据 git 提交记录,自动生成 CHANGELOG
- 统一整个工程化,比如 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 项目
- 安装 lerna:npm install –global lerna
- 初始化项目:git init lerna-repo && cd lerna-repo
- 初始化 lerna:lerna init;得到文件夹 lerna-repo/ packages/ package.json lerna.json
- 创建子应用:lernam create 子应用名称;
4 基于 pnpm 搭建 monorepo
- 调整目录结构如下
1 | # app |
- 配置 pnpm-workspace.yaml,让 pnpm 知道都有哪些 workspace。
1 | # ./pnpm-workspace.yaml |
- 配置执行脚本,如执行某个包的 dev 指令,pnpm run –filter @package/app dev
- 批量执行命令,如对所有的包进行 lint:pnpm run –filter="@app/*" lint
- 复用同仓库下的代码:假设 app 依赖于pkg1@1.5.0和pkg2@1.5.0,而后两者均依赖于pkg3@1.5.0。常规的做法是直接使用 npm 上的版本。但是如果想直接用当前正在开发中的pkg3@1.5.1,而又还没有发布到 npm 上, 就很难办了。总不能 import xxx from “../../pacakge/pkc3/xxx”吧。这个时候 workspace 就派上了用场,可以这样给 pkg1 写依赖;在设置依赖版本的时候推荐用 workspace: *,就可以保持依赖的版本是工作空间里最新版本,不需要每次手动更新依赖版本。
1 | // packages/pkg1/package.json |
5 Turborepo
Turborepo 是一个为 monorepo 而生的极快的构建系统。目的是为了解决大型 monorepo 项目构建速度缓慢的一大痛点。turbo 的核心是永远不会重新构建已经构建过的内容。turbo 会把每次构建的产物与日志缓存起来,下次构建时只有文件发生变动的部分才会重新构建,没有变动的直接命中缓存并重现日志。turbo 拥有更智能的任务调度程序,充分利用空闲 CPU,使得整体构建速度更快。另外,turbo 还具有远程缓存功能,可以与团队和 CI/CD 共享构建缓存。
优势
- 增量构建:缓存构建内容,并跳过已经计算过的内容,通过增量构建来提高构建速度
- 内容 hash:通过文件内容计算出来的 hash 来判断文件是否需要进行构建
- 云缓存:可以和团队成员共享 CI/CD 的云构建缓存,来实现更快的构建
- 并行执行:在不浪费空闲 CPU 的情况下,以最大并行数量来进行构建
- 任务管道:通过定义任务之间的关系,让 Turborepo 优化构建的内容和时间
- 约定式配置:通过约定来降低配置的复杂度,只需要几行简单的 JSON 就能完成配置
turbo 通过「智能缓存」与「任务调度」,极大的提升了构建速度,节省了计算资源。并且 turbo 配置非常简单,侵入性小,可以渐进式的采用。相信未来 turbo 会成为 monorepo 工具链上的重要一环。
搭建项目
- 将 Turborepo 添加到项目最外层的 devDependecies 中,npm install turbo -D
- 在 package.json 中增加 Turborepo 的配置项
1 | // package.json 将想要"涡轮增压"的命令添加到管道中 管道定义了 npm 包中 scripts 的依赖关系, |
build 和 test 这两个任务具有依赖性,必须要等他们的依赖项对应的任务完成后才能执行,所以这里用^来表示。 对于每个包中 package.json 中的 script 命令,如果没有配置覆盖项,那么 Turborepo 将缓存默认输出到 dist/** 和 build/**文件夹中。可以通过 outputs 数组来设置缓存的输出目录,示例中将缓存保存到.next/**文件夹中。Turborep 会自动将没个 script 的控制台 log 缓存到.turbo/turbo-