React 全家桶之 react-router

傳統的多頁模式

後端控制路由

在以前我們採用的都是一個 URL 對應一個 html 頁面的方式,由後端或者服務器去做路由控制。當一個 URL 找不到對應的頁面時就會返回 404.

單頁模式(single page application,簡稱SPA)

單頁應用現在是越來越流行了,單頁應用和傳統的多頁應用相比,前端只有一個頁面。在不刷新頁面的前提下,通過監聽 URL 的變化來渲染對應的 UI ,以此來實現多頁的功能。

前端控制路由

如果使用的是 browserHistory,這裏需要注意一個問題,我們在進入某個路由下的時候,頁面刷新就會 404。這是因爲服務器上只有一個 index.html,而你當前訪問的比如 /home 在服務器上根本就找不到。所以需要讓後端或者服務器把所有匹配不到的路徑都指向這個 index.html,然後讓前端 js 來控制路由。如 nginx 中可以這樣配置

server {
  ...
  location / {
    try_files $uri /index.html
  }
}

爲了實現 URL 變化而頁面不刷新,可以有兩種方式:

  1. 通過hash來實現,如 http://www.mysite.com#666。hash 的變化是不會引起瀏覽器刷新的,可以在頁面 onload 的時候獲取到location.hash,找到對應的 UI 模塊渲染到頁面上。另外還要 onhashchange 監聽下 hash 的變化,來更新 UI。
  2. 通過 h5 提供的 history 來實現, 如 http://www.mysite.com/666。可以在頁面 onload 的時候獲取到 location.pathname,根據自己的規則找到對應的 UI 模塊渲染到頁面上。然後通過 pushState、replaceState 和 popState 實現前進和後退(這兩位仁兄也不會引起瀏覽器刷新),再監聽一下 popState 事件來處理 UI 層的更新

history 介紹

history 是一個 JavaScript 庫,可讓您在 JavaScript 運行的任何地方輕鬆管理會話歷史記錄。history 抽象出各種環境中的差異,並提供最小的 API ,使您可以管理歷史堆棧,導航,確認導航以及在會話之間保持狀態。

history 有三種實現方式:

  1. BrowserHistory:用於支持 HTML5 歷史記錄 API 的現代 Web 瀏覽器(請參閱跨瀏覽器兼容性)
  2. HashHistory:用於舊版Web瀏覽器
  3. MemoryHistory:用作參考實現,也可用於非 DOM 環境,如 React Native 或測試

react-router 的基本原理

這老兄其實就是一些 react 組件的集合,作用就是實現 URL 和 UI 的同步,內部是基於 history.js 來實現的瀏覽器歷史記錄管理。它包括了一下幾個核心部分:

  • react-router,路由核心內容,Rrouter、Rroute、Redirect、withRouter都在這裏
  • react-router-dom,基於 react-router,針對瀏覽器做的一些封裝,如 Link 組件
  • react-router-native,基於 react-router,針對 ReactNative 做的一些封裝 

幾個核心的組件的作用

  • Router,包裹着 Route 組件,維護這一張路由與組件映射關係的路由表
  • Route,描述了每條路由與組件的匹配規則
  • Link,最終會被編譯成<a>標籤,它的 to、query、hash屬性會被組合在一起並做爲 href 屬性

我們通過一個小例子來庖丁解牛一下其中原理

import { browserHistory } from 'react-router'
React.render((
  <Router history={ browserHistory }>
    <Route path="/" component={App}>
      <IndexRoute component={Dashboard} />
      <Route path="about" component={About} />
      <Route path="inbox" component={Inbox}>
        {/* 使用 /messages/:id 替換 messages/:id */}
        <Route path="/messages/:id" component={Message} />
      </Route>
      <Link to="/about">About</Link>
      <Link to="/inbox">Inbox</Link>
    </Route>
  </Router>
), document.body)

下面我們一個一個看

Router

