React SSR 服务端渲染实现

ReactDOMServer
ReactDOMServer 对象允许你将组件渲染成静态标记。通常,它被使用在 Node 服务端上:
// ES modulesimport ReactDOMServer from \'react-dom/server\';
// CommonJS
var ReactDOMServer = require(\'react-dom/server\');
概览
下述方法可以被使用在服务端和浏览器环境。
renderToString()renderToStaticMarkup()
下述附加方法依赖一个只能在服务端使用的 package(stream)。它们在浏览器中不起作用。
renderToNodeStream()renderToStaticNodeStream()
参考
renderToString()
ReactDOMServer.renderToString(element)将 React 元素渲染为初始 HTML。React 将返回一个 HTML 字符串。你可以使用此方法在服务端生成 HTML,并在首次请求时将标记下发,以加快页面加载速度,并允许搜索引擎爬取你的页面以达到 SEO 优化的目的。
如果你在已有服务端渲染标记的节点上调用 ReactDOM.hydrate() 方法,React 将会保留该节点且只进行事件处理绑定,从而让你有一个非常高性能的首次加载体验。
renderToStaticMarkup()
ReactDOMServer.renderToStaticMarkup(element)此方法与 renderToString 相似,但此方法不会在 React 内部创建的额外 DOM 属性,例如 data-reactroot。如果你希望把 React 当作静态页面生成器来使用,此方法会非常有用,因为去除额外的属性可以节省一些字节。
如果你计划在前端使用 React 以使得标记可交互,请不要使用此方法。你可以在服务端上使用 renderToString 或在前端上使用 ReactDOM.hydrate() 来代替此方法。
renderToNodeStream()
ReactDOMServer.renderToNodeStream(element)将一个 React 元素渲染成其初始 HTML。返回一个可输出 HTML 字符串的可读流。通过可读流输出的 HTML 完全等同于 ReactDOMServer.renderToString 返回的 HTML。你可以使用本方法在服务器上生成 HTML,并在初始请求时将标记下发,以加快页面加载速度,并允许搜索引擎抓取你的页面以达到 SEO 优化的目的。
如果你在已有服务端渲染标记的节点上调用 ReactDOM.hydrate() 方法,React 将会保留该节点且只进行事件处理绑定,从而让你有一个非常高性能的首次加载体验。
注意:
这个 API 仅允许在服务端使用。不允许在浏览器使用。
通过本方法返回的流会返回一个由 utf-8 编码的字节流。如果你需要另一种编码的流,请查看像 iconv-lite 这样的项目,它为转换文本提供了转换流。
renderToStaticNodeStream()
ReactDOMServer.renderToStaticNodeStream(element)此方法与 renderToNodeStream 相似,但此方法不会在 React 内部创建的额外 DOM 属性,例如 data-reactroot。如果你希望把 React 当作静态页面生成器来使用,此方法会非常有用,因为去除额外的属性可以节省一些字节。
通过可读流输出的 HTML,完全等同于 ReactDOMServer.renderToStaticMarkup 返回的 HTML。
如果你计划在前端使用 React 以使得标记可交互,请不要使用此方法。你可以在服务端上使用 renderToNodeStream 或在前端上使用 ReactDOM.hydrate() 来代替此方法。
//下面为转载部分呢内容
早期的SSR(Server Side Rendering) : 服务端渲染,在最早期的网页开发时代,就是采用这种形式,由服务端渲染出页面结构,直接返回给客户端,首屏页面直出,SEO也较友好,但页面路由跳转会导致整个页面重新加载;
**CSR(Client Side Rendering):**随着前后端分离、提高开发效率的思想逐渐流行,react、vue等前端框架的默认支持,前端路由的无刷新切换页面,逐渐成为目前前端开发的主流形式。服务端返回的只是一个空页面,通过客户端加载js,填充生成整个页面展现给客户,减小了服务端的压力,但首屏等待时间较长,而且由于服务端返回空页面,导致对SEO并不友好。
**新时代的SSR:**为了解决CSR的痛点,开发者们重新把目光投向了SSR,结合CSR, 采用同构的模式,刷新SSR直出页面结构,之后客户端接管页面,前端路由无刷新切页,兼具了SSR和CSR的优点。目前结合react和vue也有了对应的SSR框架,next.js和nuxt.js.
本文通过实现简单的demo, 理解React SSR 服务端渲染的过程。
**同构:**同构这个概念存在于 Vue,React 这些新型的前端框架中,同构实际上是客户端渲染和服务器端渲染的一个整合。我们把页面的展示内容和交互写在一起,让代码执行两次。在服务器端执行一次,用于实现服务器端渲染,在客户端再执行一次,用于接管页面交互。
SSR 之所以能够实现,本质上是因为虚拟 DOM 的存在,dom的操作在服务端是无法实现的,而虚拟 DOM 是真实 DOM 的一个 JavaScript 对象映射,React 在做页面操作时,实际上不是直接操作 DOM,而是操作虚拟 DOM,也就是操作普通的 JavaScript 对象,这就使得 SSR 成为了可能。在服务端将虚拟dom映射成字符串返回,在客户端将虚拟dom映射为真实dom挂载到页面上。
SSR一般都需要一个node服务器作为中间层,由node处理服务端渲染,以及转发客户端到数据服务器的请求。
1. 配置webpack
既然需要node中间层, 那么就必须有node服务代码和客户端代码的入口,配置两份webpack配置
客户端 webpack.client.js:
const path = require(\'path\');module.exports = {
mode: \'development\',
entry: \'./src/client/index.js\',
output: {
filename: \'index.js\',
path: path.resolve(__dirname, \'public\')
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: \'babel-loader\',
exclude: /node_modules/,
}
}
resolve: {
extensions: [".js", ".jsx"], //引入文件时支持省略后缀,配置越多性能消耗越多
alias: {
"@": path.resolve(__dirname, "../src"), //引用文件时可以用“@”代表“src”的绝对路径,样式文件中为“~@”
}
}
}
服务端 webpack.server.js
const path = require(\'path\');const nodeExternals = require(\'webpack-node-externals\');
module.exports = {
target: \'node\',
mode: \'development\',
entry: \'./src/server/index.js\',
output: {
filename: \'bundle.js\',
path: path.resolve(__dirname, \'build\')
},
externals: [nodeExternals()],
module: {
rules: [
{
test: /\.jsx?$/,
loader: \'babel-loader\',
exclude: /node_modules/,
}
}
resolve: {
extensions: [".js", ".jsx"], //引入文件时支持省略后缀,配置越多性能消耗越多
alias: {
"@": path.resolve(__dirname, "../src"), //引用文件时可以用“@”代表“src”的绝对路径,样式文件中为“~@”
}
}
}
webpack-node-externals插件是用于在node环境下三方模块不被打包到最终的源码中,因为node环境下的npm已经安装了这些依赖;target: node 是让node 的核心模块不被webpack打包。
2. 配置路由,前后端同构 --- react-router-config;
对于页面代码我们使用的同一套,只是前后端使用的路由并不同,客户端使用BrowserRouter, 而react-router-dom为客户端渲染提供了StaticRouter, 对于路由的渲染管理建议使用react-router-config
路由配置文件:
import App from "./containers/App"import Home from "./containers/Home";
import Login from "./containers/Login";
import Personal from "./containers/Personal";
import NotFound from "./containers/NotFound";
const routes = [
{
path: "/",
component: App,
loadData: App.loadData,
routes:[
{
path: "/",
component: Home,
exact: true,
// 每个路由组件的静态方法就是为在服务端的store灌入初始数据
loadData: Home.loadData,
},
{
path: "/login",
exact: true,
component: Login,
},
{
path: "/personal",
exact: true,
component: Personal
},
{
component: NotFound,
}
]
}
]
export default routes;
client端入口路由:
import { renderRoutes } from "react-router-config";import routes from \'../Router\';
const App = () => {
return <Provider store={getClientStore()}>
<BrowserRouter>
{renderRoutes(routes)}
</BrowserRouter>
</Provider>
}
// 挂载到页面
ReactDom.render(<App/>, document.querySelector(\'#root\'))
server端入口路由:import { renderRoutes } from "react-router-config";import routes from \'../Router\';
const App = () => {
return <Provider store={getClientStore()}>
<StaticRouter location={url} context={{}}>
{renderRoutes(routes)}
</StaticRouter>
</Provider>
}
// 转换为字符串返回
return ReactDom.renderToString(<App/>)
StaticRouter的匹配需要手动传入匹配的路由地址 location={url}。
3. 结合Redux实现首页的数据直出
node转发请求, node端我采用了koa, 使用koa-server-http-proxy做代理请求
import proxy from \'koa-server-http-proxy\';...
app.use(proxy(\'/api\', {
target: \'http://xxx.com\',
changeOrigin: true
}))
...
store的创建:
// 服务端store// 服务器端的 Store 是所有用户都要用的,每个用户访问的时候,这个函数重新执行,为每个用户提供一个独立的 Store, 而不是提前创建好的一个单例:
export const getServerStore = (ctx) => createStore(reducer, applyMiddleware(logger, thunk.withExtraArgument(serverHttp)));
// 客户端store
export const getClientStore = () => {
const initState = window._content.state;
return createStore(reducer, initState, applyMiddleware(logger, thunk.withExtraArgument(clientHttp)));
}
同构的存在服务端的初始页面数据请求不需要代理,而客户端需要代理,解决方案:
axios构建两个实例clientHttp 和 serverHttp,设置不同的baseURL,在createStore应用redux-thunk中间件时 thunk.withExtraArgument(api)传入,在异步dispatch的第三个参数获取到axios实例,通过该实例派发请求。
首屏数据的获取, 通过redux和dispatch去获取
....server端解析页面需要的数据import routes from \'../Router\';
// 获取匹配到的路由
const matchedRoutes = matchRoutes(routes, ctx.url);
// 得到数据请求数组 --- 一组promise
const promiseDatas = [];
matchedRoutes.forEach(({route}) => {
if(route.loadData) {
promiseDatas.push(route.loadData(store));
}
})
// 执行数据请求,为store灌入初始数据
Promise.all(promises).then(() => {
// 生成要返回的页面
})
................................
...组件中
import {getNewsList} from \'./store/actions\';
import {useSelector, useDispatch} from \'react-redux\';
import styles from \'./index.css\';
const Home = () => {
const name = useSelector(({root}) => root.name);
const list = useSelector(({home}) => home.list);
const dispatch = useDispatch();
useEffect(() => {
if(!list.length) {
dispatch(getNewsList());
}
}, [])
return <div>
<h1 className={styles.title}>Home Page !!!</h1>
<h2>name: {name}</h2>
<ul>
{
list.map(({title, content}) => <li key={title}>
<h4>{title}</h4>
<p>{content}</p>
</li>)
}
</ul>
<button onClick={() => console.log(\'click button\')}>click</button>
</div>
}
// 此静态方法为服务端用来做数据直出
Home.loadData = (store) => {
return store.dispatch(getNewsList());
}
export default Home;
数据的脱水和注水
服务端渲染之后,拿到了首页数据,但客户端会再次渲染,store是空的。解决办法:在服务端渲染的时候将获取到的数据赋值一个全局变量(注水),客户端创建的store以这个变量的值作为初始值(脱水),这样就做到的首屏的数据直出。
// server端注水,再返回的模板字符串中注入数据`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/antd/4.8.2/antd.min.css" integrity="sha512-CPolmBEaYWn1PClN5taQQ0ucEhAt+9j7+Tiog/SblkFjZ5k6M3TioqmlpcHKwUhIcsu1s7lgnX4Plsb6T8Kq5A==" crossorigin="anonymous" />
<title>React-SSR</title>
</head>
<body>
<div id="root">${contents}</div>
<script>
window._content = {
state: ${JSON.stringify(store.getState())}
}
</script>
<script src="/index.js"></script>
</body>
</html>`
// 客户端脱水
export const getClientStore = () => {
const initState = window._content.state;
return createStore(reducer, initState, applyMiddleware(logger, thunk.withExtraArgument(clientHttp)));
}
4. 首屏样式的直出
webpack配置css解析
// webpack.client.js --- 客户端正常配置css-loader和style-loader.....
module: {
rules: [
{
test: /\.css$/i,
use: [
\'style-loader\',
{
loader: \'css-loader\',
options: {
importLoaders: 1,
esModule: false,
modules: {
compileType: \'module\',
localIdentName: \'[name]_[local]_[hash:base64:5]\'
},
}
}
]
},
]
}
.....
// webpack.server.js --- server端使用isomorphic-style-loader代替style-loader, 因为style-loader是生成style标签挂载到页面的,服务端明显不合适
module: {
rules: [
{
test: /\.css$/,
use: [\'isomorphic-style-loader\', {
loader: \'css-loader\',
options: {
esModule: false,
importLoaders: 1,
modules: {
compileType: \'module\',
localIdentName: \'[name]_[local]_[hash:base64:5]\'
},
}
}]
},
]
}
服务端改造
import React from \'React\';import {renderToString} from \'react-dom/server\';
import { renderRoutes } from "react-router-config";
import StyleContext from \'isomorphic-style-loader/StyleContext\';
// react服务端渲染路由需要使用StaticRouter
import {StaticRouter} from \'react-router-dom\';
import {Provider} from \'react-redux\';
export const render = (store, routes, url, context) => {
const css = new Set();
const insertCss = (...styles) => {
styles.forEach(style => {
css.add(style._getCss());
})
};
const contents = renderToString(
<StyleContext.Provider value={{ insertCss }}>
<Provider store={store}>
// context可以在服务端渲染时在组件的props.staticContext中获取到,以区分两端环境
<StaticRouter location={url} context={{}}>
{renderRoutes(routes)}
</StaticRouter>
</Provider>
</StyleContext.Provider>
);
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style id="ssr-style">${[...css].join(\'\n\')}</style>
<title>React-SSR</title>
</head>
<body>
<div id="root">${contents}</div>
<script>
window._content = {
state: ${JSON.stringify(store.getState())}
}
</script>
<script src="/index.js"></script>
</body>
</html>`;
}
客户端使用
import useStyles from \'isomorphic-style-loader/useStyles\';const Home = () => {
...
// 区分server端和client端
if(props.staticContext) {
useStyles(styles);
}
return <div>
....
</div>
}
最后贴一下依赖版本
"dependencies": { "@babel/core": "^7.12.3",
"@babel/plugin-proposal-function-bind": "^7.12.1",
"@babel/plugin-transform-runtime": "^7.12.1",
"@babel/preset-env": "^7.12.1",
"@babel/preset-react": "^7.12.1",
"@babel/preset-stage-0": "^7.8.3",
"@babel/runtime": "^7.12.1",
"axios": "^0.21.0",
"babel-loader": "^8.1.0",
"css-loader": "^5.0.1",
"isomorphic-style-loader": "^5.1.0",
"koa": "^2.13.0",
"koa-router": "^9.4.0",
"koa-server-http-proxy": "^0.1.0",
"koa-static": "^5.0.0",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-redux": "^7.2.2",
"react-router-config": "^5.1.1",
"react-router-dom": "^5.2.0",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"style-loader": "^2.0.0",
"webpack": "5.4.0",
"webpack-cli": "^4.1.0",
"webpack-node-externals": "^2.5.2"
},
"devDependencies": {
"redux-logger": "^3.0.6",
"webpack-merge": "^5.3.0"
}
部分摘自juejin :https://juejin.cn/post/6907164030385782791
以上是 React SSR 服务端渲染实现 的全部内容, 来源链接: utcz.com/z/383470.html

