一、服务端渲染的好处!
- 有利于SEO;
- 首屏渲染更快,移除了了加载js的时间;
- 客户端和服务端公用一套代码(同构),更易于维护;
二、react服务端渲染的思路
1、服务端
- koa2框架搭建后端服务;
- babel支持解析es6语法;
- koa-router后端路由;
- 承载react组件的字符模板;
- react组件在服务端注入数据后,转化成字符串吐给浏览器渲染;
<html>
<head></head>
<body>
<div id="root" dangerouslySetInnerHTML={{__html:content}}></div>
</body>
</html>
2、客户端
- react组件;
- react-router前端路由;
- assets静态资源;
三、项目结构
四、react服务端渲染几大问题
1、服务端如何渲染组件;
2、数据如何由服务端同步到客户端;
3、数据到达客户端如何通过前端路由分配到组件中;
4、如何做到前端路由和后端路由同步;
5、如何拆分js和css,并且在切换页面的时候加载相对应的js和css;
6、如何做到服务端渲染和前端渲染随意切换;
7、做到服务端渲染和前端渲染可随意切换后,如何保证服务端和前端数据保持同步;
五、详细介绍(主要介绍以上问题解决方案)
1、服务端如何渲染组件;
服务端在路由中异步的请求api得到数据,这时候需要将数据在服务端注入到react组件中,然后render给浏览器渲染;
app.get('/index', (ctx, next)=> {
const props = {name:'tongshuo', age:'27', sex:'man', workin:'优信二手车'};
ctx.body = renderToString(<Index {...props} />);
})
这样写是可以渲染出来Index页面的,但是问题也随之而来了,这么写渲染出一个特定的组件(页面)没有问题!
但是,如果我不想每次写新页面的时候都去写死相应的组件渲染,因为前端路由可以自动根据url来渲染响应的组件,怎么把这个方案搬到后端来呢?
这个问题我首先想到了redux,但是个人介于redux的写法极其变态,最后决定自己写个方法来解决,下面是我的思路:
- 首先明确需要做什么?——我需要把数据挂载到全局,让每一个组件都能获取到;
- 这让我想到了react的一个不稳定给的api——
context
; - 这个api在react16中已经被单独抽离出一个叫
prop-types
的库;
所以<Provider />
组件长这样:
import React from 'react';
import PropTypes from 'prop-types';
export default class Provider extends React.Component {
constructor (props,context) {
super(props, context);
}
getChildContext () {
return {
store:this.props.store
}
}
render () {
return (
<div>
{this.props.children}
</div>
)
}
}
Provider.childContextTypes = {
store: PropTypes.object
}
那么这个问题就解决掉了,我们可以这样写伪代码:
app.get('/index', (ctx, next)=> {
const props = {name:'tongshuo', age:'27', sex:'man', workin:'优信二手车'};
ctx.body = renderToString(<Provider store={props}><Router /></Provider>);
})
ok!到了这一步,数据已经可以在服务端传入到组件中了,并且通过renderToString方法转化成字符串,到目前我们能做到了根据前端路由来自动渲染静态组件,注意是静态组件,静态组件,静态组件。重要的事情说三遍!
2、那么接下来,数据如何由服务端同步到客户端???
这个时候用到了上面提到的,承载react组件的字符串模板了。我们对上面的字符串模板进行一下升级,下面是我的思路:
- 可以在服务端生成字符串模板之前,创建一个script标签;
- 在script标签中定义一个全局标量,
var staticProps = {props}
,将api接口返回的数据赋值给这个全局变量; - 将这个script标签插入到字符串模板中,那么数据在前端就完全可以通过
window.staticProps
来获取。
<html>
<head></head>
<body>
<div id="root" dangerouslySetInnerHTML={{__html:content}}></div>
<script>
var staticProps = {name:'tongshuo', age:'27', sex:'man', workin:'优信二手车'}
</script>
</body>
</html>
这样我们在客户端就能够获取到在后端路由中经过请求api返回的数据,这样实现了前后端的数据同步。
说到这里,有人肯定存在一个疑问,这个疑问将引出一连串的问题:
为什么要把数据同步到前端,页面已经在后端转成字符串吐给浏览器了,浏览器此时已经能渲染出页面了,为什么还要把数据从后端透传给浏览器呢?
- 此时浏览器确实能解析后端返回的字符串,渲染成页面,但是上面已经提到,此时的页面是静态页面,所谓静态页面是没有js事件的,也就是说没有js逻辑的页面;
那么我们想要在页面上加上js逻辑,应该怎么做?
- 由于应用react并且没有用到redux将V和C分离,所以我们的js中就包含V和C;
- 我们应用webpack将一个页面中的js代码打包成一个js文件,并且提取公共代码;
- 通过后端路由将该页面的js代码,写在字符串模板中,吐给浏览器;
代码如下:
<html>
<head></head>
<body>
<div id="root" dangerouslySetInnerHTML={{__html:content}}></div>
<script>
var staticProps = {name:'tongshuo', age:'27', sex:'man', workin:'优信二手车'}
</script>
<script src='./client/vendor.js' ></script>
<script src='./client/index.js' ></script>
</body>
</html>
这样在浏览器加载页面的时候,会去请求相应的js文件,因为页面中加载的js是react代码,并且需要初始数据跟在后端将react组件转换成字符串时候传进组件的数据保持一致,否则页面将报错,如果数据一致,那么渲染出的页面有了js代码,将不再是静态的。
到这一步,我们就能解释为什么要把数据从后端透传给浏览器了!
3、数据到达客户端如何通过前端路由分配到组件中;
在上一步中,我们做到在客户端能拿到数据了,并且也知道数据在客户端是做什么用的。那么接下来,我们要做的是将数据注入到组件中。使得浏览器在解析react代码的时候,能够顺利运行。
下面是我的思路:
- 肯定是在加载完页面之后才能获取到window对象,才能获取到数据;
- 前端路由包含所有组件,那么数据一定是通过路由组件传递到业务组件中;
- 当页面渲染时,前端路由会渲染与地址匹配的页面组件;
- 但是react-router并不支持从外向内传递数据。
- 通过以上几点,要把数据注入到组件中,使我想到了react的上下文
context
; - 所以使用
<Provider></Provider>
组件包裹路由组件,把数据挂载在react全局,跳过路由传递数据,也能把数据传入组件中;
下面是代码:
前端路由react-router
const Root = () => {
return (
<Router history={history} >
<Route path="/index" component={Index} />
<Route path="/test" component={Test} />
<Route path="/mount" component={Mount} />
</Router>
)
}
挂载数据:
const APP_PROPS = window.APP_PROPS || {};
ReactDOM.render (<Provider store={APP_PROPS}><Root /></Provider> , document.getElementById('root'))
组件内部接收数据:
import React from 'react';
import PropTypes from 'prop-types';
export default class Index extends React.Component {
constructor (props,context) {
super(props, context);
this.state = {
arr:this.context.store.arr || '',
}
}
render () {
const {arr} = this.state;
return (
<div>
{
arr.length !== 0 ? arr.map((item,index)=> {
return (
<div key={item.id} >{item.name}</div>
)
}) : ''
}
</div>
)
}
}
Index.contextTypes = {
store:PropTypes.object
}
这样组件内部就能接收到数据了。
到此我们解决了问题1、2、3,剩下的问题我们将在《React服务端渲染(二)》中继续···