引言
当前端业务复杂度上升到一定程度的时候,如何提升前端代码质量便成了老生常谈的话题。似乎前端总逃不开改他人代码,重构,修复bug的宿命。那么,我们要如何从项目代码层面,改变这一局面呢?才能保证项目A之于开发者B也是能有条不紊的介入开发,从而最大程度降低人员开销,实现真正降本提效呢?
从代码层面的问题上看,我列举了下,大概有如下几种:
- 工程化没有做好各类lint检查和约束
超长的function
- 很难单从函数名看出这个函数是作什么的
- 一个函数做了十件事
代码量超长的模块
- 内部维护了非常多逻辑,很难一眼看清某个变量是在哪里被修改的
- 一个组件夹杂了这个组件所需的所有代码,不懂职责划分的重要性
缺乏清晰的职责划分
- 哪个模块做什么,对于数据应该如何流向,如何改变没有清晰的认知
数据流紊乱
- 缺乏函数式写法的意识
变量命名极不规范
- 变量命名很含糊,能通过命名讲清楚这个函数是做什么的,却很随意对待
检验好代码的唯一标准应该是:人们能否轻而易举地修改它
业务的问题
我们知道,业务迭代往往排山倒海压来,一开始如果不做好全局规划,或者理清各个模块的关系,是很难把控好进度,进而出现赶工导致bug滋生。那么,目前的hooks 业务组件的写法有何问题呢?
基于hooks的纯业务组件写法没有做约束,ui与业务逻辑在一个函数内部维护,面条式代码滋生,容易使组件业务逻辑代码越写越长,久而久之难以维护。很容易出现一个函数内部耦合了types,constants,各类hooks(useState,useReducer,useCallback等),以及各种function,甚至是在dom层夹杂着非常多的逻辑处理。
慢慢地,复用性也会越来越差,可能需要经常重构,抽离代码 以达到复用的程度。但往往业务的排期已经没法抽开身去维护老代码,那怎么办呢?
hooks组件的分离
《重构2:改善既有代码的设计》一文提到:把复杂的代码块分解为更小的单元,与好的命名一样都很重要。
因此,我们需要在团队内部达成共识,能够产出一种固定的开发范式,能够分离代码,做到职责清晰,例如:A模块专门处理View视图组件,B模块专门处理业务逻辑,C模块专门维护ts类型types,D模块专门维护各类常量constants,E模块专门维护公用hooks逻辑,F模块专门维护css modules等。
那么,在这前提之下,我们需要实现前端UI与业务逻辑分离,目前主流的有两种方式,一种是纯逻辑抽离出去,返回函数内部方法和state;形如:
const useApp = () => {
const [name, setName] = useState('mike');
const getName = () => {};
const updateName = () => {};
return {
name,
getName,
updateName
}
}
const AppView =() => {
const { name } = useApp();
return <div>{name}</div>
}
这种方式没什么太大问题,但这种代码不内聚,没法提供通用的逻辑处理,一旦业务发生变化,就会引发多处代码的维护危机。
其次如果有很多业务团队,那么就需要考虑如何规范化统一团队内部写法,如何支持更健壮的业务代码。
UI与逻辑分离并不是最终的目的,最终的目的应该是形成一套易于维护,模块职责划分清晰,能够形成固定开发模式,易于扩展,能够规范化业务使用场景,且具备强壮生命力的方案。
如果这种方式可以实现的话,那么为何很少有人会这么干呢?原因可能在于大家的函数式组件的思维。
在hooks还没诞生之前,大家普遍对于函数式组件的认知就是没有state,所以当props是固定的,那么函数式组件每次渲染结果也都是一样的,也就是相同的输入总能得到相同的输出。但现在hooks出现了,函数组件内部可以维护state了,相同的输入并不一定能得到相同的输出了。
此外,这种方式与可复用的hooks的区别又在哪里,如果两种都使用hooks维护,又如何区分呢?
另外一种方式就是保留业务逻辑,但把UI组件抽离出去,这种方式更不推荐了。有点类似子组件,父子组件通信的既视感随之袭来。
接下来,我们再来看下纯hooks组件饱受大家诟病的一些问题:
纯hooks组件的问题
1、useState 写法难用,如果有很多state,需要一个个去维护,写法不够简洁;当业务逻辑越来越复杂,往往会出现一个模块几十个useState需要维护的尴尬局面。
2、useReducer + context
的全局状态难用,仍然需要定义很多action type
,还需要提供provider,使用useReducer跨组件共享状态很麻烦
3、useCallback 用法不够清晰,不知何时用何时不用,用法造成困惑
4、 生命周期需要引入useEffect,需要手动管理,且不够语义化
5、基于hooks的业务组件,内部方法依然难以做到复用,应抽离出去单独维护。
6、当使用useEffect模拟mounted事件时,处理异步请求函数时很麻烦。
7、当组件达到一定复杂度的时候,堆积到一起的代码会变得越来越难以维护
8、React Hook的闭包陷阱问题
9、useState 调用updater更新后,无法同步获取最新state值
10、useState updater无法实现细粒度更新对象的属性值,不得不浅拷贝一份数据再进行覆盖
hooks-view-model
想要写出健壮的,长期可持续维护的代码,就必须去理解这些在其他编程领域通用的设计模式、原则、范式。提高代码质量,除了依赖开发自测和相关流程规范化外,也应有相关工具或统一的开发范式做约束。
对于纯写业务的人来说,没有规范去强制约定,那么几乎没有人会这么处理业务逻辑与UI的关系,最终还是会写到一起。这是hooks这种弱约束的弊端。
基于上述问题,我开发了基于react hooks的UI与业务逻辑分离的方案,内部基于useState hooks的updater 实现。可实现在class内部setState,然后在View组件中响应更新。基本解决了上述react hooks的十个“老大难”问题
hooks-view-model
是一种通过拆分UI视图与业务逻辑的解决方案,可做到无需useReducer,无需redux等技术方案实现全局状态更新而不会渲染无关组件。hooks-view-model
是集状态管理,变量的存储管理和数据的持久化管理于一体的解决方案。
详情点击👉:https://github.com/hawx1993/h...
hooks-view-model
主要用于分离UI与业务逻辑,可以解决 纯hooks组件的问题,对比一下hooks-view-model的优势:
hooks组件问题 | hooks-view-model |
---|---|
useState 写法难用,如果有很多state,需要一个个去维护,写法不够简洁 | 可通过对象形式更新与解构数据,写法简洁 |
useReducer + context的全局状态难用,仍然需要定义很多action type,还需要提供provider,使用useReducer跨组件共享状态很麻烦 | 全局状态更新只需使用useGlobalState hooks,用法简单 |
生命周期需要引入useEffect,需要手动管理,且不够语义化 | 提供mounted和unmounted 钩子函数,可自动执行,语义化友好 |
基于hooks的业务组件,内部方法依然难以做到复用,应抽离出去单独维护 | class 写法可通过继承 实现复用,还可以通过useVM 引入其他viewModel进行复用,复用性高 |
当接收新的props,需要手动使用useEffect观察props变化,没有直接的钩子可以自动触发 | class 提供onPropsChanged 钩子函数,可自动触发执行 |
当组件达到一定复杂度的时候,堆积到一起的代码会变得越来越难以维护 | UI与逻辑做到了很好的分离,代码组织性强 |
React Hook的闭包陷阱问题 | 由于方法都提到class中去维护了,所以不存在此问题 |
useState 调用updater更新后,无法同步获取最新state值 | 可通过调用getCurrentState 同步获取最新值 |
调用updater无法实现细粒度更新对象属性值,需浅拷贝对象后覆盖 | 可通过updateImmerState实现细粒度更新 |
1、View:获取数据并展示数据
// AppView.tsx
import { AppViewModel } from './AppViewModel'
import { useVM } from 'hooks-view-model'
import { usePrevious } from '@/hooks';
const AppView = () => {
const { perviousAddress } = usePrevious();
const { changeAddress, useCurrentState } = useVM(AppViewModel, {
address: perviousAddress,
})
const { address = 'ZheJiang Province' } = useCurrentState()
return (
<div>
<button onClick={changeAddress}>click to change address</button>
<span>{address}</span>
</div>
)
}
2、ViewModel:管理状态和处理数据
updateGlobalStateByKey
和 updateCurrentState
相当于在class中可以使用的setState方法,只不过需要保证class中的所有方法都是箭头函数,否则会报错
// AppViewModel.ts
import StoreViewModel from 'hooks-view-model'
class AppViewModel extends StoreViewModel {
changeAddress = () => {
this.updateCurrentState(this.props.address);// 相当于setState
}
}
export { AppViewModel }
那么可能有很多人就疑惑了,明明react官方已经推崇函数式写法了,为什么还要用class?
基于class的viewModel写法与hooks有什么区别
诚然,hooks 可满足UI与逻辑分离的需求,但抽离无法被公用的业务逻辑到hooks中是否有必要?与可复用的hooks 是否容易造成混淆?hooks存在的useCallback,useReducer,以及对副作用的使用等容易造成使用困惑的,以及对useState 使用上的麻烦是否可以有其他方法简化?
其次,函数式组件的写法也并非函数式编程,相同的输入(props)并不会得到相同的输出(内部的state或全局的state都可能对结果产生影响)。
而业务逻辑抽离到class中,依然是函数式组件。class相比于function 天然的具有可组织性,可扩展性(extends),和可维护性。
首先,业务逻辑是比较复杂的,Class 具备继承能力,可实现viewModel与view都获得来自父类的能力;
其次,class 能够更好维护业务逻辑代码,在class中写业务逻辑,完全可以忽视react hooks自带的各种hooks,诸如useRef,useCallback,useReducer,useState
等,写起业务逻辑来更加纯粹;
再者,hooks 也可以与viewModel共存,只需要在view中引入hooks,然后将返回值作为props,通过useVM传给viewModel即可,两者是共存的,并不是互斥的。
基于class的viewModel可以更好的维护业务逻辑代码,可以使用装饰器,public,private等关键字,显示提高代码可维护性和扩展能力。而可复用的hooks可以用来抽象业务逻辑实现副作用观察和逻辑复用,两者具有不同的心智模型。
配置生成项目模板文件
此外,我还在hooks-view-model内置了项目的模板文件,可一键生成所需模板文件和代码,这样便可以让各个业务线的前端团队始终保持一致的开发规范和风格。
可以真正做到成员B可以低成本介入项目A中,提高代码的可维护性,可阅读性。用法如下:
执行如下步骤,可一键生成模板文件
1、添加脚本命令
在package.json的scripts中添加如下脚本命令
scripts: {
"generate": "plop --plopfile ./node_modules/hooks-view-model/generators/index.js"
}
2、根目录创建template.config.js
指明模板需要生成的相对路径地址:
const dir_to_generate = './src/pages/';
module.exports = dir_to_generate;
执行完后,便会在指定的目录下生成如下模板文件:
更好的debug能力
使用hooks,我们如果想知道当前的state值,我们需要一个个console出来,而基于hooks-view-model,我们只需要在控制台输入:globalStore,即可查看所有view对应的state,通过key区分。可大大提升debug能力。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。