React SSR - 写个 Demo 一学就会

今天写个小 Demo 来从头实现一下 reactSSR,帮助理解 SSR 是如何实现的,有什么细节。

什么是 SSR

SSRServer Side Rendering 服务端渲染,是指将网页内容在服务器端中生成并发送到浏览器的技术。相比于客户端渲染(CSR),SSR 一般用于以下场景:

  1. SEO (搜索引擎优化):由于部分搜索引擎对 CSR 内容支持不佳,所以 SSR 可以提升网站在搜索引擎结果中的排名。
  2. 首屏加载速度:由于 SSR 可以在服务器端生成完整的 HTML 页面,用户打开网页时能够更快地看到内容,不会看到长时间的白屏,可以提升用户体验。
  3. 隐藏某些数据:由于 CSR 需要从服务器将数据下载下来进行动态渲染,所以一些数据很容易被他人获取,而 SSR 由于数据到渲染的过程在服务端实现,所以可以用来隐藏一些不想让他人轻易获得的数据。

如何实现

简单的 SSR 其实实现很简单,只需要在服务端导入要渲染的组件,然后调用 react-dom/server 包中提供的 renderToString 方法将该组件的渲染内容输出为字符串后返回客户端即可。

Server 端的组件

下面写一个简单的例子:

服务端代码:

import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server'; import App from '../ui/App'; const app = express(); app.get('/', (_: unknown, res: express.Response) => {
res.send(renderToString(<App />));
}); app.listen(4000, () => {
console.log('Listening on port 4000');
});

此处要注意服务端需要支持 jsx 语法的解析,我这里直接使用 esno 执行 ts 代码,在 tsconfig.json 中配置 jsx 即可。

其实看到这里就能明白为什么在 SSR 的页面上使用 windowlocalstorage 等浏览器 API 需要放到 useEffect 里了,因为该页面的组件都会被 server 端读取解析,而 server 端并没有这些 API

然后看下 App 组件的代码:

import React, { useCallback } from 'react';

export default () => {
const log = useCallback(() => {
console.log('Hello world');
}, []); return (
<div>
<p>react ssr demo</p>
<button onClick={log}>Click me</button>
</div>
);
};

启动服务器后 server 端就会使用 renderToString<App /> 渲染成 html 字符串,然后通过 send 返回给前端,下面就是服务端返回的 html 内容:

<div>
<p>react ssr demo</p>
<button>Click me</button>
</div>

打开浏览器访问该地址即可看到服务端返回了该 html 片段:

hydrate 复活组件

如果你跟着上面的操作很快就会发现问题:为什么点按钮没法操作了?

其实原因很简单,因为我们只拿到了一个 html 并没有任何的 js,事件绑定等自然是无法实现的,要复活组件的交互我们还需要很重要的一步 - hydrate 也就是常说的水合。

hydrate 即通过 react 将对应的组件重新渲染到 SSR 渲染的静态内容上,类似于 render 差异点在于 render 会忽略 root 元素中现有的 domhydrate 则会复用并会进行内容匹配检查。

Hydration failed because the initial UI does not match what was rendered on the server.

如果遇到上述错误即表示在客户端执行 hydrate 时服务端返回的初始的 domhydrate 接收到的需要进行渲染的 dom 不匹配。

说了这么多我们再来看下代码如何编写,首先要进行 hydrate 我们需要客户端的代码来执行:

import React from 'react';
import { hydrateRoot } from 'react-dom/client'; import App from './App'; hydrateRoot(document.getElementById('root')!, <App />);

然后将该代码进行编译打包,我这里就直接使用 webpack 进行打包:

const path = require('path');

module.exports = {
entry: './ui/index.tsx',
output: {
path: path.resolve(__dirname, 'static'),
filename: 'bundle.js'
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx']
},
module: {
rules: [
{
test: /\.(t|j)sx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react', '@babel/preset-typescript']
}
}
}
]
}
};

打包完成后生成一个 bundle.js 即可在客户端使用它来进行 hydrate

然后我们再修改下 server 端的代码:

