React同構總結

最近花了點時間研究React同構實踐,遇上過年回家,心也散了,兩個月沒寫文章。javascript

同構也算是前端的一個應用模式,目的是爲了加速首屏顯示時間和seo優化,不少公司都將同構做爲前端優化的一個優化點來作,同時Raect16版本中也添加了不少對同構的支持,能夠看出FB也是默認支持這一場景使用的。css

同構原理

什麼是同構

一套代碼既能夠在服務端運行又能夠在客戶端運行,這就是同構應用。簡而言之, 就是服務端直出和客戶端渲染的組合, 可以充分結合二者的優點,並有效避免二者的不足。html

歸納地說,同構就是服務端(Node)替客戶端請求接口,獲取到數據後,將有數據和結構的頁面渲染好以後返回給客戶端,這樣避免了客戶端頁面首次渲染,同時服務端RPC比客戶端請求要快。前端

爲何要同構

  • 性能: 經過Node直出, 將傳統的三次串行http請求簡化成一次http請求,下降首屏渲染時間
  • SEO: 服務端渲染對搜索引擎的爬取有着自然的優點,雖然阿里電商體系對SEO需求並不強,但隨着國際化的推動, 愈來愈多的國際業務加入阿里你們庭,不少的業務依賴Google等搜索引擎的流量導入,好比Lazada.
  • 兼容性: 部分展現類頁面可以有效規避客戶端兼容性問題,好比白屏。

同構與SPA流程對比

clipboard.png
SPA:服務端替客戶端請求數據,完成第一次render,將render完成以後的html頁面返回給客戶端,相對於客戶端渲染,客戶端第一次獲取的html是個有數據有結構的html,結合樣式文件下載客戶端能夠較快的看到首屏內容。java

SSR:服務端Node也能夠運行React解析出頁面內容,而且要比客戶端更快;客戶端一般要在render一次以後請求數據,數據返回以後再render一次,服務端渲染能夠解決客戶端重複渲染問題。node

同構與SPA時間對比

clipboard.png
以一個常見的場景爲例:react

進入頁面,componentDidMount中請求數據,同時頁面loading,請求返回後,取消loading,頁面可交互。webpack

SPAgit

  1. 客戶端請求頁面,服務端返回SPA的html,此html不可視;(request&response)
  2. html加載完以後,去加載頁面中的js;(processing)
  3. js加載完成以後開始執行;(rendering)
  4. 頁面首次渲染完畢,向後端請求數據(loading)
  5. 請求返回,頁面再次渲染,用戶可交互(useing)

SSRes6

  1. 客戶端請求頁面,服務端去請求數據,請求返回後渲染頁面,將渲染好的html返回給客戶端,此時頁面可視;(request&response)
  2. html加載完以後,去加載頁面中的js;(processing)
  3. js加載完成以後開始執行;(rendering)
  4. js解析完畢,用戶可交互;(useing)

經過上述流程圖可發現,理論上同構要比客戶端渲染要快,並且體驗要好。

預期問題

原理了解以後,動手以前思考一些可能出現的問題:

1. Node服務器如何識別es6以及React

Node識別ES6可使用babel-register插件,該插件使用起來跟.babelrc同樣簡便。

React中有一個renderToString方法,該方法將解析好的jsx片斷以html字符串形式輸出,就是爲了同構而誕生。

2. 服務端如何引入js,css,圖片,字體等靜態資源

實現方法有多種,我這裏使用webpack-isomorphic-tools插件來實現,以後會作介紹。

3. 服務端如何路由匹配

一般咱們只作首頁,或者關鍵頁面的服務端渲染,至關於從首頁進去是服務端渲染,可是從項目其餘頁面進入就跟正常的SPA同樣。因此在服務端將要ssr的路由匹配出來,其餘的路由仍交給SPA。

4. SSR的Redux怎麼辦

一般來說,咱們從接口獲取數據,都要將一些數據放到store中,便於其餘頁面共享。

SSR中,服務端跟SPA公用一部分action和reducer,相同的reducer生成的store是同樣的,以後再經過createStore時候將store注入進入,返回給客戶端。

5. SSR的開發流程怎樣

實際上SSR開發一般是在一個項目基礎上改,而不是從新搭建一個項目,比較不少人拿它當作優化,而不是重構。

一般來講咱們一個項目按照SPA模式開發,針對特定頁面作SSR的修改,修改以後的項目既能夠SPA也能夠SSR,只不過SPA模式時對應頁面獲取不到數據,由於獲取數據的方法均被修改。

6. SSR以後,項目的JS體積是否會減少

不會減少,所謂同構,其實就是服務端藉助客戶端的JS去渲染頁面,沒有影響到客戶端的JS,仍是正常打包,客戶端作代碼分割也不會受影響。

同構實現

帶着上面的問題來看同構如何實現。

React實現同構方法有多重,並且都較爲成熟,這裏選用的webpack-isomorphic-tools插件來實現。

