基於create-react-app 和 koa2 快速搭建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字符串。

咱們能夠調用這兩個API 實現傳入ReactComponent 返回對應的html字符串到客戶端。瀏覽器端接收到這段html之後不會從新去渲染DOM樹,只是去作事件綁定等操做。這樣就提升了首屏加載的性能。ios

react-router4.x 和 服務端的路由實現同構。

react-router4.x 相對於以前的版本,作了較大的改動。 整個路由變得組件化了。 能夠着重看這裏 官方給出了詳細的例子和文檔能夠做爲基本思想的和標準參考。git

服務端渲染與客戶端渲染的不一樣之處在於其路由是沒有狀態的,因此咱們須要經過一個無狀態的router組件 來包裹APP,經過服務端請求的url來匹配到具體的路由數組和其相關屬性。 因此咱們在客戶端使用 BrowserRouter,服務端則使用無狀態的 StaticRouter。github

  • BrowserRouter 使用 HTML5 提供的 history API (pushState, replaceState 和 popstate 事件) 來保持 UI 和 URL 的同步。
  • StaticRouter 是一個不會改變地址的router組件 。 參考代碼以下所示:
// 服務端路由配置
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傳入 ,後者會根據url 自動匹配對應的React組件,這樣咱們就能實現,刷新頁面,服務端返回的對應路由組件與客戶端一致。 到這一步咱們已經能夠實現頁面刷新 服務端和客戶端保持一致了。web

Redux 服務端同構

首先下官方文檔作了簡單的介紹介紹cn.redux.js.org/docs/recipe…redux

其處理步驟以下:

  • 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極致優化

github.com/joeyguo ...

總結

咱們知道服務端渲染的 優點在於能夠極快的首屏優化 ,支持SEO,與傳統的SPA相比多了一種數據的處理方式。 缺點也很是明顯,服務端渲染至關因而把客戶端的處理流程部分移植到了服務端,這樣就增長了服務端的負載。所以要作一個好的SSR方案,緩存是必不可少的。與此同時工程化方面也是有不少值得優化的地方。這裏只是淺嘗輒止,並無作相關的處理,估計後面有時間會作一些優化歡迎你們關注。

本項目github地址 github.com/yangfan0095…

以上です

相關文章
相關標籤/搜索