搭建React服务端渲染项目知识梳理及总结

本项目github地址 react-koa2-ssrhtml

所用到技术栈 react16.x + react-router4.x + koa2.x前端

前言

前段时间业余作了一个简单的古文网 ,可是项目是使用React SPA 渲染的,不利于SEO,便有了服务端渲染这个需求。后面就想写个demo把整个过程总结一下,同时也加深本身对其的理解,期间因为工做,过程是断断续续 。总以后来就有了这个项目吧。关于服务端渲染的优缺点,vue服务端渲染官方文档讲的最清楚。讲的最清楚。 对于大部分场景最主要仍是两点 提升首屏加载速度 和方便SEO.为了快速构建开发环境,这里直接使用create-react-app 和koa2.x生成一个基础项目 。整个项目即是以此做为基点进行开发的,目前也只是完成了最基本的需求, 还有不少Bug 和能够优化的地方, 欢迎交流。vue

服务端渲染最基本的理论知识梳理

首先先后端分别使用create-react-app 和koa2的脚手架快速生成, 而后再将两个项目合并到一块儿。这样咱们省去了webpack的一些繁琐配置 ,同时服务端使用了babel编译。看这个以前 默认已经掌握webpack 和 koa2.x,babel的相关知识。
咱们直切重要的步骤吧。我以为搭建一个react-ssr环境主要只有三点
第一是react服务端提供的渲染API,二是先后端路由的同构,三则是初始化异步数据的同构。所以这个简单的demo主要从这三方面入手。react

  • react 服务端渲染的条件
  • react-router4.x 与koa2.x 路由实现同构
  • redux 初始数据同构

react 服务端渲染的条件

其实能够看 《深刻React技术栈》的第七章, 介绍的很是详细。
归纳来讲 React 之因此能够作到服务端渲染 是由于ReactDOM提供了服务端渲染的APIwebpack

  • renderToString  把一个react 元素转换成带reactid的html字符串。
  • renderToStaticMarkup 转换成不带reactid的html字符串,若是是静态文本,用这个方法会减小大批的reactid.

这两个方法的存在 ,实际上能够把react看作是一个模板引擎。解析jsx语法变成普通的html字符串。 ios

咱们能够调用这两个API 实现传入ReactComponent 返回对应的html字符串到客户端。浏览器端接收到这段html之后不会从新去渲染DOM树,只是去作事件绑定等操做。这样就提升了首屏加载的性能。git

react-router4.x 和 服务端的路由实现同构。

react-router4.x 相对于以前的版本,作了较大的改动。 整个路由变得组件化了。
能够着重看这里 官方给出了详细的例子和文档能够做为基本思想的和标准参考。github

服务端渲染与客户端渲染的不一样之处在于其路由是没有状态的,因此咱们须要经过一个无状态的router组件 来包裹APP,经过服务端请求的url来匹配到具体的路由数组和其相关属性。
因此咱们在客户端使用 BrowserRouter,服务端则使用无状态的 StaticRouter。web

  • BrowserRouter 使用 HTML5 提供的 history API (pushState, replaceState 和 popstate 事件) 来保持 UI 和 URL 的同步。
  • StaticRouter 是一个不会改变地址的router组件 。

参考代码以下所示:redux

// 服务端路由配置
import { createServer } from 'http'
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import { StaticRouter } from 'react-router'
import App from './App'

createServer((req, res) => {
  const context = {}

  const html = ReactDOMServer.renderToString(
    <StaticRouter
      location={req.url}
      context={context}
    >
      <App/>
    </StaticRouter>
  )

  if (context.url) {
    res.writeHead(301, {
      Location: context.url
    })
    res.end()
  } else {
    res.write(`
      <!doctype html>
      <div id="app">${html}</div>
    `)
    res.end()
  }
}).listen(3000)
And then the client:import ReactDOM from 'react-dom'

// 客户端路由配置
import { BrowserRouter } from 'react-router-dom'
import App from './App'

ReactDOM.render((
  <BrowserRouter>
    <App/>
  </BrowserRouter>
), document.getElementById('app'))

咱们把koa的路由url传入 <StaticRouter /> ,后者会根据url 自动匹配对应的React组件,这样咱们就能实现,刷新页面,服务端返回的对应路由组件与客户端一致。
到这一步咱们已经能够实现页面刷新 服务端和客户端保持一致了。

Redux 服务端同构

首先下官方文档作了简单的介绍介绍http://cn.redux.js.org/docs/recipes/ServerRendering.html.

其处理步骤以下:

  • 1 咱们根据对应的服务端请求API 获得对应的异步方法获取到异步数据。
  • 2 使用异步数据生成一个初始化的store const store = createStore(counterApp, preloadedState),
  • 3 而后调用const finalState = store.getState()方法获取到store的初始化state.
  • 4 将初始的initState 做为参数传递到客户端
  • 5 客户端初始化的时候回去判断 window.__INITIAL_STATE__ 下面是否有数据,若是有则做为初始数据从新生成一个客户端的store.

以下面代码所示。

服务端

<html>
      <head>
        <title>Redux Universal Example</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <script>
          window.__INITIAL_STATE__ = ${JSON.stringify(finalState)}
        </script>
        <script src="/static/bundle.js"></script>
      </body>
    </html>

客户端

...
// 经过服务端注入的全局变量获得初始 state
const preloadedState = window.__INITIAL_STATE__

