# 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状态代码。 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0808/212751_cb83cbca_9130428.png "屏幕截图.png") #### 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 ( ) } // 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