React系列---React-Router

React-Router

React官方維護的React-Router能夠幫咱們建立React單頁應用。經過管理URL,實現組件的切換和狀態的變化。
clipboard.pnghtml

本文React-Router是3.x版,與最新的4.x版不兼容。目前,官方同時維護3.x和4.x兩個版本,因此前者依然能夠用在項目中。react

本文代碼能夠從這裏獲取:https://github.com/zhutx/reac...webpack

SPA

在不一樣「頁面」以前切換,但感知不到刷新,只是局部更新,這種看起來多頁面而實際只有一個頁面的應用,被稱爲「單頁應用」(Single Page Appliaction)。git

傳統多頁面實現方式,每次切換界面,瀏覽器都會發起一個HTTP請求到服務端,服務端返回HTML,瀏覽器解析HTML,再根據解析結果繼續下載其餘好比JavsScript,CSS,圖片等資源。github

缺點很明顯:
1) 切換頁面伴隨着瀏覽器刷新,用戶體驗很很差。
2) 不一樣頁面之間每每有共同點,當在頁面之間切換的時候,最終結果只是局部的內容變化,卻要刷新整個網頁。web

路由定義

React-Router提供了兩個組件來完成路由功能,一個是Router,整個應用只須要一個實例,表明着路由表。另一個是Route,能夠有多個實例,每一個Route表明着一個URL的路由規則。npm

安裝react-router依賴包:redux

npm install react-router@3.0.5 --save

建立3個組件頁,Home、About、NotFound,代碼都同樣,只是顯示不一樣。api

Home組件,src/component/Home.jsx:瀏覽器

import React from 'react';

const Home = () => {
    return (
      <div>Home</div>
    );
};

export default Home;

定義路由組件,src/routes/index.jsx:

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

import Home from '../component/Home';
import About from '../component/About';
import NotFound from '../component/NotFound';

const Routes = () => {
    return (
      <Router history={browserHistory}>
        <Route path="home" component={Home} />
        <Route path="about" component={About} />
        <Route path="*" component={NotFound} />
      </Router>
    );
};

export default Routes;

路由表Router中包含3個Route路由規則:路徑home路由到Home組件,路徑about路由到About組件,其餘路徑則統一路由到NotFound組件。因爲路由規則按定義的順序匹配,所以NotFound組件對應的路由規則,必須放最後面。

路由表的history屬性,監聽瀏覽器地址欄的變化,並將URL解析成一個地址對象,供路由表去匹配對應的路由規則。

history屬性,一共能夠設置三種值:
1) hashHistory:
路由將經過URL的hash部分(#)切換,URL的形式相似example.com/#/some/path。

2) browserHistory:
瀏覽器的路由就再也不經過Hash完成了,而顯示正常的路徑example.com/some/path,背後調用的是瀏覽器的History API。注意,這種狀況會向服務器請求某個子路由,會顯示網頁找不到的404錯誤。若是開發服務器使用的是webpack-dev-server,加上--history-api-fallback參數就能夠了。

$ webpack-dev-server --inline --content-base . --history-api-fallback

3) createMemoryHistory:
主要用於服務器渲染。它建立一個內存中的history對象,不與瀏覽器URL互動。

const history = createMemoryHistory(location)

應用入口引入路由組件,實現路由效果。src/index.jsx:

import React from 'react';
import ReactDOM from 'react-dom';
import Routes from './routes';

const app = document.createElement('div');
document.body.appendChild(app);
ReactDOM.render(<Routes />, app);

路由連接

路由連接經常使用於應用內欄目導航。React-Router提供Link組件來支持路由連接,避免了使用HTML的<a/>連接元素,由於<a/>連接在點擊時默認行爲是網頁跳轉,這並非SPA應該有的行爲。

Link組件產生HTML連接元素,可是對這個連接元素的點擊操做並不引發網頁跳轉,而是被Link截獲,把目標路徑發送給Router路由表,這樣Router就知道匹配哪一個路由規則,從而顯示對應的組件。

路由連接示例組件,src/component/Nav.jsx:

import React from 'react';
import { Link } from 'react-router';

const Nav = () => {
    return (
      <div>
        <ul>
          <li><Link to="/home">Home</Link></li>
          <li><Link to="/about">About</Link></li>
        </ul>
      </div>
    );
};

export default Nav;

注意Link組件的to元素的路徑前面有個「/」符號,表明從根路徑開始匹配。

有了頂欄Nav組件以後,能夠在Home、About和NotFound組件中引用這個Nav組件,但這種修改每一個頁面文件的方式很笨拙。未來若是要用另一個組件替換Nav,又要修改每一個頁面。

React-Router提供的路由嵌套功能,能夠很方便的解決這個問題。

路由嵌套

咱們定義一個新的React組件App,只讓這個組件包含Nav組件。
App組件示例:src/compoments/App.js:

import React from 'react';
import Nav from './Nav';

