最新消息:看到那些跳动的图片、文字了吗?点击点击 O(∩_∩)O~~

React SSR 探索 —— 服务器端异步拉取数据

若思若想 onlyling 3646浏览

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-configreact-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,然后把它放到每次请求的头部。

参考案例

react-ssr

说了那么多,终于把代码搬上来了。

当前代码使用的方式和上面说的类似,都是在每个组件中约定 fetchData 作为异步请求的标识。

状态管理使用的是 rematchaxios 供浏览器、服务器端请求数据。

浏览器、服务器共用了请求,所以在 rematch 初始化的时候,添加了一个 Axios 的 model,在每个 effects 方法里,通过 rootState.Axios 拿到 Axios 请求数据。

每次服务器端渲染的时候,都需要重新初始化一次,避免多用户状态混乱。并且添加 Axios 的时候设置 headers,把 cookie 带上(目前还没有实现用户状态相关的内容)。

通过 react-loadable 分割代码,在服务器端也能正常渲染。

对于一个完整的 React 服务器端渲染,还需要做很多工作,比如浏览器端还原 redux 状态(虽然已经有很多案例,但没有时间做)、避免浏览器端初次访问请求数据、针对每个页面添加不同的 title 之类的。

目前存在一个问题,数据源使用 CNODE 的 API,在服务也做了代理。

构建好资源,启动服务器,第一次服务器端渲染看起来没问题,再次刷新会多一次请求,没有响应数据,然后报错。再刷新,再多一次错误请求,渐渐的失去笑容。

暂时不确定哪儿出问题了,要去搬砖了。

转载请注明:OnlyLing - Web 前端开发者 » React SSR 探索 —— 服务器端异步拉取数据