当前位置: 首页 > 科技观察

如何在Hooks时代写出高质量的React和Vue组件?_0

时间:2023-03-19 11:56:32 科技观察

vue和react都已经全面进入了hooks时代(在vue中也称为combinedapi,为方便起见统称为hooks),但是受到react中类组件和vue2写法的影响,很多开发者已经无法及时转化,以至于开发出一堆面条代码,整体代码质量不如改版前。hooks组件怎么写,我也迷茫过一阵子。特别是,我以前主要是在react开发,但是调到新的岗位后,就改用vue3开发了。花了一段时间去适应两个框架在思维方式和写作方式上的差异。幸运的是,几个月后,我发现,虽然两人的文笔有差异,但思路却很相似。所以在对比了两个框架的异同之后,我总结出了一套通用的hooksAPI抽象方法,在这里分享给大家。如有不同意见,欢迎在评论区指正。1.概述一个组件内部的所有代码——不管是vue还是react——都可以抽象成以下几个部分:code组件相关逻辑,如组件生命周期、按钮交互、事件等业务相关逻辑,如登录注册、获取用户信息、获取商品列表等与组件无关的业务抽象并不难把这三个部分分开,但是比较难是一个组件,可能写的特别复杂,可能包含多个视图,每个视图之间可能会交互;同时,它可能包含多个业务逻辑,多个业务的函数和变量随意乱放,导致后续维护时,需要在代码之间反复跳转。要写出高质量的组件,可以思考以下几个问题:2、什么时候拆组件?怎么拆?一个常见的误解是组件只有在需要重用时才会拆分。这种看法显然过于片面。你可以想想你如何抽象一个函数。是否只在代码需要复用时才提取一个函数?很明显不是。因为函数不仅具有代码重用的功能,而且还具有一定的描述性和代码封闭性。这个特性可以让我们看到一个函数,就可以大致知道这部分代码做了什么,而不用去关注代码的细节。我们还可以使用函数,将一些函数组合起来,形成更高层次的抽象。按照国内流行的说法,高层次的抽象称为粗粒度,低层次的抽象称为细粒度,不同粗粒度层次的抽象可以称为不同层次的抽象。一个理想的函数通常只包含相同抽象级别的代码。组件的拆分也可以遵循同样的原则。根据当前的结构、功能和业务,我们可以将组件拆分成功能清晰单一、对外耦合度低(即所谓高内聚低耦合)的组件。如果一个组件做了太多的事情,或者依赖太多的外部状态,那么它就不是一个易于维护的组件。components.png但是,为了保持组件的单一功能,我们是否必须将组件拆分成非常细小的块?但事实上并非如此。因为上面说了,抽象分为粗粒度和细粒度。也许一个组件从更细的粒度上看功能单一,但从更粗的粒度上看,它们的功能可能是单一的。比如登录和注册是两个不同的功能,但是从更高的抽象层次来看,它们都是用户模块的一部分。所以是否拆分组件,最重要的还是看复杂度。如果一个页面特别简单,不拆分也是可以的。有时拆分得太细可能不利于维护。如何判断一个组件是否复杂?恐怕我不能在这里给出准确的答案。毕竟代码的实现千奇百怪,很难有一个机械的标准判断。但是我们不妨站在第三方的角度来看我们的代码。如果你是一个工作了一年的程序员,你能更容易理解这里的代码吗?如果没有,考虑拆分。如果坚持机械的判断标准,我建议代码控制在200行以内。综上所述,在拆分组件时,可以参考以下原则:拆分后的组件应保持单一的功能。即组件内部代码的代码只与这个功能相关;组件应该保持低耦合度,不要与组件外部有过多的交互。例如,组件内部不要依赖过多的外部变量,父子组件之间的交互不要过于复杂等。用组件名称准确描述这个组件的功能。就像函数一样,人们无需关心组件的细节,就可以大致了解组件的作用。如果命名困难,考虑这个组件的功能是否不单一。vue.webp3.如何组织拆分的组件文件?拆分的组件应该放在哪里?一个常见的错误是把你所有的脑筋都放在一个名为components的文件夹里,最后让这个文件夹变得非常臃肿。我的建议是相关代码尽量聚合。为了将相关的代码聚合在一起,我们可以将页面做成一个文件夹,将与当前文件相关的组件存放在文件夹内,并将代表页面索引的组件命名为文件夹下的组件。然后在这个文件夹下创建一个components目录,把其他组成页面的组件放在里面。如果页面的某个部分比较复杂,需要拆分成多个组件,那么就把这部分做成一个文件夹,把拆分后的组件放在这个文件夹下。最后,还有组件重用的问题。如果一个组件在多个地方被复用,则将它单独提取出来,放在需要复用它的组件的公共抽象层次上。如下:如果只被页面中的组件复用,则放在page文件夹下。如果只是在当前业务场景的不同页面复用,就放在当前业务模块的文件夹下。如果可以在不同的业务场景下使用,放在顶层public文件夹下,或者考虑做成组件库。项目文件的组织超出了本文的范围。打算以后专门写一篇关于如何组织项目文件的文章。这里我们只讲如何在页面级别组织文件。下面是我经常使用的页面级文件组织方式:homePage//存放当前页面的文件夹|--components//存放当前页面组件的文件夹|--componentA//存放当前页面的组件A页面文件夹|--index.(vue|tsx)//组件A|--AChild1.(vue|tsx)//组件a的组件1|--AChild2.(vue|tsx)//组件a组件2|--ACommon.(vue|tsx)//只在componentA内部复用的组件|--ComponentB.(vue|tsx)//当前页面的组件B|--Common.(vue|tsx)//复用的组件incomponentAandcomponentB|--index.(vue|tsx)//目前的页面复制代码其实在抽象意义上并不完美,因为一般的组件和页面的组成有些组件是没有分开的。但是一般来说,一个页面不会提取太多的组件,为了方便把它们放在一起问题不大。但是如果你的页面真的很复杂,创建另一个名为common的文件夹也是可以的。coding.webp4.如何使用hooks提取组件逻辑?在钩子出现之前,有一种流行的设计模式,将组件分为无状态组件和有状态组件(也称为显示组件和容器组件)。前者负责控制视觉,后者负责传递数据和处理逻辑。但是有了hooks,我们就可以把容器组件中的代码放到hooks中。后者不仅更易于维护,而且更方便将业务逻辑与通用组件分离。在提取hook时,我们不仅要使用通用功能的抽象思维,如功能单一、低耦合等,还要注意组件中的逻辑可以分为两种:组件交互逻辑和业务逻辑。如何区分文章开头提到的视图、交互逻辑和业务逻辑,是衡量一个组件好坏的重要标准。以用户模块为例。一个包括查询用户信息、修改用户信息、修改密码等的钩子可以这样写://usermodulehookconstuseUser=()=>{//用户状态constuser=useState({});//reactversionvue版本的用户状态constuserInfo=ref({});//获取用户状态constgetUserInfo=()=>{}//修改用户状态constchangeUserInfo=()=>{};//检查输入两次密码是否相同constcheckRepeatPass=(oldPass,newPass)=>{}//修改密码constchangePassword=()=>{};return{userInfo,getUserInfo,changeUserInfo,checkRepeatPass,changePassword,}}复制代码交互逻辑的hook可以这样写(为了方便,只写vue版本的,大家应该看得懂)//用户模块交互逻辑hooksconstuseUserControl=()=>{//组合用户挂钩const{userInfo,getUserInfo,changeUserInfo,checkRepeatPass,changePassword}=useUser();//数据查询加载状态constloading=ref(false);//错误提示弹窗状态consterrorModalState=reactive({visible:false,//弹窗显示/隐藏errorText:'',//弹窗文案});//初始化数据constinitData=()=>{getUserInfo();}//修改密码表单提交constonChangePassword=({oldPass,newPass)=>{//判断两次密码是否一致if(checkRepeatPass(oldPass,newPass)){changePassword();}else{errorModalState.visible=true;errorModalState.text='两次输入密码不一致,请修改'}};return{//用户数据userInfo,//初始化数据initData:getUserInfo,//更改密码onChangePassword,//修改用户信息onChangeUserInfo:changeUserInfo,}}复制代码,放在组件中即可引入交互逻辑钩子即可:vueversion:复制代码reactversion:importuseUserControlfrom'./useUserControl';import{useEffect}from'react';constUserModule=()=>{const{userInfo,initData,onChangePassword,onChangeUserInfo}=useUserControl();使用效果(初始化数据,[]);return(//view部分省略,直接参考对应btn处的onChangePassword和onChangeUserInfo即可)}复制代码,将三个文件拆分到component同级目录下即可;如果需要移除的钩子较多,可以单独创建一个hooks文件夹。如果有可以复用的hook,参考组件拆分中的共享方式,放在需要复用的组件的common中。在抽象层次上。可以看到,把hooks逻辑抽取出来后,组件变得非常简单易懂,我们也实现了各个部分的分离。但是这里还有一个问题,就是上面的业务场景过于简单了。有必要把它拆分得这么细,把三个文件弄得这么复杂吗?对于逻辑不复杂的组件,我个人觉得放在一起是可以的。为了简单起见,我们只能将业务逻辑封装成hooks,组件的交互逻辑直接放在组件中。如下:复制代码但是如果逻辑比较复杂,或者一个组件包含多个复杂的业务或者复杂的交互,在需要提取更多钩子的情况下,最好分别提取每个文件。总而言之,根据代码的复杂程度,选择相对容易理解的写法。或许单个组件,你无法体会到hooks写法的优越性。但是当你封装的hook多了,你就会慢慢发现这样写的好处。正是因为不同的业务和功能被封装在hooks中,互不干扰,所以业务更容易区分和理解。大大提高了项目的可维护性和可读性。下图是vue2写法和vue3hooks写法的区别。图中相同颜色的代码块代表这些代码属于同一个功能,但是vue2的写法导致代码本来功能相同,拆到不同地方(react也容易出现同样的问题,比如当一个组件有多个功能时,不同功能的代码也很容易混在一起)。通过将它们封装到钩子中,相关代码可以很容易地聚合在一起并与其他功能分离。vue3.png题外话:全局状态管理目前前端项目中还有一个比较普遍的误区,就是滥用全局状态管理库(即redux、vuex等)。按照抽象层次的思路,其实很多项目不需要把更多的状态放在全局。这种情况下,使用react和vue本身的状态管理就足够了。如果你必须使用状态管理库,你还应该警惕在全局范围内放置更多的状态和函数。一个状态是否应该放在全局,我一般有两个判断标准:状态是否在多个页面之间共享;跳转到页面后是否返回页面,是否恢复跳转前的状态(只针对react,vue有keep-alive),全局状态管理库中的函数只放置全局状态相关的逻辑.所有其他状态都由react和vue组件本身管理。