react-ssr渲染
Client-Side Rendering-CSR
其中页面内容的生成和显示发生在用户的浏览器上,而不是在服务器上。在客户端渲染中,页面的构建和呈现主要由浏览器中的前端代码负责,通常使用JavaScript框架或库来管理页面的呈现和交互。
客户端渲染的缺点
在 SPA 模式下,所有的数据请求和 DOM 渲染都在浏览器端完成,所以当我们第一次访问页面的时候很可能会存在“白屏”等待,
首先浏览器请求URL,前端服务器直接返回一个空的静态HTML文件(不需要任何查数据库和模板组装),这个HTML文件中加载了很多渲染页面需要的 JavaScript 脚本和 CSS 样式表,
浏览器拿到 HTML 文件后开始加载脚本和样式表,并且执行脚本,这个时候脚本请求后端服务提供的API,获取数据,获取完成后将数据通过JavaScript脚本动态的将数据渲染到页面中,完成页面显示。
- 首屏速度加载慢。
- 不支持SEO和搜索引擎优化。
- 首页需要请求来初始化数据。
先回顾下页面的渲染流程:
- 浏览器通过请求得到一个HTML文本
- 渲染进程解析HTML文本,构建DOM树
- 解析HTML的同时,如果遇到内联样式或者样式脚本,则下载并构建样式规则(stytle rules),若遇到JavaScript脚本,则会下载执行脚本。
- DOM树和样式规则构建完成之后,渲染进程将两者合并成渲染树(render tree)
- 渲染进程开始对渲染树进行布局,生成布局树(layout tree)
- 渲染进程对布局树进行绘制,生成绘制记录
- 渲染进程的对布局树进行分层,分别栅格化每一层,并得到合成帧
- 渲染进程将合成帧信息发送给GPU进程显示到页面中
可以看到,页面的渲染其实就是浏览器将HTML文本转化为页面帧的过程。 而如今我们大部分WEB应用都是使用 JavaScript 框架(Vue、React、Angular)进行页面渲染的,也就是说,在执行 JavaScript 脚本的时候,HTML页面已经开始解析并且构建DOM树了,JavaScript 脚本只是动态的改变 DOM 树的结构,使得页面成为希望成为的样子,这种渲染方式叫动态渲染,也可以叫客户端渲染(client side rende)。
- 页面上的内容由于浏览器执行js脚本而渲染到页面上。
- 浏览器访问服务器,服务器返回空的HTML页面,里边有一个js脚本client
- 浏览器下载js代码,并在浏览器中运行。
- 内容呈现在页面上。
Server-Side Rendering
以前的服务端渲染
- 服务器内部获取数据。
- 服务器内部渲染HTML.
- 客户端从远程加载代码。
- 客户端开始水合。
缺点:
- 一切都是串行的,一个没有结束,后边都要等待。
- 这个操作必须是整体性的,而水合的过程比较慢,会引起卡顿。
服务端渲染(SSR) 优缺点
服务端渲染:
将组件或页面通过服务器生成html字符串,再发送到浏览器,当用户第一次请求页面时,客户端拿到手的,是可以直接渲染然后呈现给用户的 HTML 内容,不需要为了生成 DOM 内容自己再去跑一遍 JS 代码。
- 优点:首屏渲染快
相对于客户端渲染,服务端渲染在浏览器请求URL之后已经得到了一个带有数据的HTML文本,浏览器只需要解析HTML,直接构建DOM树就可以。
而客户端渲染,需要先得到一个空的HTML页面,这个时候页面已经进入白屏,之后还需要经过加载并执行 JavaScript、请求后端服务器获取数据、JavaScript 渲染页面几个过程才可以看到最后的页面。特别是在复杂应用中,由于需要加载 JavaScript 脚本,越是复杂的应用,需要加载的 JavaScript 脚本就越多、越大,这会导致应用的首屏加载时间非常长,进而降低了体验感。
- 优点:利于SEO
搜索引擎爬虫通常能够更轻松地解析和索引服务端渲染的页面,因为页面内容已经在HTML中生成。同构应用程序通过服务端渲染提供了更好的SEO支持,有助于改善搜索引擎排名。
缺点:
- 服务端压力较大
代码复杂度增加。为了实现服务端渲染,应用代码中需要兼容服务端和客户端两种运行情况,而一部分依赖的外部扩展库却只能在客户端运行,需要对其进行特殊处理,才能在服务器渲染应用程序中运行。
需要更多的服务器负载均衡。由于服务器增加了渲染HTML的需求,使得原本只需要输出静态资源文件的nodejs服务,新增了数据获取的IO和渲染HTML的CPU占用,如果流量突然暴增,有可能导致服务器down机,因此需要使用响应的缓存策略和准备相应的服务器负载。
涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。
介绍服务端渲染
SPA 使 SEO(Search Engine Optimization,即搜索引擎优化)出了问题,而且随着应用的复杂化,JavaScript 脚本也不断的臃肿起来,使得首屏渲染相比于 Web1.0时候的服务端渲染,也慢了不少。
使用 nodejs 在服务器进行页面的渲染,进而再次出现了服务端渲染。大体流程与客户端渲染有些相似,首先是浏览器请求URL,前端服务器接收到URL请求之后,根据不同的URL,前端服务器向后端服务器请求数据,服务端将我们需要的HTML文本组装好,并返回给浏览器,这个HTML文本被浏览器解析之后,不需要经过 JavaScript 脚本的执行,即可直接构建出DOM 树并展示到页面中。
请求完成后,前端服务器会组装一个携带了具体数据的HTML文本,并且返回给浏览器,浏览器得到HTML之后开始渲染页面,同时,浏览器加载并执行 JavaScript 脚本,给页面上的元素绑定事件,让页面变得可交互,当用户与浏览器页面进行交互,如跳转到下一个页面时,浏览器会执行 JavaScript 脚本,向后端服务器请求数据,获取完数据之后再次执行 JavaScript 代码动态渲染页面。


