投身烈火
Keep Walking

闲扯react组件动态加载机制

话说之前老写vue的源码有点乏味,今天咱们扯点儿别的。之前在微信群里看到大家聊起react组件动态加载,貌似都没有什么好的解决方案呢,正好最近一直忙着写java,好久没折腾react的代码了,借这个机会捡捡以前的技能,不然都要忘光了……

问题分析

之前在群里聊的时候也没看到有人说起核心的痛点,所以究竟在实际应用中会有啥问题我也不知道 (;¬_¬) ……所以关于问题的分析,其实都是我脑补出来的,所以大家不要当真,全当看热闹吧……

分析之前,先确定下目标,我的目标是简单实现react组件动态加载的机制。既然是要先串通机制,细节和环境配置就一切从简,后续再考虑实际使用和易用性。

接下来分析问题,说到react组件动态加载,从字面上看可以分为两部分“react组件”和“动态加载”。动态加载其实老早就有就解决方案了,之前打包工具还没流行起来的时候,就有各种五花八门的库来解这问题,requirejs、seajs、YUI Loader啥的,所以这个问题用第三方库应该就能搞定。

然后,跟react组件联系到一起,如何在加载完成后替换组件是个问题。其实这个问题也好解,因为react的组件可以返回新组件,所以做个透传参数的加载器应该就可以了。

另外,现在想开发react的程序,webpack+es6+jsx也算是标配了吧。如何用webpack配合打包也是个问题,要把需要异步加载的都生成chunk。还得搭建一套构建环境……毕竟我的目标是实现机制,所以打包这部分就先不考虑,环境配置也省略,直接写浏览器可执行的脚本。

好的,拉个list:

  • 动态加载问题 – 使用requirejs
  • react组件替换 – 做个加载器组件
  • webpack打包 – 后续考虑
  • 环境问题 – 先不使用jsx,不写浏览器不能直接运行的代码,公共库用unpkg加载,使用creat-react-class模块创建react组件构造函数

ok,可以开始了。

实现

之前列出的四个问题,真正需要解决的只有第二个,所以设计一个加载器组件,其实就能实现react组件动态加载的机制了吧……

调用形式应该像下面这样,在标签上指定要加载的组件名,另外,被调用的组件也会需要设置属性,所以加载器也得接收。

1
2
3
4
5
6
7
React.createElement(Loader, {
component: 'A',
propA: 1,
propB: true
})
// 用jsx写应该是下面这样的
<Loader component='A' propA={1} propB={true}/>

Loader内部,首先要有个地方记录已经加载了的组件,如果组件已经加载,就直接调用,如果没加载,就先显示loading并开始加载组件,加载完成后调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 把记录已加载组件的变量设为公共变量,为了能在多个实例之间共享
var componentList = {}
createReactClass({
displayName:'Loader',
render: function render() {
var component = this.props.component
if (componentList[component]) {
return React.createElement(componentList[component], this.props);
} else {
getComponent(component)
return React.createElement(div, null, 'loading……');
}
}
});
// 用es6写应该是这样的,没错,我就是想证明我会写但是我懒…… ( ̄. ̄)
class Loader extends React.Component {
constructor(props) {
super(props);
}
render () {
let component = {this.props}
if (componentList[component]) {
return <componentList[component] {...this.props}/>;
} else {
// 这个函数里面加载组件
getComponent(component)
return <div>loading……</div>;
}
}
}

大框架有了,继续细化。虽然上面写了加载组件的函数,但是在加载组件之后需要重新触发加载器的渲染,这就需要设置组件的state,所以必须要给加载器添加state属性,用来标记当前显示的组件,并且在getComponent对state进行设置,创建组件的地方也应该改成用state的属性创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
createReactClass({
displayName:'Loader',
getInitialState: function () {
return {
cur: ''
};
},
getComponent: function (component) {
require([component], (Component) => {
componentList[component] = Component
this.setState({
cur: Component
})
})
},
render: function render() {
var component = this.props.component
if (componentList[component]) {
return React.createElement(this.state.cur, this.props);
} else {
this.getComponent(component)
return React.createElement(div, null, 'loading……');
}
}
});

接着问题就来了,react里setState会触发render,而上面render里getComponent又会触发setState,会造成死循环,所以要给他换个地方,react的组件运行时一共四个,constructor、componentWillMount、render、componentDidMount,render不能写,文档上说componentWillMount对于web runtime没屌用,所以只能在constructor和componentDidMount里面选了,其实放到哪个里都行,但是constructor是用来初始化组件的,从含以上看,我觉得放componentDidMount里比较合适,官方文档上也说componentDidMount是用来加载异步数据的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
createReactClass({
displayName:'Loader',
getInitialState: function () {
return {
cur: ''
};
},
getComponent: function (component) {
require([component], (Component) => {
componentList[component] = Component
this.setState({
cur: Component
})
})
},
componentDidMount: function () {
this.getComponent(this.props.component)
},
render: function render() {
if (componentList[this.props.component]) {
return React.createElement(this.state.cur, this.props);
} else {
return React.createElement(div, null, 'loading……');
}
}
});

呃……貌似这样就行了呢……至少大体的机制应该就是这样的吧,还有些细节需要完善,比如props变化的时候也切加载控间,现在这个加载器只能加载一次控件,其实只要在属性变化的运行时里加上getComponent函数就行了。再比如减少控件渲染次数,控价加载失败的异常处理之类的,详细的就不说了,自己体会吧,我把我练习的代码传到github上了,可以看看吐吐槽

https://github.com/81735595/react-component-dynamic-loading

用npm start就能启动了。

关于打包

感觉在代码上直接用import替换require就能用webpack2打包了,但是我总觉得打包是个大头儿,有好多坑等着……所以等以后有时间了再补一次打包脚本联系好了。

好的那么由于时间不足,本期的流水账就先写到这里,如果不出意外的话,maybe可能也许大概下周五会更新吧,能不能准时更新,就全看米娜桑点赞转发安利留言的热情了~!

白了个白~!