const App = ({ children }) => {
    return (
      <div>
        <Nav />
        <div>{children}</div>
      </div>
    );
};

export default App;

修改路由組件src/routes/index.jsx,增長根路徑「/」的路由規則,並嵌套其餘路由規則:

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

import App from '../component/App';
import Home from '../component/Home';
import About from '../component/About';
import NotFound from '../component/NotFound';

const Routes = () => {
    return (
      <Router history={browserHistory}>
        <Route path="/" component={App}>
          <Route path="home" component={Home} />
          <Route path="about" component={About} />
          <Route path="*" component={NotFound} />
        </Route>
      </Router>
    );
};

export default Routes;

這時,假如用戶在瀏覽器中訪問http://localhost:8080/home,會根據路徑「/home」,匹配到path="/"的Route,再匹配到path="home"的Route。React-Router會渲染外層Route組件,即App組件,可是會把內層Route組件做爲children屬性傳遞給外層組件。因此在渲染App組件時,渲染children屬性也就把Home組件渲染出來了,最終效果是Nav和Home組件都顯示在頁面上。

路由嵌套的好處就是每一層Route只決定到這一層的路徑,而不是整個路徑,很是靈活。例如,咱們修改App的Route屬性path以下:

<Route path="/root" component={App}>

上面的Route規則把App的Route規則改成路徑「/root」,那麼,訪問Home的路徑就變成http://localhost:8080/root/home

而Home、About和NotFound的Route無需作任何修改,由於每一個Route只匹配本身這一層的路徑,當App已經匹配root部分以後,Home只須要匹配home部分。

默認路由

在路徑爲空時,應用也應該顯示有意義的內容,一般對應主頁內容。這裏,咱們但願路徑爲空時顯示Home組件。
React-Router提供的IndexRoute可實現本需求。IndexRoute表明一個Route下的默認路由。

<Router history={browserHistory}>
  <Route path="/" component={App}>
    <IndexRoute component={Home} />
    <Route path="home" component={Home} />
    <Route path="about" component={About} />
    <Route path="*" component={NotFound} />
  </Route>
</Router>

注意:IndexRoute是沒有path屬性的。

路徑通配符

path屬性可使用通配符。

<Route path="/hello/:name">
// 匹配 /hello/michael
// 匹配 /hello/ryan

<Route path="/hello(/:name)">
// 匹配 /hello
// 匹配 /hello/michael
// 匹配 /hello/ryan

<Route path="/files/*.*">
// 匹配 /files/hello.jpg
// 匹配 /files/hello.html

<Route path="/files/*">
// 匹配 /files/ 
// 匹配 /files/a
// 匹配 /files/a/b

<Route path="/**/*.jpg">
// 匹配 /files/hello.jpg
// 匹配 /files/path/to/file.jpg

通配符的規則以下。
(1):paramName
:paramName匹配URL的一個部分,直到遇到下一個/、?、#爲止。這個路徑參數能夠經過this.props.params.paramName取出。
(2)()
()表示URL的這個部分是可選的。
(3)*
*匹配任意字符,直到模式裏面的下一個字符爲止。匹配方式是非貪婪模式。
(4) **
** 匹配任意字符,直到下一個/、?、#爲止。匹配方式是貪婪模式。

path屬性也可使用相對路徑(不以/開頭),匹配時就會相對於父組件的路徑,能夠參考上一節的例子。嵌套路由若是想擺脫這個規則,可使用絕對路由。
路由匹配規則是從上到下執行,一旦發現匹配,就再也不其他的規則了。

<Route path="/comments" ... />
<Route path="/comments" ... />

上面代碼中,路徑/comments同時匹配兩個規則,第二個規則不會生效。
設置路徑參數時,須要特別當心這一點。

<Router>
  <Route path="/:userName/:id" component={UserPage}/>
  <Route path="/about/me" component={About}/>
</Router>

上面代碼中,用戶訪問/about/me時,不會觸發第二個路由規則,由於它會匹配/:userName/:id這個規則。所以,帶參數的路徑通常要寫在路由規則的底部。
此外,URL的查詢字符串/foo?bar=baz,能夠用this.props.location.query.bar獲取。

路由轉發

React-Router的Redirect組件用於路由的跳轉,即用戶訪問一個路由,會自動跳轉到另外一個路由。

<Route path="inbox" component={Inbox}>
  {/* 從 /inbox/messages/:id 跳轉到 /messages/:id */}
  <Redirect from="messages/:id" to="/messages/:id" />
</Route>

如今訪問/inbox/messages/5,會自動跳轉到/messages/5

根路由重定向

React-Router的IndexRedirect組件用於訪問根路由的時候,將用戶重定向到某個子組件。

<Route path="/" component={App}>
  <IndexRedirect to="/welcome" />
  <Route path="welcome" component={Welcome} />
  <Route path="about" component={About} />
</Route>