同构-虚拟 DOM为前后端同构提供了条件
React 的虚拟 DOM 以对象树的形式保存在内存中,所以可以在浏览器和 Node 中生成,这位前后端同构提供了条件。
但是像一些事件处理的方法,是无法在服务端完成,因此需要将组件代码在浏览器中再执行一遍,这种服务器端和客户端共用一套代码的方式就称之为「同构」
就是一套React代码在服务器上运行一遍,到达浏览器又运行一遍
- 服务端渲染完成页面结构
- 浏览器端渲染完成事件绑定
在浏览器里,React 通过 ReactDom 的 render 方法将虚拟 Dom 渲染到真实的 Dom 树上,生成网页
但是在 Node 环境下是没有渲染引擎的,所以 React 提供了另外两个方法,可将其渲染为 HTML 字符串:
- ReactDOMServer.renderToString
- ReactDOMServer.renderToStaticMarkup
在服务端上 Component 生命周期只会到 componentWillMount ,客户端则是完整的。
服务端渲染所有数据和 html 内容已在服务端处理完成,浏览器收到的是完整的 html 内容,可以更快地看到渲染内容。react-dom提供了服务端渲染的 renderToString方法,负责把React组件解析成html。
浏览器实现事件绑定的方式为让浏览器去拉取JS文件执行,让JS代码来控制,因此需要引入script标签 通过script标签为页面引入客户端执行的react代码,并通过express的static中间件为js文件配置路由
这种方法就能够简单实现首页的react服务端渲染,过程对应如下图:

