React服務端渲染之路06——優化

全部源代碼、文檔和圖片都在 github 的倉庫裏,點擊進入倉庫javascript

相關閱讀

1. redux 與路由優化

  • 到目前咱們已經實現了服務端的異步獲取數據,可是如今依然還有幾個問題
  • 第一,多級路由與路由精確匹配,咱們並無實現多級路由,並且對路由的校驗也比較簡單,沒有深層次的校驗
  • 第二,Promise.all 這個方法,要求的是,裏邊若是有一個方法失敗了,那麼整個 Promise.all 就是失敗的。可是這樣是有問題的,咱們是要先經過 Promise.all 獲取到數據並修改 store,而後才進行頁面渲染的。若是 Promise.all 失敗了,那麼就再也不往下執行,頁面就一直處於 loading 的狀態,渲染不出頁面,這樣明顯不是咱們想要的。咱們想要的是,無論 Promise.all 裏邊有幾個失敗請求,都不會影響到咱們客戶端渲染,同時,若是服務端請求失敗了,store 裏沒有拿到咱們想要的值,那麼客戶端還能夠繼續獲取數據,不影響用戶使用,這樣至關因而雙保險
  • 那麼接下來,咱們就要開始解決這兩個問題

1.1 創建 App 組件

  • 在解決這兩個問題以前,咱們先一個 App 組件,就像是客戶端渲染時的 App 組件,主要是爲了作總體頁面的一個入口,就是說,咱們進入到每個頁面及組件的時候,都要經過 App 組件,這樣有助於咱們創建多級路由
  • 同時,這樣咱們還能夠把 Header 組件從 src/server/render.js 裏拿出來,放在 App 組件裏,這樣便於代碼的管理
  • src/App.js
import React, { Component } from 'react';
import Header from './components/Header';

class App extends Component {

  render() {
    return (
      <div>
        <Header />
        <div className="container">

        </div>
      </div>
    );
  }
}

export default App;
  • 那麼咱們能夠想兩個問題,第一個問題是, container 裏應該放什麼,確定是路由,由於 Header 是導航,那麼導航下邊確定要放路由,這樣才能把內容顯示出來
  • 第二個問題是,路由怎麼放,咱們要創建多級路由,確定不能像以前那樣採用 routes.map() 這樣的方法顯示路由,那麼該怎麼作,要解決這個問題,咱們先修改路由文件

1.2 配置多級路由

  • 多級路由的根入口,就是 App 組件,那麼也就是說,根路由就是 App 組件,剩下的 Home 和 News 組件都是子路由,儘管 Home 頁面顯示的也是根路由,可是咱們依然把它做爲一個子路由
  • 接下來,咱們修改路由文件
  • src/routes.js
// src/routes.js
import React from 'react';
import {Route} from 'react-router-dom';
import App from './App';
import Home from './containers/Home';
import News from './containers/News';

export default [
  {
    path: '/',
    component: App,
    key: 'app',
    routes: [
      {
        path: '/',
        component: Home,
        loadData: Home.loadData,
        exact: true,
        key: '/'
      },
      {
        path: '/news',
        component: News,
        exact: true,
        key: '/news'
      }
    ]
  }
];

1.3 服務端使用多級路由

  • 配置完成了,可是咱們該怎麼使用呢,這是一個問題,react-router-dom 裏有一個 matchPath 的方法,這個方法能夠匹配路由,可是它只能匹配單級路由,多級路由不支持,因此咱們不能使用 matchPath
  • 有一個庫叫作 react-router-config,這個庫裏邊有兩個方法,一個是 matchRoutes ,另外一個是 renderRoutes,這兩個方法一個是匹配路由,一個是渲染路由,恰好能夠知足咱們的須要。可是順序咱們要先搞清楚,確定是先匹配路由,匹配完了以後,而後纔開始渲染路由
  • 下載依賴 npm i react-router-config -S
  • 首先在服務端匹配多級路由前端

    • 就這一句話,routes 就是在 src/routes.js 配置的新的路由,req.path 就是服務的請求路由,匹配完以後獲得的是一個數組對象,每一個對象都是所匹配到的路由
    • 有一個不同的地方就是,匹配完以後獲得的 matchedRoutes 裏的結構體不同了,loadData 已經不在是屬於單個 item 的,而是 item.route.loadData,其餘的沒有什麼須要改變的
// src/server/render.js
import { matchRoutes } from 'react-router-config';

let matchedRoutes = matchRoutes(routes, req.path);

let promises = [];

matchedRoutes.forEach(item => {
  let loadData = item.route.loadData;

  if (loadData) {
    const promise = loadData(store);
    promises.push(promise);
  }
});
  • 而後服務端開始渲染路由,渲染路由就比較簡單,直接調用 renderRoutes 方法,傳入 routes 參數就行
// src/server/render.js

import { renderRoutes } from 'react-router-config';

let domContent = renderToString(
  <Provider store={store}>
    <StaticRouter context={context} location={req.path}>
      {
        renderRoutes(routes)
      }
    </StaticRouter>
  </Provider>
);

