一、ReactSSR相关概念回顾
什么是客户端渲染
CSR:Client Side Rendering
服务器端仅返回 JSON 数据, DATA 和 HTML 在客户端进行渲染
什么是服务器端渲染
SSR:Server Side Rendering
服务器端返回HTML, DATA 和 HTML 在服务器端进行渲染
客户端渲染存在的问题
- 首屏等待时间长, 用户体验差
- 页面结构为空, 不利于 SEO
SPA 应用中服务器端渲染解决的问题
二、实现ReactSSR雏形
项目结构
react-ssr
src 源代码文件夹
client 客户端代码
server 服务器端代码
share 同构代码
创建 Node 服务器
实现 React SSR
- 引入要渲染的 React 组件
- 通过 renderToString 方法将 React 组件转换为 HTML 字符串
- 将结果HTML字符串想到到客户端
renderToString 方法用于将 React 组件转换为 HTML 字符串, 通过 react-dom/server 导入
三、服务器端程序webpack打包配置
webpack 打包配置
问题: Node 环境不支持 ESModule 模块系统, 不支持 JSX 语法
webpack.server.js
const path = require('path');
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base');
const nodeExternals = require('webpack-node-externals');
const config = {
target: 'node',
entry: './src/server/index.js',
output: {
path: path.join(__dirname, 'build'),
filename: 'bundle.js'
},
externals: [nodeExternals()]
}
module.exports = merge(baseConfig, config);
package.json
"scripts": {
"dev:server-build": "webpack --config webpack.server.js --watch",
},
项目启动命令配置
- 配置服务器端打包命令: “dev:server-build”: “webpack --config webpack.server.js --watch”
- 配置服务端启动命令: “dev:server-run”: “nodemon --watch build --exec “node build/bundle.js””
四、为组件元素添加事件
实现思路分析
在客户端对组件进行二次"渲染", 为组件元素附加事件.
客户端二次 “渲染” hydrate
使用 hydrate 方法对组件进行渲染, 为组件元素附加事件.
hydrate 方法在实现渲染的时候, 会复用原本已经存在的 DOM 节点, 减少重新生成节点以及删除原本 DOM 节点的开销.
通过 react-dom 导入 hydrate.
home.js
import React from "react";
import { Link } from 'react-router-dom';
function Home() {
return <div onClick={() => console.log("Hello")}>
Home works
</div>;
}
export default Home;
client.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 { Provider } from "react-redux";
import store from "./createStore";
ReactDOM.hydrate(
<Provider store={store}>
<BrowserRouter>{renderRoutes(routes)}</BrowserRouter>
</Provider>,
document.getElementById("root")
);
webpack.client.js
const path = require("path");
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base');
const config = {
entry: "./src/client/index.js",
output: {
path: path.join(__dirname, "public"),
filename: "bundle.js"
}
};
module.exports = merge(baseConfig, config);
package.json
"scripts": {
"dev:client-build": "webpack --config webpack.client.js --watch"
}
五、优化:合并webpack配置
const path = require("path");
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base');
const config = {
entry: "./src/client/index.js",
output: {
path: path.join(__dirname, "public"),
filename: "bundle.js"
}
};
module.exports = merge(baseConfig, config);
- webpack 配置
打包目的: 转换JSX语法, 转换浏览器不识别的高级 JavaScript 语法
打包目标位置: public 文件夹 - 打包启动命令配置
“dev:client-build”: “webpack --config webpack.client.js --watch”
在响应给客户端的 HTML 代码中添加 script 标签, 请求客户端 JavaScript 打包文件.
服务器端实现静态资源访问
服务器端程序实现静态资源访问功能, 客户端 JavaScript 打包文件会被作为静态资源使用
六、合并项目启动命令
七、优化:服务端打包文件体积优化
问题:在服务器端打包文件中, 包含了 Node 系统模块. 导致打包文件本身体积庞大.
解决:通过 webpack 配置剔除打包文件中的 Node 模块
element-icons.ttf
element-icons.woff
八、代码拆分
将启动服务器代码和渲染代码进行模块化拆分
优化代码组织方式, 渲染 React 组件代码是独立功能, 所以把它从服务器端入口文件中进行抽离.
server/render.js
import React from "react";
import { renderToString } from "react-dom/server";
import { StaticRouter } from "react-router-dom";
import routes from "../share/routes";
import { renderRoutes } from "react-router-config";
import { Provider } from "react-redux";
import serialize from 'serialize-javascript';
export default (req, store) => {
const content = renderToString(
<Provider store={store}>
<StaticRouter location={req.path}>{renderRoutes(routes)}</StaticRouter>
</Provider>
);
const initalState = serialize(store.getState());
return `
<html>
<head>
<title>React SSR</title>
</head>
<body>
<div id="root">${content}</div>
<script>window.INITIAL_STATE = ${initalState} </script>
<script src="bundle.js"></script>
</body>
</html>
`;
};
server/index.js
import app from './http';
import renderer from './renderer';
import createStore from './createStore';
import routes from '../share/routes';
import { matchRoutes } from 'react-router-config';
app.get('*', (req, res) => {
const store = createStore();
// 1. 请求地址 req.path
// 2. 获取到路由配置信息 routes
// 3. 根据请求地址匹配出要渲染的组件的路由对象信息
const promises = matchRoutes(routes, req.path).map(({route}) => {
// 如何才能知道数据什么时候获取完成
if (route.loadData) return route.loadData(store)
})
Promise.all(promises).then(() => {
res.send(renderer(req, store));
})
});
九、实现服务器端路由
实现思路分析
在 React SSR 项目中需要实现两端路由.
客户端路由是用于支持用户通过点击链接的形式跳转页面.
服务器端路由是用于支持用户直接从浏览器地址栏中访问页面.
客户端和服务器端公用一套路由规则
home.js
import React from "react";
import { Link } from 'react-router-dom';
function Home() {
return <div onClick={() => console.log("Hello")}>
Home works
<Link to="/list">jump to list</Link>
</div>;
}
export default Home;
router.js
import Home from '../share/pages/Home';
import List from '../share/pages/List';
export default [{
path: '/',
component: Home,
exact: true
}, {
path: '/list',
...List
}]
server/index.js
import app from './http';
import renderer from './renderer';
import createStore from './createStore';
import routes from '../share/routes';
import { matchRoutes } from 'react-router-config';
app.get('*', (req, res) => {
const store = createStore();
// 1. 请求地址 req.path
// 2. 获取到路由配置信息 routes
// 3. 根据请求地址匹配出要渲染的组件的路由对象信息
const promises = matchRoutes(routes, req.path).map(({route}) => {
// 如何才能知道数据什么时候获取完成
if (route.loadData) return route.loadData(store)
})
Promise.all(promises).then(() => {
res.send(renderer(req, store));
})
});
server/render.js
import React from "react";
import { renderToString } from "react-dom/server";
import { StaticRouter } from "react-router-dom";
import routes from "../share/routes";
import { renderRoutes } from "react-router-config";
import { Provider } from "react-redux";
import serialize from 'serialize-javascript';
export default (req, store) => {
const content = renderToString(
<Provider store={store}>
<StaticRouter location={req.path}>{renderRoutes(routes)}</StaticRouter>
</Provider>
);
const initalState = serialize(store.getState());
return `
<html>
<head>
<title>React SSR</title>
</head>
<body>
<div id="root">${content}</div>
<script>window.INITIAL_STATE = ${initalState} </script>
<script src="bundle.js"></script>
</body>
</html>
`;
};
十、实现客户端路由
client/index.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 { Provider } from "react-redux";
import store from "./createStore";
ReactDOM.hydrate(
<Provider store={store}>
<BrowserRouter>{renderRoutes(routes)}</BrowserRouter>
</Provider>,
document.getElementById("root")
);
home.js
import React from "react";
import { Link } from 'react-router-dom';
function Home() {
return <div onClick={() => console.log("Hello")}>
Home works
<Link to="/list">jump to list</Link>
</div>;
}
export default Home;
十一、实现客户端redux
client/createsStore.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from '../share/store/reducers';
export default () => createStore(reducer, {}, applyMiddleware(thunk))
client/index.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 { Provider } from "react-redux";
import store from "./createStore";
ReactDOM.hydrate(
<Provider store={store}>
<BrowserRouter>{renderRoutes(routes)}</BrowserRouter>
</Provider>,
document.getElementById("root")
);
store/action/user.action.js
import axios from 'axios';
export const SAVE_USER = 'save_user';
// 发送请求 获取用户列表数据
export const fetchUser = () => async dispatch => {
// let response = await axios.get('https://jsonplaceholder.typicode.com/users');
let response = {
data: [{id: 1, name: "</script><script>alert(1)</script>"}]
}
dispatch({
type: SAVE_USER,
payload: response
})
}
store/action/user.reducer.js
import { SAVE_USER } from "../actions/user.action";
export default (state = [], action) => {
switch(action.type) {
case SAVE_USER:
return action.payload.data;
default:
return state;
}
}
store/action/user.index.js
import { combineReducers } from 'redux';
import userReducer from './user.reducer';
// {user: []}
export default combineReducers({
user: userReducer
});
遇到问题:浏览器不支持异步函数
webpack配置下
十二、实现服务器端redux
1、
server/createStore.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from '../share/store/reducers';
export default () => createStore(reducer, {}, applyMiddleware(thunk))
server/index.js
import app from './http';
import renderer from './renderer';
import createStore from './createStore';
import routes from '../share/routes';
import { matchRoutes } from 'react-router-config';
app.get('*', (req, res) => {
const store = createStore();
// 1. 请求地址 req.path
// 2. 获取到路由配置信息 routes
// 3. 根据请求地址匹配出要渲染的组件的路由对象信息
const promises = matchRoutes(routes, req.path).map(({route}) => {
// 如何才能知道数据什么时候获取完成
if (route.loadData) return route.loadData(store)
})
Promise.all(promises).then(() => {
res.send(renderer(req, store));
})
});
server/render.js
import React from "react";
import { renderToString } from "react-dom/server";
import { StaticRouter } from "react-router-dom";
import routes from "../share/routes";
import { renderRoutes } from "react-router-config";
import { Provider } from "react-redux";
import serialize from 'serialize-javascript';
export default (req, store) => {
const content = renderToString(
<Provider store={store}>
<StaticRouter location={req.path}>{renderRoutes(routes)}</StaticRouter>
</Provider>
);
const initalState = serialize(store.getState());
return `
<html>
<head>
<title>React SSR</title>
</head>
<body>
<div id="root">${content}</div>
<script>window.INITIAL_STATE = ${initalState} </script>
<script src="bundle.js"></script>
</body>
</html>
`;
};