前言
随着前端技术的发展,前端项目的规模不断增大。在这种情况下,前端项目的性能和用户体验就显得尤为重要。而服务端渲染(SSR)就是一种提高性能和用户体验的方法。在一些重视SEO的C端项目中,为了提高页面的SEO排名,也会使用服务端渲染(SSR)技术。本文将介绍什么是服务端渲染,以及如何搭建一个简单的SSR项目。项目代码点击这里
服务端渲染
服务端渲染简称SSR,指得是在服务端就将包含页面内容的HTML渲染好,再返回给浏览器。这样浏览器拿到的就是一个已经包含了内容的完整的HTML,而不是一个空的HTML,然后再去请求数据,再渲染页面。这样做的好处是,用户在浏览器中看到的页面是有内容的,而不是一个空白的页面,这样可以提高用户体验。而且对于搜索引擎来说,它们可以直接拿到完整的页面内容,这样可以更好的收录页面,提高页面的SEO。一般在 C端页面对SEO有较高要求的项目中会使用服务端渲染。
使用SSR时候组件开发和组件的运行方式会有一些不同。在服务端,组件不会Rerender,只会在每次请求时执行一次。所以有些生命周期函数是不会执行的。
- 对于类组件来说,只会执行componentWillMount,componentWillReceiveProps,componentWillUpdate。而componentDidMount,componentDidUpdate则不会执行
- 对于函数式组件来说,useEffect也不会执行
除了生命周期函数之前,SSR还涉及到水合、注水等概念。下面通过搭建一个简单的SSR项目来了解这些概念。
项目搭建
既然是服务端渲染,首先从启动一个express服务开始。先执行npm init -y
初始化一个Nodejs项目,然后创建server/index.js文件,初始内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
const express = require('express');
const app = express();
app.get('/', async (req, res) => { const html = ` <html> <head> <title>My React App</title> </head> <body> <div id="app"></div> </body> </html> `; res.send(html); });
app.listen(3000, () => { console.log('Server is running on http://localhost:3000'); });
|
安装express依赖之后,执行node server/index.js就可以启动服务,访问http://localhost:3000
,可以看到一个空白的页面,因为目前只是返回了一个空白的HTML。接下来需要将React组件渲染到HTML中。先创建src/containers/Home/index.js文件,初始内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
import React from 'react';
import './index.css';
function Home() { return ( <div className='book-list'> <h2>My React SSR App-Home</h2> </div> ); }
export default Home;
|
index.css文件内容如下:
1 2 3 4 5 6 7
| .book-list h2 { margin: 0; padding: 0; font-size: 1.5em; color: red; }
|
接下来将Home组件渲染到HTML中。修改server/index.js文件,引入react-dom/server模块,如果想在服务端Nodejs运行时中将React组件渲染成对应的HTML字符串,需要使用renderToString方法。修改代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import express from 'express'; import React from 'react'; import { renderToString } from 'react-dom/server'; import Home from '../src/containers/Home';
const app = express();
app.get('/', async (req, res) => { const content = renderToString(<Home />);
const html = ` <html> <head> <title>My React App</title> </head> <body> <div id="app">${content}</div> </body> </html> `; res.send(html); });
app.listen(3000, () => { console.log('Server is running on http://localhost:3000'); });
|
通常情况下,我们的组件采用的都是ES6+JSX的写法,所以为了方便,这里也将server/index.js改为import语法。此时执行node server/index.js
将会报错。因为Nodejs不支持import语法,更不支持Home组件的JSX语法。所以我们需要Webpack打包工具将server/index.js打包成Nodejs可以运行的代码。
接下来安装Webpack,然后创建webpack.server.js文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const path = require('path'); const nodeExternals = require('webpack-node-externals');
module.exports = { target: 'node', entry: path.resolve(__dirname, 'server/index.js'), externals: [nodeExternals()], output: { path: path.resolve(__dirname, 'server-build'), filename: 'index.js', clean: true, }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: 'babel-loader', }, { test: /\.css$/, exclude: /node_modules/, use: ['css-loader'], }, ], }, };
|
webpack打包的目标环境是Nodejs, 所以target设置为node。entry为server/index.js,externals设置为nodeExternals(),这样可以排除node_modules中的模块。output设置为server-build/index.js。为了支持CSS文件配置了css-loader。
另外为了让babel支持JSX语法还需要添加@babel/preset-react预设。创建.babelrc.json文件,内容如下:
1 2 3
| { "presets": ["@babel/preset-env", "@babel/preset-react"] }
|
为了打包方便在package.json中添加打包命令:
1 2 3 4 5 6 7
| { "scripts": { "dev:build-server": "NODE_ENV=development webpack --config webpack.server.js --mode=development -w", "dev:start": "nodemon ./server-build/index.js", "dev": "npm-run-all --parallel dev:*" } }
|
安装webpack、webpack-cli、babel和相关的loader依赖之后,执行npm run dev,再访问页面,可以看到组件内容已经渲染到HTML中了。
此时我们尝试在Home组件中添加生命周期函数和事件处理函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
function Home() { const [count, setCount] = useState(0); useEffect(() => { setCount(1); }, []); return ( <div className='book-list'> <h2 onClick={() => alert('ok')}>My React SSR App-Home {count}</h2> </div> ); }
|
可以看到页面上显示的count还是0,这是因为SSR时,组件每次请求只会执行一次,没有re-render的过程,所以useEffect不会执行。点击文字也不会弹出alert,这是因为在服务端Nodejs环境中是没有DOM的,调用renderToString方法时,只是将组件渲染成了HTML字符串,没有事件绑定。
所以需要在客户端再次渲染组件,这个过程就是水合。水合是指在服务端渲染完成后,将组件在客户端重新渲染一次,这样就可以绑定事件,执行生命周期函数等。
客户端水合
为了让页面在客户端进行水合,需要将组件入口文件再打包一份客户端bundle。首先创建客户端渲染的入口文件src/index.js,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
import React from 'react'; import { hydrateRoot } from 'react-dom/client';
import Home from './containers/Home';
const App = () => { return ( <Home /> ); };
hydrateRoot(document.getElementById('app'), <App />);
|
注意这里使用的是hydrateRoot方法,而不是render方法。hydrateRoot方法是用来将服务端渲染的内容进行水合的方法。水合时React无需重新生成新的Dom元素,而是利用现有的元素进行事件绑定。这里需要强调的是,客户端水合时首次渲染的Dom结构必须和服务端渲染的Dom结构一致,否则在React18中会水合失败,提示Dom不一致,在React18之前的版本中则会删除不一致的元素。
接下来需要对入口进行打包构建出客户端的JS bundle文件,创建webpack.client.js文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| const path = require('path');
const isProduction = process.env.NODE_ENV === 'production';
module.exports = { mode: isProduction ? 'production' : 'development', devtool: isProduction ? 'source-map' : 'cheap-module-source-map', entry: path.resolve(__dirname, 'src/index.js'), output: { path: path.resolve(__dirname, 'client-build'), filename: 'bundle.js', clean: true, }, module: { rules: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, use: 'babel-loader', }, { test: /\.css$/, exclude: /node_modules/, use: ['style-loader', 'css-loader'], }, ], } };
|
以上代码的入口文件为src/index.js,输出文件为client-build/bundle.js。为了执行水合过程,需要在服务端返回的HTML中引入客户端bundle。修改server/index.js文件,将客户端bundle引入到HTML中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
app.use(express.static('client-build'));
app.get('/', async (req, res) => { const content = renderToString(<Home />);
const html = ` <html> <head> <title>My React App</title> </head> <body> <div id="app">${content}</div> <script src="/bundle.js"></script> </body> </html> `; res.send(html); });
|
注意这里还使用express.static方法将client-build目录设置为静态资源目录,这样就可以直接访问client-build目录下的文件。然后在package.json中添加客户端打包命令:
1 2 3 4 5 6 7 8
| { "scripts": { "dev:build-client": "NODE_ENV=development webpack --config webpack.client.js --mode=development -w", "dev": "npm-run-all --parallel dev:*" } }
|
再次执行npm run dev,访问页面,可以看到点击文字弹出了alert。useEffect也执行了,count的值为1。
抽取样式文件
但是这里还有一个问题,组件还缺少样式。因为CSS文件并没有被加载,为了让SSR渲染的HTML包含样式,我们需要将CSS文件单独打包成一个文件,然后在服务端返回的HTML中引入CSS文件。首先需要安装mini-css-extract-plugin插件,然后修改webpack.client.js文件,将CSS文件单独打包:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
module: { rules: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, use: 'babel-loader', }, { test: /\.css$/, exclude: /node_modules/, use: [MiniCssExtractPlugin.loader, 'css-loader'], }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: 'styles.css', }), ],
|
在HTML中引入CSS文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
app.get('/', async (req, res) => { const content = renderToString(<Home />);
const html = ` <html> <head> <title>My React App</title> <link rel="stylesheet" href="/styles.css"> </head> <body> <div id="app">${content}</div> <script src="/bundle.js"></script> </body> </html> `; res.send(html); });
|
再次刷新页面可以看到SSR的首屏渲染元素已经包含样式。
添加路由
通过以上代码我们就搭建了一个简单的SSR项目,但是在实际项目中,页面通常不止一个,需要添加路由。使用react-router-dom来添加路由。首先安装react-router-dom,创建src/Routers.js文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
import React from 'react'; import { Route, Routes } from 'react-router-dom';
import Home from './containers/Home'; import Todo from './containers/Todo';
const Routers = () => { return ( <Routes> <Route path="/" element={<Home />} /> <Route path="/todo" element={<Todo />} /> </Routes> ); };
export default Routers;
|
接下来修改客户端入口文件src/index.js,将Routers组件渲染到页面中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import React from 'react'; import { hydrateRoot } from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom';
import Routers from './Routers';
const App = () => { return ( <BrowserRouter> <Routers /> </BrowserRouter> ); };
hydrateRoot(document.getElementById('app'), <App />);
|
然后修改Home组件,并添加Todo组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import React, { useEffect, useState } from 'react';
import Layout from '../../Layout';
import './index.css';
function Home() { const [count, setCount] = useState(0); useEffect(() => { setCount(1); }, []); return ( <Layout> <div className='book-list'> <h2 onClick={() => alert('ok')}>My React SSR App-Home {count}</h2> </div> </Layout> ); }
export default Home;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import React from 'react';
import Layout from '../../Layout';
function Todo() { return ( <Layout> <div className='book-list'> <h2>My React SSR App-Todo</h2> </div> </Layout> ); }
export default Todo;
|
Home组件和Todo组件都引入了Layout组件,Layout组件内容如下(Layout.css文件内容省略):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import React from 'react';
import './Layout.css';
const Layout = ({ children }) => { return ( <div> <header> <ul> <li> <a href="/">Home</a> </li> <li> <a href="/todo">Todo</a> </li> </ul> </header> <div className="main">{children}</div> </div> ); };
export default Layout;
|
这样客户端的部份就完成了,为了保证SSR和客户端水合时首次渲染Dom一致,还需要在服务端添加react-router-dom的路由。将Routers组件渲染到HTML中,这里我们创建一个server/utils.js文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import React from 'react'; import { renderToString } from 'react-dom/server'; import { StaticRouter } from 'react-router-dom/server';
import Routers from '../src/Routers';
export const renderHtml = (res, req) => { const content = renderToString( <StaticRouter location={req.url}> <Routers /> </StaticRouter>, );
return `<html> <head> <title>My React App</title> <link rel="stylesheet" href="/styles.css"> </head> <body> <div id="app">${content}</div> <script src="/bundle.js"></script> </body> </html>`; };
|
这里和客户端有一点小区别,在服务端调用的是StaticRouter组件,并将req.url传入location属性,这样服务端渲染的HTML就会根据URL进行路由匹配。然后在server/index.js文件中引入renderHtml方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
import express from 'express';
import { renderHtml } from './utils';
const app = express();
app.use(express.static('client-build'));
app.get('/', async (req, res) => { res.send(renderHtml(res, req)); });
app.get('/todo', async (req, res) => { res.send(renderHtml(res, req)); });
app.listen(3000, () => { console.log('Server is running on http://localhost:3000'); });
|
然后访问http://localhost:3000/todo
,可以看到页面已经切换到了Todo页面。点击Home可以看到页面切换到了Home页面。
添加Redux和数据请求
下一步添加Redux和数据请求。首先安装redux和react-redux以及@reduxjs/toolkit,创建store文件夹,创建src/store/index.js文件(使用Redux最新版本推荐的Hook方式),内容如下:
1 2 3 4 5 6 7 8 9
| import { configureStore } from '@reduxjs/toolkit'; import bookReducer from './bookSlice';
export const store = configureStore({ reducer: { book: bookReducer, }, });
|
然后创建src/store/bookSlice.js文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
import { createSlice } from '@reduxjs/toolkit';
export const bookSlice = createSlice({ name: 'book', initialState: { keyword: '刘慈欣', bookList: [], }, reducers: { setBookList: (state, action) => { state.bookList = action.payload; }, setKeyword: (state, action) => { state.keyword = action.payload; }, }, });
export const selectBookList = (state) => state.book.bookList; export const selectKeyword = (state) => state.book.keyword; export const { setBookList, setKeyword } = bookSlice.actions;
export const getBookList = () => { return (dispatch, getState) => { const state = getState(); const keyword = state.book.keyword; return fetch(`/api/bookList?keyword=${keyword}`) .then((res) => { return res.json(); }) .then((data) => { dispatch(setBookList(data)); }); }; };
export default bookSlice.reducer;
|
通过以上两步我们就创建了Redux的store和对应的book slice模块。接下来需要将store注入到React组件中。我们还是先修改客户端的入口文件src/index.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import React from 'react'; import { hydrateRoot } from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import { Provider } from 'react-redux';
import Routers from './Routers'; import { store } from './store';
const App = () => { return ( <Provider store={store}> <BrowserRouter> <Routers /> </BrowserRouter> </Provider> ); };
hydrateRoot(document.getElementById('app'), <App />);
|
还是老规矩,客户端的部份完成了还需要在服务端渲染Redux的store。修改server/utils.js文件,将store注入到Provider组件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
import { Provider } from 'react-redux';
import { store } from '../src/store';
export const renderHtml = (res, req) => { const content = renderToString( <Provider store={store}> <StaticRouter location={req.url}> <Routers /> </StaticRouter> </Provider> );
return `<html> <head> <title>My React App</title> <link rel="stylesheet" href="/styles.css"> </head> <body> <div id="app">${content}</div> <script src="/bundle.js"></script> </body> </html>`; };
|
数据注入到React组件中后,我们就可以在Home组件中使用Redux的数据和方法了。修改src/containers/Home/index.js文件,内容如下(index.css文件内容省略):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import React, { useEffect } from 'react'; import { useSelector } from 'react-redux';
import { selectBookList } from '../../store/bookSlice';
import Search from '../../components/Search'; import Layout from '../../Layout';
import './index.css';
const Book = ({ data }) => { return ( <div className="book"> <img src={data.cover} alt={data.title} /> <h2>{data.title}</h2> <p>{data.author}</p> </div> ); };
function Home() { const bookList = useSelector(selectBookList);
return ( <Layout> <div> <Search /> <div className="book-list"> {bookList.map((book, index) => ( <Book key={index} data={book} /> ))} </div> </div> </Layout> ); }
export default Home;
|
以上代码引用了Search组件,我们创建src/components/Search.js文件,内容如下(index.css文件内容省略):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
import React from 'react'; import { useSelector, useDispatch } from 'react-redux';
import { selectKeyword, setKeyword, getBookList } from '../../store/bookSlice';
import './index.css';
const Search = () => { const keyword = useSelector(selectKeyword); const dispatch = useDispatch();
const handleChange = (e) => { dispatch(setKeyword(e.target.value)); };
const handleClick = () => { if (keyword) { dispatch(getBookList()); } };
return ( <div className="search-box"> <input type="text" onChange={handleChange} value={keyword} placeholder="Search..." /> <button onClick={handleClick}>Search</button> </div> ); };
export default Search;
|
重启服务后,访问http://localhost:3000
,可以看到页面中有一个搜索框,输入关键字点击搜索,会调用/api/bookList接口。所以我们还需要创建一个/api/bookList接口。在server/index.js文件中添加接口:
1 2 3 4 5 6 7 8 9 10 11 12
|
import { searchBook } from './request';
app.get('/api/bookList', async (req, res) => { const keyword = req.query.keyword || '刘慈欣'; const data = await searchBook(keyword); res.send(data); });
|
这里依赖了request.js文件,创建server/request.js文件,内容如下:
1 2 3 4 5 6 7 8
|
export const searchBook = (keyword) => { return fetch(`https://book-db-v1.saltyleo.com/?keyword=${keyword}`).then( (res) => res.json(), ); };
|
刷新页面后点击搜索框,可以看到页面中显示了搜索结果。
此时应用的功能已经正常了,但是这里有几个潜在的问题,首先就是store/index.js导出的store是一个全局的单例store,服务端所有请求都会使用同一个store,这会导致数据混乱。所以我们需要为每个请求创建一个新的store。修改store/index.js文件,将store创建的过程封装成一个函数:
1 2 3 4 5 6 7 8 9 10 11 12
|
import { configureStore } from '@reduxjs/toolkit'; import bookReducer from './bookSlice';
export const makeStore = () => { return configureStore({ reducer: { book: bookReducer, }, }); };
|
修改了store/index.js文件之后,需要修改客户端的入口文件src/index.js和服务端的utils.js文件,将store的创建过程改为调用makeStore方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
import { makeStore } from './store';
const App = () => { const store = makeStore(); return ( <Provider store={store}> <BrowserRouter> <Routers /> </BrowserRouter> </Provider> ); };
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
import { makeStore } from '../src/store';
export const renderHtml = (res, req) => { const store = makeStore(); const content = renderToString( <Provider store={store}> <StaticRouter location={req.url}> <Routers /> </StaticRouter> </Provider> );
return `<html> <head> <title>My React App</title> <link rel="stylesheet" href="/styles.css"> </head> <body> <div id="app">${content}</div> <script src="/bundle.js"></script> </body> </html>`; };
|
这样就解决了store单例问题,另外一个问题是应用既然是SSR,那么首屏就应该渲染出有意义的数据,而不是客户端请求的数据。所以我们需要在服务端请求数据,然后将数据注入到store中。修改server/index.js文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
import { makeStore } from '../src/store'; import { setBookList } from '../src/store/bookSlice';
app.get('/', async (req, res) => { const store = makeStore(); const state = store.getState(); const keyword = state.book.keyword; const data = await searchBook(keyword); store.dispatch(setBookList(data)); res.send(renderHtml(res, req, store)); });
|
修改server/utils.js文件,将store作为参数传入renderHtml方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
export const renderHtml = (res, req, store) => { const content = renderToString( <Provider store={store}> <StaticRouter location={req.url}> <Routers /> </StaticRouter> </Provider> );
return `<html> <head> <title>My React App</title> <link rel="stylesheet" href="/styles.css"> </head> <body> <div id="app">${content}</div> <script src="/bundle.js"></script> </body> </html>`; };
|
现在服务端获取的数据已经注入到store中,但是刷新页面却并没有看到数据,控制台还有报错。
错误显示首次渲染的UI和服务端渲染返回的UI不一致导致水合失败,我们禁用JS功能看一下服务端返回的内容。
刷新一下页面,可以看到SSR确实已经渲染出数据了。
出现这个问题时因为服务端渲染组件时数据已经被注入到store中了,但是在客户端水合首次渲染时,客户端的store的数据是空的,所以两次渲染的结果不一致而导致水合失败。所以我们需要在水合时将服务端已经获取到的数据注入到客户端,这个过程就称之为注水。
做法也很简单,只需要在服务端将store的数据序列化成字符串,然后在客户端将字符串反序列化成store即可。修改server/utils.js文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
export const renderHtml = (res, req, store) => { const content = renderToString( <Provider store={store}> <StaticRouter location={req.url}> <Routers /> </StaticRouter> </Provider> );
return `<html> <head> <title>My React App</title> <link rel="stylesheet" href="/styles.css"> <script>window.INITIAL_STATE = ${JSON.stringify(store.getState())}</script> </head> <body> <div id="app">${content}</div> <script src="/bundle.js"></script> </body> </html>`; };
|
然后我们需要修改store/index.js文件,新创建一个makeClientStore方法,将window.INITIAL_STATE上的数据,注入到客户端store中:
1 2 3 4 5 6 7 8 9 10 11 12
|
export const makeClientStore = () => { const defaultState = window.INITIAL_STATE || {}; return configureStore({ reducer: { book: bookReducer, }, preloadedState: defaultState, }); };
|
接着还需要修改客户端入口文件src/index.js,将store的创建过程改为调用makeClientStore方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
import { makeClientStore } from './store';
const App = () => { const store = makeClientStore(); return ( <Provider store={store}> <BrowserRouter> <Routers /> </BrowserRouter> </Provider> ); };
|
注水过程的本质是数据的跨环境传输,即从服务端传输到客户端,这个过程需要一些额外考虑。由于这里是把数据序列化成JSON字符串,所以需要强调并不是所有类型的数据都支持序列化,比如函数、Symbol等类型的数据是不支持序列化的。支持序列化和反序列化的数据类型可以参考这里。
这样就完成了注水的过程,刷新页面可以看到数据已经正常显示了。到这里一个基本的可运行的SSR项目就完成了。以上待见过程基本涵盖了SSR所有基本要点。但是这里还要很多问题以及可优化的点。比如流式渲染,多页面代码分割等。
参考资料