前言:写出一个能用的react组件很简单,但是如何写出一个优雅的组件呢?这就需要对组件有深入的理解。今天我们就来学习一下如何设计出一个好的react组件吧!
React 组件的分类, 社区中有非常经典的分类模式:
- 把只作展示、独立运行、不额外增加功能的组件,称为哑组件,或无状态组件,还有一种叫法是展示组件;
- 把处理业务逻辑与数据状态的组件称为有状态组件,或灵巧组件,灵巧组件一定包含至少一个灵巧组件或者展示组件。
展示组件的复用性更强,灵巧组件则更专注于业务本身。
1.展示组件
展示组件内部是没有状态管理的,正如其名,就像一个个“装饰物”一样,完全受制于外部的 props 控制。展示组件具有极强的通用性,复用率也很高,往往与当前的前端工程关系相对薄弱,甚至可以做到跨项目级的复用。
代理组件
常用于封装常用属性,减少重复代码。是展示组件中最常用的代理组件,如:
const Button = props =>
<button type="button" {...props}>
虽然进行封装感觉是多此一举,但切断了外部组件库的强依赖特性。在大厂中引入外部组件库需要考虑两点:
- 如果当前组件库不能使用了,是否能实现业务上的无痛切换;
- 如果需要批量修改基础组件的字段,如何解决?
代理组件的设计模式很好地解决了上面两个问题。从业务上看,代理组件隔绝了 Antd,仅仅是一个组件 Props API 层的交互。这一层如若未来需要替换,是可以保证兼容、快速替换的,而不需要在原有的代码库中查找修改。其次,如果要修改基础组件的颜色、大小、间距,代理组件也可以相对优雅地解决,使得这些修改都内聚在当前的 Button 组件中,而非散落在其他地方。
基于展示组件的思想,可以封装类似的其他组件,比如样式组件。
样式组件
样式组件也是一种代理组件,只是又细分了处理样式领域,将当前的关注点分离到当前组件内。
布局组件
布局组件的基本设计与样式组件完全一样,但它基于自身特性做了一个小小的优化。主要用于安放其他组件
<Layout
Top={<NavigationBar />}
Content={<Article />}
Bottom={<BottomBar />}
/>
布局本身是确定的,不需要根据外部状态的变化去修改内部组件。所以这也是一个可以减少渲染的优化点。
由于布局组件无需更新,就可以通过写死shouldComponentUpdate 的返回值直接阻断渲染过程。对于大型前端工程,类似的小心思可以带来性能上的提升。当然,这也是基于代理组件更易于维护而带来的好处。
2.灵巧组件
由于灵巧组件面向业务,所以相对于展示组件来说,其功能更为丰富、复杂性更高,而复用度更低。展示组件专注于组件本身特性,灵巧组件更专注于组合组件。那么最常见的案例则是容器组件。
容器组件
容器组件几乎没有复用性,它主要用在两个方面:拉取数据与组合组件。
const CardList = ({ cards }) => (
<div>
{cards.map(card => (
<CardLayout
header={<Avatar url={card.avatarUrl} />}
Content={<Card {...card} />}
/>
{comment.body}-{comment.author}
))}
</div>
);
class CardListContainer extends React.Component {
state = { cards: [] }
async componentDidMount() {
const response = await fetch('/api/cards')
this.setState({cards: response})
}
render() {
return <CardList cards={this.state.cards} />
}
}
像这样切分代码后,容器组件内非常干净,没有冗余的样式与逻辑处理,这也是采取了关注点分离的策略,
高阶组件
React 的官方文档将高阶组件称为 React 中复用组件逻辑的高级技术。高阶组件本身并不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。简而言之,高阶组件的参数是组件,返回值为新组件的函数。
这样听起来有一些高阶函数的味儿了。那什么是高阶函数呢?如果一个函数可以接收另一个函数作为参数,且在执行后返回一个函数,这种函数就称为高阶函数。在 React 的社区生态中,有很多基于高阶函数设计的库,比如 reselector 就是其中之一。
思想一脉相承,React 团队在组件方向也汲取了同样的设计模式。源自高阶函数的高阶组件,可以同样优雅地抽取公共逻辑。
抽取公共逻辑
const checkLogin = () => {
return !!localStorage.getItem('token')
}
class CartPage extends React.Component {
...
}
class UserPage extends React.Component {
componentDidMount() {
if(!checkLogin) {
// 重定向跳转登录页面
}
}
...
}
class OrderPage extends React.Component {
componentDidMount() {
if(!checkLogin) {
// 重定向跳转登录页面
}
}
...
}
虽然已经抽取了一个函数,但还是需要在对应的页面添加登录态的判断逻辑。然而如果有高阶组件的话,情况会完全不同。
const checkLogin = () => {
return !!localStorage.getItem('token')
}
const checkLogin = (WrappedComponent) => {
return (props) => {
return checkLogin() ? <WrappedComponent {...props} /> : <LoginPage />;
}
// 函数写法
class RawUserPage extends React.Component {
...
}
const UserPage = checkLogin(RawUserPage)
// 装饰器写法
@checkLogin
class UserPage extends React.Component {
...
}
@checkLogin
class OrderPage extends React.Component {
...
}
链式调用
由于高阶组件返回的是一个新的组件,所以链式调用是默认支持的。基于 checkLogin 与 PV 两个例子,链式使用是这样的:
// 函数调用方式
class RawUserPage extends React.Component {
...
}
const UserPage = checkLogin(PV('用户页面')(RawUserPage))
// 装饰器调用方式
@checkLogin
@PV('用户页面')
class UserPage extends React.Component {
...
}
在链式调用后,装饰器会按照从外向内、从上往下的顺序进行执行。 除了抽取公用逻辑以外,还有一种修改渲染结果的方式,被称为渲染劫持。
渲染劫持
渲染劫持可以通过控制 render 函数修改输出内容,常见的场景是显示加载元素,如下情况所示:
function withLoading(WrappedComponent) {
return class extends WrappedComponent {
render() {
if(this.props.isLoading) {
return <Loading />;
} else {
return super.render();
}
}
};
}
通过高阶函数中继承原组件的方式,劫持修改 render 函数,篡改返回修改,达到显示 Loading 的效果。
参考: