目前组件化开发思想已经成为了前端的主流。三大框架(Vue, React, Angular)都是组件化开发最好的引领者。
社区有很多的文章告诉我们如何做组件划分,应该遵循哪些原则。并且在做项目的过程中你也一定有一些自己的关于组件划分的心得体会。
所有的这些,归根结底,都是在教导我们如何对组件进行更好的拆分,以使其更加的可复用。
但是我要问你一个问题,组件是什么?
组件是UI+逻辑的组合。
这也就意味着,无论你怎么拆分组件,最终都是把UI和逻辑封装在一起处理的。
比如antd的Collapse,内置的UI样式及逻辑代码对使用者来说就是一个黑盒,用户只能做有限的定制化修改。
这样就会带来一个问题,当使用者需要对UI部分做定制化处理,可能就需要再写一个组件,而这两个组件逻辑处理部分有可能是一样的,唯一的不同就在UI展示上。
另外像一些获取鼠标实时位置,获取屏幕实时大小,获取hover的状态等等,所有的这些基础的逻辑代码,可能在我们的项目中到处都是。
所以有没有一种方式,可以针对开发过程中经常用到的基础逻辑部分做封装处理,而把UI渲染交给具体的使用者来做,从而实现更大的自由度。
高阶组件,Render Props
在React Hooks出现之前,React有两种办法可以做这件事: 高阶组件,Render Props。
1. Render Props
举个例子,比如我们想要获得鼠标的实时位置,然后做一些业务处理。 部分代码如下:
<Mouse>
(mouse) => (<MyComponent mouse={mouse} />)
</Mouse>
Render Props的具体用法这里不介绍了,可以参考官网的用法。
我们在Mouse组件中封装了获取鼠标位置的逻辑代码,然后通过在props上挂载一个函数作为参数返回鼠标位置的数据。 这样子组件就可以根据拿到的mouse随意做一些处理了。
2. 高阶组件
部分代码如下:
function withMouse(WrappedComponent) {
// 获取鼠标位置
const mouse = getMouse()
return class extends React.component {
...
render() {
// 返回包装后的组件
return <WrappedComponent mouse={mouse} {...this.props}>
}
}
}
使用的时候可能像这样:
const MyComponentWithMouse = withMouse(MyComponent)
现在如果需求变为: 在知道鼠标位置的情况下,还要知道window.size。 这时我们就需要再创建一个获取window.size的组件。代码可能会变为这样:
<WindowSize>
(size) => (
<Mouse>
(mouse) => (<MyComponent size={size} mouse={mouse} />)
</Mouse>
)
</WindowSize>
以上代码很容易会让你想到嵌套地狱,这是不能容忍的。同时高阶组件也存在类似的问题。这可能是导致逻辑复用不能流行起来的一个非常重要的原因。
React Hooks
React Hooks的出现,很好的解决了逻辑复用的难题。社区中也出现了很多的Hooks库。
随着Hooks诞生,利用它的Custom Hook可以很方便的对逻辑进行封装,而且使用起来和正常的hooks用法一样,不需要任何额外的代码。
下面我们来看一个react use的例子。 还是以上面的获取鼠标位置信息来说明。看下react use是如何定义这个hooks的。
const useMouse = (ref: RefObject<Element>): State => {
const [state, setState] = useRafState<State>({
docX: 0,
docY: 0,
posX: 0,
posY: 0,
elX: 0,
elY: 0,
elH: 0,
elW: 0,
});
useEffect(() => {
const moveHandler = (event: MouseEvent) => {
if (ref && ref.current) {
const { left, top, width: elW, height: elH } = ref.current.getBoundingClientRect();
const posX = left + window.pageXOffset;
const posY = top + window.pageYOffset;
const elX = event.pageX - posX;
const elY = event.pageY - posY;
setState({
docX: event.pageX,
docY: event.pageY,
posX,
posY,
elX,
elY,
elH,
elW,
});
}
};
document.addEventListener('mousemove', moveHandler);
return () => {
document.removeEventListener('mousemove', moveHandler);
};
}, [ref]);
return state;
};
export default useMouse;
- 首先在useEffect中注册了mousemove事件
- 然后在回调函数moveHandler中获取鼠标的位置信息,并更新state
- 把最新的state返回
- 当组件卸载时,移除mousemove事件
由于这部分逻辑几乎都是通用的,所以不需要我们去关心具体的实现逻辑。具体业务中我们只关心返回的位置信息是什么,然后根据返回结果来做具体的业务处理。
所以,接下来我们来重点看下用法,这部分才是我们需要关注的。
import {useMouse} from 'react-use';
const Demo = () => {
const ref = React.useRef(null);
const mouse = useMouse(ref);
return (
<div ref={ref}>
<div>Mouse position is: {JSON.stringify(mouse)} </div>
</div>
);
};
核心代码只有这一行
const mouse = useMouse(ref);
再对比下上面提到的高阶函数和Render Props,是不是感觉清爽了好多。
更重要的是我们不用去关心组件之间的层级关系,哪里需要就可以在哪里用。而且开发人员可以将精力都放在业务相关的代码上,开发效率也会大大提升。
社区库
react-use是社区中比较流行的库,它包含了众多的基础逻辑。在开发中一定会用到它。
ahooks是阿里的一个Hooks库,也是目前使用比较多的一个库,它里面有些Hooks也是基于react-use来的。
由于基础逻辑几乎都是一样的,所以社区中很多的hooks库,他们的代码几乎都是一样的。
相比于其它的库 ahooks有更加丰富的文档及API规范,另外也丰富了一些其它库没有的hooks。 尤其是useRequest的用法, 相较于react-use的useAsync用法,增加了轮询、并行请求、防抖、节流等处理。
另外useRequest可以通过配置requestMethod参数来使用自己的请求库。 比如使用axios发送请求,默认的话会使用fetch发送请求。
import { useRequest } from 'ahooks';
import React from 'react';
import axios from 'axios';
export default () => {
const { data, error, loading } = useRequest('https://helloacm.com/api/random/?n=8&x=4', {
requestMethod: (param: any) => axios(param),
});
if (error) {
return <div>failed to load</div>;
}
if (loading) {
return <div>loading...</div>;
}
return <div>Number: {data?.data}</div>;
};
除此之外还可以通过 UseRequestProvider 在项目的最外层设置全局 options。
import { UseRequestProvider } from 'ahooks';
export function ({children})=>{
return (
<UseRequestProvider value={{
refreshOnWindowFocus: true,
requestMethod: (param)=> axios(param),
...
}}>
{children}
</UseRequestProvider>
)
}
总结
最后,鉴于其他两种语言语法层面的原因,目前还没有一种类似React Hooks这种可以方便的对逻辑进行共享的方式。
但是Hooks给了我们一个很好的启发,并向我们展示了社区对于逻辑共享的热情及需求。所以我相信,在不久的将来肯定也会出现一种针对Angular及Vue的便利的逻辑共享方式。
到那个时候,所有底层通用的逻辑可能都有人帮你写好了。开发人员就真的可以将精力放在业务逻辑相关的代码上了。