跳到内容
+

服务端渲染

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

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

服务器端的 Material UI

Material UI 从一开始设计时就考虑了在服务器上渲染的约束,但这取决于你来确保它被正确集成。重要的是为页面提供所需的 CSS,否则页面将仅使用 HTML 渲染,然后等待客户端注入 CSS,从而导致页面闪烁 (FOUC)。为了将样式注入到客户端,我们需要

  1. 在每次请求时创建一个全新的 emotion cache 实例。
  2. 使用服务端收集器渲染 React 树。
  3. 提取 CSS。
  4. 将 CSS 传递给客户端。

在客户端,CSS 将被第二次注入,然后再移除服务端注入的 CSS。

设置

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

主题

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

theme.js
import { createTheme } from '@mui/material/styles';
import { red } from '@mui/material/colors';

// Create a theme instance.
const theme = createTheme({
  palette: {
    primary: {
      main: '#556cd6',
    },
    secondary: {
      main: '#19857b',
    },
    error: {
      main: red.A400,
    },
  },
});

export default theme;

服务端

以下是服务端外观的概要。我们将使用 Express middlewareapp.use 设置一个 Express 中间件 来处理所有进入服务器的请求。如果你不熟悉 Express 或 middleware,请了解 handleRender 函数将在服务器每次收到请求时被调用。

server.js
import express from 'express';

// We are going to fill these out in the sections to follow.
function renderFullPage(html, css) {
  /* ... */
}

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

const app = express();

// This is fired every time the server-side receives a request.
app.use(handleRender);

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

处理请求

我们在每次请求时需要做的第一件事是创建一个新的 emotion cache

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

服务端渲染的关键步骤是在将组件发送到客户端之前渲染组件的初始 HTML。为此,我们使用 ReactDOMServer.renderToString()

Material UI 使用 Emotion 作为其默认的样式引擎。我们需要从 Emotion 实例中提取样式。为此,我们需要为客户端和服务器共享相同的缓存配置

createEmotionCache.js
import createCache from '@emotion/cache';

export default function createEmotionCache() {
  return createCache({ key: 'css' });
}

通过这个,我们创建了一个新的 Emotion 缓存实例,并使用它来提取 HTML 的关键样式。

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

import express from 'express';
import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider } from '@mui/material/styles';
import { CacheProvider } from '@emotion/react';
import createEmotionServer from '@emotion/server/create-instance';
import App from './App';
import theme from './theme';
import createEmotionCache from './createEmotionCache';

function handleRender(req, res) {
  const cache = createEmotionCache();
  const { extractCriticalToChunks, constructStyleTagsFromChunks } =
    createEmotionServer(cache);

  // Render the component to a string.
  const html = ReactDOMServer.renderToString(
    <CacheProvider value={cache}>
      <ThemeProvider theme={theme}>
        {/* CssBaseline kickstart an elegant, consistent, and simple baseline
            to build upon. */}
        <CssBaseline />
        <App />
      </ThemeProvider>
    </CacheProvider>,
  );

  // Grab the CSS from emotion
  const emotionChunks = extractCriticalToChunks(html);
  const emotionCss = constructStyleTagsFromChunks(emotionChunks);

  // Send the rendered page back to the client.
  res.send(renderFullPage(html, emotionCss));
}

const app = express();

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

// This is fired every time the server-side receives a request.
app.use(handleRender);

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

注入初始组件 HTML 和 CSS

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

function renderFullPage(html, css) {
  return `
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <title>My page</title>
        ${css}
        <meta name="viewport" content="initial-scale=1, width=device-width" />
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
        <link
          rel="stylesheet"
          href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
        />
      </head>
      <body>
        <div id="root">${html}</div>
      </body>
    </html>
  `;
}

客户端

客户端很简单。我们所需要做的就是使用与服务端相同的缓存配置。让我们看一下客户端文件

client.js
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider } from '@mui/material/styles';
import { CacheProvider } from '@emotion/react';
import App from './App';
import theme from './theme';
import createEmotionCache from './createEmotionCache';

const cache = createEmotionCache();

function Main() {
  return (
    <CacheProvider value={cache}>
      <ThemeProvider theme={theme}>
        {/* CssBaseline kickstart an elegant, consistent, and simple baseline
            to build upon. */}
        <CssBaseline />
        <App />
      </ThemeProvider>
    </CacheProvider>
  );
}

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

参考实现

这里是本教程的参考实现。你可以在 GitHub 仓库的 /examples 文件夹下找到更多 SSR 实现,请参阅其他示例