真正學會 React 是一個漫長的過程。html
你會發現,它不是一個庫,也不是一個框架,而是一個龐大的體系。想要發揮它的威力,整個技術棧都要配合它改造。你要學習一整套解決方案,從後端到前端,都是全新的作法。前端
舉例來講,React 不使用 HTML,而使用 JSX 。它打算拋棄 DOM,要求開發者不要使用任何 DOM 方法。它甚至還拋棄了 SQL ,本身發明了一套查詢語言 GraphQL 。固然,這些你均可以不用,React 照樣運行,可是就發揮不出它的最大威力。
這樣說吧,你只要用了 React,就會發現合理的選擇就是,採用它的整個技術棧。
本文介紹 React 體系的一個重要部分:路由庫React-Router。它是官方維護的,事實上也是惟一可選的路由庫。它經過管理 URL,實現組件的切換和狀態的變化,開發複雜的應用幾乎確定會用到。react
本文針對初學者,儘可能寫得簡潔易懂。預備知識是 React 的基本用法,能夠參考我寫的《React 入門實例教程》。
另外,我沒有準備示例庫,由於官方的示例庫很是棒,由淺入深,分紅14步,每一步都有詳細的代碼解釋。我強烈建議你先跟着作一遍,而後再看下面的API講解。
([說明] 本文寫做時,React-router 是 2.x 版,本文的內容只適合這個版本,與最新的 4.x 版不兼容。目前,官方同時維護 2.x 和 4.x 兩個版本,因此前者依然能夠用在項目中。2017年3月)
1、基本用法
React Router 安裝命令以下。webpack
$ npm install -S react-router
使用時,路由器Router就是React的一個組件。web
import { Router } from 'react-router';
render(<Router/>, document.getElementById('app'));
Router組件自己只是一個容器,真正的路由要經過Route組件定義。npm
import { Router, Route, hashHistory } from 'react-router';後端
render((
<Router history={hashHistory}>api
<Route path="/" component={App}/>
</Router>
), document.getElementById('app'));
上面代碼中,用戶訪問根路由/(好比http://www.example.com/),組件APP就會加載到document.getElementById('app')。
你可能還注意到,Router組件有一個參數history,它的值hashHistory表示,路由的切換由URL的hash變化決定,即URL的#部分發生變化。舉例來講,用戶訪問http://www.example.com/,實際會看到的是http://www.example.com/#/。
Route組件定義了URL路徑與組件的對應關係。你能夠同時使用多個Route組件。瀏覽器
<Router history={hashHistory}>
<Route path="/" component={App}/>
<Route path="/repos" component={Repos}/>
<Route path="/about" component={About}/>
</Router>
上面代碼中,用戶訪問/repos(好比http://localhost:8080/#/repos)時,加載Repos組件;訪問/about(http://localhost:8080/#/about)時,加載About組件。
2、嵌套路由
Route組件還能夠嵌套。服務器
<Router history={hashHistory}>
<Route path="/" component={App}>
<Route path="/repos" component={Repos}/> <Route path="/about" component={About}/>
</Route>
</Router>
上面代碼中,用戶訪問/repos時,會先加載App組件,而後在它的內部再加載Repos組件。
<App>
<Repos/>
</App>
App組件要寫成下面的樣子。
export default React.createClass({
render() {
return <div> {this.props.children} </div>
}
})
上面代碼中,App組件的this.props.children屬性就是子組件。
子路由也能夠不寫在Router組件裏面,單獨傳入Router組件的routes屬性。
let routes = <Route path="/" component={App}>
<Route path="/repos" component={Repos}/>
<Route path="/about" component={About}/>
</Route>;
<Router routes={routes} history={browserHistory}/>
3、 path 屬性
Route組件的path屬性指定路由的匹配規則。這個屬性是能夠省略的,這樣的話,無論路徑是否匹配,老是會加載指定組件。
請看下面的例子。
<Route path="inbox" component={Inbox}>
<Route path="messages/:id" component={Message} />
</Route>
上面代碼中,當用戶訪問/inbox/messages/:id時,會加載下面的組件。
<Inbox>
<Message/>
</Inbox>
若是省略外層Route的path參數,寫成下面的樣子。
<Route component={Inbox}>
<Route path="inbox/messages/:id" component={Message} />
</Route>
如今用戶訪問/inbox/messages/:id時,組件加載仍是原來的樣子。
<Inbox>
<Message/>
</Inbox>
4、通配符
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獲取。
5、IndexRoute 組件
下面的例子,你會不會以爲有一點問題?
<Router>
<Route path="/" component={App}>
<Route path="accounts" component={Accounts}/> <Route path="statements" component={Statements}/>
</Route>
</Router>
上面代碼中,訪問根路徑/,不會加載任何子組件。也就是說,App組件的this.props.children,這時是undefined。
所以,一般會採用{this.props.children || <Home/>}這樣的寫法。這時,Home明明是Accounts和Statements的同級組件,卻沒有寫在Route中。
IndexRoute就是解決這個問題,顯式指定Home是根路由的子組件,即指定默認狀況下加載的子組件。你能夠把IndexRoute想象成某個路徑的index.html。
<Router>
<Route path="/" component={App}>
<IndexRoute component={Home}/> <Route path="accounts" component={Accounts}/> <Route path="statements" component={Statements}/>
</Route>
</Router>
如今,用戶訪問/的時候,加載的組件結構以下。
<App>
<Home/>
</App>
這種組件結構就很清晰了:App只包含下級組件的共有元素,自己的展現內容則由Home組件定義。這樣有利於代碼分離,也有利於使用React Router提供的各類API。
注意,IndexRoute組件沒有路徑參數path。
6、Redirect 組件
<Redirect>組件用於路由的跳轉,即用戶訪問一個路由,會自動跳轉到另外一個路由。
<Route path="inbox" component={Inbox}>
{/ 從 /inbox/messages/:id 跳轉到 /messages/:id /}
<Redirect from="messages/:id" to="/messages/:id" />
</Route>
如今訪問/inbox/messages/5,會自動跳轉到/messages/5。
7、IndexRedirect 組件
IndexRedirect組件用於訪問根路由的時候,將用戶重定向到某個子組件。
<Route path="/" component={App}>
<IndexRedirect to="/welcome" />
<Route path="welcome" component={Welcome} />
<Route path="about" component={About} />
</Route>
上面代碼中,用戶訪問根路徑時,將自動重定向到子組件welcome。
8、Link
Link組件用於取代元素,生成一個連接,容許用戶點擊後跳轉到另外一個路由。它基本上就是元素的React 版本,能夠接收Router的狀態。
render() {
return <div>
<ul role="nav"> <li><Link to="/about">About</Link></li> <li><Link to="/repos">Repos</Link></li> </ul>
</div>
}
若是但願當前的路由與其餘路由有不一樣樣式,這時可使用Link組件的activeStyle屬性。
<Link to="/about" activeStyle={{color: 'red'}}>About</Link>
<Link to="/repos" activeStyle={{color: 'red'}}>Repos</Link>
上面代碼中,當前頁面的連接會紅色顯示。
另外一種作法是,使用activeClassName指定當前路由的Class。
<Link to="/about" activeClassName="active">About</Link>
<Link to="/repos" activeClassName="active">Repos</Link>
上面代碼中,當前頁面的連接的class會包含active。
在Router組件以外,導航到路由頁面,可使用瀏覽器的History API,像下面這樣寫。
import { browserHistory } from 'react-router';
browserHistory.push('/some/path');
9、IndexLink
若是連接到根路由/,不要使用Link組件,而要使用IndexLink組件。
這是由於對於根路由來講,activeStyle和activeClassName會失效,或者說老是生效,由於/會匹配任何子路由。而IndexLink組件會使用路徑的精確匹配。
<IndexLink to="/" activeClassName="active">
Home
</IndexLink>
上面代碼中,根路由只會在精確匹配時,才具備activeClassName。
另外一種方法是使用Link組件的onlyActiveOnIndex屬性,也能達到一樣效果。
<Link to="/" activeClassName="active" onlyActiveOnIndex={true}>
Home
</Link>
實際上,IndexLink就是對Link組件的onlyActiveOnIndex屬性的包裝。
10、histroy 屬性
Router組件的history屬性,用來監聽瀏覽器地址欄的變化,並將URL解析成一個地址對象,供 React Router 匹配。
history屬性,一共能夠設置三種值。
browserHistory
hashHistory
createMemoryHistory
若是設爲hashHistory,路由將經過URL的hash部分(#)切換,URL的形式相似example.com/#/some/path。
import { hashHistory } from 'react-router'
render(
<Router history={hashHistory} routes={routes} />,
document.getElementById('app')
)
若是設爲browserHistory,瀏覽器的路由就再也不經過Hash完成了,而顯示正常的路徑example.com/some/path,背後調用的是瀏覽器的History API。
import { browserHistory } from 'react-router'
render(
<Router history={browserHistory} routes={routes} />,
document.getElementById('app')
)
可是,這種狀況須要對服務器改造。不然用戶直接向服務器請求某個子路由,會顯示網頁找不到的404錯誤。
若是開發服務器使用的是webpack-dev-server,加上--history-api-fallback參數就能夠了。
$ webpack-dev-server --inline --content-base . --history-api-fallback
createMemoryHistory主要用於服務器渲染。它建立一個內存中的history對象,不與瀏覽器URL互動。
const history = createMemoryHistory(location)
11、表單處理
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)
},
})
12、路由的鉤子
每一個路由都有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,將阻止路由的切換,不然就返回一個字符串,提示用戶決定是否要切換。
(完)