获取中...

-

Just a minute...

遇到了invalid hook call报错

上周,业务下游的同事反馈了一个生产问题,说我们传的值有问题,领导让我帮忙排查一下。我最近一直没做那个项目相关的需求,打开那个项目切到release分支,拉代码,npm i然后启动,结果就白屏了。。。

打开控制台,看到了invalid hook call的报错,发生在公共组提供的一个组件里面。这个报错其实还有点讲究,我们都知道react的hook因为实现方式的限制,必须保证每次render都按相同的顺序执行,所以hook的使用是有限制的。当违反这些限制的时候就会报invalid hook call的错。但是除了刚接触react的新手,几乎没人会犯这样的错误。

这个报错的另外两个可能的原因,分别是react和react-dom的版本不匹配,和项目里存在多个版本的react。我在node_modules里面查看了报错的那个组件下面的node_modules,发现果然这个组件又单独装了个react的包进去。这样一来,我们项目里面就存在了两个react版本,导致了invalid hook call报错。

项目的实际依赖结构如下:

project
— node_modules
—— react 16.14.0
—— 某组件
——— node_modules
———— react 17.2.0

为什么会装两个react

我最近一直没碰过那个项目,那其他人呢,其他人开发没遇见这个问题?我真忍不了了,开始翻那个组件的依赖,看到它peerDependencies里面有个react,但是这个应该不是问题,peerDependencies不会真的被安装。我又翻这个组件的深层的依赖,终于看到有个包直接把react写在了dependencies里面。然后我先暂时用force-resolution统一了项目里面的react版本,然后跟公共组的同事说了之后,他们就把dependencies里面的react和react-dom去掉了。

其实我们直接用npm ls react命令也能看出来,项目里面就精装了多少个react和每个react的版本。

npm的各种依赖

devDependencies

接下来就发散发散,说说npm的各种依赖吧。还记得半年前我刚工作的时候,还不懂devDependencies和dependencies的意义,改了组件的devDependencies,结果最后项目里没生效,还是向别人请教后才知道,组件的devDependencies最后不会被真的安装到项目里面去。

字面意思,devDependencies是指开发环境用到的依赖,比如用于预览组件的storybook,实际项目是用不到的,它是用来开发组件的时候预览用的,所以storybook就是典型的要写在devDependencies里面的。总结就是你有一个组件A,它依赖了一个npm包B,如果B写在dependencies里面,那么当项目安装A的时候也会安装B。而如果B是写在A的devDependencies里面,那么项目安装A就不会自动安装B。有的时候如果你错把一些依赖写在devDependencies里面,可能会导致项目里面少安装了一些依赖。这种时候建议使用peerDependencies

peerDependencies

peerDependencies意思就是我必须需要这个依赖,但是我不会自带这个东西,需要安装我的项目去安装这个依赖。比如有的组件只能运行在react项目中,则可以把react写在peerDependencies里面。然后安装这个组件的项目需要自己指明react为dependencies。如果实际项目中缺少这个依赖,则会提示报错。

我们应该明白各种依赖的意义,在实际开发过程中加以区分,将依赖的npm包写在最合适的位置。

对devDependencies的补充

(后续追加更新,修正上面的内容。以上针对devDependencies的内容,对于发布npm包来说是正确的。而在web站点项目中,dependencies和devDependencies仅作为一个规范,并没有实际区别。这也解释了为什么公司有的项目把依赖的业务组件写在devDependencies,却能正常运行。虽不规范,却不报错。待我去公司再确认一下,项目在生产的install命令是什么)

二次追加更新,今天和同事聊天的时候,又意识到一个问题(虽然这个同事平时有种摸鱼的感觉,但是每次和他交流都感觉收获满满),即被打包和被安装是两码事。所谓的devDependencies不会被安装确实是没错,但是组件里面的devDependencies也是会被打包的。因为打包器不会去管这个依赖是dependencies还是devDependencies甚至在不在package.json里,只要node_modules里面能找到这个包,并且从打包入口开始根据import或require构建依赖关系时,确实遇到了这个包,这个包就会被打到最后的代码里面,除非用external字段指明不打这个包。用同事的话说,就是被塞到最后那个面目全非的打包后的js里面去了。

确实我之前也发现过这个问题,我写过一个简单的npm包,把react写在devDependencies里面,但是打包出来的产物快三千行,只有不到一百行是我的代码,剩下全是react我真的很难绷,后来我也是用external字段指明不打包react,最终才得到了简洁干净的打包产物。当时我还有点纳闷,明明是写在devDependencies里面,也没写在devDependencies里,怎么还被打包呢,今天才明白,写在devDependencies里面只会让我在安装我自己的npm包的时候不会再安装一个react进去,但是不能避免我的npm包里面含有react的代码。

