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=registry.npm.taobao.org 安装淘宝镜像
npm search 搜索词 -g 用于搜索npm仓库,它后面可以跟字符串,也可以跟正则表达式
npm update [-g] 更新模块
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;如果磁盘上没有主目录,那么将在文件系统的根目录中创建该存储。 例如,如果安装发生在挂载在 /mnt 的文件系统上,那么存储将在 /mnt/.pnpm-store 处创建。 Windows系统上也是如此。可以在不同的磁盘上设置同一个存储,但在这种情况下,pnpm 将复制包而不是硬链接它们,因为硬链接只能发生在同一文件系统同一分区上。如图可以看到在使用 pnpm 对项目安装依赖的时候,如果某个依赖在 sotre 目录中存在了话,那么就会直接从 store 目录里面去 hard-link,避免了二次安装带来的时间消耗,如果依赖在 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
- 调整目录结构如下
# 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
- 配置pnpm-workspace.yaml,让pnpm知道都有哪些workspace。
# ./pnpm-workspace.yaml
packages:
# root directory
- "."
# all packages in subdirs of packages/
- "packages/**"
# exclude packages that are inside test/ directories
- "!**/test/**" # '!' means exclude
- 配置执行脚本,如执行某个包的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: *,就可以保持依赖的版本是工作空间里最新版本,不需要每次手动更新依赖版本。
// 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 工具链上的重要一环。
搭建项目
- 将 Turborepo 添加到项目最外层的devDependecies中,npm install turbo -D
- 在 package.json 中增加 Turborepo 的配置项
// 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-
6 参考资料
【一库】yalc: 可能是最好的前端link调试方案(已经非常谦虚了)