react-router

靜態路由和動態路由

react-router v4是一個很是大的版本改動,具體體如今從「靜態路由」到「動態路由」的轉變上。通常將「靜態路由」看做一種配置,當啓動react項目時,會先生成好一個路由表,發生頁面跳轉時,react會根據地址到路由表中找到對應的處理頁面或處理方法。而動態路由不是做爲一個項目運行的配置文件存儲在外部,它在項目render的時候纔開始定義,router的做者認爲route應當和其它普通組件同樣,它的做用不是提供路由配置,而是一個普通的UI組件。而這也符合react的開發思想——一切皆組件。 因爲我本身對以前版本的路由瞭解很少,這裏就不作比較了,有興趣的小夥伴能夠本身去了解一下。這裏引一段router做者爲何要作這樣大的改動的解釋:html

To be candid, we were pretty frustrated with the direction we’d taken React Router by v2. We (Michael and Ryan) felt limited by the API, recognized we were reimplementing parts of React (lifecycles, and more),
and it just didn’t match the mental model React has given us for composing UI.
We ended up with API that wasn’t 「outside」 of React, an API that composed, or naturally fell into place, 
with the rest of React.
坦率地說,咱們對於以前版本的Route感到十分沮喪,我和個人小夥伴意識到咱們在從新實現react的部分功能,
好比生命週期和其它更多的,可是這一點都不符合react的模型設計(UI組件)。
咱們真正想要開發出的不是脫離了react的API ,而是一個自己就屬於react一部分的API.這纔是咱們想要的route(英語功底太差,你們將就着看吧)
——引自react-router的做者

複製代碼

在Web前端開發中,咱們常常會須要處理頁面路由問題。習慣上,路由信息會在一個地方集中配置好,咱們能夠稱之爲「靜態路由」,或者叫「中心化式路由」。以react-router v3版本爲例,代碼相似下面這樣:前端

import { Router, Route, IndexRoute, browserHistory } from 'react-router'

const App = () => (
  <Router history={browserHistory}>
    <Route path="/" component={RootPage}>
      <IndexRoute component={HomePage} />
      <Route path="/users" component={UsersPage} />
    </Route>
  </Router>
)

render(<App />, document.getElementById('app'))


複製代碼

能夠看到,在程序的頂層組件上配置好了全部路由信息,並經過嵌套關係體現不一樣的層次。可是,react-router v4版本進行了革命性的改動,使之更加符合React的「組件化」思想,咱們能夠稱之爲「動態路由」,或者借用區塊鏈中的術語,稱之爲「去中心化路由」。用v4版本改寫後的代碼相似於下面這樣:node

import { BrowserRouter, Route } from 'react-router-dom'

const App = () => (
  <BrowserRouter>
    <RootPage />
  </BrowserRouter>
)

const RootPage = () => (
  <div>
    <Route path="/" exact component={HomePage} />
    <Route path="/users" component={UsersPage} />
  </div>
)

render(<App />, document.getElementById('app'))


複製代碼

能夠發現,路由的配置再也不是所有位於頂層組件中了,而是分散在不一樣的組件中,經過組件的嵌套關係來實現路由的層次。另外,和靜態路由事先定義好全部頁面不一樣,動態路由能夠在渲染時根據路徑匹配結果,動態決定渲染哪些組件,這樣就能夠充分實現頁面的複用,減小重複渲染。react

安裝

正如我前面所說,對於web應用,咱們只須要安裝react-router-dom:webpack

不過在node_modules下你依然會看到react-router的身影,這是react-router-dom依賴的包,另外還有一個history包,這個下面會提到。nginx

<Router>git

<Router>是實現路由最外層的容器,通常狀況下咱們再也不須要直接使用它,而是使用在它基礎之上封裝的幾個適用於不一樣環境的組件,react-router-dom的Router有四種:github

通常咱們不多會用到<MemoryRouter>和<StaticRouter>,在web應用中更多的是用react-router-dom擴展出來的<BrowserRouter>和<HashRouter>,這兩個就是我前面提到的前端路由的兩種解決辦法的各自實現。web