Next.js

先插一嘴,如今有個叫Next框架,索性也試了下,真的很簡便,快速搭建SSR項目,可是問題也很明顯:

  1. 框架高度封裝,擴展性有限。
  2. 適合從頭搭建項目,不適合現有項目SSR遷移。
  3. 上手很快,可是初學者不知道里面原理如何,適合熟練手玩。

因此沒有選用該框架進行嘗試,不過該框架憑藉着簡單易上手,將來仍是頗有市場的。

webpack-isomorphic-tools

上面第二個問題就提到了服務端如何處理靜態資源,這裏使用webpack-isomorphic-tools插件,該插件處理靜態資源的。

首先一個webpack-isomorphic-tools-configuration.js文件配置你想要處理的文件格式與處理方法,跟webpack配置loader相似:

const webpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');
const config = require('../config/');

module.exports = {
    webpack_assets_file_path: `${config.base_path}/webpack-assets.json`,
    webpack_stats_file_path: `${config.base_path}/webpack-stats.json`,
    assets: {
        images: {
            extensions: ['png', 'jpg', 'gif', 'ico', 'svg']
        },
        fonts: {
            extensions: ['woff', 'woff2', 'eot', 'ttf', 'swf', 'otf']
        },
        // styles: {
        //     extensions: ['scss', 'css'],
        //     filter: function(module, regex, options, log) {
        //         if (options.development) {
        //             // in development mode there's webpack "style-loader",
        //             // so the module.name is not equal to module.name
        //             return webpackIsomorphicToolsPlugin.style_loader_filter(module, regex, options, log);
        //         } else {
        //             // in production mode there's no webpack "style-loader",
        //             // so the module.name will be equal to the asset path
        //             return regex.test(module.name);
        //         }
        //     },
        //     // How to correctly transform kinda weird `module.name`
        //     // of the `module` created by Webpack "css-loader"
        //     // into the correct asset path:
        //     path: webpackIsomorphicToolsPlugin.style_loader_path_extractor,
        //
        //     // How to extract these Webpack `module`s' javascript `source` code.
        //     // Basically takes `module.source` and modifies its `module.exports` a little.
        //     parser: webpackIsomorphicToolsPlugin.css_loader_parser
        // }
    }
}

該文件配置能夠參考官方文檔

而後在webpack中,配置對應的資源的時候,引入該文件

// 同構處理靜態資源的插件
const webpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');
const webpackIsomorphicToolsPluginIns =
    new webpackIsomorphicToolsPlugin(require('./webpack-isomorphic-tools-configuration')).development();
...
module: {
  rules: [
    ...
    {
                test: webpackIsomorphicToolsPluginIns.regular_expression('images'),
                loader: 'url-loader?limit=8192', // 這樣在小於8K的圖片將直接以base64的形式內聯在代碼中,能夠減小一次http請求。
                options: {
                    name: 'assets/images/[name]_[hash:8].[ext]'
                }
            },
            {
                test: webpackIsomorphicToolsPluginIns.regular_expression('fonts'),
                loader: 'url-loader',
                options: {
                    name: 'assets/fonts/[name].[ext]'
                }
            }
  ]
}
plugins: [
  webpackIsomorphicToolsPluginIns,
  ....
]

而後運行webpack,將文件打包以後,會生成一個webpack-assets.json文件,該文件就是存儲靜態資源的映射關係的json:

{
  "javascript": {
    "app": "/assets/js/app_360a53bf78ee0e398bb2.js",
    "vendor": "/assets/js/vendor_360a53bf78ee0e398bb2.js"
  },
  "styles": {
    "app": "/assets/css/app_360a53bf78ee0e398bb2.css"
  },
  "assets": {
    "./public/images/react.svg": "data...."
  },
  "webpack": {
    "version": "2.7.0"
  }
}

這樣咱們經過該json文件就能夠獲取到對應靜態資源。

Express服務

這裏選用Express框架做爲服務器,緣由就是簡單,不少人也選用koa,都同樣。

這裏服務端啓動部分跟正常的Express啓動相似:

import render from "./render";
import fetch from "./fetch";

app.use('*', (req, res, next) => {

    const { promises, store } = fetch(req);

    Promise.all(promises).then(data => {
        const html = render(req, res, store);
        res.send(html);
    }).catch(err =>{
        console.log('err');
        console.log(err);
        res.end('server error,please visit later')
    })

});

核心在於路由部分:這裏匹配全部路由(也能夠是首頁路由),使用fetch方法獲取到了promisesstore,而後再用render方法生成了html返回給客戶端,這兩個方法都是封裝過的,

公用Action,Reducer

咱們在SPA開發中,請求通常都封裝成actionCreator,方便調用與修改,SSR中就共用了actionCreator和reducer。

fetch方法以下:

import 'isomorphic-fetch';

import { createStore } from "redux";
import {actions} from '../src/actions/';

