关于React服务端渲染SSR那点事
目录
一、服务端渲染基础
二、react服务端渲染原理
三、同构
四、在ssr中引入路由机制
五、异步数据服务端渲染
六、数据的注水和脱水
七、其他
一、 服务端渲染基础
什么是csr,什么是ssr?
从上图我们可以发现,客户端渲染主要经历了4个阶段,首先从远程服务器中下载了HTML文档,解析文档并下载相应的JS文件,然后在浏览器中运行react代码,最后向用户展示页面这样一个大致过程。
客户端渲染带来的优势:
- 前后端交互通过ajax和json进行,简单方便
客户端渲染带来的劣势:
- 客户端渲染首屏加载速度慢
- seo的问题,不利于爬虫获取有利数据
那么这个时候,我们推出了ssr这个概念,ssr即在服务器中就渲染好了需要展示给用户的首屏页面模板,浏览器拿到模板进行渲染
二、react服务端渲染原理
使用react编码的代码,csr渲染流程
那么,React代码在浏览器上执行,消耗的是用户浏览器的性能
使用react编码的代码,ssr渲染流程
React代码在服务器上执行,消耗的是服务器端的性能
三、同构
同构的概念
一套React代码,在服务器端上执行一次,在客户端再执行一次
我们分析一下我们常规写法,将一个页面需要的数据请求action放在生命周期componentDidMout中进行请求,会如何
- 首先服务器会接收到一个请求,这个时候的store是空的,服务器不会去执行componentDidMount,所以我们需要的数据获取不到,然后将模板返回给客户端。
- 客户端代码运行,这个时候store依然是空的,页面需要的数据依然没有展示。
- 客户端执行服务器端不会执行的componentDidMount生命周期钩子函数,然后获取到了相应的数据,这个时候store被更新
- 客户端渲染出了store中的数据
细心的网友会发现,根据上面的分析,我们可以很容易得知(可以通过查看源码),服务端返回的模板中是不带有数据的。如果我们禁止chrome中的JavaScript,我们页面就始终获取不到数据,那么我们该如何解决呢?不慌,我们一步一步慢慢道来:
uploading-image-992120.png
四、在ssr中引入路由机制
1. 客户端路由流程 browserRouter
<Provider store={store}>
<BrowserRouter>
<div>
{
Routes
}
</div>
</BrowserRouter>
</Provider>
2. 服务端路由流程 staticRouter
我们在上图的第一条,服务器返回HTML之前加入服务端路由做做文章
2.1 首先我们在写一个简单的通用路由Routes.js
// 配置路由条目 exact严格访问该路径
import React from 'react';
import { Route } from 'react-router-dom';
import Home from './containers/Home';
export default (
<div>
<Route path='/' exact component={Home} ></Route>
</div>
)
2.2 配置客户端路由
import React from 'react';
import ReactDom from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import Routes from '../Routes';
const App = () => {
return (
<BrowserRouter>
{Routes}
</BrowserRouter>
)
}
2.3 配置服务端路由
import express from 'express'; // 由于webpack打包转码 ,我们可以使用esModule语法,也可以使用commonJS语法require('express');
import React from 'react';
import { StaticRouter } from 'react-router-dom';
import { renderToString } from 'react-dom/server';
import Routes from '../Routes';
const app = express();
app.use(express.static('public'));// 使用express的中间件,开放服务器上的静态资源
// 在服务端StaticRouter中,它不像浏览器端BrowserRouter那样能自动获取到url,因此需要设置一个localtion属性,其次是需要一个context,用于传递数据
app.get('/', function(req, res) {
const content = renderToString((
<StaticRouter location={req.path} context={{}}>
{Routes}
</StaticRouter>
));
res.send(
`<html>
<head>ssr</head>
<body>
<div id='root'>${content}</div>
<script src='/index.js'></script>
</body>
</html> `
);
});
var server = app.listen(8888)
多页面跳转
上面我们实现了一个简单的服务端路由,对于多页面跳转只需要配置对应路由即可
3.1 配置对应的路由
<div>
<Route path='/' exact component={Home} ></Route>
<Route path='/login' exact component={Login} ></Route>
</div>
3.2 在对应页面使用路由,相当于a标签
<Link to='/'>Home</Link>
要注意在服务端,每个用户请求都生成一个store,而不能使用同一个store
原有
store = createStore(thunk, applyMiddle(thunk));
改为
getStore() = () => }{
return createStore(thunk, applyMiddle(thunk));
}
五、异步数据服务器渲染
我们现在已经加入了路由了,接下来我们回到之前谈到的如何让服务器端返回的模板中带有数据
1.我们对原有路由进行改造
// 配置路由条目 exact严格访问该路径
import React from 'react';
import { Route } from 'react-router-dom';
import Home from './containers/Home';
import Login from './containers/Login';
import App from './App';
export default [{
path: '/',
component: App,
routes: [
{
path: '/',
component: Home,
exact: true, // 严格匹配
loadData: Home.loadData, // Home加载时,使用这个方法加载数据
key: 'Home',
}, {
path: '/login',
component: Login,
exact: true,
key: 'Login'
}
]
}]
2. 然后我们可以通过数据遍历的方式,将数组路由渲染出来
import React from 'react';
import { StaticRouter, Route } from 'react-router-dom';
import { renderToString } from 'react-dom/server';
import { Provider } from 'react-redux';
import { matchRoutes } from 'react-router-config';
export const render = (req, store, Routes) => {
// 根据路由的路径,来往store中添加数据
// 让matchRoutes里面所有的组件,对应的loadData执行一次
const matchRoutes = matchRoutes(Routes, req.path);
const content = renderToString((
<Provider store={store}>
<StaticRouter location={req.path} context={{}}>
<div>
{
matchRoutes.map(route => {
return <Route {...route} />
})
}
</div>
</StaticRouter>
</Provider>
));
return (
`
<html>
<head>ssr</head>
<body>
<div id='root'>${content}</div>
<script src='/index.js'></script>
</body>
</html>
`
)
}
export default render;
3. 细心的网友会发现,上述代码路由数组中,有个loadData:Home.loadData这个是什么鬼?
当我加载显示Home这个组件时,我希望调用Home.loadData这个方法,提前获取到必要的异步数据装载,然后在做服务端渲染,把页面返回到客户端
// Home组件代码
import React, { Component } from 'react';
import Header from '../../components/Header';
import { connect } from 'react-redux';
import * as HomeActions from './store/actions';
class Home extends Component {
render() {
return <div>
<div>111</div>
<p>this is {this.props.name} </p>
<div>{this.props.list}</div>
<button onClick={() => alert(1)}>click me </button>
</div>
}
// 在服务器端不执行的
componentDidMount() {
if (!this.props.newList) {
this.props.getHomeList(false);
}
}
}
Home.loadData = (store) => {
// 这个函数,负责在服务端渲染之前,把数据加载好
return store.dispatch(HomeActions.getListInfo(true));
}
const mapStateToProps = (state) => ({
name: state.home.name,
list: state.home.newList
})
const mapDispatchToProps = (dispatch) => ({
getHomeList() {
dispatch(HomeActions.getListInfo())
}
})
export default connect(mapStateToProps, mapDispatchToProps)(Home);
4. 在此过程中需要注意两个问题
4.1 服务端多级路由问题
所以,我们在服务器端代码中,使用了matchRoutes替代matchPath.
原因分析:因为MatchPath只能匹配一层路由,如果匹配/Home/ttt,只能拿到外层/Home,需要解决这个问题,使用react-router-config的matchRoutes,可以都匹配出来,分别获取对应的数据
4.2 需要注意的是,在routes数组遍历的外层需要一个div包裹一下,否则会报错
六、数据的注水和脱水
上面的操作使得我们在服务端也拿到了对应的数据,好像服务端拿到了对应的数据,客户端也拿到了对应的数据,好像就结束了的样子?
细心的网友又发现了问题,怎么有数据闪屏问题,我们禁止chrome中的JavaScript,服务端返回的模板确实携带了数据,怎么打开了JavaScript后页面有一定时间的没有数据,直到客户端请求的数据返回?
首先明确一个概念是,服务端渲染指的是首屏的时候进行数据的服务端渲染。
代码会先在服务端执行一遍,然后拿到了数据,保证了用户看到了首屏的数据,源码中也存在对应的结构。然后到了客户端又使用js渲染一次,获取到数据。那么由于客户端再执行一次的开端的时候,也创建了一个store,并且在数据请求回来以前是个空的store,就会造成闪屏一下,直到数据请求回来渲染出来。这就是数据的脱水。
解决办法
在服务端,将服务端拿到的数据绑定到window.context上,客户端在创建时store的时候,拿到window.context上的defaultState,然后作为createStore的第二个参数传入进去。这样去保证客户端渲染最初,store不是一个全新的store。
1. 服务端代码
import React from 'react';
import { StaticRouter, Route } from 'react-router-dom';
import { renderToString } from 'react-dom/server';
import { Provider } from 'react-redux';
import { renderRoutes } from 'react-router-config';
export const render = (req, store, Routes) => {
const content = renderToString((
<Provider store={store}>
<StaticRouter location={req.path} context={{}}>
<div>
{
// Routes.map(route => {
// return <Route {...route} />
// })
renderRoutes(Routes)
}
</div>
</StaticRouter>
</Provider>
));
return (
`
<html>
<head>ssr</head>
<body>
<div id='root'>${content}</div>
<script>
// 将数据绑定到window.context中
window.context = {
state: ${JSON.stringify(store.getState())}
}
</script>
<script src='/index.js'></script>
</body>
</html>
`
)
}
export default render;
2. 然后改造一下创建store
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import { reducer as homeReducer } from '../containers/Home/store';
import clientRequest from '../client/request';
import serverRequest from '../server/request';
const reducers = combineReducers({
home: homeReducer
});
export const getStore = (req) => {
// 改变服务端内容,就一定要用serverRequest
return createStore(reducers, applyMiddleware(thunk.withExtraArgument(serverRequest(req))))
}
export const getClientStore = () => {
// 改变客户端内容,就一定要使用clientRequest
const defaultState = window.context.state;
return createStore(reducers, defaultState, applyMiddleware(thunk.withExtraArgument(clientRequest)))
}
七、其他
由于在服务端执行一次,客户端又执行一次,一个比较折中优化的办法是,在客户端的componentDidMount中判断下,要请求的数据是否已经存在了,如果还没有就发送一个请求,如果有就不请求。
看到这里,关于ssr先暂到此处,后续会再更新一篇关于在ssr中引入中间层的概念和应用,关于如何处理服务端请求数据的时候,携带cookie,以及如何借助context属性返回一个待404状态码的not Found页面