爲了避免被後面的一些配置弄迷糊,咱們從<Router>的實現源碼來看看路由到底傳了些什麼東西。算法

router.js

class Router extends React.Component {
  //檢測接收的參數
  static propTypes = {
    history: PropTypes.object.isRequired, //必須傳入
    children: PropTypes.node
  }

  //設置傳遞給子組件的屬性
  getChildContext() {
    return {
      router: {
        ...this.context.router, 
        history: this.props.history, //核心對象
        route: {
          location: this.props.history.location, //history裏的location對象
          match: this.state.match //當路由路徑和當前路徑成功匹配,一些有關的路徑信息會存放在這裏,嵌套路由會用到它。
        }
      }
    }
  }
    state = {
      match: this.computeMatch(this.props.history.location.pathname)
    }

  computeMatch(pathname) {
    return {
      path: '/',
      url: '/', 
      params: {}, //頁面間傳遞參數
      isExact: pathname === '/'
    }
  }
}
複製代碼

這裏面最重要的就是須要咱們傳入的history對象,我前面提到過咱們通常不會直接使用<Router>組件,由於這個組件要求咱們手動傳入history對象,但這個對象又很是重要,並且不一樣的開發環境須要不一樣的history,因此針對這種狀況react-router才衍生了兩個插件react-router-dom和react-router-native(我認爲這是比較重要的緣由,瀏覽器有一個history對象,因此web應用的路由都是在此對象基礎上擴展的)。 接着讓咱們來看一下react-router-dom用到的來自history的兩個方法:

  • createBrowserHistory 適用於現代瀏覽器(支持h5 history API)

  • createHashHistory 適用於須要兼容老版本瀏覽器的狀況

這兩個方法就分別對應了兩個組件:<BrowserRouter>和<HashRouter>,它倆返回的history對象擁有的屬性是同樣的,可是各自的實現不一樣。

//createHashHistory.js
var HashChangeEvent = 'hashchange'; //hash值改變時會觸發該事件
var createHashHistory = function createHashHistory() {
  var globalHistory = window.history; //全局的history對象
  var handleHashChange = function handleHashChange() {} //hash值變化時操做的方法
}
//createBrowserHistory.js
var PopStateEvent = 'popstate'; //監聽url的變化事件
var HashChangeEvent = 'hashchange'; //依然監聽了hash改變的事件,可是多加了一個判斷是是否須要監聽hash改變,若是不須要就不綁定該事件。
var createBrowserHistory = function createBrowserHistory() {
  var globalHistory = window.history; //全局的history對象
  var handlePop = function handlePop(location) {} //出棧操做
}

//createHashHistory.js,createBrowserHistory.js導出的history對象
const history = {
    length: globalHistory.length, //globalHistory就是window.history
    action: "POP", //操做歷史狀態都屬於出棧操做
    location: initialLocation, //最重要的!!前面的Router.js源碼向子組件單獨傳遞了這個對象,由於路由匹配會用到它。
    createHref, //生成的url地址樣式,若是是hash則加一個'#'
    push, //擴展history.pushState()方法
    replace, //擴展history.replaceState()方法
    go, //history.go()方法
    goBack, //history.back()方法
    goForward, //history.forward()方法
    block,
    listen
}
複製代碼

咱們從控制檯打印一下看看這個history:

因此,咱們直接用<BrowserRouter>與使用<Router>搭配createBrowserHistory()方法是同樣的效果。

import {
    Router,
} from 'react-router-dom'
import createBrowserHistory from 'history/createBrowserHistory';

const history = createBrowserHistory();

const App = () => (
    <Router history={history}>
        <div>{/*其它*/}</div>
    </Router>
)

複製代碼

就等於:

import {
    BrowserRouter,
} from 'react-router-dom'

const App = () => (
    <BrowserRouter>
        <div>{/*其它*/}</div>
    </BrowserRouter>
)