上面代碼中,用戶訪問根路徑時,將自動重定向到子組件welcome。

路由鉤子

每一個路由都有Enter和Leave鉤子,用戶進入或離開該路由時觸發。

<Route path="about" component={About} />
<Route path="inbox" component={Inbox}>
  <Redirect from="messages/:id" to="/messages/:id" />
</Route>

上面的代碼中,若是用戶離開/messages/:id,進入/about時,會依次觸發如下的鉤子。

/messages/:id的onLeave
/inbox的onLeave
/about的onEnter

下面是一個例子,使用onEnter鉤子替代<Redirect>組件。

<Route path="inbox" component={Inbox}>
  <Route
    path="messages/:id"
    onEnter={
      ({params}, replace) => replace(`/messages/${params.id}`)
    } 
  />
</Route>

onEnter鉤子還能夠用來作認證。

const requireAuth = (nextState, replace) => {
    if (!auth.isAdmin()) {
        // Redirect to Home page if not an Admin
        replace({ pathname: '/' })
    }
}
export const AdminRoutes = () => {
  return (
     <Route path="/admin" component={Admin} onEnter={requireAuth} />
  )
}

下面是一個高級應用,當用戶離開一個路徑的時候,跳出一個提示框,要求用戶確認是否離開。

const Home = withRouter(
  React.createClass({
    componentDidMount() {
      this.props.router.setRouteLeaveHook(
        this.props.route, 
        this.routerWillLeave
      )
    },

    routerWillLeave(nextLocation) {
      // 返回 false 會繼續停留當前頁面,
      // 不然,返回一個字符串,會顯示給用戶,讓其本身決定
      if (!this.state.isSaved)
        return '確認要離開?';
    },
  })
)

上面代碼中,setRouteLeaveHook方法爲Leave鉤子指定routerWillLeave函數。該方法若是返回false,將阻止路由的切換,不然就返回一個字符串,提示用戶決定是否要切換。

表單操做的路由處理

Link組件用於正常的用戶點擊跳轉,可是有時還須要表單跳轉、點擊按鈕跳轉等操做。這些狀況怎麼跟React Router對接呢?
下面是一個表單。

<form onSubmit={this.handleSubmit}>
  <input type="text" placeholder="userName"/>
  <input type="text" placeholder="repo"/>
  <button type="submit">Go</button>
</form>

第一種方法是使用browserHistory.push

import { browserHistory } from 'react-router'

// ...
  handleSubmit(event) {
    event.preventDefault()
    const userName = event.target.elements[0].value
    const repo = event.target.elements[1].value
    const path = `/repos/${userName}/${repo}`
    browserHistory.push(path)
  },

第二種方法是使用context對象。

export default React.createClass({

  // ask for `router` from context
  contextTypes: {
    router: React.PropTypes.object
  },

  handleSubmit(event) {
    // ...
    this.context.router.push(path)
  },
})

react-router-redux

因爲咱們依然但願用Redux來管理應用的狀態,所以要集成Redux。

安裝依賴包:

npm install --save redux react-redux react-router-redux

咱們在store.js中添加Redux Store的代碼:

import { createStore, compose, combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';

const reducer = combineReducers({
    routing: routerReducer
});
const win = window;
const storeEnhancers = compose(
    (win && win.devToolsExtension) ? win.devToolsExtension() : (f) => f
);

const initialState = {};

export default createStore(reducer, initialState, storeEnhancers);

上面定義的Store只是一個例子,並無添加實際的reducer和初始狀態,主要使用了Redux Devtools。
react-router-redux的工做原理是在Redux Store的狀態樹的routing字段中保存當前路由信息,由於修改Redux狀態只能經過reducer,因此上面增長了routing字段上的規約函數routerReducer。

使用React-Redux庫的Provider組件,做爲數據提供者,Provider必須居於React組件頂層。
修改src/index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

import Routes from './routes';
import store from './store';

const app = document.createElement('div');
document.body.appendChild(app);
ReactDOM.render(
  <Provider store={store}>
    <Routes />
  </Provider>, app);

使用React-Router,路由信息存儲在瀏覽器URL上,利用Redux Devtools調試時,沒法重現網頁之間的切換,由於當前路由做爲應用狀態根本沒體如今Redux的Store上。

爲了克服這個缺陷,能夠利用react-router-redux庫來同步路由信息至Redux的Store。

reducer須要由action驅動,修改傳給Router的history變量,讓history可以協同URL和Store上的狀態。
修改src/routes/index.jsx:

import {syncHistoryWithStore} from 'react-router-redux';

const history = syncHistoryWithStore(browserHistory, store);

react-router-redux庫的syncHistoryWithStore方法將React-Router的browserHistory和store關聯起來,當瀏覽器URL變化時,會向store派發action對象,同時監聽store的變化,當狀態樹下routing字段發生變化時,反過來會更新瀏覽器URL。

相關文章
相關標籤/搜索