关于react.js:Hooks时代如何写出高质量的react和vue组件

6次阅读

共计 5232 个字符,预计需要花费 14 分钟才能阅读完成。

0、概述
一个组件外部的所有代码——无论 vue 还是 react——都能够形象成以下几个局部:

组件视图,组件中用来形容视觉效果的局部,如 css 和 html、react 的 jsx 或者 vue 的 template 代码
组件相干逻辑,如组件生命周期,按钮交互,事件等
业务相干逻辑,如登录注册,获取用户信息,获取商品列表等与组件无关的业务形象

独自拆分这三块并不难,难的是一个组件可能写得特地简单,外面可能蕴含了多个视图,每个视图相互之间又有交互;同时又可能蕴含多个业务逻辑,多个业务的函数和变量横七竖八地随便搁置,导致后续保护的时候要在代码之间重复横跳。
要写出高质量的组件,能够思考以下几个问题:
1. 组件什么时候拆?怎么拆?
一个常见的误区是,只有须要复用的时候才去拆分组件,这种认识显然过于全面了。你能够思考一下,本人是如何形象一个函数的,你只会在代码须要复用的时候才抽出一个函数吗?显然不是。因为函数不仅有代码复用的性能,还具备肯定的形容性质以及代码封闭性。这种个性使得咱们看到一个函数的时候,不用关注代码细节,就能大略晓得这部分代码是干啥的。
咱们还能够再用函数将一部分函数组合起来,造成更高层级的形象。按国内风行的说法,高层级的形象被称为粗粒度,低层级的形象被称为细粒度,不同粗细粒度的形象能够称它们为不同的形象层级。并且一个现实的函数外部,个别只会蕴含同一形象层级的代码。
组件的拆分也能够遵循同样的情理。咱们能够依照以后的构造或者性能、业务,将组件拆分为性能清晰且繁多、与内部耦合水平低的组件(即所谓高内聚,低耦合)。如果一个组件外面干了太多事,或者依赖的内部状态太多,那么就不是一个容易保护的组件了。

然而,为了放弃组件性能繁多,咱们是不是要将组件拆分得特地细才能够呢?事实并非如此。因为下面说过,形象是有粗细粒度之分的,兴许一个组件从较细的粒度来讲性能并不繁多,然而从较粗的粒度来说,可能他们的性能就是繁多的了。例如登录和注册是两个不同的性能,然而你从更高层级的形象来看,它们都属于用户模块的一部分。
所以是否要拆分组件,最要害还是得看复杂度。如果一个页面特地简略,那么不进行拆分也是能够,有时候拆分得过于细可能反而不利于保护。
如何判断一个组件是否简单?恐怕这里不能给出一个精确的答案,毕竟代码的实现形式千奇百怪,很难有一个机械的规范评判。然而咱们无妨站在第三方角度看看本人的代码,如果你是一个工作一年的程序员,是否能比拟容易地看懂这里的代码?如果不能就要思考进行拆分了。如果你非要一个机械的判断规范,我倡议是代码管制在 200 行内。
总结一下,拆分组件的时候能够参考上面几个准则:

拆分的组件要放弃性能繁多。即组件外部代码的代码都只跟这个性能相干;
组件要放弃较低的耦合度,不要与组件内部产生过多的交互。如组件外部不要依赖过多的内部变量,父子组件的交互不要搞得太简单等等。
用组件名精确形容这个组件的性能。就像函数那样,能够让人不必关怀组件细节,就大略晓得这个组件是干嘛的。如果起名比拟艰难,思考下是不是这个组件的性能并不繁多。

2. 如何组织拆分出的组件文件?
拆分进去的组件应该放在哪里呢?一个常见的错误做法是一股脑放在一个名为 components 文件夹里,最初搞得这个文件夹特地臃肿。我的倡议是相关联的代码最好尽量聚合在一起。
为了让相关联的代码聚合到一起,咱们能够把页面搞成文件夹的模式,在文件夹外部寄存与以后文件相干的组成部分,并将示意页面的组件命名为 index 放在文件夹下。再在该文件夹下创立 components 目录,将组成页面的其余组件放在外面。
如果一个页面的某个组成部分很简单,外部还须要拆分成更细的多个组件,那么就把这个组成部分也做成文件夹,将拆分出的组件放在这个文件夹下。
最初就是组件复用的问题。如果一个组件被多个中央复用,就把它独自提取进去,放到须要复用它的组件们独特的形象层级上。如下:

如果只是被页面内的组件复用,就放到页面文件夹下。
如果只是在以后业务场景下的不同页面复用,就放到以后业务模块的文件夹下。
如果能够在不同业务场景间通用,就放到最顶层的公共文件夹,或者思考做成组件库。

对于我的项目文件的组织形式曾经超过本文探讨的领域,我打算放到当前专门出一篇文章说下如何组织我的项目文件。这里只说下页面级别的文件如何进行组织。上面是我罕用的一种页面级别的文件的组织形式:
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) // 组件 A 和组件 B 里复用的组件
|-- index.(vue|tsx) // 以后页面

复制代码
实际上这种组织形式,在形象意义上并不完满,因为通用组件和页面组成部分的组件并没有辨别开来。然而一般来说,一个页面也不会抽出太多组件,为了不便放到一起也不会有太大问题。然而如果你的页面切实简单,那么再创立一个名为 common 的文件夹也未尝不可。

