随着前端项目越来越大,我们往往会将一些逻辑、组件或资源从项目中抽离出来,形成单独的npm包。这样,这些内容就可以非常容易地在其他项目中进行复用。然而,当我们需要联调的时候,问题就变得复杂起来,我们需要让依赖包能够响应被依赖的包的变更。这时候,基于文件链接的npm link就极大地方便了我们调试。
文件链接
文件链接是类Unix操作系统中的概念,又分为软链和硬链。软链又被称为符号链接,其是一个单独的特定类型的文件,存储的内容只是一个路径的字符串,如./tsconfig.json
,/var/tmp
这种,和Windows中的快捷方式非常类似。而硬链则是在文件目录中不同的索引节点指向相同的文件,也就是说,硬链和被链接的文件是完全等价的。
下面执行ls -il
的截图可以很好地展示软链和硬链的原理上的区别:
在这个文件夹中,a.txt是原始文件,b.txt是a.txt的一个硬链,可以看到在ls的结果中两个文件除了文件名,其他信息,包括inode号(文件系统中的唯一序号,上图输出中的第一列)是完全一致的。而c.txt则是a.txt的一个硬链,其文件id与a.txt不同。更进一步,我们可以看到a.txt和b.txt的引用计数(输出中的第3列)是2,而c.txt的引用计数是1,这是因为硬链会增加inode上的引用计数,当引用计数归零时,就说明一个文件在文件系统中已经没有任何索引了,操作系统可以删除该文件的物理存储,而软链和原始文件则是不同文件,因此引用计数就重新从1开始计数了。
由于软链和硬链上面原理的不同,带来了它们使用上的区别:
- 软链可以链接到文件与文件夹,甚至可以创建一个指向软链的软链,而硬链只支持链接到具体的文件(在软链上创建硬链会直接链接到软链指向的具体文件)
- 软链的目标可以是不存在的,而硬链则必须是真实存在的文件
- 软链可以跨磁盘,而硬链则不行
- 删除软链对原始文件没有影响,删除硬链则会减少原始文件的引用计数
- 移动原始文件或链接文件,软链可能会失效,而硬链则不会
调试跨包代码
要调试跨包代码,首先需要区分两个大场景
- 跨包代码在同一个仓库
- 在这种情况下,项目结构一般就是monorepo的形式,以lerna为例,在
lerna bootstrap
时,就会链接任何交叉依赖项
- 在这种情况下,项目结构一般就是monorepo的形式,以lerna为例,在
- 跨包代码在不同仓库
- 在这种情况下,项目的依赖默认会从npm下载,因此需要手工通过
npm link
进行链接
- 在这种情况下,项目的依赖默认会从npm下载,因此需要手工通过
npm link
举例,应用引用一个叫@test的包,首先需要
在@test包中执行npm link
——— 在npm的全局地址(npm —global的缓存处)用软链保存了@test包
在应用中执行npm link @test
——— 在应用的node_modules中将@test替换成软链地址
而用这种方式,可能会碰到几种场景
- @test包又引用了@child包,而在实际的代码修改中,你想要修改的是@child的代码
- 先在@test包对@child进行一次link
- @test包引用了@types/react,并且万恶的写在了dependencies中,你的应用也引入了@types/react
- 这个背景是项目中不能存在不同实例的@types/react,如果是工具包可以写在devDependencies或者peerDependencies中
- 而如果实在是这种场景下,@test使用的可能是自己node_modules中的@types/react,而你的应用用的是另一个,可以靠webpack的alias或者rollup的global来解决,指定@test的react依赖包位置
ps:如果是产物引用的方式的话,需要在webpack中增加watch,监控代码生成产物
在包管理工具中软、硬链的应用
npm、yarn
npm和yarn都是通过软链的形式来支持workspace的。对于workspace中匹配的包,npm和yarn会在顶层的node_modules里创建同名的软链指向workspace中的文件夹,从而在依赖包中可以引用。
pnpm
pnpm结合使用了硬链和软链:
- 在全局会有一个.pnpm-store文件夹存储所有下载过的依赖的文件
- 在每个项目的node_modules文件夹中,pnpm会创建一个.pnpm文件夹,存储项目所依赖的所有依赖包,而依赖包中的文件则是通过硬链的方式指向.pnpm-store文件夹中的文件,从而实现所有包仅需下载一次
- 在项目的node_modules中则只创建项目直接依赖的包的软链,指向.pnpm文件夹中对应的包的具体版本,.pnpm文件夹中包的依赖也通过类似的方式用软链的方式处理
整体的逻辑如下图:
- 在项目的package.json中,我们定义了两个dependency: rimraf与glob
- pnpm为这两个包在node_modules里分别创建了软链,指向.pnpm文件夹中对应的包
- 由于rimraf本身也依赖了glob,因此pnpm在rimraf@3.0.2文件夹的node_modules里也创建了glob的软链
- rimraf与glob文件夹中的具体文件则硬链到.pnpm-store文件夹中,从而实现全局缓存