# React_SSR_demo
**Repository Path**: liangcuncun/react_-ssr_demo
## Basic Information
- **Project Name**: React_SSR_demo
- **Description**: react服务端渲染
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2021-12-23
- **Last Updated**: 2021-12-24
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 什么是服务端渲染
内容由服务端产生,浏览器直接显示服务端返回的 html 就可以了。
```js
let express = require("express");
let app = express();
app.get("/", (req, res) => {
res.send(`
hello
`);
});
app.listen(8080);
```
# 什么是客户端渲染
页面上的内容由于浏览器运行 JS 脚本而渲染到页面上的,服务端只返回一个 html 模板。
```js
let express = require("express");
let app = express();
app.get("/", (req, res) => {
res.send(`
`);
});
app.listen(9090);
```
# 服务端渲染(React)
服务端渲染 React 组件的流程
1. 浏览器发起请求
2. 服务端运行 React 代码生成页面
- react-dom/server 将 react 组件转成字符串
- 引入的 react 组件是 jsx,浏览器识别不了,需要 webpack 打包编译
3. 服务器返回页面
src.server.js
```js
import React from "react";
import Home from "../client/index";
import { renderToString } from "react-dom/server";
// 注意:直接运行node ./src/server/index.js 报错,需要进行打包编译,因为浏览器不能直接识别jsx
/**
* renderToString把Home组件转成字符串
* 由于是jsx,浏览器不能识别,因此需要转译(webpack)
* 转译后react.createElement(),因此需要引入react(import React from 'react')
*/
let html = renderToString();
let express = require("express");
let app = express();
app.get("/", (req, res) => {
res.send(`
${html}
`);
});
app.listen(9090);
```
webpack.config.js
```js
let path = require("path");
let nodeExternals = require("webpack-node-externals");
module.exports = {
target: "node", //打包的是服务器端node文件
mode: "development", //开发模式
entry: "./src/server/index.js",
output: {
path: path.resolve(__dirname, "build"),
filename: "server.js",
},
externals: [nodeExternals()], // 在node环境运行,可以不用将path fs等模块打包进去
module: {
rules: [
{
test: /\.js$/,
loader: "babel-loader",
exclude: /node_modules/,
options: {
presets: [
[
"@babel/preset-env",
{
targets: {
browsers: ["last 2 versions"],
},
},
],
"@babel/preset-react",
],
},
},
],
},
};
```
package.json
```js
"start": "node ./build/server.js",
"build": "webpack --config webpack.config.js"
```
查看服务端返回的就是经过 react 处理的内容

以上如果加上事件是无法触发的`${html}`是一串静态的 html
## 实现事件绑定
1. 服务器端运行 React 代码渲染出 HTML
2. 服务器把渲染出的 HTML 页面发送给了浏览器
3. 浏览器接受到 HTML 会渲染到页面上
4. 浏览器发现页面引用的 index.js 文件会去下载
5. 浏览器下载得到的 index.js 文件并在浏览器端执行
6. 浏览器中的代码接管了页面的所有内容,后面和客户端渲染是一样的
``绑定事件
```js
app.get("/", (req, res) => {
res.send(`
${html}
`);
});
```
客户端,服务端分别打包
```js
"dev":"npm-run-all --parallel dev:**",
"dev:start": "nodemon ./build/server.js",
"dev:build:server": "webpack --config webpack.server.js --watch",
"dev:build:client": "webpack --config webpack.client.js --watch"
```
事件生效了:

⚠️ 此时控制台报了个警告,我们需要把 ReactDOM.render()替换成 ReactDOM.hydrate()
ReactDOM.render()会将服务端已经渲染的内容替换,造成不必要的重渲染
ReactDOM.hydrate()表示把服务端未完成的工作完成,比如事件绑定

# 优化启动流程
对于 dev 环境,更改内容需要频繁的触法打包,构建
- `cnpm i nodemon -g`
- `cnpm npm-run-all -g`
```js
"dev":"npm-run-all --parallel dev:**", // 运行以dev:开头的所有命令
"dev:start":"nodemon './server/build/bundle.js'",
"dev:build":"webpack --config server/webpack.config.js --watch"
```
# 跳转路由
当客户端路由跳转时
client/index.js
```js
}>
}>
```
server/index.js
```js
let html = renderToString();
```
此时页面访问根/目录的时候,虽然页面显示 Home 中的内容,但是右键查看源码发现,此时的代码仍然是 Counter 组件中的,是因为此时路由跳转只是客户端配置,服务端返回的仍然还是 Counter 组件的代码
而我们希望切换路由时,显示对应的源代码,这样才是合理的;
需要在服务端做路由跳转
```js
<>
{Routers}
>
```
# 使用 redux
安装 redux 全家桶
```js
yarn add redux react-redux redux-logger redux-thunk
```
分客户端、服务端两个仓库
```js
export function getClientStore() {
// 创建仓库
return createStore(reducer, applyMiddleware(thunk, logger));
}
export function getServerStore() {
// 创建仓库
return createStore(reducer, applyMiddleware(thunk, logger));
}
```
server.render.js 服务端也需要绑定仓库
```js
let html = renderToString(
<>
{Routers}
>
);
```
## 异步数据
```js
getList() {
return function (dispatch) {
axios.get('http://localhost:5000/api/user').then((res) => {
let list = res.data;
dispatch({
type: types.SET_HOME_LIST,
payload: list
});
})
}
}
```
数据显示出来了,但是右键查看源码发现数据是空的

server.render.js 在服务端,我们在渲染组件之前就要把数据拿到
那要怎么做???
首先 src/routes.js 代码需要做点改动
将导出组件改为配置项
```js
export default [
{
path: "/",
component: Home,
exact: true,
key: "/home",
loadData: Home.loadData, // 加载异步数据,如果配置项有这个属性,那么意味着需要加载异步数据
},
{
path: "/counter",
component: Counter,
key: "login",
exact: true,
},
];
/**
export default (
)
*/
```
客户端和服务端匹配路由
```js
{Routers.map((route) => (
))}
```
server/render.js
拿到匹配的路由配置 matchedRoutes
`window.context = { state:${JSON.stringify(store.getState())} }` ,方便客户端拿到状态,避免借口多次调用
```js
import { matchPath } from "react-router-dom";
// matchPath是react-router-dom提供的工具方法,可以用来判断路径和路由是否匹配上
export default function render(req, res) {
let store = getServerStore();
let matchedRoutes = Routers.filter((route) => {
return matchPath(req.path, route.path); // 当前路径和路由配置项匹配,返回boolean值
});
let promises = [];
matchedRoutes.forEach((route) => {
if (!!route.loadData) {
promises.push(route.loadData(store));
}
});
console.log("ll", promises);
// 全部执行完后渲染
Promise.all(promises).then(function () {
/**
* renderToString把Home组件转成字符串
* 由于是jsx,浏览器不能识别,因此需要转译(webpack)
* 转译后react.createElement(),因此需要引入react(import React from 'react')
* context可以组件进行通信
*/
let html = renderToString(
<>
{Routers.map((route) => (
))}
>
);
res.send(`
react ssr demo
${html}
`);
});
}
```