# e-react-ssr-mobx
**Repository Path**: ymcdhr/e-react-ssr-mobx
## Basic Information
- **Project Name**: e-react-ssr-mobx
- **Description**: React + SSR 流程与 示例方案
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2021-08-07
- **Last Updated**: 2021-11-15
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# React SSR + Mobx + Koa + React-router
### 1、项目说明
实现一个简单的服务端渲染 SSR 功能,并没有做复杂的 WWebpack 配置:服务端预渲染 SSR + 客户端接管 + 共用路由规则、Mobx Store状态数据
### 2、项目使用
#### 安装依赖
```
> npm install
```
#### 开发调试
```
> npm run dev
```
#### 项目配置
1. Webpack 简单配置:参考源码
- 公用配置
- 服务端配置
- 客户端配置
2. Npm Scripts:
```js
"scripts": {
"dev": "npm-run-all --parallel dev:*",
"dev:server-run": "nodemon --watch server --exec \"node server/bundle.js\"",
"dev:server-build": "webpack --config webpack.server.js --watch",
"dev:server-db": "json-server --watch ./db.json --port 3001",
"dev:client-build": "webpack --config webpack.client.js --watch"
},
```
#### 依赖说明
```js
// 开发依赖
// 开发启动服务工具
"nodemon": "^2.0.12",
"npm-run-all": "^4.1.5",
// babel
"@babel/cli": "^7.14.8",
"@babel/core": "^7.15.0",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.14.5",
"babel-loader": "^8.2.2",
// Webpack
"webpack": "^5.49.0",
"webpack-cli": "^4.7.2",
"webpack-merge": "^5.8.0",
"webpack-node-externals": "^3.0.0"
```
```js
// 生产依赖
// koa
"@koa/router": "^10.1.0",
"koa": "^2.13.1",
"koa-static": "^5.0.0",
// mobx
"mobx": "^6.3.2",
"mobx-react": "^7.2.0",
// react
"react": "^17.0.2",
"react-dom": "^17.0.2",
// xss工具
"serialize-javascript": "^6.0.0"
```
### 3、SSR原理
#### 3.1 CSR存在的问题
1. 首屏等待时间长,用户体验差
2. 页面结构为空,不利于SEO
#### 3.2 SSR具备的优势
同构指的是客户端和服务端共用一份代码,最大程度实现代码复用。
浏览器发起请求 => 服务端接受请求 => 服务端渲染 html => 服务端发送 html => 客户端接管页面
#### 3.3 SSR流程
1. 服务端启动HTTP服务;
2. 服务端通过renderToString方法渲染组件(转换为HTML字符串),返回给客户端;
3. 客户端拿到HTML文件进行解析渲染,然后下载脚本接管渲染。
4. 注意服务端渲染和客户端渲染共用一部分路由代码和Store状态代码。

