Alan

此刻想举重若轻,之前必要负重前行

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

技术栈

  • 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结构进行初始化工作。

评论