跳到主要内容

React 服务端渲染简单实现

· 阅读需 4 分钟
3Alan

关于概念,我觉得这篇文章讲的很形象

技术栈

  • React
  • Express
  • Redux

源码

查看

大致流程

流程图

服务端

服务端主要是对相应组件进行缩水处理(renderToString),处理 Redux,处理getServerSideProps, api 仿照了 Next.js

查找相应组件

首先,为了方便双端路由的统一管理,我们单独维护一个 routes 列表。

import React from 'react';
import Detail from './pages/Detail';
import List from './pages/List';

const routes = [
{
path: '/',
element: <List />
},
{
path: '/detail/:name',
element: <Detail />
}
];

export default routes;

首先拿到用户访问的 url 并通过 matchRoutes 查找路由对应的组件。

import { renderToString } from 'react-dom/server';
import { matchRoutes, renderMatches } from 'react-router-dom';

app.get('*', (req, res) => {
const matchedComponent = matchRoutes(routes, req.url);

let reduxState = {};
if (matchedComponent) {
const { getServerSideProps } = matchedComponent[0].route.element.type;
if (getServerSideProps) {
const res = getServerSideProps();
reduxState = { ...reduxState, ...res };
}
}

const store = configureStore(reduxState);

// renderMatches是react-router提供的渲染matchRoutes结果的api,最后的content为注入了redux数据的组件,缩水/脱水过程
const content = renderToString(<Provider store={store}>{renderMatches(matches)}</Provider>);

// 将content拼接到提前设置好的html模板中
const response = template(store.getState(), content);

// 将html返回给用户
res.setHeader('Cache-Control', 'assets, max-age=604800');
res.send(response);
});

客户端

注水

上面的 template 的代码如下

function template(initialState = {}, content = '') {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/assets/style.css">
</head>
<body>
<div id="app">
${content}
</div>

<script>
window.__STATE__ = ${JSON.stringify(initialState)}
</script>
<script src="/assets/bundle/client.js"></script>
</body>
</html>
`;
}

注水/水合操作

import React from 'react';
import { hydrate } from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from './redux/configureStore';
import { BrowserRouter } from 'react-router-dom';
import App from './App';

const state = window.__STATE__;
delete window.__STATE__;

// 初始化store,保持双端统一
const store = configureStore(state);

hydrate(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.querySelector('#app')
);

组件写法

import axios from 'axios';
import React from 'react';
import { Helmet } from 'react-helmet';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';

const Detail = () => {
const { name } = useParams();
const serverData = useSelector(state => state.serverData);

return (
<div>
<Helmet>
<title>{name}</title>
</Helmet>
<div>{name}</div>

<h4>
<strong style={{ color: 'red' }}>{serverData}</strong>{' '}
</h4>
</div>
);
};

Detail.getServerSideProps = () => {
return {
serverData: 'content init from serverSide'
};
};

export default Detail;

总结

简单点来说就是服务端通过 url 查找到需要渲染的组件,注入 getServerSideProps 返回的数据到组件中后使用 renderToString 进行缩水并返回到客户端。

客户端通过 hydrate 进行注水,这一阶段 React 会尽可能的复用 HTML 结构,React 会根据服务端返回的 HTML 结构进行初始化工作。

本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。