服务端渲染
服务器端呈现的最常见用例是在用户(或搜索引擎爬虫)首次请求您的应用时处理初次渲染。
当服务器收到请求时,它会将所需的组件呈现为 HTML 字符串,然后将其作为响应发送给客户端。 从那时起,客户端将接管渲染的职责。
在服务器端的 Material-UI
Material-UI 最初设计受到了在服务器端渲染的约束,但是您可以完全负责它的正确整合。 为页面提供所需的 CSS 是至关重要的,否则页面只会渲染 HTML 而等待客户端注入 CSS,从而导致浏览器样式闪烁(FOUC)。 若想将样式注入客户端,我们需要:
- 在每个请求上创建一个全新的
ServerStyleSheets
实例。 - 用服务端收集器渲染 React 树组件。
- 将 CSS 单独拿出。
- 将 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
包裹在 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)
查看常见问题解答:我的应用程序在服务端上不能正确渲染。