React服务端渲染(一)

一、服务端渲染的好处!
  • 有利于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服务端渲染(二)》中继续···

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值