React服务端渲染之路02——最简单的服务端渲染

李衮
• 阅读 3513

所有源代码、文档和图片都在 github 的仓库里,点击进入仓库

相关阅读

1. 最简单的服务端渲染

1.1 Home 组件

  • 在 src/containers 下新建 Home 组件,每一个组件都是一个文件夹,组件名都采用 index.js
  • 以 Home 组件为例,Home 组件就是 src/container/Home/index.js
// 一个非常简单的组件
import React, { Component } from 'react';

class Home extends Component {
  render() {
    return (
      <div>
        <h1>HELLO, HOME PAGE</h1>
      </div>
    );
  }
}

export default Home;

1.2 创建 server

  • 在 src/server 文件夹下新建 index.js 文件
  • 由于我们使用了 Babel,所以我们可以直接采用 ES6/7/8 语法来写服务端代码
  • 这里渲染的原理就是通过 react-dom/server 的一个方法 renderToString,把 React 组件转为普通的 HTML 字符串,直接把这个 HTML 字符串返回给浏览器,浏览器接收到这串 HTML,会自动解析渲染到浏览器上。
  • 这样我们就创建了一个端口为 3000 的服务,在浏览器直接打开 http://localhost:3000,就可以看到 Home 组件里的内容已经展示在页面上,查看网页源代码,就发现网页的源代码就是 renderToString(<Home />) 生成的 HTML 字符串
  • 这样,我们就实现了一个最简单的服务端渲染
  • 注意,renderToString 要与 renderToMarkUp 区分
在 react-dom/server 中,还有一个方法 renderToStaticMarkup,这个方法与 renderToString 的主要作用都是将 React Component 转化成 HTML 字符串。区别在于 renderToString 生成的 HTML 中的 DOM 会带有额外的属性,比如 data-reactroot="",在 renderToStaticMarkup 生成的 HTML 中的 DOM 没有额外的属性,可以节省 HTML 字符串的大小。

renderToString 生成的 HTML 里边的 DOM 属性,在客户端渲染 React 组件的时候,会根据 DOM 的属性,判断属性值是否相等,如果相等,那么不需要渲染组件,如果不相等,那么就要重新渲染组件,可以提高页面性能。而 renderToStaticMarkup 生成的 HTML 里的 DOM 没有属性,所以页面数据变更的时候,会重新渲染组件,覆盖掉服务器端的组件。所以,如果页面是一个纯粹的静态页面,最好使用 renderToStaticMarkup,否则,最好使用 renderToString。

import express from 'express';
import React from 'react';
// react-dom 提供的一个方法,用来把 React 组件转为普通的 html 字符串
// 使用方法就是直接把组件放入这个方法里即可
import { renderToString } from 'react-dom/server';
import Home from '../containers/Home';

const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
  let html = renderToString(<Home />);
  console.log(html);
  // 在控制台输入 html,得到的就是一个非常简单的 HTML 字符串
  // <div data-reactroot=""><h1>HELLO, HOME PAGE</h1></div>
  res.send(html);
});

app.listen(PORT, err => {
  if (err) {
    console.log(err);
  } else {
    console.log(`Server is running at http://localhost:${PORT}`);
  }
});
  • home 页面效果

React服务端渲染之路02——最简单的服务端渲染

  • home 页面源码

React服务端渲染之路02——最简单的服务端渲染

2. 同构

  • 在刚才的页面上,我们可以看到服务端渲染的页面,以及页面展示到页面上的效果,但是这并不能满足我们的需要,我们还需要做一些其他的操作

2.1 注册事件

  • 我们可以在 Home 组件里添加一个按钮,给按钮注册一个 click 事件,每次点击,都会加 1。
  • 修改 Home/index.js 里的代码,这样,我们就给 Home 组件注册了一个事件,在页面上可以看到效果
  • 但是我们发现,点击按钮的时候,没有任何改变,state 里的 number 的值没有发生改变,console.log 也没有输出任何值。
  • 因为我们是服务端渲染,我们的 HTML 代码是从服务端获取的,而我们的事件是绑定在 DOM 元素上的,服务端没有类似于客户端的 click,mouseover 等事件。所以,点击这个按钮没有任何的效果
import React, { Component } from 'react';

class Home extends Component {

  state = {
    number: 0
  };

  handleClick = () => {
    this.setState({
      number: this.state.number + 1
    });
    console.log(this.state.number);
  };

  render() {
    return (
      <div>
        <h1>HELLO, HOME PAGE</h1>
        <h2>number: {this.state.number}</h2>
        <button onClick={this.handleClick}>click</button>
      </div>
    );
  }
}

export default Home;
  • 此时我们查看页面的源代码,我们会发现,页面上只有 HTML 代码,没有任何的 js 代码
  • 原因就在于服务端使用 react-dom/server 的 renderToString 方法的时候,只能够处理 HTML,而不能处理事件
  • 因为服务端是没有客户端的 click,mouseout 等事件的,以前我们能够在页面点击发送请求之类的事件,都是客户端自己创建的,而不是服务端给的,所以我们需要一种方法把事件也注册到 DOM 节点上,所以我们需要 同构

2.2 同构

  • 什么是同构?

    • 同构就是前后端采用同一套 js 代码,采用不同的构建方式,就比如说同一段 js 代码,既可以运行在浏览器端,也可以运行在 Node 端。
  • 为什么要同构?

    • 优点是提高代码的复用,减少代码的开发,体验 SSR 带来的好处。
    • 缺点

      • 需要在不同的平台上进行不同的构建,有一定的构建成本和开发成本
      • 最主要的是性能损失,客户端和服务端都要渲染页面,虽然我们可以通过 DOM DIFF 来优化,但是这个问题,依然不可避免
  • 有一点需要注意到的是,服务端预渲染帮助客户端获取到的数据资源,客户端也要能够去获取,因为如果服务端获取失败,客户端依然可以获取
  • 在上边的例子中,我们仅仅是在服务端构建了 React 组件,客户端没有构建,所以我们需要在客户端构建同样的 React 组件代码

