跳转到内容

服务端渲染

服务器端呈现的最常见用例是在用户(或搜索引擎爬虫)首次请求您的应用时处理初次渲染。

当服务器收到请求时,它会将所需的组件呈现为 HTML 字符串,然后将其作为响应发送给客户端。 从那时起,客户端将接管渲染的职责。

在服务器端的 Material-UI

Material-UI 最初设计受到了在服务器端渲染的约束,但是您可以完全负责它的正确整合。 为页面提供所需的 CSS 是至关重要的,否则页面只会渲染 HTML 而等待客户端注入 CSS,从而导致浏览器样式闪烁(FOUC)。 若想将样式注入客户端,我们需要:

  1. 在每个请求上创建一个全新的 ServerStyleSheets 实例。
  2. 用服务端收集器渲染 React 树组件。
  3. 将 CSS 单独拿出。
  4. 将 CSS 传递给客户端。

在删除服务器端注入的 CSS 之前,客户端将第二次注入 CSS。

配置

在下面的配置中,我们将了解如何设置服务器端的渲染。

主题

创建一个在客户端和服务端之间共享的主题:

theme.js

import { createTheme } from '@material-ui/core/styles';
import red from '@material-ui/core/colors/red';

// 创建一个主题的实例。
const theme = createTheme({
  palette: {
    primary: {
      main: '#556cd6',
    },
    secondary: {
      main: '#19857b',
    },
    error: {
      main: red.A400,
    },
    background: {
      default: '#fff',
    },
  },
});

export default theme;

服务器端

下面的大纲可以大致展现一下服务器端。 我们将使用 app.use 建立一个 Express 中间件 来处理所有进入服务器的请求。 如果您不熟悉 Express 或中间件(middleware)的概念,那么只需要知道每次服务器收到请求时都会调用 handleRender 函数就可以了。

server.js

import express from 'express';

// 我们将在章节中填写这些需要遵守的内容。
function renderFullPage(html, css) {
  /* ... */
}

function handleRender(req, res) {
  /* ... */
}

const app = express();

// 每当服务器端接收到一个请求时,这个功能就会被触发。
app.use(handleRender);

const port = 3000;
app.listen(port);

处理请求

对于每次请求,我们首先需要做的是创建一个 ServerStyleSheets

当渲染时,我们将把根组件 App 包裹在 StylesProviderThemeProvider 中,这样组件树中的所有组件都可以使用样式配置和 theme

服务端渲染的关键步骤是,在将组件的初始 HTML 发送到客户端之前,就开始进行渲染。 我们用 ReactDOMServer.renderToString() 来实现此操作。

然后我们就可以使用 sheets.toString() 方法从表单(sheets)中获取 CSS。 由于我们也使用 emotion 作为默认的样式引擎,所以我们也需要从 emotion 实例中提取样式。 为此,我们需要为客户端和服务端共享相同的缓存定义:

cache.js

import createCache from '@emotion/cache';

const cache = createCache({ key: 'css' });

export default cache;

这样做之后,我们就可以在服务器上创建新的 Emotion 实例,并用它来提取 html 的关键样式。

我们将看到在 renderFullPage 函数中,是如何传递这些信息的。

import express from 'express';
import * as React from 'react';
import ReactDOMServer from 'react-dom/server';
import { ServerStyleSheets, ThemeProvider } from '@material-ui/core/styles';
import createEmotionServer from '@emotion/server/create-instance';
import App from './App';
import theme from './theme';
import cache from './cache';

const { extractCritical } = createEmotionServer(cache);

function handleRender(req, res) {
  const sheets = new ServerStyleSheets();

  // 将组件渲染成字符串
  const html = ReactDOMServer.renderToString(
    sheets.collect(
      <CacheProvider value={cache}>
        <ThemeProvider theme={theme}>
          <App />
        </ThemeProvider>
      </CacheProvider>,
    ),
  );

  // 从 sheet 中抓取 CSS。
  const css = sheets.toString();

  // 从 emotion 中抓取 CSS
  const styles = extractCritical(html);

  // 将渲染好的页面发回给客户端。
  res.send(renderFullPage(html, `${css} ${styles.css}`));
}

const app = express();

app.use('/build', express.static('build'));

// 每当服务器端接收到一个请求时,这个功能就会被触发。
app.use(handleRender);

const port = 3000;
app.listen(port);

注入组件的初始 HTML 和 CSS

服务端渲染的最后一步,则是将初始组件的 HTML 和 CSS 注入到客户端要渲染的模板当中。

function renderFullPage(html, css) {
  return `
    <!DOCTYPE html>
    <html>
      <head>
        <title>我的页面</title>
        <style id="jss-server-side">${css}</style>
      </head>
      <body>
        <div id="root">${html}</div>
      </body>
    </html>
  `;
}

客户端

客户端则是简单明了的。 我们只需要移除服务器端生成的 CSS。 让我们来看看客户端的文件:

client.js

import * as React from 'react';
import ReactDOM from 'react-dom';
import { ThemeProvider } from '@material-ui/core/styles';
import { CacheProvider } from '@emotion/react';
import App from './App';
import theme from './theme';
import cache from './cache';

function Main() {
  React.useEffect(() => {
    const jssStyles = document.querySelector('#jss-server-side');
    if (jssStyles) {
      jssStyles.parentElement.removeChild(jssStyles);
    }
  }, []);

  return (
    <CacheProvider value={cache}>
      <ThemeProvider theme={theme}>
        <App />
      </ThemeProvider>
    </CacheProvider>
  );
}

ReactDOM.hydrate(<Main />, document.querySelector('#root'));

参考实现

你可以在 GitHub仓库/examples 文件夹下找到我们托管的不同范例项目。

故障排除(Troubleshooting)

查看常见问题解答:我的应用程序在服务端上不能正确渲染