说到调试npm包,就想起去年溪森堡刚入职不久对npm也没什么概念,只知道从终端进入项目package.json同级目录之后敲下npm i(install)就会产生一个node_modules目录,这里有我们web应用所依赖的文件。终于有一天需要修改公司内部维护的一个npm包,把这个包的代码克隆下来之后一头雾水,两个工程代码如何建立关联?终于灵机一动,对!对说干咱就干,打包之后把它塞到引用工程的的node_modules下去!于是,打包、搬代码、调试,打包、搬代码、调试……溪森堡吭哧吭哧地搬了一下午代码之后觉得这样不行,绞尽脑汁想出来一个“好办法”,于是那天下午又为了调试一个功能将那个npm包发了n个版本。最后结果可想而知,溪森堡被其他组大佬抓出来公开处刑了。。。
今天,让我们来帮帮一年前的溪森堡想想办法,怎么快速调试这个npm包呢?
准备工作:
1. 先创建三个项目目录projectA,projectB,projectC模拟三个独立的前端项目目录;
2. 依次对这三个目录执行npm init初始化项目
然后按照提示依次键入三个工程的packageName,version,author等信息,就可以看到三个项目都初始化成功并且各自生成了一个package.json文件;
3. 分别为projectB,projectC创建入口文件并编写如下js代码, 定义变量name分别为"projectB"和"projectC",并且都有一个logCurrentName方法输出当前项目的name。
为projectA创建入口文件index.js,编写如下js代码:
为了省事,上面用了最简单的Node.js模块导出name和logCurrentName,调试projectA也在node环境下进行,实际开发应用场景肯定比这里的复杂的多,这里不整那么多华丽胡哨的东西让我们把更多精力放在关注调试方法上!
准备工作做好了,接下来我们将会让projectA依赖projectB,projectB依赖projectC。现在假设projectA依赖projectB,projectB为我们上面故事的主角---npm包,我们需要对projectB进行功能迭代,接下来介绍如何在projectA中调试projectB。
方法一:直接将dependencies修改成引用本地文件地址,然后像安装普通的npm包一样在projectA根目录下执行npm i,这样本地计算机上projectA和projectB就建立了引用关系(如果是window系统,请修改成对应的文件系统路径 eg. file:///C:/Users/hst/Desktop/npmTest/projectB)。
执行npm i之后,我们可以看到projectA目录下会生成一个node_modules目录,并且里边已经生成了project-b目录,在目录右侧有一个箭头表示这是一个符号链接(软链接)
这里恐怕需要插播一下硬链接和软链接相关基础知识,已经对这个很熟悉的大佬请直接跳过。
我们的文件保存在计算机硬盘中,硬盘最小存储单位称作扇区(Sector),每个扇区约0.5KB(512字节),为了提升效率,操作系统在读取硬盘上的数据时,是一次性读取8个连续的扇区即一个“块”(Block)的。我们文件数据存储在一个个块中,但是文件的其他一些信息如创建者、创建时间、修改时间、文件大小、文件读写权限、软硬链接数 等信息(文件的元信息)还需要有一个地方来存储,而存储文件元信息的地方就叫inode(索引节点)每个文件都有对应的inode,用stat命令可查看:
每个inode都有一个号码,操作系统用它来标识不同的文件(不是文件名),
这里直接引用阮一峰先生博文的原话:
表面上,用户通过文件名,打开文件。实际上,系统内部这个过程分成三步:首先,系统找到这个文件名对应的inode号码;其次,通过inode号码,获取inode信息;最后,根据inode信息,找到文件数据所在的block,读出数据。阮一峰 网页链接;action=edit&type=77&appmsgid=100000379&token=1774051625&lang=zh_CN
硬链接(hard link):
一般情况下,一个inode号码对应一个文件名称,但是有些操作系统是允许多个文件名指向同一个inode号码,意味着我们可以通过不同的文件名访问到相同的内容(对js中对象访问熟悉的同学对这个进行类比理解应该也不难)。
如果a,b两个文件是硬链接的关系,那么用户修改了a的内容,b内容也会跟着变更,修改b同样a也会变更(这里的a,b是指文件名,它们指向同一个inode)。
创建硬链接:
ln 源文件 目标文件(目标文件为还不存在的文件)
查看硬链接:
ls -l 长格式的形式查看当前目录下所有可见文件的详细属性。-i显示inode
a.txt和b.txt有着相同的inode号,链接数为2,
因为a.txt和b.txt指向了同一个inode,所以不管以谁为源文件再添加硬链接效果都是相同的,每添加一个硬链接,连接数就会相应地加1;
如果需要删除硬链接,删除不需要用到的文件名即可,这样连接数会相应地减1。
即使最开始的源文件a.txt被删除,b.txt等其他文件还是可以继续使用。
创建一个同名源文件a.txt这时候也不是指向同一个inode了,硬连接数不会加1
注意:
a. 硬链接可以是多个文件之间进行,不限于两个;
b. 不能对目录进行硬链接;
c. 不同的文件系统eg.ext4,xfs等之间不可以做硬链接;
d. 硬链接指向相同的inode节点。
软链接/符号链接(soft link / symbolic link):
可以理解为:为一个源文件创建一个快捷方式。
创建软链接:
ln -s 源文件 目标文件
以a.txt为源文件对文件d.txt创建软链接,它们的inode号码是不一样的,新增软链接不会增加链接数;并且d.txt的文件操作权限项的前面多了一个'l',文件名后面有一个 -> 指向源文件。文件d.txt的内容其实是文件a.txt的路径,当我们试图读取d.txt的内容时,系统会将访问者导向文件a.txt,所以最终读取的其实就是a.txt的内容。d.txt被成为a.txt的软链接。
如果源文件被删除,软链接无法继续使用,重新创建一个同名源文件,软链接将继续指向重新被创建的文件。
删除软链接:rm 软链接名
不仅可以对文件进行软链接,还可以对目录进行链接
对目录创建软链接:ln -s 源目录 目标目录
删除目录的软链接:rm -r 软链接名
介绍完软链接硬链接,我们回到第一种调试方法:
可见这里是为 projectB创建了一个软链接:
/XXX/projectA/node_modules/project-b。
在node_modules目录下还生成了一个 .package-lock.json,内容如下:
package-lock.json在npm v5之后被引入,会在首次执行npm install之后生成,用来锁定生成的依赖树结构以及版本信息。它表明了依赖的模块结构、名称、版本、获取地址等信息。
在上图这个package-lock.json中,"packages"属性是包含package位置等相关信息的一个对象
"node_modules/project-b"中resolved指明了模块实际位置"../projectB",link:true 表示它是一个符号链接。
这其实就很明了了,它其实就是在projectA的node_modules目录下给projectB创建了一个软链接/XXX/projectA/node_modules/project-b,我们这就用projectC来实验下是不是这样的:
可以看到projectC的内容也被链接到projectA的node_modules/目录下成为它依赖大军中的一员了,不过这样手动创建链接不会更新/node_modules下面的package-lock.json
我们看看运行效果:
再修改一下packageB的index.js
ok,好使!这种方法其实就是在projectA的 node_modules直接创建了一个指向引用的npm包的软链接,总体来说还是比较方便的。我们接着介绍下一种调试方法。让我们先复原,删掉这一步创建的软链接和node_modules
方法二:npm link
npm官方给出的介绍是 Symlink a package folder(符号链接包文件夹)
有了方法一中提及的相关只是储备,我们不难看出这其实还是用了软链接。
重复一下场景:projectA依赖projectB,我们需要修改projectB并在projectA本地调试
使用方法:
1. 在projectB根目录下执行 npm link
这一步是在全局文件夹{prefix}/lib/node_modules/<package>中(windows系统可能没有lib)创建一个符号链接,链接到npm link执行命令的包(即 projectB) 被全局安装的npm包(npm i XXX -g)也在这个目录下
这个"prefix",默认是包含package.json文件或node_modules目录的最近父目录,可用npm prefix 查看
要查看全局的prefix 需要加-g参数
让我们进来这个全局node_modules目录看看,这里的确被创建了一个指向projectB的软链接
2. 在projectA根目录下执行 npm link project-b (project-b是projectB中的package.json中的name属性的值,如果是scoped的也需要带上 @xxx/project-b),不需要执行npm install
刷新一下vsCode,还是一样的软链接和package-lock.json的内容
首先创建了一个全局的软链接指向projectB,然后将全局安装目标链接到了projectA的node_modules/中。这时候我们修改projectB内容就会同步到projectA的目录下(有时候可能需要自己刷新下)我们来测试一下:
3. 测试完成之后需要删除链接
a.首先在projectA项目根目录下执行 npm unlink packagename (unlink 是uninstall的别名,相当于删除依赖)
unlink之后,noed_modules/中的project-b被清理
b. 全局node_modules目录下的软链接还存在,我们一般我们不用的时候也要删除,注意需要带上-g参数(或者去全局的node_modules目录下执行rm XXX)
npm link 通过软链接的形式将所依赖的包提升到全局node_modules中,会在projectA的node_modules目录下生成一个package-lock.json文件,但是不会去修改projectA的package.json文件且npm v6以上版本就能用,无需安装其他插件;缺点:引用工程和npm包本质上是两个相互独立的项目,都有自己的package.json和node_modules,当他们依赖同一个第三方npm包且该包不支持多例(如React Hooks)就会报一些奇奇怪怪的错误。
方法三:yalc
“Better workflow than npm | yarn link for package authors.” 溪森堡日常开发其实更偏向于使用yalc,因为使用npm link经常会出现一些奇奇怪怪的报错,尤其是deadline将近的时候,这些报错无疑是让人头疼的。yalc充当了我们本地环境中需要共享的本地开发包的一个本地存储库。
使用方法:
1. 全局安装yalc(因为要跨项目目录使用): npm i yalc -g
2. 在projectB项目根目录执行 yalc publish,
执行yalc publish之后,npm包的代码将被抓取并被存放在一个全局存储库中(linux系统中一般是 ~/.yalc/packages 目录下,windows应该会在home/.yalc/packages)
可以用yalc dir命令查看全局存储库的位置:
我们还留意到,全局.yalc目录下还有一个installations.json文件,这里包含了我们 yalc publish之后的package名称以及包的引用(安装)地址
我们不妨做个实验,移除对project-c的引用 (yalc remove packageName),
再回去.yalc目录下看,相应包的配置项也跟着被删除了。
a. 如果你的npm包有这些npm生命周期脚本:prepublish、prepare、prepublishOnly、prepack、preyalcpublish,它们将被按此顺序运行。
b. 如果有这些:postyalcpublish, postpack, publish, postpublish,它们将按此顺序运行。
c. 如果不想如上生命周期的脚本被执行,可加上--no-scripts参数。
3. 在projectA中执行 yalc add project-bb (projectB的package.json的name为project-bb)
我们先来看看paojectA会有啥变化:
a. 当我们运行 yalc add packageName之后, 引用工程会将npm包的内容拉进当前项目目录下的.yalc文件夹,
b.在projectA的package.json中添加依赖项dependencies,指向 file:.yalc/packageName
c. projectA的node_modules目录中多了project-bb目录
d. yalc还添加了一个yalc.lock文件来锁定依赖版本,作用类似与package-lock.json。
e. yalc为每个包生成了一串数字签名用来计算npm包的内容,存放在各个包的yalc.sig文件中,并且会修改每个包的package.json文件,为之添加 yalcSig属性对应yalc.sig的内容
注意:如果用 yalc link packageName 而不是yalc add packageName, 就会在node_modules中创建指向当前项目的.yalc目录下同名package的软链接且不会去修改package.json(跟npm link类似)
使用yalc实践:
a. 修改下projectA的index.js内容如下:
b. 更新projectB的脚本内容,会发现projectA并没有跟着更新,我们需要执行一下yalc push (也可用yalc update)将更改推送到全局存储库 ~/.yalc/packages,projectA中的project-b才会被更新
查看当前项目有哪些yalc 引用
我们已经知道当使用yalc add packageName时,projectA的package.json是会被更改的。使用yalc check命令可以检查package.json中的yalc引用的包
如果刚好你在precommit钩子中使用了yalc check,请记得删除它,不然可能会报错。
移除对指定yalc add 方式添加的引用:yalc remove packageName (移除所有引用:yalc remove --all)
移除yalc link 方式添加的引用:
执行remove命令的时候,会去projectA的package.json中的dependencies 读取对应包实际地址,然后再执行删除操作,通过yalc link方式添加的project-c在package.json中没有相应的依赖项,只是一个软链接 删除软链接:rm XXX
移除引用之后因执行yalc add 命令所产生的更改都会被回退
画草草草图总结一下:
提个问题:如果我需要调试依赖包的依赖包怎么办?
eg. projectA依赖projectB,projectB依赖projectC,我们需要在运行着project A的时候 看到projectC的更新效果
答:只需要在projectC中 yalc publish 并且在projectA中 yalc add projectC即可,不用管projectB,因为我们用yalc改变了node_modules下的package,当我们安装依赖的时候本质上是执行了安装主依赖以及安装其依赖的依赖;npm v3之后npm通过扁平化的方式来将子依赖项安装在主依赖项的所在目录中(提升) npm还实现了一套依赖查找算法,它会递归向上查找node_modules,如果找到相同版本的包就不会再重新安装,解决了依赖地狱的问题。(详细内容请查阅Alibaba F2E的文章:关于依赖管理的真相 - 前端包管理器探究)
更多思考(想办法偷懒):
projectB更改之后每次都要执行yalc push吗?太麻烦了点,我们可不可自动监听projectB的变化然后自动把变更内容推送到store呢?答案是nodemon+yalc。
nodemon是可自动检测文件变更,然后自动重启应用的一种node工具,在调试node.js程序的时候非常有用。
a. 全局安装nodemon npm i nodemon -g
b. 为projectB创建src目录并增加src/index.js文件
c. 修改projectB的index.js内容如下
d. 修改projectB的脚本, 当在projectB根目录下执行npm run watch时会监听src目录下所有文件的变化 并且 执行 npm run yalcpush (当然这里只是最简单的脚本备至,可以根据项目实际情况来配置)
npm脚本相关配置参数:
--ignore XXX/ #忽略xxx目录下的文件
--watch xxx/ #监听xxx目录下的文件变化
-C #后次启动不执行后面的命令,有文件变更才执行
-e js,css,ts #监听制定后缀的文件变化
--x npm run xxx #指向npm run xxx命令
在projectB根目录执行npm run watch,src目录下所有文件的修改都会被自动监听并push到全局的.yalc 存储库
演示效果:
本文介绍了三种本地调试npm包的方法,并介绍了软链接和硬链接的概念以及差异。三种调试方法基本都是运用了软链接(yalc中的link方式),具体使用那种方法看个人喜好。
尊重他人劳动成果,传播请著明出处;如文中存在谬误,敬请指出!
如果本文有帮助到你,欢迎点赞加关注并转发分享给身边的同学哦~
相关链接:
https://docs.npmjs.com/cli/v8/configuring-npm/package-lock-json#packages
网页链接;action=edit&type=77&appmsgid=100000379&token=1774051625&lang=zh_CN
https://docs.npmjs.com/cli/v8/commands/npm-link
https://docs.npmjs.com/cli/v8/commands/npm-prefix
https://github.com/wclr/yalc
本文使用 文章同步助手 同步