# 正文
在之前的小节中我们已经完成了数据同构,如果用来进行实际项目开发的话也能满足,但是有些时候用起来不够舒服,因为还存在一些不足和一些可以优化的空间。
当然这也是可以理解的,我们之前的阶段是建造阶段,为的是满足需求而已,现在是装修优化阶段,所以是时候把这些瑕疵给干掉了。
到底有哪些地方需要优化呢?下面一步一步来看。
# 组件内 state 初始化存在重复逻辑
下面这段代码是list页面组件构造函数内的数据获取逻辑,通过__SERVER__这个全局变量来判断是否是服务端渲染还是客户端渲染,最后给到state初始值。
//...
constructor(props) {
super(props);
let initialData = null;//初始化数据
if(__SERVER__){
//如果是在服务端执行
initialData = props.staticContext.initialData||{};
}else{
//客户端渲染
initialData = props.initialData || {};
}
this.state=initialData;
}
//...
一个正常的项目,都会有多个页面,那么上面的逻辑会出现每一个页面内,而且都是重复的代码。
# 组件componentDidMount存在重复代码
下面代码实现了当没有初始化数据的时候会在客户端进行异步数据获取,然后更新渲染。
另外还会设置当前页面的 title。
逻辑上好像没什么毛病,但是这段代码也会同时出现在h很多页面内。
componentDidMount() {
if (!this.state.fetchData) {
//如果没有数据,则进行数据请求
Index.getInitialProps().then(res => {
this.setState({
fetchData: res.fetchData || [],
page:res.page
});
//设置 title
document.title = res.page.tdk.title;
})
}
if (this.state.page && this.state.page.tdk) {
//设置 title
document.title = this.state.page.tdk.title;
}
}
# 直出的页面无法更新数据
如何理解这个问题?
这个问题隐藏的比较深,如果不仔细观察可能会被忽略。
我就详细的描述的下。
首次进入一个页面/A,/A页面肯定是服务端渲染的,浏览器接管页面后会进行继续渲染,完成页面事件和交互处理。
但是当在浏览器端进行路由切换,再回到这个页面/A时,数据仍然是服务端直出的数据。
无论你切换路由的方式是PUSH还是POP,/A页面的数据永远不会更新。
本骨架现在是通过组件的属性带入直出的数据。
props.initialData
目前我们只实现了通过该属性进行数据的获取,但是缺少路由切换时的更新机制,所以每次切换到/A页面,数据永远都是当初直出的数据。
# 解决问题
# 代码重复问题
前两个代码重复代码的问题应该是比较好解决,可以使用高阶组件来解决,同时还可以统一页面组件内获取数据的属性字段。
比如在服务端或者前端环境都使用 props.initailData 来获取数据,其他逻辑均可以封装在高阶组件内。
高阶组件代码参考
export default (SourceComponent) => {
return class HoComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
initialData: {},
getProps: false//浏览器端是否需要请求数据
}
}
//用于服务端调用
static async getInitialProps(props) {
return SourceComponent.getInitialProps ? await SourceComponent.getInitialProps(props) : {};
}
async componentDidMount() {
if(!this.state.initialData || !this.state.initialData.fetchData){
HoComponent.getInitialProps().then(res=>{
//...渲染数据
})
}
}
render() {
const props = {
initialData: {},
...this.props
};
if (__SERVER__) {
//服务端渲染
props.initialData = this.props.staticContext.initialData || {};
} else {
//客户端渲染 props.initialData=this.props.initialData;
}
return <SourceComponent {...props}></SourceComponent>
}
}
}
在页面组件中的使用
//组件
class Index extends React.Component {
constructor(props) {
super(props);
}
static async getInitialProps() {
return {
fetchData: //...,
page:{
tdk:{
// ...
}
}
};
}
render() {
//渲染数据
const {fetchData,page} = this.props.initialData;
const { code, data } = fetchData||{};
return <div>
//....
</div>
}
}
export default PageContainer(Index);
从上面的代码可以看出,页面组件干净了很多,我们只需要关心数据获取和渲染部分即可。
# 直出组件数据不更新问题
问题原因我们已经分析过。
- props.history.action=PUSH 跳转 不会更新
- props.history.action=POP 后退 or 前进 不会更新
所以解决办法也很明确,判断action的值即可。
真的那么简单吗?
if(props.history.action==='PUSH' || props.history.action==='POP')
update();
我们一步一步分析.
action = PUSH 这个操作没问题,action=POP就有问题,因为第一次进入页面的时候action的值也是POP。
看来很多问题并不是我们想象中那么简单,我们该如何处理POP呢?
不过我们离答案已经很近了。非常近了。
上面都是在分析问题,现在直接说结果吧。
当第一次进入页面的时候action值为POP,但是不会触发popstate事件,触发事件的时候都属于是客户端渲染。
我们可以在popstate事件内进行数据更新,当action=PUSH时更新数据,其他情况使用默认数据。
//伪代码
const popStateCallback = ()=> {
// 使用popStateFn保存函数防止addEventListener重复注册
update();
};
async componentDidMount() {
//注册事件,用于在页面回退的时候触发
window.addEventListener('popstate', popStateCallback);
if(this.props.history.action === 'PUSH'){
update();
}
}
下面来看下这个高阶组件的完整代码,可结合注释进行理解。
let _this = null;//保存当前渲染的组件实例
const popStateCallback = ()=> {
// 使用popStateFn保存函数防止addEventListener重复注册
if (_this && _this.getInitialProps) {
_this.getInitialProps();
}
};
//高阶函数
export default (SourceComponent)=>{
return class HoComponent extends React.Component {
constructor(props) {
super(props);
this.state={
initialData:{},
canClientFetch:false//浏览器端是否需要请求数据的状态
}
}
//用于服务端进行数据预取
static async getInitialProps(props){
return SourceComponent.getInitialProps ? await SourceComponent.getInitialProps(props):{};
}
//用于封装处理
async getInitialProps(){
// ssr首次进入页面以及,切换路由时才调用组件的getInitialProps方法
const props = this.props;
const res = SourceComponent.getInitialProps ? await SourceComponent.getInitialProps(props) : {};
this.setState({
initialData: res,
canClientFetch: true
});
let { tdk } = res.page;
if (tdk) {
document.title = tdk.title;
}
}
//组件挂载完成事件
async componentDidMount() {
_this = this; // 保证_this指向当前渲染的页面组件
//注册事件,用于在页面回退的时候触发
window.addEventListener('popstate', popStateCallback);
const canClientFetch = this.props.history && this.props.history.action === 'PUSH';//路由跳转的时候可以异步请求数据
if (canClientFetch) {
await this.getInitialProps();
}
}
render() {
// 只有在首次进入页面需要将window.__INITIAL_DATA__作为props,路由切换时不需要
const props = {
initialData:{},
...this.props
};
if(__SERVER__){
//服务端渲染时
props.initialData = this.props.staticContext.initialData||{};
}else{
//客户端渲染
if (this.state.canClientFetch) {
//获取异步请求数据
props.initialData = this.state.initialData||{};
} else {
//首次加载使用页面数据
props.initialData = window.__INITIAL_DATA__;
window.__INITIAL_DATA__={};//使用过后清除数据,否则其他页面会使用
}
}
return <SourceComponent {...props}></SourceComponent>
}
}
}
# 干掉对路由的入侵
上面已经介绍过本骨架目前的同构渲染初始化数据是通过为路由增加属性,从而为路由对应的组件带入数据。
具体代码如下,通过matchRoute方法查找路由,然后为路由设置initialData属性,并赋值初始化数据。
function clientRender() {
let initialData = JSON.parse(document.getElementById('ssrTextInitData').value);
//查找路由
let matchResult = matchRoute(document.location.pathname, routeList);
let { targetRoute } = matchResult;
if (targetRoute) {
//设置组件初始化数据
targetRoute.initialData = initialData;
}
//渲染index
ReactDom.hydrate(<BrowserRouter>
<App routeList={routeList} />
</BrowserRouter>
, document.getElementById('root'))
}
然后在App组件内遍历route时会进行逻辑判断,如果路由存在item.initialData属性,则渲染时将initialData作为组件的属性,同时带入数据,这样页面组件就可以通过使用props.initialData属性来获取页面上的数据了。
function App({routeList}) {
return (
<Layout>
<Switch>
{
routeList.map(item=>{
return item.initialData ? <Route key={item.path} exact={item.exact} path={item.path} render={(props)=>{
return <item.component {...props} initialData={item.initialData}></item.component>
}}></Route> : <Route key={item.path} {...item}></Route>
})
}
<Route to="*" component={Page404}></Route>
</Switch>
</Layout>
);
}
这种数据和路由关联的方式没有问题,但是会对路由产生修改,侵入路由,并不推荐这样处理。
其实在上面的高级组件中我们已经解决了,就是将页面直出的数据作为全局变量。
function clientRender(routeList) {
let initialData = JSON.parse(document.getElementById('ssrTextInitData').value);
window.__INITIAL_DATA__ = initialData;
//...
}
那么下面这段用于在渲染前设置路由参数,并带入初始数据的代码就不需要了,简化了代码,降低了耦合。
//....
//查找路由
let matchResult = matchRoute(document.location.pathname, routeList);
let { targetRoute } = matchResult;
if (targetRoute) {
//设置组件初始化数据
- targetRoute.initialData = initialData;
}
我们的App组件也更清晰了
function App({routeList}) {
return (
<Layout>
<Switch>
{
routeList.map(item=>{
- return item.initialData ? <Route key={item.path} exact={item.exact} path={item.path} render={(props)=>{
props.initialData = item.initialData;
return <item.component {...props} />
}}></Route> : <Route key={item.path} {...item}></Route>
+ return <Route key={item.path} {...item} />
})
}
</Switch>
</Layout>
);
}
# 小结
本节咱们主要是对已有的数据同构进行优化,通过使用高阶组件将重复的逻辑进行提取,让页面组件变得更加简洁,开发者只需要关注数据和渲染即可。
然后解决了一个隐含的数据更新问题,直出到页面的数据会被注入组件,客户端路由切换时数据也不会更新。
我们采用的history action结合popstate事件结合处理,确定了客户端进行异步数据请求的时机。同时也清除了客户端渲染前对路由和组件的入侵,降低了耦合。
本节完整代码已上传
← 基于路由的按需渲染 CSS 资源同构直出 →