其实这又进一步解释了为什么web站点项目中dependencies和devDependencies意义不大,我今天特地看了我们pipeline的命令,安装这一步没有指定是生产环境,所以devDependencies依赖也被装到node_modules里面去了,所以最后build的时候当然也会把devDependencies打进去。

npm机制

扁平化

接下来再说说npm的机制。我们经常发现,有的时候项目里面明明package.json里面写的各种依赖很少,但是项目node_modules里面却有着巨多的文件夹。这其实是因为npm的扁平化机制。npm在安装包的过程中,会试图将依赖的依赖,以及更深层次的依赖,都提取到最外层来。提取到最外层,更有利于减少包的重复安装。

例如一个项目,依赖了A和B,而A和B又都依赖了C。npm在早期没有做扁平化处理的时候,会普通的嵌套安装,A里面装一个C,B里面装一个C,造成了重复安装。而扁平化之后,C会被提取出来,安装在最外层,也就是和A,B同级(前提是A和B依赖的C的版本是兼容的,比如相同版本,或者指定的版本范围存在交集)。如果A依赖C@1.0.0,B依赖C@1.0.1,那么不一定哪个版本的C会被提取出来安装到最外层,然后另一个版本的C被安装到自己的父级下面,这取决于A和B的安装顺序了。这样的扁平化管理,一定程度上减少了包的重复安装,但是具有一定的不确定性,被提升上去的版本可能会在某次更改后发生变化。

第三次追更:我是不是中了邪啊,前一天在文章里写的知识点,第二天就发作了。还是同一个倒霉项目,技术中心那边有一个包,把react-router-dom从dependencies里面去掉了,我们的组件库依赖了这个包,并且没锁版本,导致昨天好好的,今天发布的集成分支跑流水线的时候install出来的结果和之前不一样。react-router-dom里面自带了react-router包,原本装在node_modules最外层的是5.x版本,结果技术中心那个包把react-router-dom去掉了之后,安装的顺序发生了改变,最外层的变成4.x版本了,5.x版本被装到里层了,真的服气,老项目不上传packagelock真的是很坑。别人升级一个包,我们这边三个人找白屏原因找两小时。

package-lock.json

第三次追更内容:既然说到了package-lock.json,就展开来说说。

作用大家都知道了,锁版本用的。你可能以为,你package.json里面不用模糊版本号,你的依赖就不会变,大错特错,你不能保证你的依赖在依赖别人时有没有锁版本号,你也改变不了。这就容易导致很多问题,我前面已经讲过惨痛的教训了。包括很多人,可能遇到git冲突,或者其他问题时,喜欢把package-lock.json删了,然后重新install,这是非常危险的,因为很多深层级的依赖可能都被你不知不觉的升级了。

package-lock.json里面一些有用的字段:

  • version不用说,实际安装的版本号
  • requires里面是这个子依赖的package.json里面的dependencies的东西
  • dependencies字段是这个子依赖的package.json里面的dependencies实际被安装到子依赖的node_modules的东西(即没被扁平化提取到最外层的包,因为版本冲突)
  • integrity:校验用哈希值
  • resolved:安装的源。同时也通过这个源的地址去校验缓存是否可用,如果源地址一致,则从缓存中拿依赖压缩包。不一致则去新的源地址下载依赖压缩包。所以我们也能知道,必须有package-lock.json,npm才有可能使用缓存

幽灵依赖

这种扁平化导致了一个问题就是幽灵依赖。即,我明明没有安装过某个包,但是我却可以使用它。还拿上面的例子,项目依赖了A和B,并没有直接依赖C,也就是说项目指明的依赖里面是没有C的,但是因为扁平化机制,C被提升到node_modules的最外层,项目里直接从C里面import东西,也是能做到的,这个就很恐怖了。

幽灵依赖会导致一些问题,首先最直接的一点就是,如果引入了幽灵依赖的原始依赖被移出,幽灵依赖也不存在了,但是去除原始依赖的人可能没察觉到这一点,直接导致项目挂掉。再次,更神不知鬼不觉的情况就是,你升级了原始依赖的版本,新版本的原始依赖不包含幽灵依赖了,项目还是直接挂掉。

