所有源代码、文档和图片都在 github 的仓库里,点击进入仓库
相关阅读
- React服务端渲染之路01——项目基础架构搭建
- React服务端渲染之路02——最简单的服务端渲染
- React服务端渲染之路03——路由
- React服务端渲染之路04——redux-01
- React服务端渲染之路05——redux-02
- React服务端渲染之路06——优化
- React服务端渲染之路07——添加CSS样式
- React服务端渲染之路08——404和重定向
- React服务端渲染之路09——SEO优化
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 区分
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 页面源码
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 页面事件源代码
2.7 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 模板中
- 这样,就实现了最简单的服务端渲染