所有源代码、文档和图片都在 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 页面效果

Home 页面效果

  • home 页面源码

Home 页面源代码

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 页面事件

home 页面事件

  • home 页面事件源代码

home 页面事件源代码

2.7 hydrate

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

hydrate

  • 这个警告信息是说,如果我们客户端渲染的和服务端渲染的内容一样的话,就要使用 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 模板中
    • 这样,就实现了最简单的服务端渲染
相关阅读

karuru
329 声望64 粉丝