# 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 处理的内容 ![](2021-12-23-14-00-37.png) 以上如果加上事件是无法触发的`${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" ``` 事件生效了: ![](2021-12-23-14-36-37.png) ⚠️ 此时控制台报了个警告,我们需要把 ReactDOM.render()替换成 ReactDOM.hydrate() ReactDOM.render()会将服务端已经渲染的内容替换,造成不必要的重渲染 ReactDOM.hydrate()表示把服务端未完成的工作完成,比如事件绑定 ![](2021-12-23-14-41-12.png) # 优化启动流程 对于 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 }); }) } } ``` 数据显示出来了,但是右键查看源码发现数据是空的 ![](2021-12-24-11-32-19.png) 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}
`); }); } ```