服务端返回给浏览器的源码中应该还有一个 js 文件,包含了你的代码的所有的事件绑定。
那么怎么创建出这个 js 文件呢? 这里就需要使用 ReactDom 的 hydrate 方法
react服务端渲染过程
node server 接收客户端请求,得到当前的请求url 路径,然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 props、context或者store 形式传入组件
然后基于 react 内置的服务端渲染方法 renderToString()把组件渲染为 html字符串在把最终的 html进行输出前需要将数据注入到浏览器端
浏览器开始进行渲染和节点对比,然后执行完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 html 节点,整个流程结束
同构时,服务端结合数据将 Component 渲染成完整的 HTML 字符串并将数据状态返回给客户端,客户端会判断是否可以直接使用或需要重新挂载。
React 在客户端通过 checksum 判断是否需要重新render 相同则不重新render,省略创建 DOM 和挂 载DOM 的过程,接着触发 componentDidMount 等事件来处理服务端上的未尽事宜(事件绑定等),从而加快了交互时间;不同时,组件将客户端上被重新挂载 render。
renderToStaticMarkup 则不会生成与 react 相关的 data-* ,也不存在 checksum. 在客户端时组件会被重新挂载,客户端重新挂载不生成 checknum( 也没这个必要 ),所以该方法只当服务端上所渲染的组件在客户端不需要时才使用。
checknum 实际上是 HTML 片段的 adler32 算法值,实际上调用
React.render(<MyComponent />, container);
webpack.server.config.js
- @babel/preset-env 这是一个预设,它允许你使用最新的 JavaScript 语法,而无需对目标环境需要哪些语法转换进行管理。
- @babel/preset-react 将react转换成js文件包。
const path = require('path')
const config = {
mode: 'development',
// 代码运行环境:node
target: 'node',
// 入口文件
entry: './server/index.js',
// 导出
output: {
// 打包路径:使用path的API进行路径拼接
path: path.join(__dirname, 'build'),
// 打包文件名
filename: 'bundle.js',
},
// 配置打包规则
module: {
rules: [
{
// js文件规则
test: /\.(js|jsx)$/,
// 忽略node_modulse文件夹
exclude: /node_modulse/,
use: {
// 使用babel转换
loader: 'babel-loader',
// 配置babel
options: {
// babel预制
presets: ['@babel/preset-env', '@babel/preset-react'],
},
},
},
],
}
}
同构解决浏览器事件绑定
https://juejin.cn/post/7096396341940584461
// index.js
import express from 'express';
import { renderToString } from 'react-dom/server';
import React from 'react'
import Home from './Home';
import Text from './Text';
const app = express();
const content = renderToString(<Home />);
const text = renderToString(<Text text={'hello world!'} />)
app.get('/', function (req, res) {
res.send( `
<html lang="">
<head>
<title>ssr</title>
</head>
<body>
<div id="root">${content} ${text}</div>
</body>
</html> `
);
})
app.listen(8090, () => { console.log('listen:8090') })
// Home.js
import React from 'react';
const Home = () => {
return (
<div>
<div>This is wbh's ssr demo</div>
</div>
)
}
export default Home
// Text.js
import React from 'react';
const Text = (props) => {
return (
<div onClick={() => console.log('click')}>
<div>{props.text}</div>
</div>
)
}
export default Text
刷新页面,点击 Text 组件部分,发现 click 事件并没有和我们预想的一样触发,我们添加的 click 事件是没有绑定到对应的元素上的。我们的 click 事件消失了,这说明,目前我们的服务端渲染是不完全的,事件绑定是无效的,renderToString 函数并不会帮我们做这一步的处理!
import React from "react";
import App from "./page/App/index.jsx";
import { hydrateRoot } from "react-dom/client";
const root = document.getElementById("root");
const element = <App />
hydrateRoot(root, element);
- service.js文件
const express = require("express");
const register = require("@babel/register");
register({
ignore: [/node_modules/],
presets: ["@babel/preset-env", "@babel/preset-react"],
plugins: ["@babel/plugin-transform-modules-commonjs"],
});
const static = require('serve-static');
const webpack = require("webpack");
const render = require("./oldRender");
const webpackConfig = require("./webpack.config");
webpack(webpackConfig, (error, status) => {
const statusJson = status.toJson({ assets: true });
const assets = statusJson.assets.reduce((item, { name }) => {
item[name] = `/${name}`;
return item;
}, {});
console.log(assets, 'assets')
const app = express();
app.get("/", (req, res) => {
render(req, res, assets);
});
app.use(static('build'));
app.listen(8100, () => {
console.log("运行在8100端口");
});
});
- render函数代码
import React from "react";
import App from "./src/page/App";
import { renderToString } from "react-dom/server";
function render(req, res, assets) {
const html = renderToString(<App />);
res.statusCode = "200";
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.send(
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ssr</title>
</head>
<body>
<div id="root">${html}</div>
<script src="${assets["main.js"]}"></script>
</body>
</html>`
);
}
module.exports = render;
在做完初始渲染的时候,一个应用会存在路由的情况,配置信息如下:
import React from 'react' //引入React以支持JSX
import { Route } from 'react-router-dom' //引入路由
import Home from './containers/Home' //引入Home组件
export default (
<div>
<Route path="/" exact component={Home}></Route>
</div>
)
import React from 'react'
import ReactDom from 'react-dom'
import { BrowserRouter } from'react-router-dom'
import Router from'../Routers'
const App= () => {
return (
<BrowserRouter>
{Router}
</BrowserRouter>
)
}
ReactDom.hydrate(<App/>, document.getElementById('root'))
这时候控制台会存在报错信息,原因在于每个Route组件外面包裹着一层div,但服务端返回的代码中并没有这个div
解决方法只需要将路由信息在服务端执行一遍,使用使用StaticRouter来替代BrowserRouter,通过context进行参数传递 这样也就完成了路由的服务端渲染:
import express from 'express'
import React from 'react'//引入React以支持JSX的语法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import { StaticRouter } from 'react-router-dom'
import Router from '../Routers'
const app = express()
app.use(express.static('public'));
//使用express提供的static中间件,中间件会将所有静态文件的路由指向public文件夹
app.get('/',(req,res)=>{
const content = renderToString((
//传入当前path
//context为必填参数,用于服务端渲染参数传递
<StaticRouter location={req.path} context={{}}>
{Router}
</StaticRouter>
))
res.send(`
<html>
<head>
<title>SSR demo</title>
</head>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`)
})
app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))