前言
store采用的是mobx作为状态管理,mobx使用起来确实是比redux简单,没有那么多繁琐的配置。router是react-router4.2。其实实现简单的服务端渲染并不难,react服务端渲染的简单配置这里之前有简单介绍了一下。我个人认为主要的难点是在实现store和router在服务端和客户端的同步。
服务端渲染的路由
在实现服务端渲染的路由比较简单,因为官方都给我们提供了api。
BrowserRouter
在客户端渲染的时候,我们一般会采用BrowserRouter作为前端路由。使用 HTML5 History API(pushState,replaceState 和 popstate 事件)的 <Router>
来保持 UI 与 URL 同步。
StaticRouter
服务器端渲染是一种无状态的渲染。基本的思路是,将<BrowserRouter>
替换为无状态的<StaticRouter>
。将服务器上接收到的URL传递给路由用来匹配,同时支持传入context
特性。
// client
<BrowserRouter>
<App/>
</BrowserRouter>
// server (not the complete story)
<StaticRouter
location={req.url}
context={context}
>
<App/>
</StaticRouter>
当在浏览器上渲染一个<Redirect>
时,浏览器历史记录会改变状态,同时将屏幕更新。在静态的服务器环境中,无法直接更改应用程序的状态。在这种情况下,可以在context
特性中标记要渲染的结果。如果出现了context.url
,就说明应用程序需要重定向。从服务器端发送一个恰当的重定向链接即可。
<StaticRouter context={routerContext} location={url}>
<App />
</StaticRouter>
const routerContext = {}
const stores = createStoreMap()
const App = createApp(stores, routerContext, req.url)
asyncBootstrap(App).then(() => {
//bootstrap异步方法执行完毕后,执行完余下的渲染方法后,执行此回调。此时的App就是已经插好值的
if (routerContext.url) {
res.status(302).setHeader('Location', routerContext.url);
res.end()
return
}
store的同步
我个人认为store的同步是服务端渲染的难点之一。
异步请求
我们在做服务端渲染的时候,有一些服务器请求到的数据是需要在首屏就可以看到的。那么这个请求的操作最好就是在服务端渲染的时候就拿到了,而不是来到客户端渲染的时候才进行请求。
1、使用react-async-bootstrapper这个库,把我们服务端渲染的组件包装起来,先执行异步方法,执行完毕后再进行余下的ssr渲染。我们在组件内部定义一个bootstrap()的异步方法,这个就代表我们先要执行的异步操作。
class App extends React.Component {
bootstrap() {
return new Promise((resolve) => {
setTimeout(() => {
this.props.appState.count = 3
resolve(true)
})
})
}
render(){
return(
<div className="app">
<Link to="/">首页</Link>
<Link to="/detail">详情页</Link>
<Routes />
</div>
)
}
}
const asyncBootstrap = require('react-async-bootstrapper')
asyncBootstrap(App).then(() => {
//bootstrap异步方法执行完毕后,执行完余下的渲染方法后,执行此回调。此时的App就是已经插好值的
if (routerContext.url) {
res.status(302).setHeader('Location', routerContext.url);
res.end()
return
}
const helmet = Helmet.rewind()
const state = getStoreState(stores)
const content = ReactDomServer.renderToString(App);
//res.send(template.replace('<!--app-->', content))
// console.log('helmet', new helmet())
console.log('initialState', state)
const html = ejs.render(template, {
appString: content,
initialState: serialize(state),
meta: helmet.meta.toString(),
title: helmet.title.toString(),
style: helmet.style.toString(),
link: helmet.link.toString(),
})
res.send(html)
resolve()
})
2、对store进行改造
App-state.js
export default class AppState {
constructor({count,name} = {count:0,name:'bb'}){
this.count=count
this.name=name
}
@observable count
@observable name
@computed get msg(){
return `${this.name} say count is ${this.count}`
}
@action add() {
this.count += 1
}
@action changeName(name){
this.name = name
}
//此方法用于ssr服务端渲染时调用,获取当前服务端渲染时的store状态,注入到客户端,使得服务端和客户端的store可以同步
toJson(){
return {
count:this.count,
name:this.name
}
}
}
export const AppState = AppStateClass
export default {
AppState,
}
//此函数专门用于SSR,
export const createStoreMap = () => {
return {
appState: new AppState(),
}
}
把app-state封装成一个类,方便把服务端渲染时的store获取到,注入给客户端。
3、获取服务端渲染后的store
const getStoreState = (stores) => {
return Object.keys(stores).reduce((result, storeName) => {
result[storeName] = stores[storeName].toJson()
return result
}, {})
}
//bundle是webpack对服务端渲染打包后的代码,再获取里面的createStoreMap获取到当前store实例
const createStoreMap = bundle.createStoreMap
const state = getStoreState(stores)
//这样,state获取到就是通过服务端渲染时的store
4、使用模板引擎,把服务端获取到的store注入到客户端,实现同步
这里使用的是ejs模板引擎。
首先,要对客户端的入口文件进行修改,使得客户端是从全局变量当中获取到store的:
const root = document.getElementById('root');
const initialState = window.__INITIAL__STATE__ || {}
const render = Component => {
const renderMethod = module.hot ? ReactDom.render : ReactDom.hydrate;
ReactDom.hydrate(
<AppContainer>
<Provider appState={new AppState(initialState.appState)}>
<BrowserRouter>
<Component />
</BrowserRouter>
</Provider>
</AppContainer>
,root);
}
render(App);
把服务端渲染后的代码转化为html字符串后,使用ejs模板引擎,把html内容、initialState(就是服务端的store)插入到html模板。
const state = getStoreState(stores)
const content = ReactDomServer.renderToString(App);
const html = ejs.render(template, {
appString: content,
initialState: serialize(state),
meta: helmet.meta.toString(),
title: helmet.title.toString(),
style: helmet.style.toString(),
link: helmet.link.toString(),
})
res.send(html)
html模板要改为ejs的模板
server.template.ejs
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<%%- meta %>
<%%- title %>
<%%- link %>
<%%- style %>
</head>
<body>
<div id="root">
<%%- appString %>
</div>
<script>
window.__INITIAL__STATE__ = <%%- initialState %>
</script>
</body>
</html>
到此,比较完整的服务端渲染就完成了~