複製代碼

<BrowserRouter>和<HashRouter>使用注意點

<HashRouter>生成的url路徑看起來是這樣的:

http://localhost:8080/#/user
複製代碼

咱們知道hash值是不會傳到服務器端的,因此使用hash記錄狀態不須要服務器端配合,可是<BrowserRouter>生成的路徑是這樣的:

http://localhost:8080/user
複製代碼

這時候在此目錄下刷新瀏覽器會從新向服務器發起請求,服務器端沒有配置這個路徑,因此會出現can't GET /user這種錯誤,而解決方法就是,修改devServer的配置(前面咱們配置了熱替換,其實就是用webpack-dev-server搭了一個本地服務器): webpack.config.js

devServer: {
        publicPath: publicPath,
        contentBase: path.resolve(__dirname, 'build'),
        inline: true,
        hot: true,  
        historyApiFallback: true, //增長
    },
複製代碼

例子

import React from 'react'
import { Router, Route, Link } from 'react-router'

const App = React.createClass({
  render() {
    return (
      <div>
        <h1>App</h1>
        <ul>
          <li><Link to="/about">About</Link></li>
          <li><Link to="/inbox">Inbox</Link></li>
        </ul>
        {this.props.children}
      </div>
    )
  }
})

const About = React.createClass({
  render() {
    return <h3>About</h3>
  }
})

const Inbox = React.createClass({
  render() {
    return (
      <div>
        <h2>Inbox</h2>
        {this.props.children || "Welcome to your Inbox"}
      </div>
    )
  }
})

const Message = React.createClass({
  render() {
    return <h3>Message {this.props.params.id}</h3>
  }
})

React.render((
  <Router>
    <Route path="/" component={App}>
      <Route path="about" component={About} />
      <Route path="inbox" component={Inbox}>
        <Route path="messages/:id" component={Message} />
      </Route>
    </Route>
  </Router>
), document.body)
複製代碼

添加首頁

想象一下當 URL 爲 / 時,咱們想渲染一個在 App 中的組件。不過在此時,App 的 render 中的 this.props.children 仍是 undefined。這種狀況咱們可使用 IndexRoute 來設置一個默認頁面。

import { IndexRoute } from 'react-router'

const Dashboard = React.createClass({
  render() {
    return <div>Welcome to the app!</div>
  }
})

React.render((
  <Router>
    <Route path="/" component={App}>
      {/* 當 url 爲/時渲染 Dashboard */}
      <IndexRoute component={Dashboard} />
      <Route path="about" component={About} />
      <Route path="inbox" component={Inbox}>
        <Route path="messages/:id" component={Message} />
      </Route>
    </Route>
  </Router>
), document.body)
複製代碼

如今,App 的 render 中的 this.props.children 將會是 <Dashboard>這個元素。這個功能相似 Apache 的DirectoryIndex 以及 nginx的 index指令,上述功能都是在當請求的 URL 匹配某個目錄時,容許你制定一個相似index.html的入口文件。

讓 UI 從 URL 中解耦出來

若是咱們能夠將 /inbox 從 /inbox/messages/:id 中去除,而且還可以讓 Message 嵌套在 App -> Inbox 中渲染,那會很是贊。絕對路徑可讓咱們作到這一點。

React.render((
  <Router>
    <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>
    </Route>
  </Router>
), document.body)
複製代碼

在多層嵌套路由中使用絕對路徑的能力讓咱們對 URL 擁有絕對的掌控。咱們無需在 URL 中添加更多的層級,從而可使用更簡潔的 URL。

兼容舊的 URL