// 使用初始 state 建立 Redux store
const store = createStore(counterApp, preloadedState)

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

这个基本上就是一个标准的redux同构流程, 其实更多的官方是在给咱们提供一种标准化的思路,咱们能够顺着这个作更多的优化。
首先咱们并不须要直接经过API做为映射 服务端和客户端各搞一套异步加载的方法,这样显得很是冗余。
react-router 包里面提供了react-router-config主要用于静态路由配置。
提供的 matchRoutes API能够根据传入的url 返回对应的路由数组。咱们能够经过这个方法在服务端直接访问到对应的React组件。 若是要从路由中直接获取异步方法,我看了不少相似的同构方案,

  • 主要有两种方式一种是直接在路由中增长一个thunk方法,经过这个方法直接去获取初始化的异步数据,

我以为优势是比较明确直观,直接在路由层就把这个事情解决了。

  • 第二种是利用class 的静态方法。咱们能够经过路由访问到组件的类下面的static方法。 这样咱们就直接能够在容器组件内部同时声明服务端初始化方法和客户端初始化方法了 这样处理的层级放到了组件里面我本身以为更能体现组件的独立性吧。

本项目采用了第二种方案,先看一下代码:

/**
 * 渲染服务端路由
 */
module.exports.render = async(ctx,next) =>{
    const { store ,history} = getCreateStore(ctx);
    const branch = matchRoutes(router, ctx.req.url);
    const promises = branch.map(({route}) => {
        const fetch = route.component.fetch;
        return fetch instanceof Function ? fetch(store) : Promise.resolve(null)
    });
    await Promise.all(promises).catch((err)=>{
        console.log(err);
    }); 

    const html = ReactDOMServer.renderToString(
                <Provider store={store}>
                            <StaticRouter
                            location={ctx.url}
                            context={{}}>
                                <App/>
                            </StaticRouter>
                </Provider>
        )
        let initState=store.getState();
        const body =  layout(html,initState);
   ctx.body =body;
}

对应容器组件提供了一个静态的fetch方法

class Home extends Component {
  ...
  static fetch(store){
        return store.dispatch(fetchBookList({page:1,size:20}))
  }

这是咱们的 actions

/**
 * 获取书籍目录
 * @param {*} param 
 */
export const fetchBookList = (params) => {
    return async (dispatch, getState) => {
        await axios.get(api.url.booklist, {
            params: params
        }).then((res) => {
            dispatch(booklist(res.data.result));
        }).catch((err) => {

        })
    }
}

首先咱们经过 matchRoutes 拿到当前路由下全部的路由,再对其遍历获得有关一个异步方法的Promise数组,这里咱们所谓的异步方法就是actions中的异步方法。因为咱们在服务端也初始化的store因此咱们能够直接在服务端调用actions,这里咱们须要给容器组件的static方法传入store ,这样咱们就能够经过store.dispatch(fetchBookList({page:1,size:20}))调用actions了。上面的方法咱们获得了一个Promise 数组。咱们使用 Promise.all将异步所有执行。这个时候实际上 store的运行跟客户端是同样的。 咱们在异步的过程当中 将初始数据所有写入了 store中。因此咱们经过store.getState()就能够拿到初始化数据了。客户端的初始化跟Redux官方例子是同样的。直接判断是否传入初始化state,若是传入就作为初始化数据。咱们服务端的初始化异步和客户端的初始化异步 如何避免重复。 这里咱们直接先获取store中的对应初始数据 ,看是否存在,若是不存在咱们再进行加载。

到这一步咱们已经能够实现刷新页面异步数据服务端处理,不刷新页面前端处理,一个基本的同构方案主体就出来了,剩下的就是一些优化项和一些项目定制性的东西了。

服务端页面分发

对于服务器而言不只会收到前端路由的请求还会收到各类其余静态资源的请求 import {matchPath} from 'react-router-dom'; 咱们这里使用react-router-dom包里面的 matchPath API 来匹配当前请求路由是否与咱们客户端的路由配置相同若是不一样咱们默认为请求的是静态资源或其余。若是不匹配当前路由咱们直接执行 next() 进入到下一个中间件 。由于咱们这个项目实际上仍是是一个先后端分离的项目 只不过增长了服务端渲染的方式而已。 若是服务端还要处理其余请求,那么其实咱们也能够在经过服务端 增长其余路由 ,经过映射来匹配对应的渲染页面和API。

其余

写这个demo看了不少的github项目以及相关文章,这些资料对本项目有很大的启发

Vue.js 服务器端渲染指南

react-server

beidou

react-ssr-optimization

React-universal-ssr

fairy

D2 - 打造高可靠与高性能的React同构解决方案

Egg + React 服务端渲染开发指南

服务端渲染与 Universal React App

React同构直出优化总结

React移动web极致优化

https://github.com/joeyguo
...

总结

咱们知道服务端渲染的
优点在于能够极快的首屏优化 ,支持SEO,与传统的SPA相比多了一种数据的处理方式。
缺点也很是明显,服务端渲染至关因而把客户端的处理流程部分移植到了服务端,这样就增长了服务端的负载。所以要作一个好的SSR方案,缓存是必不可少的。与此同时工程化方面也是有不少值得优化的地方。这里只是浅尝辄止,并无作相关的处理,估计后面有时间会作一些优化欢迎你们关注。

本项目github地址 https://github.com/yangfan0095/react-koa2-ssr

以上です

相关文章
相关标签/搜索