从零开始 React 服务器渲染

2023-11-12

一. 前言

当我们选择使用 Node+React 的技术栈开发 Web 时,React 提供了一种优雅的方式实现服务器渲染。使用 React 实现服务器渲染有以下好处:

        1. 利于 SEO:React 服务器渲染的方案使你的页面在一开始就有一个 HTML DOM 结构,方便 Google 等搜索引擎的爬虫能爬到网页的内容。

        2. 提高首屏渲染的速度:服务器直接返回一个填满数据的 HTML,而不是在请求了 HTML 后还需要异步请求首屏数据。

        3. 前后端都可以使用 js

二. 神奇的 renderToString 和 renderToStaticMarkup

有两个神奇的 React API 都可以实现 React 服务器渲染:renderToString 和 renderToStaticMarkup。renderToString 和 renderToStaticMarkup 的主要作用都是将 React Component 转化为 HTML 的字符串。这两个函数都属于 react-dom(react-dom/server) 包,都接受一个 React Component 参数,返回一个 String。

也许你会奇怪为什么会有两个用于服务器渲染的函数,其实这两个函数是有区别的:

        1.renderToString:将 React Component 转化为 HTML 字符串,生成的 HTML 的 DOM 会带有额外属性:各个 DOM 会有data-react-id属性,第一个 DOM 会有data-checksum属性。

        2.renderToStaticMarkup:同样是将 React Component 转化为 HTML 字符串,但是生成 HTML 的 DOM 不会有额外属性,从而节省 HTML 字符串的大小。

下面是一个在服务器端使用 renderToStaticMarkup 渲染静态页面的例子:

npm 包安装:

1

npm -S install express react react-dom

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

var express = require('express');

var app = express();

var React = require('react'),

    ReactDOMServer = require('react-dom/server');

var App = React.createFactory(require('./App'));

app.get('/', function(req, res) {

    var html = ReactDOMServer.renderToStaticMarkup(

        React.DOM.body(

            null,

            React.DOM.div({id: 'root',

                dangerouslySetInnerHTML: {

                    __html: ReactDOMServer.renderToStaticMarkup(App())

                }

            })

        )

    );

    res.end(html);

});

app.listen(3000, function() {

    console.log('running on port ' + 3000);

});

App.js:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

var React = require('react'),

    DOM = React.DOM, div = DOM.div, button = DOM.button, ul = DOM.ul, li = DOM.li

module.exports = React.createClass({

  getInitialState: function() {

   return {

     isSayBye: false

   }

  },

  handleClick: function() {

   this.setState({

     isSayBye: !this.state.isSayBye

   })

  },

  render: function() {

    var content = this.state.isSayBye ? 'Bye' : 'Hello World';

    return div(null,

      div(null, content),

      button({onClick: this.handleClick}, 'switch')

    );

  }

})

运行:

1

node server.js

结果:

结果1

三. 动态的 React 组件

上例的页面中,点击“switch” 按钮是没有反应的,这是因为这个页面只是一个静态的 HTML 页面,没有在客户端渲染 React 组件并初始化 React 实例。只有在初始化 React 实例后,才能更新组件的 state 和 props,初始化 React 的事件系统,执行虚拟 DOM 的重新渲染机制,让 React 组件真正“ 动” 起来。

或许你会奇怪,服务器端已经渲染了一次 React 组件,如果在客户端中再渲染一次 React 组件,会不会渲染两次 React 组件。答案是不会的。秘诀在于data-react-checksum属性:

上文有说过,如果使用 renderToString 渲染组件,会在组件的第一个 DOM 带有data-react-checksum属性,这个属性是通过 adler32 算法算出来:如果两个组件有相同的 props 和 DOM 结构时,adler32 算法算出的 checksum 值会一样,有点类似于哈希算法。

当客户端渲染 React 组件时,首先计算出组件的 checksum 值,然后检索 HTML DOM 看看是否存在数值相同的data-react-checksum属性,如果存在,则组件只会渲染一次,如果不存在,则会抛出一个 warning 异常。也就是说,当服务器端和客户端渲染具有相同的 props 和相同 DOM 结构的组件时,该 React 组件只会渲染一次

在服务器端使用 renderToStaticMarkup 渲染的组件不会带有data-react-checksum属性,此时客户端会重新渲染组件,覆盖掉服务器端的组件。因此,当页面不是渲染一个静态的页面时,最好还是使用 renderToString 方法。

上述的客户端渲染 React 组件的流程图如下:

客户端渲染流程 2

四. 一个完整的例子

下面使用 React 服务器渲染实现一个简单的计数器。为了简单,本例中不使用 redux、react-router 框架,尽量排除各种没必要的东西。

项目目录如下:

目录

npm 包安装:

1

npm install -S express react react-dom jsx-loader

