🦊【前端技巧】跨包代码如何调试?带你了解npm link和monorepo的适用场景🦄️

2,028 阅读5分钟

随着前端项目越来越大,我们往往会将一些逻辑、组件或资源从项目中抽离出来,形成单独的npm包。这样,这些内容就可以非常容易地在其他项目中进行复用。然而,当我们需要联调的时候,问题就变得复杂起来,我们需要让依赖包能够响应被依赖的包的变更。这时候,基于文件链接的npm link就极大地方便了我们调试。

文件链接

文件链接是类Unix操作系统中的概念,又分为软链和硬链。软链又被称为符号链接,其是一个单独的特定类型的文件,存储的内容只是一个路径的字符串,如./tsconfig.json/var/tmp这种,和Windows中的快捷方式非常类似。而硬链则是在文件目录中不同的索引节点指向相同的文件,也就是说,硬链和被链接的文件是完全等价的。

下面执行ls -il的截图可以很好地展示软链和硬链的原理上的区别: image.png

在这个文件夹中,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时,就会链接任何交叉依赖项
  • 跨包代码在不同仓库
    • 在这种情况下,项目的依赖默认会从npm下载,因此需要手工通过npm link进行链接

npm link

举例,应用引用一个叫@test的包,首先需要

在@test包中执行npm link ——— 在npm的全局地址(npm —global的缓存处)用软链保存了@test包

在应用中执行npm link @test ——— 在应用的node_modules中将@test替换成软链地址

而用这种方式,可能会碰到几种场景

  1. @test包又引用了@child包,而在实际的代码修改中,你想要修改的是@child的代码
    1. 先在@test包对@child进行一次link
  2. @test包引用了@types/react,并且万恶的写在了dependencies中,你的应用也引入了@types/react
    1. 这个背景是项目中不能存在不同实例的@types/react,如果是工具包可以写在devDependencies或者peerDependencies中
    2. 而如果实在是这种场景下,@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文件夹中包的依赖也通过类似的方式用软链的方式处理

整体的逻辑如下图: image.png

  • 在项目的package.json中,我们定义了两个dependency: rimraf与glob
  • pnpm为这两个包在node_modules里分别创建了软链,指向.pnpm文件夹中对应的包
  • 由于rimraf本身也依赖了glob,因此pnpm在rimraf@3.0.2文件夹的node_modules里也创建了glob的软链
  • rimraf与glob文件夹中的具体文件则硬链到.pnpm-store文件夹中,从而实现全局缓存