3. 如何用 hooks 抽离组件逻辑?
在 hooks 呈现之前,曾风行过一个设计模式,这个模式将组件分为无状态组件和有状态组件(也称为展现组件和容器组件),前者负责管制视觉,后者负责传递数据和解决逻辑。但有了 hooks 之后,咱们齐全能够将容器组件中的代码放进 hooks 外面。后者不仅更容易保护,而且也更不便把业务逻辑与个别组件辨别开来。
在抽离 hooks 的时候,咱们不仅应该沿用个别函数的抽象思维,如性能繁多,耦合度低等等,还应该留神组件中的逻辑可分为两种:组件交互逻辑与业务逻辑。如何把文章结尾说的视图、交互逻辑和业务逻辑辨别开来,是掂量一个组件品质的重要规范。
以一个用户模块为例。一个蕴含查问用户信息,批改用户信息,批改明码等性能的 hooks 能够这样写:
// 用户模块 hook
const useUser = () => {

// react 版本的用户状态
const user = useState({});
// vue 版本的用户状态
const userInfo = ref({});

// 获取用户状态
const getUserInfo = () => {}
// 批改用户状态
const changeUserInfo = () => {};
// 查看两次输出的明码是否雷同
const checkRepeatPass = (oldPass,newPass) => {}
// 批改明码
const changePassword = () => {};

return {
    userInfo,
    getUserInfo,
    changeUserInfo,
    checkRepeatPass,
    changePassword,
}

}
复制代码
交互逻辑的 hook 能够这么写(为了不便只写 vue 版本的,大家应该也都看得懂):
// 用户模块交互逻辑 hooks
const useUserControl = () => {

// 组合用户 hook
const {userInfo, getUserInfo, changeUserInfo, checkRepeatPass, changePassword} = useUser();
// 数据查问 loading 状态
const loading = ref(false);
// 谬误提醒弹窗的状态
const errorModalState = reactive({
    visible: false, // 弹窗显示 / 暗藏
    errorText: '',  // 弹窗文案
});

// 初始化数据
const initData = () => {getUserInfo();
}
// 批改密码表单提交
const onChangePassword = ({oldPass, newPass) => {
    // 判断两次明码是否统一
    if (checkRepeatPass(oldPass, newPass)) {changePassword();
    } else {
        errorModalState.visible = true;
        errorModalState.text = '两次输出的明码不统一,请批改'
    }
};
return {
    // 用户数据
    userInfo,
    // 初始化数据
    initData: getUserInfo,
    // 批改明码
    onChangePassword,
    // 批改用户信息
    onChangeUserInfo: changeUserInfo,
}

}
复制代码
而后只有在组件外面引入交互逻辑的 hook 即可:
vue 版本:
<template>

<!-- 视图局部省略,在对应 btn 处援用 onChangePassword 和 onChangeUserInfo 即可 -->

</template>
<script setup>
import useUserControl from ‘./useUserControl’;
import {onMounted} from ‘vue’;

const {userInfo, initData, onChangePassword, onChangeUserInfo} = useUserControl();
onMounted(initData);
<script>

复制代码
react 版本:
import useUserControl from ‘./useUserControl’;
import {useEffect} from ‘react’;

const UserModule = () => {

const {userInfo, initData, onChangePassword, onChangeUserInfo} = useUserControl();
useEffect(initData, []);
return (// 视图局部省略,在对应 btn 处援用 onChangePassword 和 onChangeUserInfo 即可)

}

复制代码
而拆分出的三个文件放在组件同级目录下即可;如果拆出的 hooks 较多,能够独自开拓一个 hooks 文件夹。如果有能够复用的 hooks,参考组件拆分外面分享的办法,放到须要复用它的组件们独特的形象层级上即可。
能够看到抽离出 hooks 逻辑后,组件变得非常简略、容易了解,咱们也实现了各个局部的拆散。不过这里还有一个问题,那就是下面的业务场景切实太过简略,有必要拆分得这么细,搞出三个文件这么简单吗?
针对逻辑并不简单的组件,我集体感觉和组件放到一起也未尝不可。为了简便,咱们能够只把业务逻辑封装成 hooks,而组件的交互逻辑就间接放在组件外面。如下:
<template>

<!-- 视图局部省略,在对应 btn 处援用 changePassword 和 changeUserInfo 即可 -->

</template>
<script setup>
import {onMounted} from ‘vue’;
// 用户模块 hook
const useUser = () => {

// 代码省略

}

const {userInfo, getUserInfo, changeUserInfo, checkRepeatPass, changePassword} = useUser();
// 数据查问 loading 状态
const loading = ref(false);
// 谬误提醒弹窗的状态
const errorModalState = reactive({

visible: false, // 弹窗显示 / 暗藏
errorText: '', // 弹窗文案

});

// 初始化数据
const initData = () => { getUserInfo(); }
// 批改密码表单提交
const onChangePassword = ({oldPass, newPass) => {};

onMounted(initData);
<script>
复制代码
然而如果逻辑比较复杂,或者一个组件外面蕴含多个简单业务或者简单交互,须要抽离出多个 hooks 的状况,还是独自抽出一个个文件比拟好。总而言之,根据代码复杂度,抉择绝对更容易了解的写法。
兴许独自一个组件,你并不能体会出 hooks 写法的优越性。但当你封装出更多的 hooks 之后,你会逐步发现这样写的益处。正因为不同的业务和性能被封装在一个个 hooks 外面,彼此互不烦扰,业务能力更容易辨别和了解。对于我的项目的可维护性和可读性晋升是十分之大的。
下图展现了 vue2 写法和 vue3 hooks 写法的区别。图中雷同色彩的代码块代表这些代码是属于同一个性能的,但 vue2 的写法导致原本是雷同性能的代码,却被拆散到了不同中央(react 其实也容易有雷同的问题,例如当一个组件有多个性能时,不同性能的代码也很容易混淆到一起)。而通过封装成一个个 hooks,相关联的代码就很容易被聚合到了一起,且和其余功能区离开了。

正文完
 0