webpack.config.js:webpack 配置文件,作用是在客户端中可以使用代码模块化和 jsx 形式的组件编写方式:

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

var path = require('path');

var assetsPath = path.join(__dirname, "public", "assets");

var serverPath = path.join(__dirname, "server");

module.exports = [

    {

        name: "browser",

        entry: './app/entry.js',

        output: {

            path: assetsPath,

            filename: 'entry.generator.js'

        },

        module: {

            loaders: [

                { test: /\.js/, loader: "jsx-loader" }

            ]

        }

    },

    {

        name: "server-side rending",

        entry: './server/page.js',

        output: {

            path: serverPath,

            filename: "page.generator.js",

            // 使用page.generator.js的是nodejs,所以需要将

            // webpack模块转化为CMD模块

            library: 'page',

            libraryTarget: 'commonjs'

        },

        module: {

            loaders: [

                { test: /\.js$/, loader: 'jsx-loader' }

            ]

        }

    }

]

app/App.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

var React = require('react');

var App = React.createClass({

    getInitialState: function() {

        return {

            count: this.props.initialCount

        };

    },

    _increment: function() {

        this.setState({ count: this.state.count + 1 });

    },

    render: function() {

        return (

            <div>

                <span>the count is: </span>

                <span onClick={this._increment}>{this.state.count}</span>

            </div>

        )

    }

})

module.exports = App;

server/index.js:服务器入口文件:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

var express = require('express');

var path = require('path');

var page = require("./page.generator.js").page;

var app = express();

var port = 8082;

app.use(express.static(path.join(__dirname, '..', 'public')));

app.get('/', function(req, res) {

    var props = {

        initialCount: 9

    };

    var html = page(props);

    res.end(html);

});

app.listen(port, function() {

    console.log('Listening on port %d', port);

});

server/page.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

var React = require('react');

var ReactDOMServer = require("react-dom/server");

var App = require('../app/App');

var ReactDOM = require('react-dom');

module.exports = function(props) {

    

    var content = ReactDOMServer.renderToString(

        <App initialCount={props.initialCount}></App>

    );

    var propsScript = 'var APP_PROPS = ' + JSON.stringify(props);

    var html = ReactDOMServer.renderToStaticMarkup(

        <html>

            <head>

            </head>

            <body>

                <div id="root" dangerouslySetInnerHTML={

                    {__html: content}

                } />

                <script dangerouslySetInnerHTML={

                    {__html: propsScript}

                }></script>

                <script src={"assets/entry.generator.js"}></script>

            </body>

        </html>

    );

    return html;

}

为了让服务器端和客户端的 props 一致,将一个服务器生成的首屏 props 赋给客户端的全局变量 APP_PROPS,在客户端初始化根组件时使用这个 APP_PROPS 根组件的 props。

app/entry.js:客户端入口文件,用于在客户端渲染根组件,别忘了使用在服务器端写入的 APP_PROPS 初始化根组件的 props

1

2

3

4

5

6

7

8

9

10

var React = require('react'),

    ReactDOM = require('react-dom'),

    App = require('./App');

var APP_PROPS = window.APP_PROPS || {};

ReactDOM.render(

    <App initialCount={APP_PROPS.initialCount}/>,

    document.getElementById('root')

);

源代码放在 github 上,懒得复制粘贴搭建项目的同学可以猛戳这里

github 上还有其他的服务器渲染的例子,有兴趣的同学可以参考参考:

1. 无 webpack 无 jsx 版本

2. 使用 webpack 版本

参考文章:

1.Rendering React Components on the Server

2. 一看就懂的 React Server Rendering(Isomorphic JavaScript)入門教學

3.Clientside react-script overrides serverside rendered props

4.React 直出实现与原理

5.React Server Side Rendering 解决 SPA 应用的 SEO 问题

6.Server-Side Rendering with React + React-Router

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

从零开始 React 服务器渲染 的相关文章

