关于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中进行请求,会如何

  1. 首先服务器会接收到一个请求,这个时候的store是空的,服务器不会去执行componentDidMount,所以我们需要的数据获取不到,然后将模板返回给客户端。
  2. 客户端代码运行,这个时候store依然是空的,页面需要的数据依然没有展示。
  3. 客户端执行服务器端不会执行的componentDidMount生命周期钩子函数,然后获取到了相应的数据,这个时候store被更新
  4. 客户端渲染出了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页面

posted @ 2019-07-14 21:02  林璡  阅读(396)  评论(0编辑  收藏  举报