每次接到一个需求时,最先考虑的问题就是如何实现。可能有的同学一把梭一个文件直接干完,可能有的同学会拆分组件。
对于老司机来说肯定是喜欢拆分组件的,那么问题来了怎么拆分呢?
我打算用下面的例子来和同志们探讨下,例子是使用 React-hooks 实现的,但是这个并不影响你使用其他的技术。重点不是哪种技术,而是怎么拆分的思考。
首先我先定义了三层职责分别是:
一个可以加减的数量编辑器,并且有最大和最小值的要求。
例如下面这个组件
const NumberSelect = () => {
const [value, setValue] = useState(0)
const [message, setMessage = useState('')
const onClickMinus = () => {
if(value > 0) {
setValue(value -1)
setMessage('')
}else {
setMessage('不能再减小啦~')
}
}
const onClickPlus = () => {
if(value< 10) {
setValue(value +1)
setMessage('')
}else {
setMessage('已经最多了噢~')
}
}
return (
<div className="quantity-selector">
<button onClick={onClickMinus} className="button">
-
</button>
<div className="number">{value}</div>
<button onClick={onClickPlus} className="button">
+
</button>
<div className="message">{message}</div>
</div>
);
}
我们大部分的第一直觉就是向上面这样编写代码。但是这样写的组件存在一个问题。那就是没有分离职责,对于一个小组件来说可能没有什么问题,但是如果这个组件不断扩展,可能因为新的功能需要添加更多的逻辑。那么很快这个组件就会变得臃肿不好维护。
首先,我们把这个组件分为两个部分:
const useNumberSelect = () => {
const [value, setValue] = useState(0)
const [message, setMessage] = useState('')
const onClickMinus = () => {
if (value > 0) {
setValue(value - 1)
setMessage('')
} else {
setMessage('不能再减小啦~')
}
}
const onClickPlus = () => {
if (value < 10) {
setValue(value + 1)
setMessage('')
} else {
setMessage('已经最多了噢~')
}
}
return {
value,
message,
onClickMinus,
onClickPlus,
}
}
在这个自定义 hook 中我们返回了 2 个状态值,以及操作这两个状态值的业务函数。
const NumberSelect = () => {
const { value, message, onClickMinus, onClickPlus } = useNumberSelect()
return (
<div className='quantity-selector'>
<button onClick={onClickMinus} className='button'>
-
</button>
<div className='number'>{value}</div>
<button onClick={onClickPlus} className='button'>
+
</button>
<div className='message'>{message}</div>
</div>
)
}
现在这个组件看上去还可以了,因为他内部没有业务逻辑,只是负责展示。
但并不是说这样就可以了,我们还可以把业务逻辑和实现逻辑进行分离,还记得开头定义的三层职责吧:)
因为这个组件并不复杂,所以我把业务逻辑以及实现逻辑放在了一个文件内展示,如果逻辑复杂可以把这两层职责放在各自的文件中实现。
import { useState } from 'react'
// 业务逻辑 干净、可测试的函数
const increase = (prevValue, max) => {
return {
value: prevValue < max ? prevValue + 1 : prevValue,
message: prevValue < max ? '' : '已经最多了噢~',
}
}
const decrease = (prevValue, min) => {
return {
value: prevValue > min ? prevValue - 1 : prevValue,
message: prevValue > min ? '' : '不能再减小啦~',
}
}
// 实现/框架逻辑。在这里封装state和effect
const useNumberSelect = () => {
const [state, setState] = useState({
value: 0,
message: '',
})
const onClickPlus = () => {
setState(increase(state.value, 10))
}
const onClickMinus = () => {
setState(decrease(state.value, 0))
}
return { onClickPlus, onClickMinus, state }
}
export default useNumberSelect
代码的上半部分是两个业务逻辑的纯函数。它们只用来负责计算value
和设置message
。如果把请求、dom 操作等其他和业务逻辑无关的杂项与业务逻辑分离后,测试、重构和维护业务逻辑会变得很容易。
代码的下半部分是实现逻辑。这段代码就和框架比较相关了,使用框架的 api 来实现状态的存储和操作。其实就是一个 React hook 的封装。
当从自定义 hook 获取封装完的状态后,我们可能需要对数据进行进一步的处理,比如说过滤、排序等。这些操作应该放在自定义 hook 外。因为这些操作可以是一个或者多个纯函数的组合来完成,这样也更好维护和测试。
如果你想给组件添加更多的功能,比如说加入到购物车。你可以使用相同的原则来创建一个新的 hook,比如 useAddToCart
。不要把新的业务逻辑添加到现有的自定义 hook 中。而是应该创建新的,如果需要使用其中的某些值,那就应该把参数传给新的自定义 hook,始终保持单一职责的思想。
将代码分离为业务和实现逻辑可以在各种应用程序中实践,例如后端或游戏开发,并且与框架无关。
上面只是一个很小的组件示例,但如果是一个业务很复杂的大组件,很明显按照上面的方法进行拆分的组件会比所有东西都写在一起的要更好。