import reducer from "../src/reducers";

const fetchHomeList = (store) => {
    return fetch('http://localhost:9000/api/aaa')
        .then((response)=>{
            console.log('then response------');
            return response.json();
        })
        .then((res)=>{
            console.log(res.data.length);
            store.dispatch(actions.updateHomeList(res.data));

            return res;
        })
        .catch((res)=>{
            console.log('catch res------');
            console.log(res);
        });
};

export default function (req) {
    const store = createStore(reducer);

    const promises = [
        fetchHomeList(store)
    ];

    return {
        promises,
        store
    }
}

fetch文件中,咱們將首頁須要獲取的數據經過isomorphic-fetch來獲取,而後跟SPA同樣,dispatchstore中,而後暴露出去。

Render頁面

SSR返回的是首次渲染事後的html,首次渲染就是在render方法中實現的:

import fs from 'fs'
import path from 'path'

import React from 'react';
// import ReactDOM from 'react-dom';
import { StaticRouter as Router } from "react-router-dom"
import { renderToString } from "react-dom/server"
import { Provider } from "react-redux"
import Routes from '../src/route';

function getAssets() {
    return getAssets.assets || (() => {
            getAssets.assets = JSON.parse(fs.readFileSync(path.join(__dirname, '../webpack-assets.json')));
            return getAssets.assets
        })()
}

export default function render(req, res, store) {
    const context = {};

    const html = renderToString(
        <Provider store={store}>
            <Router location={req.baseUrl} context={context}>
                <Routes />
            </Router>
        </Provider>
    );

    // <Route>中訪問/,重定向到/home路由時
    if (context.url) {
        res.redirect('/home');
        return;
    }

    const main = getAssets();
    const app = main.javascript.app;
    const vendor = main.javascript.vendor;
    const style = main.styles.app;

    return `
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link href=${style} rel="stylesheet"></link>
        <title>SSR</title>
    </head>
    <body>
        <div id="root">
            ${html}
        </div>
    </body>
    <script>
        window.__INITIAL_STATE__ = ${JSON.stringify(store.getState())}
    </script>
    <script src=${vendor}></script>
    <script src=${app}></script>
    </html>
    `
}

這裏顯而易見,咱們準備一段html模板,跟SPA那個html模板相似,將renderToString的片斷塞進去,同時根據webpack-assets.json獲取到打包好的js和css,塞進去;最後,將上面剛剛配置好的store注入進去。

效果

咱們製做一個接口,使用setTimeout 500ms模擬網絡開銷,效果以下:
clipboard.png
(gif上傳不上去不知道爲啥。。。)

能夠看到SSR要比SPA明顯的更快速獲得首屏效果。

思考

項目完成的同時,也在思考一些問題:

1. 既然SSR首屏速度快,爲什麼不全部路由全都SSR

全部頁面SSR能夠作,這樣每一個頁面的首屏都會很快,同時js也會小不少。可是帶來的問題服務器壓力會很大,維護起來成本較高。並且服務端畢竟是模擬客戶端環境渲染,一些地方仍是不同的好比沒有Document,沒有window對象,沒法進行DOM操做等。因此推薦首頁等重要頁面進行SSR。

2. 若是接口時間過長,是否是白屏時間較長

確實有這個問題,理論上講,RPC要比客戶端請求快不少,這樣能夠節省不少時間;可是若是接口很慢會形成白屏時間過長,得不償失。因此接口很慢的頁面不建議作SSR,同時接口也應該有嚴格的規範控制接口返回時間。

3. 若是項目首頁有很重的邏輯,或者Layout中有重邏輯該如何

頁面若是有很重的邏輯好比判斷不少不一樣條件,作出不少相應處理;依次請求不少接口,或者一塊兒請求大量數據等狀況,這些邏輯處理都須要一同寫進SSR中。

4. Node服務器帶來的維護及併發壓力等問題

使用Node服務器的話,還涉及到服務器的平常維護問題,日誌收集,錯誤報警等問題,以及性能問題。要求前端(SA)有必定的Node服務器的維護經驗,這時前端已經不是純前端了。

5. 什麼項目適合SSR

這個問題纔是關鍵的問題。並非全部項目都適合SSR,就好像不是全部項目都適合Redux同樣。根據SSR特色適合場景:

  1. 項目要求SEO,SSR就很合適。
  2. 需求項目某頁面首屏時間要求很快,SSR能夠減小白屏時間。
  3. 通常是首頁,列表頁等大量數據頁面使用比較常見。

歡迎你們提出些不一樣意見用以討論。

參考

  1. xiyuyizhi/movies
  2. 教你如何搭建一個超完美的React.js服務端渲染開發環境
  3. React 同構與極致的性能優化
  4. 從零開始React服務端渲染
  5. 精讀先後端渲染之爭

項目源碼:https://github.com/Aus0049/re...

相關文章
相關標籤/搜索