记一次npmlink本地开发引发的bug
|最近在开发一个个人项目时,使用了npm link
引用本地其他目录下的包,碰到了库代码重复打包、Vue版本不一致等问题,最后发现是rollup和node_modules包寻址的问题,稍作记录。
<!--more-->
1. 项目结构
简单介绍一下整体的背景,应用Project A
是一个Vue3项目,包含Vue3、VueRouter和Pinia等库,此外还依赖了本地的包Module B
,B模块是一个pnpm monorepo项目,大概结构是
├── sdk-core
├── sdk-react
├── sdk-vue-base
├── sdk-vue2
├── sdk-vue3
其中sdk-core
是框架无关的核心包、sdk-vue-base
是基于Vue框架提供的通用功能,sdk-vue2
和sdk-vue3
则分别是为Vue2和Vue3不同版本提供的相关API。
Project A
实际上需要引入的是sdk-vue3
,而sdk-vue3
又依赖于sdk-vue-base
和sdk-core
。
Module B
使用rollup打包,分别输出es
和cjs
文件
"main": "lib/index.js",
"module": "esm/index.js",
"types": "esm/index.d.ts",
由于Module B
的还不是正式版,直接发布到私有npm仓库在通过常规npm i 的方式安装会导致调试比较麻烦,因此先通过link的方式直接引用本地模块。
流程是:通过rollup将sdk-vue3
打包之后,再通过pnpm link --global
将其链接到全局执行环境,然后在Project A
中通过pnpm link sdk-vue3 --global
添加依赖。
大概背景介绍完了,接下来就是整个过程中碰到的一些问题
2. 问题1:Rollup重复打包
由于sdk-vue-base
依赖sdk-core
,而sdk-vue3
同时依赖sdk-vue-base
和sdk-core
,
在打包脚本中,首先会打包sdk-vue-base
,然后再打包sdk-vue3
,在最后sdk-vue3
的打包产物中发现,sdk-core
的代码被重复打包了两次!
经过搜索,找到了类似的issue
不管是哪种前端打包工具,其功能的本质是多文件打包成单文件,于是打包工具会分析依赖,按照各个模块各自的依赖,按顺序合并打包,最终输出到一个文件中,rollup也是如此
因此在没有特别声明的情况下,sdk-vue-base
会合并sdk-core
的代码,sdk-vue3
又会再合并sdk-core
的代码,这就导致sdk-vue3
中有重复代码。
本质的原因是,sdk-vue3
并不知道sdk-vue-base
的打包产物中已经合并了sdk-core
的代码,因此他就会重新从sdk-core
这里再合并一次代码。
2.1. 方案1:修改依赖顺序
第一种解决办法是修改sdk-vue3
的依赖,只让其依赖于sdk-vue-base
,而sdk-core
的接口通过sdk-vue-base
暴露出去
// sdk-vue-base
export * from 'sdk-core'
// sdk-vue3
// import { xxxApi } from 'sdk-core'
import { xxxApi } from 'sdk-vue-base'
这样只有sdk-vue-base
会合并sdk-core
的代码,sdk-vue3
不会感知到sdk-core
,也就不会有重复代码了。
但是这种方式本质上会改变包的编写方式,需要手动控制包的依赖,个人并不推荐。
2.2. 方案2:external
第二种解决办法是通过rollup的external
配置项声明,不将sdk-core
的代码进行打包,而是保留原始的项目依赖
import { xxxApi } from 'sdk-core'
external配置如下
const external = ['vue', 'vue-router', 'react']
if (['sdk-vue','sdk-vue3', 'sdk-vue2'].includes(name)) {
external.push('sdk-core')
}
这种方式的好处非常明显,由于sdk-vue-base
和sdk-vue3
都会将sdk-core
视为外部依赖,就不会将其进行代码合并,也就不会出现重复代码的情况了。
缺点在于,sdk-core
现在需要作为公共包被外界感知到,在本地开发时,需要也将其link到全局执行环境,在生产环境时,需要也将其发布到包仓库中取。
同时sdk-vue3
这些包应该需要依赖对应固定版本的sdk-core
,因此这里还需要声明一下peerDependencies。
可以看见,上面对于vue
、vue-router
等第三方库也是同理,对于打个包而言,如果依赖的是外部包,那么就不应该再将其重复打包。
接下来的第二个问题就是跟这些第三方库相关的。
3. 问题2:node_modules寻址
由于sdk-vue3
也依赖了vue
等第三方库,于是在external
里面也进行了配置。但由于sdk-vue3
目录下面包含了一个example项目,展示sdk部分使用示例,因此在node_modules里面还是安装了vue
相关的依赖。
在本地项目B中的node_modules中,也安装了vue
相关依赖。
在项目开发运行时发现,部分状态并没有成功设置,比如onMounted
钩子等,后来发现sdk-vue3
依赖的Vue版本并不是项目B中node_modules的版本。
按照预期,link到项目B的sdk-vue3
,他应该去本地项目B的node_modules里面找到vue(而不是去他自己所在目录为examle项目提供依赖的node_modules去找)。
但我忘记了,NodeJS 寻找依赖的顺序并不是这样的。
以Node支持的CommonJS 为例,require
支持下面多种参数形式
- 原生模块:http、fs等,在require解析完文件名之后,会先判断是否是原生模块,如果是则直接返回
- 非原生模块的文件模块,在每个文件中可以通过
module.paths
找到改文件对应的搜索路径,nodejs会按照这个路径按顺序依次搜索模块- 绝对路径的文件模块:/xxx_path/mod,直接找到指定绝对路径的文件,速度最快
- 相对路径的文件模块:./mod或../mod,相对于当前调用require的文件去查找,如果按照文件名没有找到模块,则会带上
.js
、.json
、.node
等扩展名按顺序查找并加载,还是找不到,则进入上一层path寻找 - 目录作为模块:./dirname,如果发现给的的文件是一个目录,会尝试读取目录的
package.json
,并根据其中的main
字段来查找,如果没有指定main字段,则会尝试使用index.js
或index.node
文件作为模块
我们要关注的就是途中“查找文件模块”这一个步骤。重现一下依赖步骤
require('sdk-vue3')
- 发现这个模块不是个原生模块,就会一层一层目录向上开始找
node_modules
目录 - 在项目B根目录下发现
node_modules
,进去看一下,找到了link过来的sdk-vue3
目录 - sdk-vue3里面的文件依赖了
vue
,需要去找vue
文件。 - 注意这里是根据当前正在运行require的那个文件来查找,可以看见
sdk-vue3
对应的module.paths
,应该是sdk-vue3根目录下的module_modules,而不是项目B目录下的node_modules
这就是为什么sdk-vue3无法使用项目B目录下的node_modules的原因。有没有什么办法能让npm link 过来的脚本,按照当前的node_modules目录查找呢?换言之,如何修改nodejs的模块查询规则?
可以手动控制文件的位置,但会影响sdk-vue3模块本身的打包。这种方式直接被否决了。
一种临时的方案是移除对应的依赖,通过函数参数的形式修改依赖
即将
const vue = require('vue')
// vue.xxx
修改为
function install(Vue){
// vue.xxx
}
这种形式,由于需要修改SDK的接口,我并没有采取这种方案。
最后,为了解决这个问题,我还是将sdk-vue3这个包发布到了私有仓库中,然后通过正式的安装步骤,将其安装到项目真正的node_modules中。
4. yalc:npm link的替代方案
经历了上面两个问题的踩坑后,我发现npm link
貌似并不是一个很好的实践方案。
- 受到不同node 版本的影响
- link的包太多容易混乱,不太容易调试link多包的场景
- 文件软链接影响node_modules依赖查询
在搜索时发现了一个新的工具yalc,yalc可以模拟npm包的发布,看了一下其原理大概是在将包发布到本地存储,然后npm、yarn等包管理工具就可以直接从本地存储中下载过来,可以达到快速调试本地包的目的
全局安装
npm i yalc -g
发布本地包mod1
yalc publish
在本地项目中使用依赖
yalc add mod1
改动mod1之后提交更新
yalc push
会触发本地项目的自动更新,甚至触发项目的HMR。
与npm link
相比,唯一的工作量是多了yalc push
这一步,但可以把本地包当做是真实的包一样,体验还是不错的。
5. 小结
其他语言也很少有像nodejs这样每个项目一个node_modules的情况,大部分都被一个全局store仓库代替了。
npm link作为调试本地包的利器,软链接的方式感觉还是有点过于古老,本文总结了使用npm link在本地开发时遇见的一些问题,最后了解到了yalc
作为npm link的替代方案。接下来会在项目中进一步使用yalc,看看有没有其他问题或使用技巧。