等一下,咱們剛剛改變了一個 URL! 這樣很差。 如今任何人訪問 /inbox/messages/5 都會看到一個錯誤頁面。:(

不要擔憂。咱們可使用 <Redirect> 使這個 URL 從新正常工做。

import { Redirect } from 'react-router'

React.render((
  <Router>
    <Route path="/" component={App}>
      <IndexRoute component={Dashboard} />
      <Route path="about" component={About} />
      <Route path="inbox" component={Inbox}>
        <Route path="/messages/:id" component={Message} />

        {/* 跳轉 /inbox/messages/:id 到 /messages/:id */}
        <Redirect from="messages/:id" to="/messages/:id" />
      </Route>
    </Route>
  </Router>
), document.body)
複製代碼

如今當有人點擊 /inbox/messages/5 這個連接,他們會被自動跳轉到 /messages/5。 :raised_hands:

進入和離開的Hook

Route 能夠定義 onEnter 和 onLeave 兩個 hook ,這些hook會在頁面跳轉確認時觸發一次。這些 hook 對於一些狀況很是的有用,例如權限驗證或者在路由跳轉前將一些數據持久化保存起來。

在路由跳轉過程當中,onLeave hook 會在全部將離開的路由中觸發,從最下層的子路由開始直到最外層父路由結束。而後onEnter hook會從最外層的父路由開始直到最下層子路由結束。

繼續咱們上面的例子,若是一個用戶點擊連接,從 /messages/5 跳轉到 /about,下面是這些 hook 的執行順序:

  • /messages/:id 的 onLeave

  • /inbox 的 onLeave

  • /about 的 onEnter

替換的配置方式

由於 route 通常被嵌套使用,因此使用 JSX 這種自然具備簡潔嵌套型語法的結構來描述它們的關係很是方便。然而,若是你不想使用 JSX,也能夠直接使用原生 route 數組對象。

上面咱們討論的路由配置能夠被寫成下面這個樣子:

const routeConfig = [
  { path: '/',
    component: App,
    indexRoute: { component: Dashboard },
    childRoutes: [
      { path: 'about', component: About },
      { path: 'inbox',
        component: Inbox,
        childRoutes: [
          { path: '/messages/:id', component: Message },
          { path: 'messages/:id',
            onEnter: function (nextState, replaceState) {
              replaceState(null, '/messages/' + nextState.params.id)
            }
          }
        ]
      }
    ]
  }
]

React.render(<Router routes={routeConfig} />, document.body)
複製代碼

路由匹配原理

路由擁有三個屬性來決定是否「匹配「一個 URL:

  • 嵌套關係

  • 路徑語法

  • 優先級

嵌套關係

React Router 使用路由嵌套的概念來讓你定義 view 的嵌套集合,當一個給定的 URL 被調用時,整個集合中(命中的部分)都會被渲染。嵌套路由被描述成一種樹形結構。React Router 會深度優先遍歷整個路由配置來尋找一個與給定的 URL 相匹配的路由。

路徑語法

路由路徑是匹配一個(或一部分)URL 的 一個字符串模式。大部分的路由路徑均可以直接按照字面量理解,除了如下幾個特殊的符號:

  • :paramName – 匹配一段位於 /、? 或 # 以後的 URL。 命中的部分將被做爲一個參數
  • () – 在它內部的內容被認爲是可選的
    • – 匹配任意字符(非貪婪的)直到命中下一個字符或者整個 URL 的末尾,並建立一個 splat 參數
<Route path="/hello/:name">         // 匹配 /hello/michael 和 /hello/ryan
<Route path="/hello(/:name)">       // 匹配 /hello, /hello/michael 和 /hello/ryan
<Route path="/files/*.*">           // 匹配 /files/hello.jpg 和 /files/path/to/hello.jpg
複製代碼

若是一個路由使用了相對路徑,那麼完整的路徑將由它的全部祖先節點的路徑和自身指定的相對路徑拼接而成。使用絕對路徑可使路由匹配行爲忽略嵌套關係。

優先級

最後,路由算法會根據定義的順序自頂向下匹配路由。所以,當你擁有兩個兄弟路由節點配置時,你必須確認前一個路由不會匹配後一個路由中的路徑。例如,千萬不要這麼作:

<Route path="/comments" ... />
<Redirect from="/comments" ... />
複製代碼
相關文章
相關標籤/搜索