SSR -> Server-side rendering
服务器端渲染
上一部分我们已经实现了静态组件在服务器端直出 HTML 代码,接下来,我们结合拉取异步数据做服务器端渲染。
异步拉取数据
假设这是一个被使用到的组件
import React from 'react';
class Node extends React.Component {
constructor(props) {
super(props);
this.state = {
demoList: []
};
}
componentDidMount = () => {
this.fetchData();
};
fetchData = async () => {
const data = await new Promise((resolve) => {
setTimeout(() => {
resolve({
data: [{ id: 1 }, { id: 2 }]
});
}, 500);
});
this.setState({
demoList: data
});
};
render() {
return (
<div>
<ul>
{this.state.demoList.map((item) => {
return <li key={item.id}>{item.id}</li>;
})}
</ul>
</div>
);
}
}
在这个组件里面,我们定义了一个 fetchData
的方法,在组件初次渲染后拉取数据。
假设服务器直接渲染这个组件,我们得到的页面都会是一个空列表。
react-router-config
、react-redux
等文档都提到,需要等待异步请求结束才再去渲染组件。
react-router-config
文档上是在配置路由文件的时候附带上一个请求函数,react-redux
的文档上是直接指定某个请求完成后再渲染。这两个方案都有点蹩手蹩脚的感觉,并不想采用。
约定组件异步请求数据方式
要正确的渲染出页面,需要找到当前应用下所有组件的请求(应该说等待异步请求结束),通过 Promise.all
等待所有请求结束,再渲染。
找来找去,最后找到一个遍历组件的方式,看到 react-async-component
react-async-bootstrapper 这两个仓库,突然发现,可以抄一下。
我们约定所有组件异步请求数据都在组件的 fetchData
方法里面,并且它返回一个 Promise
。
然后在 entry-server.jsx
里面修改一下。
// 新增一个依赖
import reactTreeWalker from 'react-tree-walker';
// 搜集所有请求
const asyncFetchData = async (App) => {
let promises = [];
const visitor = (element, instance) => {
if (instance && typeof instance.fetchData === 'function') {
promises.push(instance.fetchData());
}
};
await reactTreeWalker(App, visitor);
return Promise.all(promises);
};
// SSR - render 里面添加一些代码
if (context.url) {
// 如果需要重定向
return {
context
};
}
// 在渲染组建前卡一下,等待所有请求结束
await asyncFetchData(AppRoot);
const html = ReactDOMServer.renderToString(AppRoot);
组件的 componentDidMount 生命周期里我们去请求数据,在这里又主动请求,会不会重复?
并不会,服务端渲染只会执行到
compnentWillMount
上,React 16 以后也取消了这生命周期。
用户状态
某些页面需要用户信息,在浏览器中,我们可以通过 cookie 的方式带上。在服务器端渲染的方式下,也能拿到请求的 cookie,然后把它放到每次请求的头部。
参考案例
说了那么多,终于把代码搬上来了。
当前代码使用的方式和上面说的类似,都是在每个组件中约定 fetchData
作为异步请求的标识。
状态管理使用的是 rematch,axios 供浏览器、服务器端请求数据。
浏览器、服务器共用了请求,所以在 rematch 初始化的时候,添加了一个 Axios 的 model,在每个 effects
方法里,通过 rootState.Axios
拿到 Axios 请求数据。
每次服务器端渲染的时候,都需要重新初始化一次,避免多用户状态混乱。并且添加 Axios 的时候设置 headers,把 cookie 带上(目前还没有实现用户状态相关的内容)。
通过 react-loadable
分割代码,在服务器端也能正常渲染。
对于一个完整的 React 服务器端渲染,还需要做很多工作,比如浏览器端还原 redux 状态(虽然已经有很多案例,但没有时间做)、避免浏览器端初次访问请求数据、针对每个页面添加不同的 title 之类的。
目前存在一个问题,数据源使用 CNODE 的 API,在服务也做了代理。
构建好资源,启动服务器,第一次服务器端渲染看起来没问题,再次刷新会多一次请求,没有响应数据,然后报错。再刷新,再多一次错误请求,渐渐的失去笑容。
暂时不确定哪儿出问题了,要去搬砖了。