代码的组织形式
所有的前端框技术要面对一个问题:如何组织UI布局与页面逻辑。
对于UI布局,基本上都提供xml布局 与手写两种方式。xml布局适合简单的场景,相对来说更加直观;手写则适合复杂的,逻辑较多的场景,相对来说更加灵活。
当使用xml做UI布局时:
- 有些框架将UI和逻辑做物理隔离,通过某种形式关联二者,如Android原生、iOS原生等;
- 有些框架则只做代码区块的隔离,在同一个文件的不同区块中,分别组织UI和逻辑,如Vue;
而React在JSX的规则下,UI、逻辑代码可以混写,既有xml的便捷性,又有手写的灵活性。
而代码的组织形式,直接影响开发的开发习惯、开发中的共性问题、封装代码的模式、流行的三方库等。
前端开发面临的问题
具体某种前端技术的组织形式,是由框架的官方设计者来决定的。但是基于这种组织形式,在实际开发中所遇到的主要问题,往往是随着技术的推广以及应用的复杂逐渐放大暴露出来的。如原生开发的超级Controller,React中的超级组件等。
这些问题看似与具体的技术平台有关,但抽象地来看,前端开发者面临两个共性问题:
- 「如何避免单个页面文件过大」
- 「将共同点优雅的抽取出来,在不同的地方复用」
一般来说,官方会给出一些设计模式的建议,高级开发者会先研究学习,之后通过内部分享、博客、招聘等形式推广到广大初中级开发者,形成模式的流行,如Android的MVP,React中的HOC等。虽然问题是共性的,但解决问题的方式是与语言特性以及代码的组织形式相关的,每种技术有各自流行模式 。
React中的复用场景
简单的场景
当组件过大时,最简单的方式是拆分组件和工具函数。这是最基础的、应用最广泛的两种代码复用方式。
拆分组件可解决文件过大的问题,也可以通过在多个地方复用。而工具函数是纯函数,比较适合封装纯逻辑,如格式化、公式计算等,无法封装带状态的逻辑。用好这两种方式,可以支持一般的业务功能的开发维护。
复杂的场景
实际的项目中,往往有着更复杂的封装需求,如在多个组件中进行一些统一的行为:
- 增加页面的切入和切出日志(封装逻辑)
- 在页面加载时获取及处理数据,并在页面卸载时销毁相关的监听、定时器等(封装状态与逻辑)
- 在props中增加router对象(props增强)
- 为页面增加生命周期函数(组件增强)
- 在列表数据为空的时候,显示为无数据的状态(渲染劫持)
如何复用这些封装,往往是库开发者、项目主程、以及复杂的业务模块负责人要面临的问题。
高阶组件(HOC)
组件可以看成是一个输入为props,输出为「界面及交互」的纯函数,高阶组件则可以看成一个输入为组件,输出为增强后的组件的一个纯函数。
高阶组件有两种实现方式:代理式与继承式。官方推荐的代理式,相对来说使用场景更广泛。但在某些场景下,继承式更加灵活方便。
代理式HOC的例子
最常见的就是props增强:
scala
export const InjectRouter = (ComponsedComponent) => {
return class extends React.Component {
...
render() {
return (
<ComponsedComponent {...props} router={RouterManager.getRouter()} />
);
}
}
}
}
我们可以方便的在代码中调用this.props.router.push
,而无需显式的import相关的router对象。
渲染拦截:
scala
export const withLoading = (ComponsedComponent) => {
return class extends React.Component {
constructor() {
super();
this.state = {
loading: false
}
}
render() {
return (
this.state.loading ?
<Loading/ > :
<ComponsedComponent {...props} setLoading={value => this.setState(value) }/>
);
}
}
}
}
在组件中,可以通过调用this.props.setLoading()
来控制loading显示。
封装状态逻辑:
kotlin
// 此函数接收一个组件...
function withSubscription(WrappedComponent, selectData) {
// ...并返回另一个组件...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ...负责订阅相关的操作...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... 并使用新数据渲染被包装的组件!
// 请注意,我们可能还会传递其他属性
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
基本上所有对数据逻辑的封装,都可以通过这种形式。
继承式HOC的例子
继承式最典型的场景就是生命周期增强,可以直接访问到父组件的内部的生命周期函数:
scala
// 继承式生命周期增强HOC
const withMoreLifecycle = WrappedComponent => {
return class C extends WrappedComponent {
componentDidMount() {
super.componentDidMount && super.componentDidMount()
setTimeout(() => {
super.componentDidMount2Seconds && super.componentDidMount2Seconds();
}, 2000)
}
render() {
const element = super.render();
return element;
}
}
}
继承式也可以做拦截渲染,比代理式更强大,可以操作到组件内部:
javascript
class Index extends React.Component{
render(){
return <div>
<ul>
<li>react</li>
<li>vue</li>
<li>Angular</li>
</ul>
</div>
}
}
function HOC (Component){
return class Advance extends Component {
render() {
const element = super.render()
const otherProps = {
name:'alien'
}
/* 替换 Angular 元素节点 */
const appendElement = React.createElement('li' ,{} , `hello ,world , my name is ${ otherProps.name }` )
const newchild = React.Children.map(element.props.children.props.children,(child,index)=>{
if(index === 2) return appendElement
return child
})
return React.cloneElement(element, element.props, newchild)
}
}
}
export default HOC(Index)
继承式HOC的主要问题在于,在HOC嵌套的情况下,不允许在它的前面有其他代理式的HOC,因为代理式HOC会拦截所有的super。 另外一个问题是,继承式不允许使用函数组件,只能使用类组件。
代理与继承的对比
从上面的例子可以看出,代理式更多的是在一个组件的外围做工作,如增强props、条件渲染等。而继承式似乎更容易深入到一个组件的内部去,如调用组件的生命周期函数、访问组件的props、state等。
但两种没有绝对的界限,大部分封装,两种都可以做,用哪种不用哪种,往往会基于两个因素来考量:
- 方不方便,很多时候两种都可以实现,我们倾向于更方便的那个,如props增强更倾向于使用代理式,而生命周期增强则倾向于使用继承式。
从下面这个表可以看出在不同的场景下,哪种更方便:
场景 | 更方便的方式 |
---|---|
props增强 | 代理式 |
封装状态逻辑 | 代理式 |
生命周期增强 | 继承式 |
渲染劫持 | 根据情况而定 |
- 有什么后续的影响,继承式HOC往往对组件有更多的侵入性,对组件有一定假设基础,适用于内部项目的某一类组件。如果使用了继承式的HOC,则要保证团队内达成共识,即"在继承式HOC其前面,不会有代理式HOC"。
他们的差异可以从下面这个表来对比:
特性 | 代理式 | 继承式 | 备注 |
---|---|---|---|
侵入性 | 弱 | 强 | |
适用范围 | 类组件与函数组件 | 类组件 | |
静态属性 | 无法继承 | 可以继承 |
总结
HOC是React16.8之前组件间复用代码的主要方式,从场景上来说可用于props增强、封装状态逻辑、生命周期增强、渲染劫持等场景;从形式上来说可分为代理式与继承式HOC,这两种各有各的优缺点,需要根据项目类型和场景来决定。
React从16.8开始,提供了一种全新的方式-hook来复用代码