2.3 配置客户端的 webpack.client.js

  • 在 package.json 中添加 dev:build:client 的启动命令,命令内容是 webpack --config webpack.client.js --watch
const path = require('path');

module.exports = {
  mode: 'development',
  target: 'web',
  entry: './src/client/index.js',
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'client.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  }
};
  • 我们可以发现,在 webpack.server.js 和 webpack.client.js 里,都有相同的 module 和 mode 属性,在后边我们还会添加其他的属性,所以我们可以把他们相同的内容提取出来,减少代码的重复。

2.4 公共的 webpack 代码

  • 我们使用 webpack-merge 这个库,可以把 webpack 的配置组装起来,类似于 Object.assign 方法,可以添加很多个 webpack 配置对象,后边的会把前边的相同的属性覆盖掉。
  • 把公共的代码添加到 webpack.base.js 中
// webpack.base.js
module.exports = {
  mode: 'development',
  target: 'web',
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  }
};
  • 修改 webpack.server.js
// webpack.server.js
const path = require('path');
const merge = require('webpack-merge');
const WebpackNodeExternals = require('webpack-node-externals');
const baseConfig = require('./webpack.base');

module.exports = merge(baseConfig, {
  target: 'node',
  entry: './src/server/index.js',
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'server.js'
  },
  externals: [WebpackNodeExternals()],
});
  • 修改 webpack.client.js
const path = require('path');
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base');

module.exports = merge(baseConfig, {
  entry: './src/client/index.js',
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'client.js'
  }
});
  • 这样,我们就配置好了客户端和服务端的 webpack,包括 webpack 的基础配置,接下来就可以构建客户端的代码

2.5 构建客户端代码

  • 在 client 下创建 index.js 文件
  • 这也是一个 React 文件,所以我们要引入 react,react-dom
  • 由于同构时需要把前后端都用到的代码进行构建,所以我们要把 Home 组件构建到客户端代码中
// client/index.js
import React from 'react'
import { render } from 'react-dom';
import Home from '../containers/Home';

render(<Home/>, window.root);

2.6 服务端添加 HTML 模板

  • 给服务端渲染的内容添加一个模板,在模板中添加一个容器位置,供客户端使用
  • 同时,要把客户端构建的 js 代码,加载到 HTML 页面中
  • 加载的时候就需要有静态资源路径,所以我们用 express 开启静态资源服务 app.use(express.static('public'));,这个目录就是我们在 webpack.client.js 里配置生成的目录,里边的 client.js 文件就是客户端 webpack 打包后生成的代码
  • 这时,刚才的按钮的点击就有效果了,可以看到 number 的改变
app.use(express.static('public'));

app.get('/', (req, res) => {
  let domContent = renderToString(<Home />);
  let html = `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
  <title>react-ssr</title>
</head>
<body>
<div id="root">${domContent}</div>
<script src="/client.js"></script>
</body>
</html>
`;
  res.send(html);
});
  • home 页面事件

React服务端渲染之路02——最简单的服务端渲染

  • home 页面事件源代码

React服务端渲染之路02——最简单的服务端渲染

2.7 hydrate

  • 但是现在我们在控制台看到了一个警告信息

React服务端渲染之路02——最简单的服务端渲染

  • 这个警告信息是说,如果我们客户端渲染的和服务端渲染的内容一样的话,就要使用 hydrate 替换掉 render,所以,我们把客户端里的 render 渲染方法替换成 hydrate 渲染方法就可以了
  • 这个警告信息如果不处理也可以,不影响操作,但是在 react 后边的版本里,如果需要使用 hydrate 但是却使用了 render,那么是会报错的,所以建议还是处理掉
  • 这样,就没有了警告信息,同时按钮也可以正常点击
// client/index.js
import React from 'react'
import { hydrate } from 'react-dom';
import Home from '../containers/Home';

hydrate(<Home/>, window.root);

3. 总结

  • 到这里,我们已经实现了一个最简单的 react 服务端渲染,并且可以触发浏览器的事件
  • 原理

    • 服务端建立一个 HTML 的模板,通过 react-dom/server 下的 renderToString 方法,把 react 组件转换成纯粹的 HTML 字符串,代码里叫做 domContent
    • 服务端把 react 组件转换后的 domContent 字符串,作为 HTML 模板的内容,填充到模板中,对应的是 id = "root" 的容器
    • 但是现在仅仅是服务端渲染了 HTML 字符串,没有事件,我们通过同构的方式,把用到的组件,在客户端也生成同样的一份 js 代码,作为 js 脚本加载到 html 模板中
    • 这样,就实现了最简单的服务端渲染
相关阅读
点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Easter79 Easter79
4年前
typeScript数据类型
//布尔类型letisDone:booleanfalse;//数字类型所有数字都是浮点数numberletdecLiteral:number6;lethexLiteral:number0xf00d;letbinaryLiteral:number0b101
Wesley13 Wesley13
4年前
VBox 启动虚拟机失败
在Vbox(5.0.8版本)启动Ubuntu的虚拟机时,遇到错误信息:NtCreateFile(\\Device\\VBoxDrvStub)failed:0xc000000034STATUS\_OBJECT\_NAME\_NOT\_FOUND(0retries) (rc101)Makesurethekern
Wesley13 Wesley13
4年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
4年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
4年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
4年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Python进阶者 Python进阶者
2年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这