Router-view 背後的想法

原帖在論壇發了一遍 http://react-china.org/t/router-view/2940前端

什麼是 router-view

router-view 是我爲簡聊開發的一個路由組件
原本本身寫的, 後來用 CoffeeScript 重構放到 teambition 團隊維護了
https://github.com/teambition/router-view
本來簡聊用的是 react-router, 但我仍是冒險替換掉了
從結果看, 好處達到了, 但可維護性並不滿意node

這篇文章我固然是想解釋一遍 router-view 究竟好在哪值得冒險
特別是組件背後的對於路由的理解, 這是對總體架構相當重要的
另外 router-view 受到 elm 和 redux 影響實際上不小
前面的文章介紹的 actions-recorder 也是 router-view 的肇因
而 router-view 初稿時 redux-router 還沒發佈, 談不上借鑑react

路由的類別

在 actions-recorder 或者 redux 的觀念當中, single store 很是明確了
特別是涉及到整個 store 的回溯, 這一點必須優先保證的
然而在路由問題上, 當時發現了問題, 就是調試回溯不能將路由歸入控制
這主要是在調試工具的可用性上大打折扣了, 因此我開始從新思考
目標主要是路由能夠被 single store 所控制, 以及回溯git

早在 2013 年秋冬, 我和寸志討論 Backbone 路由問題時就想到過
Backbone 的路由簡單精悍, 但對於嵌套的路由實現比較吃力
當時咱們以爲界面是隨着路由渲染的, 那就渲染唄. 然而不知道怎麼實現
Backbone 的路由相似事件, 能綁定 controller 方法, 而後操做
這是能夠去調用渲染, 只是這樣終究只是調用, 不是普通的渲染過程
回頭看我認爲這是對服務端路由的模仿, 帶一點誤解在裏邊github

以我 React 的開發經驗再審視路由, 我認爲前端的路由就是一個 View
好比說, 讓你實現一遍地址欄, 前進後退按鈕, 用 React, 簡單吧
我寫一下僞代碼:redux

React.createClass
  displayName: 'addressbar'

  propTypes:
    router: React.PropTypes.string # 表示路由的數據或者字符串
    onChange: React.PropTypes.func # 路由更新的事件
  
  getInitialState: ->
    history: [@props.router] # 歷史記錄, 用於返回
    pointer: 0 # 在歷史記錄的位置上切換

  onBack: -> # 處理
  onForward: -> # 處理
  onChange: -> # 處理

  render: -> # 兩個按鈕, 一個輸入框
    div null,
      div null, onClick: @onBack, '<'
      div null, onClick: @onForward, '>'
      textarea value: @props.router, onChange: @onChange

從這個角度看, 路由就是和組件基本一致的, 包含一下一些特徵:瀏覽器

  • 根據一個當前的狀態渲染, 狀態改變時調用回調函數react-router

  • 有對應界面, 以及交互架構

  • 內部有私有狀態mvc

只是區別在於, 地址欄是瀏覽器原生實現的, 要去封裝, 須要些奇技淫巧
那我說想的重要的一點就是, 路由屬於 MVC 的 V, 而不是 C
...補充一下, 或者說地址欄是 V, 由於路由確實包含一些別的東西, 繼續下文

Single Source of Truth (SSOT)

回到數據流的角度, 也就是 SSOT, 一樣也是 single store 所陳述的問題
若是路由是個獨立於 single store 存在的部分, 那麼它是什麼角色?
store 做爲 Model 控制着界面的狀態以及顯示, 但是路由也有這個功能
因此我認爲, 明確前面的地址之後, 那麼路由的當前狀態是屬於 store 的

這裏說的路由其實一直很模糊, 並且在各類框架裏也顯得很不同
那這裏, 我按照 MVC 把路由進行拆解, M 是狀態, V 是地址欄, 很明確
而 C 是對 M 進行操做的代碼, 即使在 React 中也模糊, 這裏不細化
而 router-view 給出的方案, 就是對地址欄進行封裝, 對 V 進行明確
而 M 天然做爲 single store 的一部分, 附着在 Model 當中