render() {
    return (
      <RouterContext.Provider
        children={this.props.children || null}
        value={{
          history: this.props.history,
          location: this.state.location,
          match: Router.computeRootMatch(this.state.location.pathname),
          staticContext: this.props.staticContext
        }}
      />
    );
  }

Router 組件是基於 RouterContext 來實現的,也就是 react 中的上下文對象 context,只不過這個 context 是利用 'mini-create-react-context' 這個包去創建的,和原生的 context 用法有些不同,但是目的都是讓數據可以跨組件縱向傳遞。

Provider 是生產數據的地方,這裏會放入 history、location、match 三個對象,以便子孫組件可以方便的使用

Route

render() {
    return (
      <RouterContext.Consumer>
        {context => {
          invariant(context, "You should not use <Route> outside a <Router>");

          const location = this.props.location || context.location;
          const match = this.props.computedMatch
            ? this.props.computedMatch // <Switch> already computed the match for us
            : this.props.path
            ? matchPath(location.pathname, this.props)
            : context.match;

          const props = { ...context, location, match };

          let { children, component, render } = this.props;

          // Preact uses an empty array as children by
          // default, so use null if that's the case.
          if (Array.isArray(children) && children.length === 0) {
            children = null;
          }

          return (
            <RouterContext.Provider value={props}>
              {props.match
                ? children
                  ? typeof children === "function"
                    ? __DEV__
                      ? evalChildrenDev(children, props, this.props.path)
                      : children(props)
                    : children
                  : component
                  ? React.createElement(component, props)
                  : render
                  ? render(props)
                  : null
                : typeof children === "function"
                ? __DEV__
                  ? evalChildrenDev(children, props, this.props.path)
                  : children(props)
                : null}
            </RouterContext.Provider>
          );
        }}
      </RouterContext.Consumer>
    );
  }

Link

return (
      <RouterContext.Consumer>
        {context => {
          invariant(context, "You should not use <Link> outside a <Router>");

          const { history } = context;

          const location = normalizeToLocation(
            resolveToLocation(to, context.location),
            context.location
          );

          const href = location ? history.createHref(location) : "";
          const props = {
            ...rest,
            href,
            navigate() {
              const location = resolveToLocation(to, context.location);
              const method = replace ? history.replace : history.push;

              method(location);
            }
          };

          // React 15 compat
          if (forwardRefShim !== forwardRef) {
            props.ref = forwardedRef || innerRef;
          } else {
            props.innerRef = innerRef;
          }

          return React.createElement(component, props);
        }}
      </RouterContext.Consumer>
    );

<Link> 組件最終會被轉譯成 <a> 標籤,然後給  <a> 標籤加一個 click 事件,並組織 <a> 標籤的默認行爲(跳轉),然後執行 history.push(to) 或 history.replace(to) 來實現跳轉。

withRouter

return (
      <RouterContext.Consumer>
        {context => {
          invariant(
            context,
            `You should not use <${displayName} /> outside a <Router>`
          );
          return (
            <Component
              {...remainingProps}
              {...context}
              ref={wrappedComponentRef}
            />
          );
        }}
      </RouterContext.Consumer>
    );

withRouter 是一個高階組件,它的入參是一個組件假設爲 A 。經過它封裝之後返回一個新的組件,這個新組件會把 history、location、match 三個對象當做屬性傳遞給組件 A。這也是爲什麼我們使用 withRouter 包裝之後的組件,可以在內部使用 props.xxx 調用這三者的原因。

那麼 withRouter 這個高階組件又是從哪裏獲取的這三個對象呢?

還記得之前在 Router 裏,使用 Provider 提供的這三個對象嗎? 這裏使用了 Consumer 來消費數據,其實就是通過 react 的上下文對象 context 來跨組件傳遞數據的。

問題

<Link>標籤和<a>標籤有什麼區別

react-router:只更新變化的部分從而減少DOM性能消耗

而 <a> 標籤是整個頁面刷新,重新渲染

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