聊聊 React Router v4 的設計思想

React Router v4 發佈已經有幾個月了,但好像並沒有得到太多人的青睞,大家(包括我們團隊自己)還是習慣使用v2、v3版本。這一方面是因爲v4版本是一次破壞性的升級,從v2、v3 升級到v4,必需要大量重寫原有的路由相關的代碼,對於已經穩定的項目,一般是不會輕易嘗試這種變更的;另一方面,即使是新項目,很多開發者也依然選擇使用v2、v3老版本,因爲v4新的設計思想,意味着你必須改變原有的使用路由的思維,才能正確的使用新版本。

React Router v4 最大的變更,不是API的變更,而是從靜態路由到動態路由的變化。什麼是靜態路由呢?靜態路由是一堆在應用運行前就已經定義好的路由配置,應用需要在啓動時,加載這些配置,構建出整個應用的路由表,然後當接收到某一請求時,根據請求地址,到應用路由表中找到對應的處理頁面或處理方法。不管是前端開發,還是後端開發,只要涉及到路由,大部分情況下,其實我們使用的都是靜態路由。例如,React Router v3版本中,我們會配置一個類似下面形式的路由:

<Router history={browserHistory}>
  <Route path='/' component={App}>
    <Route path='about' component={About}>
    <Route path='contact' component={Contact}>
    // ...
  </Route>
</Router>

它的基本工作流程是:Router組件根據所有的子組件Route,生成全局的路由表,路由表中記錄了path與UI組件的映射關係,然後Router監聽path的變化,當path變化時,根據新的path,找出對應所需的所有UI組件,按一定層級將這些UI組件渲染出來。

對於已經很熟悉靜態路由使用方式的開發者來說,上面的工作流程顯得很自然,理解起來也毫不費力。既然如此,React Router的作者爲什麼還要把這一切推翻呢?原因是React Router不是普通的Router,它是“React”的Router。React致力於提供一個高效簡潔的組件化方案,組件就是React的核心,在React的設計思想中,一切皆是組件。那麼什麼是組件呢?組件定義的是界面上一個區域的UI及UI的交互行爲,關注點是UI。現在讓我們回頭來看看上面靜態路由的例子,是不是感覺到什麼奇怪的地方呢?雖然Route形式上是React組件,但它其實與UI無任何關係,它只是披着React組件的外衣,提供了一條路由配置項而已。我們也可以從Route源碼中看出這一點:

const Route = createReactClass({
  // 省略無關代碼

  /* istanbul ignore next: sanity check */
  render() {
    invariant(
      false,
      '<Route> elements are for router configuration only and should not be rendered'
    )
  }

})

Route的render方法中,沒有做任何UI渲染相關的工作,這確實不是一個正宗的React組件。當然你也可以用React Router的另一種配置路由的方式:

const routes = {
  path: '/',
  component: App,
  childRoutes: [
    {
      path: 'about',
      component: About,
    },
    {
      path: 'contact',
      component: Contact,
    },
    // ...
  ]
}

<Router history={browserHistory} routes={routes} />

現在你又可以理直氣壯的說,我沒有使用Route這個僞組件了,這次和React的設計思想沒有衝突了吧?好吧,讓我們再來看看其他部分。React Router v3提供了很多類似生命週期方法的API,例如onEnter, onUpdate, and onLeave ,用來爲處於不同階段的路由提供鉤子方法。但是,請不要忘了,React組件本身已經有一套很完善的生命週期方法了,如果一個Route就是一個組件,那麼我們完全可以直接利用組件的生命週期方法,來作爲路由不同階段的鉤子方法。例如,我們可以使用componentDidMount 或 componentWillMount替代onEnter,使用 componentDidUpdate或 componentWillUpdate 替代onUpdate,使用componentWillUnmount替代onLeave。

React Router v2、v3的問題,是在React組件思想之外,設計了一套API,是一種侵入式的設計。React Router的作者意識到了這個問題,所以在v4中,對React Router 進行了重寫,將Route作爲普通React組件看待,每個Route也負責UI的渲染工作,讓React Router在React的大框架下運轉。我們用v4版本實現上面的例子:

<BrowserRouter>
  <div>
    <Route path='/' component={App} />
    <Route path={'/about} component={About} />
    <Route path={'/contact'} component={Contact} />
  </div>
</BrowserRouter>

但從表面上看,並不能很直觀地看出Route工作機制的變化。這裏做一簡單說明:Route的作用不是提供路由配置,而是一個普通的UI組件,不管請求的路徑是什麼,Route組件總是會被渲染,只不過在Route內部會判斷請求路徑是否與當前的path匹配,如果匹配,就會把Route component屬性指向的組件作爲子組件渲染出來,如果不匹配,會渲染一個null。可以從新版Route 的render方法源碼中印證這個流程:

class Route extends React.Component {
  //...省略無關代碼

  render() {
    const { match } = this.state
    const { children, component, render } = this.props
    const { history, route, staticContext } = this.context.router
    const location = this.props.location || route.location
    const props = { match, location, history, staticContext }

    return (
      component ? ( // component prop gets first priority, only called if there's a match
        match ? React.createElement(component, props) : null
      ) : render ? ( // render prop is next, only called if there's a match
        match ? render(props) : null
      ) : children ? ( // children come last, always called
        typeof children === 'function' ? (
          children(props)
        ) : !Array.isArray(children) || children.length ? ( // Preact defaults to empty children array
          React.Children.only(children)
        ) : (
          null
        )
      ) : (
        null
      )
    )
  }
}

這種模式的路由就是動態路由。可見,動態路由發揮作用的時間是在組件渲染時,而不是通過提前配置的方式,在應用剛收到請求時,就已經知道該渲染哪些組件了。

從上面的分析,可以得出動態路由的一個優點是,它會同時負責UI的渲染工作,而不是單純的路由配置工作。此外,動態路由的另外一個優點是,你可以在任意時間、任意地點自由添加新的Route。例如,在上面的例子中,我想在About組件內定義兩個新的路由,可以這麼做:

<BrowserRouter>
  <div>
    <Route path='/' component={App} />
    <Route path={'/about} component={About} />
    <Route path={'/contact'} component={Contact} />
  </div>
</BrowserRouter>

const About = (props) => (
  <div>
    <Route path={`${props.match.url}/a`} component={AboutA} />
    <Route path={`${props.match.url}/b`} component={AboutB} />
  </div>
)

這樣,當訪問 /about/a 時,組件AboutA 會被作爲About的子組件渲染,當訪問 /about/b 時,組件AboutB 會作爲About的子組件渲染。而且,/about/a 和 /about/b 我們是直接定義到 About 組件內的,並不需要像靜態路由那樣做集中配置,充分體現了動態路由的靈活性。

總結一下,雖然React Router v4 重構了路由使用的思想,但卻和React的設計思想更加切合,個人認爲是一個巨大的進步。使用React Router v4 時,你需要忘掉以前使用靜態路由的思維方式,把路由當成普通組件看待,習慣了這個思維轉變後,你就會發現React Router v4的魅力所在了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章