app.get('/', (_: unknown, res: express.Response) => {
res.send(
`
<div id="root">${renderToString(<App />)}</div>
<script src="/bundle.js"></script>
`
);
}); app.use(express.static('static'));

我们在静态内容的外层套上 root 元素,然后在下方引入我们刚刚编译的脚本,然后就可以在客户端看到我们想要的结果:

可以看到事件可以正常触发了。

此处还有个注意点,在 server 端要注意将静态字符串包裹在 root 元素中不要添加换行空格等,不然 reacthydrate 时依旧会因为内容不匹配而提示 Hydration failed(仅在 hydrateRoot 时出现,如果使用 hydrate 不会报错,不过 18 中 hydrate 已经被弃用。)

动态数据

此时有些同学可能发现一些问题:前面的内容所渲染的内容都是静态的,如果要针对用户渲染出不同的内容比如用户信息等如何是好?

其实很简单,只需要在服务端将对应的信息作为 props 进行渲染即可,我们下面使用 userName 模拟一下:

app.get('/', (_: unknown, res: express.Response) => {
const userName = ['张三', '李四', '王五', '赵六'][(Math.random() * 4) | 0];
res.send(
`
<div id="root">${renderToString(<App userName={userName} />)}</div>
<script src="/bundle.js"></script>
`
);
});

可是客户端要如何与服务端匹配呢?此处有两种解决方案:

  1. 客户端获取对应的信息并在信息获取完成后再进行 hydrate 操作。
  2. 服务端将获取到的信息放在页面中。

可以看出方案 1 会带来明显的延时,所以一般会采用方案 2,实现一般可以使用全局变量或特定标签来实现:

app.get('/', (_: unknown, res: express.Response) => {
const userName = ['张三', '李四', '王五', '赵六'][(Math.random() * 4) | 0];
res.send(
`
<div id="root">${renderToString(<App userName={userName} />)}</div>
<script>
window.__initialState = { userName: '${userName}' };
</script>
<script src="/bundle.js"></script>
`
);
});
import React from 'react';
import { hydrateRoot } from 'react-dom/client'; import App from './App'; hydrateRoot(document.getElementById('root')!, <App {...window.__initialState} />);

总结

  1. React 中的 SSR 可以通过 renderToString 来实现,但是只能输出静态内容,要让页面支持交互需要搭配 hydrate 使用。
  2. 实现 SSR 时服务端需要支持 jsx 语法的解析,因为服务端也需要读取组件。
  3. hydrate 会检查服务端与客户端的内容是否匹配。
  4. 要实现动态数据需要在客户端与服务端之间做好如何使用初始 props 的约定。

最后

本文的 demo 代码放置在 React SSR Demo 中,可自行取阅。

