服务端渲染
服务器端呈现的最常见用例是在用户(或搜索引擎爬虫)首次请求您的应用时处理初次渲染。
当服务器收到请求时,它会将所需的组件呈现为 HTML 字符串,然后将其作为响应发送给客户端。 从那时起,客户端将接管渲染的职责。
在服务器端的 Material-UI
Material-UI 最初设计受到了在服务器端渲染的约束,但是您可以完全负责它的正确整合。 为页面提供所需的 CSS 是至关重要的,否则页面只会渲染 HTML 而等待客户端注入 CSS,从而导致浏览器样式闪烁(FOUC)。 若想将样式注入客户端,我们需要:
- 在每个请求上创建一个全新的 ServerStyleSheets实例。
- 用服务端收集器渲染 React 树组件。
- 将 CSS 单独拿出。
- 将 CSS 传递给客户端。
在删除服务器端注入的 CSS 之前,客户端将第二次注入 CSS。
配置
在下面的配置中,我们将了解如何设置服务器端的渲染。
主题
创建一个在客户端和服务端之间共享的主题:
theme.js
import { createMuiTheme } from '@material-ui/core/styles';
import red from '@material-ui/core/colors/red';
// 创建一个主题的实例。
const theme = createMuiTheme({
  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 包裹在 StylesProvider 和 ThemeProvider 中,这样组件树中的所有组件都可以使用样式配置和 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)
查看常见问题解答:我的应用程序在服务端上不能正确渲染。