打造可降級的React服務端同構框架

什麼是可降級的同構框架

這裏指的可降級與服務器降級意思相近(主邏輯失敗採用備用邏輯的過程),考慮到服務端渲染併發及高負載的問題,在主服務器沒法提供正常服務的時候可降級爲"低功耗模式",採用客戶端渲染方式來減輕服務器負載。javascript

框架選型

next.js

我看了一下next.js,他們的開發方式很新穎,尤爲是路由配置,徹底不用本身配,按照他的規則來作就好了。但我這我的比較求穩,這種約定大於配置的方式對於我來講總以爲會有坑,沒法本身掌控,找不到解決辦法還得看源碼才知道。css

仍是求穩的我,畢竟服務端渲染是須要消耗服務器性能的,尤爲是高併發和內存溢出,可能會致使服務器響應變慢甚至掛掉,因此我但願個人框架還能降級,這個降級與服務器降級相似,在服務器高負載的時候,切換另外一臺機器,啓動"低功耗模式"(也就是客戶端渲染的單頁應用)來繼續運做,我查了next.js好像沒有這個功能(若是有請在評論告知)。前端

cra-ssr

實在找不到既能服務端渲染又能客戶端渲染的框架,最後我就找了這個項目https://github.com/cereallarceny/cra-ssr 進行改造,改造仍是蠻成功的,過程遇到不少坑,在這裏分享一些過程步驟。java

改造之旅

其實cra-ssr這個項目自己已經作好了服務端同構渲染,在改造以前最好先理解cra-ssr實現同構渲染的流程原理再看下面的步驟,下面是cra-ssr實現同構渲染的流程圖 node

同構渲染流程圖

下圖是降級改造後的流程圖: react

服務端同構降級改造流程圖

上圖裏黃色區域的frontload是用在服務端渲染時預先處理的接口請求預取數據,而後放入準備好的global_state,global_state是一個store,做用是存放預取數據再connect給組件渲染,同時也能夠做爲服務端與客戶端均可用的全局變量;git

藍色節點是服務器環境,橙色節點是客戶端環境,當服務器渲染響應給客戶端以後,用戶進行了路由跳轉,這時前端無刷新跳轉到新頁面,再走黃色區域取到頁面數據,交給client_render渲染出新頁面github

環境區分🤔

同構框架因爲是同一套代碼,但環境不一樣有些對象是不自帶的,咱們要加以區分避免出現問題,這個項目有提供一個isServer變量用來判斷是服務端環境仍是客戶端環境,而咱們作降級了就還要加多一個環境判斷是不是客戶端渲染的模式來區分,後面就知道爲何要作區分了。typescript

咱們須要作3個環境的區分express

1.服務端渲染環境;

2.服務端渲染完成後的客戶端環境;

3.客戶端渲染環境;

我是這麼作環境區分的:

// 經過node環境獨有的process和具體的環境變量區分isServer
export const isServer = !!process&&!!process.env&&(process.env.RENDER_ENV == 'server');
// 只要不是服務端渲染環境都屬於isClient
export const isClient = !isServer;
// 區分降級後的客戶端渲染isCSR,CSR是沒有服務端渲染事後填充的數據__PRELOADED_STATE__的
export const isCSR = !isServer && !window.__PRELOADED_STATE__;
複製代碼

首屏接口數據預取🤓

cra-ssr是使用react-frontload來作首屏異步處理的,用法看這個文件src/app/routes/profile/index.js,在服務端經過預取數據交給狀態管理redux的store,插入到window.__PRELOADED_STATE__做爲初始store,在客戶端拿window.__PRELOADED_STATE__做爲初始store,connect組件獲得數據填充,

而客戶端渲染是沒有預取數據window.__PRELOADED_STATE__的,因此frontload要同時知足下面3個條件:

①frontload在服務端渲染完以後到客戶端首屏不能重複執行發請求

②降級後客戶端渲染模式能執行frontload發請求拉取數據並保證跟服務端一樣的寫法

③服務端渲染完以後到客戶端通過用戶點擊路由跳轉又要能像客戶端同樣請求拉取數據

爲了保證一樣的寫法,服務端渲染與客戶端渲染必須統一用狀態管理的store,須要封裝一個特定的global_state用來傳首屏數據的store,再封裝一下frontload,寫了個Adapter:

import { frontloadConnect } from 'react-frontload';
import {isServer, isCSR} from 'xxx/xxx';
import {change_state, get_state} from 'xxx/global_state';
import {connect} from 'dva';

export const frontload = (frontload_fn) => {
  return (Target) => {
    // 爲組件默認注入global_state
    return connect(({global}) => ({global}))(frontloadConnect(async (props) => {
      // get_state('client_load') => 在客戶端觸發路由跳轉的時候爲true
      if (get_state('client_load') || isServer || isCSR) {
        await frontload_fn(props)
      }
    })(Target))
  }
}
複製代碼

用這個frontload來代替原來的frontloadConnect函數,不只簡化了寫法,還保持了同一套代碼知足上面三個條件。

在組件裏能夠這麼寫:

//(本項目引入了dva、Typescript、scss和react-css-modules)
import * as React from 'react';
import { isCSR } from 'xxx/xxx';
const styles = require('./index.scss');
const CSSModules = require('react-css-modules');
import { frontload } from 'xxx/adapter'; 
import { API } from 'xxx/http';
import {config} from 'src/utils/config';


@frontload(async (props) => {
    const res = await API.get('/getNews');
    // 經過更新store的global達到統一服務端與客戶端的寫法。
    props.dispatch({type: 'global/set', payload: {data_list: res.data}})
})
@CSSModules(styles)
export default class News extends React.PureComponent<any,any>{
    constructor(props){
        super(props);
        this.state = {
            news_list: [], 
        }
    }
    componentDidMount() {
        this.setState({
            // 經過frontload預取數據填充了global數據,並自動connect當前組件
            // 因此組件不須要再connect就能夠訪問global數據
            news_list: this.props.global.data_list,
        })
    }
    render() {
        const news_list = this.props.global.data_list;
        return (
          news_list.map((item) => {
            return <section> <p>標題:{item.title}</p> <p>摘要:{item.abstract}</p> </section>
          })
          
        )
    }
}
複製代碼

添加客戶端渲染啓動服務器😎

還要添加一個啓動客戶端渲染的服務器文件,好比csr_server.js

var path = require('path')
var express = require('express')
var compression = require('compression')
var app = express()
var data = require('./data_config.json');
app.use(compression());
// 構建後的資源文件夾
app.use(express.static(path.join(__dirname, '../dist/')));

port = 8080
app.listen(port, function () {
  console.log('The app server is working at ' + port)
})
複製代碼

這就是我改造服務端同構框架降級的一些步驟,具體仍是要本身好好理解才行,根據自身項目需求調整(好比個人項目還引入了dva、typescript之類的),會遇到不少坑的😂

這篇文章只是在前端的角度講降級的,至於具體在服務器上怎麼降級,就得從運維角度看怎樣的規則適合降級,怎麼降級這些問題了,就不在此文講述了。

相關文章
相關標籤/搜索