聊聊 React Router v4 的設計思想

艾特老幹部發表於2017-08-06

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的魅力所在了。


歡迎關注我的公眾號:老幹部的大前端,領取21本大前端精選書籍!

聊聊 React Router v4 的設計思想

相關文章