#### 3.4 SSR公用逻辑:
1. 共用页面组件,包括入口组件也差不多;
2. 共用Store代码,且都需要使用Mobx Store;
3. 共用路由规则代码,且都需要使用React-router-dom路由配置;
#### 3.5 SSR不同逻辑:
1. 服务端需要启动 HTTP 服务,除了渲染页面还需要发挥静态资源文件;
2. 服务端需要利用Store预取数据,还需要将它发送给客户端;客户端使用已有数据避免重复获取;
3. 服务端预取数据需要在Store构造函数里面执行;客户端预取数据可以放到组件生命周期中,还需要注意在客户端时Store的构造函数也会执行,最好判断一下避免重复预取数据。
4. 服务端路由使用StaticRouter;客户端使用BrowserRouter;
5. 服务端路由是在刷新页面的时候返回整个页面;客户端是点击跳转时走的SPA页面路由。
### 4、服务器渲染原理
#### Koa提供HTTP服务
```js
// serve/http.js
const Koa = require('koa')
// 创建HTTP服务
const app = new Koa()
// 启动HTTP服务
app.listen(3000, () => {
console.log('http://localhost:3000')
})
export default app
```
#### Koa渲染React组件 => 返回给客户端
1. renderToString()将React组件渲染成Html;
2. 如果渲染量特别大,可以使用renderToNodeStream() + pipe()采用流式渲染;
```js
import { renderToString } from "react-dom/server";
import Home from '../share/pages/Home';
const Router = require('@koa/router');
const router = new Router();
// ...
const content = renderToString()
// koa 路由插件:@koa/router
router.get('/', ctx => {
ctx.body = `
React SSR
${content}
`;
})
```
#### Koa服务端路由:刷新浏览器时使用服务端路由,直接返回页面
1. 路由匹配:分为koa路由和react服务端路由;
- koa路由:@koa/router
- koa路由:直接全匹配就行,将具体路由交给react-router-dom;
- koa路由:因为koa的路由和express不同,不太方便全匹配,所以直接使用app.use替换
- react路由:使用了react-router-dom、react-router-config两个插件;根据配置的路由规则进行匹配URL,规则和客户端共用一份代码。具体参考代码:server/renderer.js
- react路由:服务端使用StaticRouter,客户端使用BrowserRouter
- react路由:处理路由规则renderRoutes
2. 静态资源托管:koa-static,参考代码:server/index.js
3. 错误处理:监听'error'事件,参考代码:server/index.js
```js
// server/index.js
import app from './http';
import renderer from './renderer';
const serve = require('koa-static')
const path = require('path')
const Router = require('@koa/router')
const router = new Router()
import React from "react";
// 因为koa的路由不好全匹配,就不用了
app.use(async (ctx, next)=>{
ctx.body = renderer(ctx)
await next()
})
// 静态资源托管;需要放到路由后面。
app.use(serve(path.join(__dirname, '../client')))
// 错误处理
app.on('error', (err, ctx) => {
console.error('server error:')
console.error(err)
});
```
```
// server/renderer.js
import React from "react";
import { renderToString } from "react-dom/server";
import { StaticRouter } from "react-router-dom";
import { renderRoutes } from "react-router-config";
import routes from "../share/routes";
import serialize from 'serialize-javascript';
import Home from '../share/pages/Home';
import { RootStoreProvider } from "../share/store"
import { rootStore } from "../share/store"
export default (cxt) => {
console.log("cxt.request.path:",cxt.request.path)
// 1、renderToString()的参数是组件或者DOM字符串;
// 2、如果渲染量特别大,可以使用renderToNodeStream采用流式渲染
const content = renderToString(
{renderRoutes(routes)}
)
// 1、将服务端初始状态传递给客户端
// 2、serialize 转义恶意代码,防止XSS攻击
const initalState = serialize(rootStore);
return `
React SSR
${content}
`;
};
```
#### Koa服务端使用Mobx状态
1. 创建状态(observer、action、reducer、computed都在同一个文件)
- 异步数据获取:(1)放到构造函数里面,服务端渲染时会执行;(2)如果只想客户端渲染时执行,那就放到组件的生命周期里面调用;
- 异步数据获取:有几种方法,使用flow类型的yield写法更简单(类似于Generate函数);
- 状态可以有子状态,例如:users数组的数据类型是UserStore;
- 注意状态的初始化,例如:如果创建实例时有初始值应该要赋值给它;
- 注意action中绑定this指向:使用action.bound
```js
// store/UserListStore.js
import { observable, action, flow, makeObservable } from "mobx"
import axios from 'axios'
import UserStore from "./UserStore"
export default class UserListStore {
constructor(users) {
// 已有数据时的初始化
this.users = users ? users.map(user => new UserStore(user)) : []
// 必须放到成员声明后面
makeObservable(this, {
users: observable,
addUser: action.bound,
delUser: action.bound,
loadData: flow.bound
})
// 如果服务端使用异步Mobx预取数据就放到这里执行
// 如果客户端使用异步Mobx取数据就放到组件useEffect里面调用
this.users.length === 0 && this.loadData()
}
addUser(user) {
this.users.push(new UserStore(user))
}
delUser() {
this.users.pop()
}
// flow 替代 async/await 写法进行异步请求
*loadData() {
let response = yield axios.get("http://localhost:3001/users")
response.data.map(user => {
this.users.push(new UserStore(user))
})
}
}
```
2. 拆分Store:将多个Store组合到RootStore
- 通过context方式将rootStore注入到Provider;
- 通过useRootStore自定义Hook导出Store,在组件中使用;
- 服务端渲染时导出rootStore,客户端渲染时可以接受该Store;
```js
import React from "react"
import UserListStore from "./UserListStore"
const { createContext, useContext } = React
// 1、总的RootStore,在构造函数里面添加子Store作为成员
// 1.1、注意初始化Store,如果服务端传递过来了数据,用该数据初始化
class RootStore {
constructor() {
const initialStore = typeof window !=="undefined" && window.INITIAL_STATE
const users = initialStore && initialStore.userListStore.users
this.userListStore = new UserListStore(users)
}
}
// 2、通过context方式将rootStore传递给子元素
// 2.1、导出Provider给入口
// 2.2、导出Store给组件
export const rootStore = new RootStore()
const RootStoreContext = createContext()
export const RootStoreProvider = ({ children }) => {
return (
{children}
)
}
export const useRootStore = () => {
return useContext(RootStoreContext)
}
```
3. 在React入口包裹Provider
```js
import { RootStoreProvider } from "../share/store"
const content = renderToString(
{renderRoutes(routes)}
)
```
4. 在组件中使用Store
- 引入Store,解构根Sotre获取到子Store;
- 使用observer(组件)包裹组件;
```js
import React, { useEffect } from 'react'
import { useRootStore } from "../store"
import { observer } from "mobx-react"
function Users() {
// 1、从根Store中获取子Store
const { userListStore } = useRootStore()
const { users = [], loadData } = userListStore
// 如果想用服务端渲染就放到Store里面调用
// 如果想用客户端渲染去做异步Mobx请求就这样用
// useEffect(() => {
// loadData()
// }, [])
return (
{users.map((user, index) => (
-
{user.name}: {user.age}
)
)}
)
}
// 2、observer包裹组件
export default observer(Users)
```
5. 服务端将Sotore同步给客户端
- 在服务端渲染之前将Store传递给客户端;
- 客户端拿到全局变量初始化Store,参见上面代码;
```
import { rootStore } from "../share/store"
import serialize from 'serialize-javascript';
// 1、将服务端初始状态传递给客户端
// 2、serialize 转义恶意代码,防止XSS攻击
const initalState = serialize(rootStore);
return `
React SSR
${content}
`;
```
### 5、客户端渲染原理
#### 客户端渲染组件
- hydrate和render有区别?据说hydrate会重用dom元素?
- Mobx Store:共用服务端代码,预取数据不同;需要考虑接受服务端的Store进行初始化;
- Router:共用路由规则,BrowserRouter 不同;
```js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import { renderRoutes } from "react-router-config";
import routes from "../share/routes";
import { RootStoreProvider } from "../share/store"
ReactDOM.hydrate(
{renderRoutes(routes)}
,
document.getElementById("root")
);
```
#### 客户端路由(类似服务端,共用路由规则代码):点击Link跳转,客户端走SPA路由
#### 客户端使用Mobx状态(类似服务端,共用Store代码)
1. 创建状态(observer、action、reducer、computed都在同一个文件)
2. 拆分Store:将多个Store组合到RootStore
3. 在React入口包裹Provider
4. 在组件中使用Store