随机推荐

  • 深度学习: Epoch、batchsize、iterations 是什么?

    Epoch 英文 时代 阶段 一波 一轮 一个epoch 表示 所有的数据送入网络中 完成了一次前向计算 反向传播的过程 由于一个epoch 常常太大 分成 几个小的 baches 将所有数据迭代训练一次是不够的 需要反复多次才能拟合 收敛
  • String数组中扩容与填加元素

    String deepCode1 350000 350100 350102 String split deepCode1 split System out println String数组原来的长度为 split length 追加扩容 w
  • vue高德地图的实现 根据经纬度回显地理位

    效果图 1 首先 下载vue amap 插件 2 在main js中引入 import VueAMap from vue amap Vue use VueAMap VueAMap initAMapApiLoader key 你自己的key
  • 深度探索c++对象模型之template中的名称决议方式

    我们应该能够区分以下两种意义 一个是c standard标准中的 scope of the template definition 模板定义域 另一个是c standard标准中的 scope of the template instant
  • SpringMvc

    简述 基于Java实现Mvc模型的轻量级web框架 配置案例过程 导入maven
  • 神经网络——非线性激活

    torch官网 torch nn PyTorch 1 11 0 documentation 非线性变换的主要目的就是给网中加入一些非线性特征 非线性越多才能训练出符合各种特征的模型 常见的非线性激活 ReLU 官网给出的例子 gt gt g
  • C语言求平均成绩小程序(以五个学科为例)

    include
  • 客户好评“收割机”,NPS高达0.7, 实在RPA6.8.0重磅升级解析

    近期 实在智能大模型新品 TARS RPA Agent 发布会召开 通过底层软件架构的全新优化和全面结合大语言模型实现 超进化 持续以AI技术为RPA行业提供领先的超自动化解决方案 同时在发布会上亮相的 还有备受关注的最新版RPA产品 实在
  • buuctf MD5

    打开是一串MD5密文 md5加密后是32位的字符 也有16位的 是去除32位的前后各八位所得 由字母和数字组成 字母要么全是大写要么全是小写 MD5加密是不可逆的加密 无法解密 但是可以爆破出来 给大家推荐一个可以爆破MD5加密的网站htt
  • 小码哥学习感想第一天

    开班须知 本小节知识点 了解 课堂纪律要求 了解 上课的时间和内容安排 了解 学习方法 了解 教学思想和目标 1 课堂纪律要求 手机静音 保持安静 很容易错过精彩 关键瞬间 低调听课 尊重他人 多点反馈 多点互动 积极思考 积极回答 大家一
  • pycharm内无法激活conda虚拟环境

    仅供参考 问题描述 在pycharm终端里conda activate xxx 没报错 但是并没有激活指定的xxx虚拟环境 解决方法 检查是否已将conda加入到系统环境变量内 查找了其他教程 说conda没有加入到环境变量内 但我的已经加
  • 签好软件定制开发合同,需要注意什么

    签订好一份责权分明 细节清晰的软件定制开发合同 对于任何软件定制开发合同的双方而言都是百利无害的 尤其对于软件开发软件定制开发合同这种非常容易引起争议的项目 签订合同的时候更是要慎之又慎 前期做好充足的准备 后期才能达到一个良好的效果 那么
  • Fastapi 学习笔记之请求多个参数

    1 混合使用 Path Query 和请求体参数 from fastapi import FastAPI Path from typing import Optional from pydantic import BaseModel app
  • 人工智能-目标识别:古典目标识别、R-CNN、SPP-NET、Fast-R-CNN、Faster-R-CNN、YOLO

    古典目标识别 第一部分 训练集构造 负样本 使用 select search ss 方法对区域进行融合 gt 计算每个候选区域域真实标记区域 GRadeonTruts GT 之间的重合 如果区域A与GT的重合度在20 50 之间 而且A与其
  • Android LCD(四):LCD驱动调试篇

    关键词 android LCD TFTSN75LVDS83B TTL LVDS LCD电压背光电压平台信息 内核 linux2 6 linux3 0系统 android android4 0 平台 samsung exynos 4210 e
  • Error:Execution failed for task ‘:app:mergeDebugResources‘. > Error: java.util.concurrent.ExecutionE

    我的解决办法是 点击Gradle Scripts下的build gradle Module app 添加如下两行 aaptOptions cruncherEnabled falseaaptOptions useNewCruncher fal
  • 计算机导论 复习 第一章 计算机学什么

    一 核心知识点 1 计算系统构成 硬件 系统软件 操作系统 应用程序 软件 2 算法的特征 算法的高级程序实现方法 3 程序设计语言 机器语言 汇编语言 高级语言 4 计算机发展简史 二 选择题 1 冯 诺伊曼体系结构是现代计算机基础 被人
  • 左程云 Java 笔记--前缀树 贪心算法

    文章目录 前缀树 贪心算法 例1 字典序排序 例3 哈夫曼编码 例四 堆的一个应用 N皇后 总结 前缀树 介绍前缀树 何为前缀树 如何生成前缀树 例子 一个字符串类型的数组arr1 另一个字符串类型的数组arr2 arr2中有哪些字符 是a
  • 【Java】使用Swing组件弹窗展示九十九乘法表

    目录 一 效果展示 二 完整代码 三 代码思路 一 效果展示 直接先上效果图 二 完整代码 import java awt import javax swing public class MultiplicationTable extend
  • 从零开始 React 服务器渲染

    一 前言 当我们选择使用 Node React 的技术栈开发 Web 时 React 提供了一种优雅的方式实现服务器渲染 使用 React 实现服务器渲染有以下好处 1 利于 SEO React 服务器渲染的方案使你的页面在一开始就有一个