React SSR - 写个 Demo 一学就会的更多相关文章

  1. 打算写一个《重学Node.js》系列,希望大家多多支持

    先放上链接吧,项目已经开始2周了:https://github.com/hellozhangran/happy-egg-server 想法 现在是2019年11月24日,还有人要开始学习Node.js ...

  2. 放弃antd table,基于React手写一个虚拟滚动的表格

    缘起 标题有点夸张,并不是完全放弃antd-table,毕竟在react的生态圈里,对国人来说,比较好用的PC端组件库,也就antd了.即便经历了2018年圣诞彩蛋事件,antd的使用者也不仅不减,反 ...

  3. 序(转) · 为 React 而写 -- 废话比较多, 你也可以说丰满

    流形 2 年前 (废话比较多       从今年开始,就一直在规划技术沉淀这事. 在阿里巴巴工作的这些年,前端团队日益壮大,同时聚集了一帮志趣相投的伙伴. 作为团队负责人,一方面是借团队在技术道路上的 ...

  4. 手写Spring+demo+思路

    我在学习Spring的时候,感觉Spring是很难的,通过学习后,发现Spring没有那么难,只有你去学习了,你才会发现,你才会进步 1.手写Spring思路: 分为配置.初始化.运行三个阶段如下图 ...

  5. 如何写好demo——学习感悟

    文章标题:教你如何写好Demo应用 如何制作出最有用的demo呢? 简,易 在demo中,我们要专注于单一的主题.我们的教学覆盖了很大的知识范围,因此,化整为零是非常必要的. 例如,我们要说明Andr ...

  6. React SSR in Action

    React SSR in Action react render HTML string from the server ReactDOMServer https://reactjs.org/docs ...

  7. React 学习资源分享 菜鸟刚学5天 博客写的不多 不懂写博客的套路

    http://www.ruanyifeng.com/blog/2015/03/react.html 首先个人强烈推荐 阮一峰的React基础 细细过一遍,看得出大师的用心良苦 然后就开始看书般的过ht ...

  8. 写简单游戏,学编程语言-python篇

    好吧, 首先得承认这个题目写的夸大了,人才菜鸟一枚,游戏相关编程也是知道点概念.但是本人对游戏开发比较感兴趣,相信大多数喜欢玩玩游戏,因为它给人确实带来很多乐趣,而编程语言的学习最少对于我来说比较乏味 ...

  9. 阿里react整合库dva demo分析

    p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 24.0px "Helvetica Neue"; color: #404040 } p. ...

  10. Python flask+react+antd实现登陆demo

    这两天在研究flask和antd,想把这俩个东西结合来使用,单独学antd的时候用的是dva来配置,但是发现这样与flask结合的话需要启动两个服务,作为flask只是作为数据的接口,并没用用到其强大 ...

随机推荐

  1. argparser Python包使用

    argparser Python包使用 导入 import argparse 定义parser对象(参数为介绍) parser = argparse.ArgumentParser('test pars ...

  2. 快速部署Ceph分布式高可用集群

    快速部署Ceph分布式高可用集群 Ceph简介 Ceph是一个PB,EB级别的分布式存储系统,可以提供文件存储,对象存储.和块存储,它可靠性高,易扩展,管理简便,其中对象存储和块存储可以和其他云平台集 ...

  3. Linux文件系统故障,Input/output error

    事情是这样的,在启动某一个应用程序的时候,出现 Input/output error 的报错,磁盘以及目录无法使用的情况下,进行了重启,重启完成后是可以正常使用的,过一段时间后就会再次出现这个问题,一 ...

  4. [Java SE]javac编译时编码错误

    1 问题复现 Information:java: javacTask: 源发行版 8 需要目标发行版 1.8 Information:java: Errors occurred while compi ...

  5. 四月七号java基础学习

    1.数据类型分为基本数据类型以及引用数据类型 基本数据类型有整型.浮点型.字符型.布尔型 引用数据类型有类.数组以及接口 2.常量的声明需要用关键字final来标识 3.JAVA语言的变量名称由数字, ...

  6. 面向对象编程(python)和部分面向对象高级编程

    1.类和对象 在python中定义类 class 类名(首字母最好大写)Student (Object(父类)): def __init__(self): self.属性 1= 参数1 self.属性 ...

  7. 第7章. 部署到GiteePages

    Gitee Pages 是一个免费的静态网页托管服务,您可以使用 Gitee Pages 托管博客.项目官网等静态网页.如果您使用过 Github Pages 那么您会很快上手使用 Gitee 的 P ...

  8. Mac + IOS + Safari 抓取网络请求

    第一步:打开苹果手机 设置>Safari浏览器>高级>网页检查器 第二步:打开 Mac 上的Safari浏览器>偏好设置>高级>在菜单栏中显示"开发&qu ...

  9. [OpenCV-Python] 21 OpenCV 中的轮廓

    文章目录 OpenCV-Python:IV OpenCV中的图像处理 21 OpenCV 中的轮廓 21.1 初识轮廓 21.1.1 什么是轮廓 21.1.2 怎样绘制轮廓 21.1.3 轮廓的近似方 ...

  10. 【CSS】使元素在父元素中居中显示的几种方法

    在页面元素布局时经常会有把元素居中的需求,大多都是用弹性盒或者定位,下面来说一下使用方法 一.使用边距进行固定位置 这种方法需要把父元素和子元素的宽度固定,然后利用二者宽高之差添加边距移动元素的位置 ...