还有一些问题就是,幽灵依赖的具体版本不可控,项目会在没有察觉的情况下使用新版本的幽灵依赖,而新版本和老版本可能不兼容,比如去掉了某个API之类的,还是会导致项目报错。

pnpm

针对npm的扁平化提升遗留的缺点,和幽灵依赖的问题,新的包管理器pnpm出现了。pnpm通过硬链接,将node_modules下面的包都指向了全局的一个pnpm store当中。多个项目用到相同的包,其实最后硬盘里只会安装一份,这样也节省了硬盘的空间。总的来说,pnpm其实是具有很多优点的。我们的项目也尝试过使用pnpm,但是因为自身项目不规范等等的原因(比如项目本身存在使用幽灵依赖的情况),失败了,最后又转回了npm。

如果有机会从头开始规范做的话,还是挺建议使用pnpm的

相关文章
评论
分享
  • 动手实现react-ssr 2

    仓库链接:本篇代码 运行方法参考readme,请使用pnpm,因为本次使用的rspack尚未推出1.0版本,预计后续可能会有很多breakchange,所以lock文件至关重要,而我只上传了pnpm的lock,非常抱歉。 环境:no...

    动手实现react-ssr 2
  • 动手实现react ssr

    关于ssr相关的介绍,可以移步之前的一篇博文,本文不再赘述 日常工作中,我们可能都接触过ssr,不过我们可能通常是借助于框架(例如Next.js)的支持来完成ssr。今天我想尝试,摆脱高度封装的前端框架,只依赖react相关的库,来实...

    动手实现react ssr
  • React中副作用的执行时机

    为什么一定要useEffect?之前看到过,说不要把副作用直接写在函数式组件的函数体内,需要用useEffect把副作用包裹起来。我当时的理解就是,函数组件重新渲染的话,整个函数都会被执行一遍。如果我就想让一个副作用,每次组件渲染都重...

    React中副作用的执行时机
  • 聊聊react与函数式编程

    react不是魔法 今天我想来聊一聊函数式编程,和它在react中的体现。 何为函数式编程我们可以把函数式编程理解成一种范式,一个规范。纯函数是函数式编程的关键概念,函数式编程希望我们尽可能多地使用纯函数。正是依赖于纯函数的特点,...

    聊聊react与函数式编程
  • react的一些坑

    记录一些刚写react踩过的坑吧,想到多少就写多少 从一个工作中的需求开始有个需求是,点击按钮出来一个弹窗。本来弹窗有个关闭按钮,但是UED在视觉稿上面加了行字,“点击关闭按钮或弹窗外的其他地方,关闭弹窗”。我想了想,合理,避免有...

    react的一些坑
  • 使用react的一些心得与感想

    刚忙完公司的新人训练营,9个人的团队2周完成一个小项目。我负责用remax开发一个微信小程序。remax就是蚂蚁的一个开源框架,让开发者能用react的语法去开发微信小程序。开发体验整体来讲非常不错,感觉也加深了我对react的理解...

    使用react的一些心得与感想
  • 基于tapable来模拟webpack插件机制

    前两天看到ByteFE公众号发了篇文章,从源码去讲webpack。我之前也从很多方面去学习webpack,只是源码一直没太看得进去,这一次我想仔细地读一读。 简要介绍tapable说实话,之前看webpack源码一直没看的太懂,...

    基于tapable来模拟webpack插件机制
  • 深入js——作用域,作用域链,执行栈(一)

    最近在补js基础。当初自学js的时候,总是感觉学的不扎实。只能说不去上手,再怎么死读书,也理解不了。积累了经验,然后自己去思考,这个时候再去看理论,就容易理解了。 感觉工作一年以来,确实学到了很多,但是过于专注于应用方面的知识,反倒...

    深入js——作用域,作用域链,执行栈(一)
  • webpack运行时代码简要分析

    开个坑,主要也是怕自己后面忘了 简单写写,有空了补上 __webpack_modules是一个数组,每个元素都是一个函数,函数接受三个参数:第一个参数module好像没用到,第二个是一个对象(会把本模块要导出的属性,赋值给这个对象),...

    webpack运行时代码简要分析
  • webpack动态加载原理简述

    为什么需要动态加载上一篇讲我写webpack针对markdown的loader的时候,提到了希望能动态加载以优化性能。篇幅原因没有展开说,打算单独写一篇文章讲一下。 当我们访问网站的时候,浏览器会向服务器请求页面资源。首先请求的当然是...

    webpack动态加载原理简述
Please check the parameter of comment in config.yml of hexo-theme-Annie!