於是在個人方案當中, View, 也就是地址欄, 大概就是組件的形態了:

React.createElement addressbar,
  route: store.get('router') # 當前狀態
  onPopstate: (info, event) -> # 回調函數
  rules: routes # 一些路由規則
  inHash: false # 是否使用 Hash 的路由
  skipRendering: false # 處理一些特殊的渲染狀況

而路由中對應 Model 的數據, 我用更方便操做的對象來表示:

initialStore =
  message: {}
  topics: {}
  router:
    name: 'topic'
    data: {topicId: "c4d6a940d"}
    query: {}

這樣作以後, View 和 Model 都成了整個大的 View 和 Model 的部分
因而應用的總體也就往 single store 更靠攏了一步
以前的路由不受回溯控制的問題, 天然而然獲得瞭解決

做爲試驗, 你能夠打開 http://r.nodejs-china.org/
而後經過 Command+Shift+a 快捷鍵打開調試工具
找到 "router" 字段, 而後選擇左側的 Action 位置, 來嘗試效果
或者直接看開發組件用的 Demo http://router-view.mvc-works.org/

不足

最主要的問題是 Model 和 View 分離以後, 封裝特殊邏輯不方便了
如今 Store 和 Component 當中分別有 router 代碼, 不少須要手寫
具體我在下面展開:

首先是路由的嵌套寫法問題, 原本 router-view 是帶來了好處的
由於路由狀態是用數據存儲的, 任意深度或者奇怪的嵌套都能寫
只須要在想判斷的 render 代碼里加上 switch, 後面就輕鬆實現了
事實上越是複雜的路由, switch 就會越長, 對可讀性有不小的影響
特別是和 react-router 的聲明式寫法相比. 還好, 只是觀感的差異

其次是初次加載, 或者切換時, 自動計算路由結果的問題
對比 react-router 直接聲明, 在 router-view 裏很差作
由於初始化時須要把地址欄的信息翻譯到 Store 的對象上去
這中間存在一些囉嗦的代碼, 並且爲了 Store 獨立, 不能隨意抽象

前面主要是影響代碼風格和長度, 其實還有渲染的問題
更明確地說是瀏覽器處理機制的影響, 就是 popstate 事件不能取消
https://developer.mozilla.org/en-US/docs/Web/Events/popstate
想象一下, 簡聊經過後退按鈕切換話題, 中間可能須要抓取對應數據
爲了界面顯示準確, 咱們用在先發請求抓取數據, 完成後切換路由渲染頁面
然而瀏覽器默認行爲是點擊後退直接改路由, 這就致使了狀態不一致
可能出現的問題是路由出現屢次的切換, 破壞掉歷史記錄的機制
無奈只能加 skipRendering 參數, 在加載過程容許界面狀態不一致

另外還有個意外的 Hash 地址的問題, 也算實現上
基於 Hash 的路由事件, 除了不能取消, 甚至 JavaScript 代碼都能觸發事件
因而須要在加載過程中屏蔽掉地址欄回調事件.. 總之很奇怪
但我想這個問題過於冷僻, 應該不多有人會遇到了

總結

從總體效果看, router-view 是比 react-router 控制起開更靈活的
然而從可維護性上, 加上是本身從頭實現的, 至關不完善
我推薦看我文章的同窗嘗試跑 Demo, 觀察單向數據流是怎樣運做的
可是想在生成環境用, 至少先看懂組件不到兩百行的代碼
addressbar.coffee 是組件部分, path 是路由的匹配邏輯

我認爲 React 應用的核心就是單向數據流怎樣設計以及, 全部的 Store 的部分, 全部的 View 的部分, 怎樣契合這套數據流梳理清楚單向數據流以後, 路由就是瀏覽器實現很差對付的特例罷了

相關文章
相關標籤/搜索