1.4 客戶端使用多級路由

  • 客戶端使用的多級路由,和服務端區別不大, 不同的地方就是客戶端不須要匹配路由,直接渲染就能夠
import { renderRoutes } from 'react-router-config';

hydrate(<Provider store={store}>
  <BrowserRouter>
    {renderRoutes(routes)}
  </BrowserRouter>
</Provider>, window.root);

1.5 App 組件使用多級路由

  • App 組件是客戶端和服務端都要使用到的組件,因此這個組件就不太同樣,渲染路由須要傳遞參數
  • renderRoutes 這個方法比較有意思,在服務端使用的時候,直接就獲取到了全部匹配到的路由,同時,把匹配到的路由做爲屬性值保留在組件的 props 裏,因此,在服務端 {renderRoutes(routes)} 的時候,就已經把匹配到的信息保留了下來。那麼在 App 裏使用的時候,直接調用 props 裏的屬性值就能夠了
  • 咱們先修改代碼,src/App.js
// src/App.js
import React, { Component } from 'react';
import { renderRoutes } from 'react-router-config';
import Header from './components/Header';

class App extends Component {

  render() {
    console.log(this.props);
    return (
      <div>
        <Header />
        <div className="container">
          {
            renderRoutes(this.props.route.routes)
          }
        </div>
      </div>
    );
  }
}

export default App;
  • 咱們在控制檯查看一下 this.props 的值是什麼

App-props-route

  • 能夠看到,props 裏有路由的三個屬性,history,location 和 match,還有一個靜態屬性 staticContext,這個咱們後邊再說。最重要的是 route 屬性,route.routes 裏就是咱們定義的路由文件裏的 routes 屬性,因此咱們直接使用這個 routes 屬性渲染路由
  • 此時咱們再進行頁面的切換,服務端異步獲取數據,客戶端同步修改數據,均可以正常操做,沒有任何的問題

1.6 解決 Promise.all 的問題

  • Promise.all 的問題實際上就是每個 promise 的問題,而每個 promise 的問題就是這個 promise 的狀態可能會失敗,那麼咱們須要解決的就是,如何把 promise 失敗的狀態,也改成成功的狀態。就算失敗了,不修改 store 的值也不要緊,客戶端能夠修改,最重要的是不能引發頁面一直 loading 而不渲染頁面
  • 因此咱們修改 promise 的狀態,把失敗的狀態也改成成功的狀態,這樣就解決了 promise 失敗的狀態,能夠嘗試一下把接口改成一個不存在的接口,而後調用接口,看一下頁面是否會正常渲染,同時查看一下 store 裏的值
  • src/server/render.js
// src/server/render.js
matchedRoutes.forEach(item => {
  let loadData = item.route.loadData;

  if (loadData) {
    const promise = new Promise((resolve) => {
      loadData(store).then(resolve).catch(resolve);
    });
    promises.push(promise);
  }
});
  • 這裏還須要再進行一步操做,就是在有 loadData 屬性的組件裏,咱們要在 componentDidMount 或者 componentWillMount 生命週期方法裏去判斷 store 裏是否有咱們想要的值,若是沒有,在這兩個生命週期方法裏進行調用,這樣就不會由於注水失敗,而致使頁面沒有數據
  • 咱們修改 Home 組件裏的代碼,src/containers/Home/index.js
// src/containers/Home/index.js

componentDidMount () {
  if (!this.props.user.schoolList.length) {
    this.props.propGetSchoolList();
  }
}
  • 可是,若是服務端沒有獲取到數據,客戶端也沒有數據,那就是出 bug 了,這個須要測試接口,還須要測試前端的代碼

2. 服務端代理轉發

  • 咱們開發的時候,由於有安全問題,因此儘可能不讓客戶端去直接調用第三方接口。可是咱們服務端渲染的時候,不提供第三方的接口,這該怎麼辦?咱們能夠採用代理
  • 代理,跟代購(海淘)很像,咱們要買一雙空軍一號耐克鞋,可是因爲國內粉絲熱情,一鞋難求,並且還有黃牛囤貨,價錢太貴。國內沒有空軍一號,可是美國有呀,咱們能夠去美國買呀。這下就有問題了,去美國就得買機票辦簽證定酒店,並且還有拒籤的風險,再說,也不可能去美國專門買一雙鞋回來的呀。這成本加起來,別說買一雙了,買 10 雙都夠了,何須呢,因此,找代購,代購纔多花多少錢,花不了多少錢,比起簽證機票酒店便宜了的海了去了,因此,代購,就是咱們最好的選擇
  • 咱們把咱們須要購買的物品告訴代購,代購去美國購買,購買完以後,再把物品帶回來給我。這個過程,代購就是一箇中間人的過程,跟代理是如出一轍的,咱們的客戶端是創建在 Node 服務器上的,客戶端發送請求給 Node 服務器,Node 服務器把請求信息轉發給第三方服務器,好比 Java,那麼 Node 服務器去請求 Java 服務器,Java 服務器把數據返回給 Node 服務器,Node 服務端再把數據返回給客戶端。這裏的 Node 服務器,就是中間層代理

代理

  • 咱們採用 express-http-proxy 這個庫來作轉發,可是轉發以前咱們要考慮一下,哪些須要轉發?哪些不須要轉發?確定不是全部的都轉發,咱們要知道第三方服務是作數據提供的,不是作頁面渲染和資源提供的,因此只須要轉發 api 數據接口就行,若是有須要,也能夠轉發須要的請求頭,好比 cookie 信息,其餘的不須要轉發,因此,咱們統一把以 api 開頭的接口,轉發給 Java 服務器
  • src/server/index.js
import proxy from 'express-http-proxy';

app.use('/api', proxy('http://localhost:8757', {
  proxyReqPathResolver(req) {
    return `/api${req.url}`;
  }
}));
  • 因此,咱們實際上僅僅是把以 api 開頭的接口進行了轉發,其餘的都沒有修改

3. axios 的請求優化

3.1 爲何要作 axios 的請求優化

  • 既然服務端已經把請求的接口進行了轉發,那麼咱們的客戶端在請求的時候,就不須要直接請求第三方服務,直接去請求服務端就能夠,由於服務端已經幫助咱們作了一層代理
  • 還有一個問題,客戶端請求服務端要轉發,那麼服務端請求的話,服務端就不須要轉發,由於服務端直接請求的就是第三方服務,換句話說,客戶端和服務端請求的路徑,仍是有一些不同的,咱們能夠對不一樣的端請求進行不一樣的處理

3.2 如何對 axios 進行優化

  • 咱們經過 axios.create 方法建立一個實例,這個實例本質上與 axios 是同樣的,只不過是說,建立出來的作個實例,咱們能夠對請求頭和響應頭作統一的處理,這樣更加方便。
  • 在 src/client/ 建立一個 request.js 文件,供客戶端請求使用
import axios from 'axios';

export default axios.create({
  baseURL: `/`
});
  • 在 src/server/ 建立一個 request.js 文件,供服務端請求使用
import axios from 'axios';

const serverAxios = axios.create({
  baseURL: 'http://localhost:8757'
});

export default serverAxios;

3.3 如何使用 axios 實例

  • 定義 axios 實例以後,咱們須要考慮如何去使用
  • 咱們知道,redux 的 action 返回的是一個對象,經過 redux-thunk 能夠返回一個方法。redux-thunk 還有一個 withExtraArgument 的屬性方法,咱們能夠把 axios 的實例做爲 withExtraArgument 的參數進行傳遞,在 action 中直接使用這個參數
  • 把 axios 的實例傳遞到 store 裏,src/store/index.js
// src/store/index.js
import clientAxios from '../client/request';
import serverAxios from '../server/request';

export const getServerStore = (req) => createStore(
  reducers,
  composeWithDevTools(applyMiddleware(thunk.withExtraArgument(serverAxios(req)), logger))
);

export const getClientStore = () => {
  let initState = window.context.state;
  return createStore(
    reducers,
    initState,
    composeWithDevTools(applyMiddleware(thunk.withExtraArgument(clientAxios), logger))
  )
};
  • 在 action 中使用,src/store/user/createActions.js
// src/store/user/createActions.js
export const getSchoolList = () => {
  return (dispatch, getState, axiosInstance) => {
    return axiosInstance.get('http://localhost:8758/api/getSchoolList').then(res => {
      if (res.status === 200) {
        let schoolList = res.data.schoolList;
        dispatch({
          type: Types.GET_SCHOOL_LIST,
          payload: schoolList
        });
      }
    });
  }
}
  • 這個時候,咱們就實現了針對客戶端和服務端不同,請求的方式也不同,也方便咱們後期對不一樣的端的 axios 請求作一些擴展

3.4 關於 cookie

  • 咱們知道,咱們如今的瀏覽器向服務器發送請求的時候,是有 cookie 信息的,然而如今服務端在向 Java 服務器請求的時候,是沒有 cookie 信息的,可是咱們常常須要作登陸校驗的判斷,因此咱們還須要把 cookie 轉發給 Java 服務器
  • 咱們修改一下服務端的 axios 實例
  • src/server/render.js
// src/server/render.js
let store = getServerStore(req);
  • src/store/index.js
export const getServerStore = (req) => createStore(
  reducers,
  composeWithDevTools(applyMiddleware(thunk.withExtraArgument(serverAxios(req)), logger))
);
  • src/server/request.js
// src/server/request.js
const serverAxios = axios.create({
  baseURL: 'http://localhost:8757',
  headers: {
    cookie: req.get('cookie') || ''
  }
});
  • 實際上就是,咱們把請求信息所有經過 getServerStore 傳遞給 serverAxios 實例,而後在 serverAxios 裏修改請求頭信息,服務端在向 Java 服務器發送請求的時候,就能夠把 cookie 攜帶上,也能夠攜帶其餘的信息,好比請求的其餘參數之類的

相關閱讀

相關文章
相關標籤/搜索