真正學會 React 是一個漫長的過程。
你會發現,它不是一個庫,也不是一個框架,而是一個龐大的體系。想要發揮它的威力,整個技術棧都要配合它改造。你要學習一整套解決方案,從後端到前端,都是全新的做法。
舉例來說,React 不使用 HTML,而使用 JSX 。它打算拋棄 DOM,要求開發者不要使用任何 DOM 方法。它甚至還拋棄了 SQL ,自己發明了一套查詢語言 GraphQL 。當然,這些你都可以不用,React 照樣執行,但是就發揮不出它的最大威力。
這樣說吧,你只要用了 React,就會發現合理的選擇就是,採用它的整個技術棧。
本文介紹 React 體系的一個重要部分:路由庫React-Router
。它是官方維護的,事實上也是唯一可選的路由庫。它通過管理 URL,實現元件的切換和狀態的變化,開發複雜的應用幾乎肯定會用到。
本文針對初學者,儘量寫得簡潔易懂。預備知識是 React 的基本用法,可以參考我寫的《React 入門例項教程》。
另外,我沒有準備示例庫,因為官方的示例庫非常棒,由淺入深,分成14步,每一步都有詳細的程式碼解釋。我強烈建議你先跟著做一遍,然後再看下面的API講解。
([說明] 本文寫作時,React-router 是 2.x 版,本文的內容只適合這個版本,與最新的 4.x 版不相容。目前,官方同時維護 2.x 和 4.x 兩個版本,所以前者依然可以用在專案中。2017年3月)
一、基本用法
React Router 安裝命令如下。
$ npm install -S react-router
使用時,路由器Router
就是React的一個元件。
import { Router } from 'react-router'; render(<Router/>, document.getElementById('app'));
Router
元件本身只是一個容器,真正的路由要通過Route
元件定義。
import { Router, Route, hashHistory } from 'react-router'; render(( <Router history={hashHistory}> <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
元件。
二、巢狀路由
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}/>
三、 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>
四、萬用字元
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
獲取。
五、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
。
六、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
。
七、IndexRedirect 元件
IndexRedirect
元件用於訪問根路由的時候,將使用者重定向到某個子元件。
<Route path="/" component={App}> <IndexRedirect to="/welcome" /> <Route path="welcome" component={Welcome} /> <Route path="about" component={About} /> </Route>
上面程式碼中,使用者訪問根路徑時,將自動重定向到子元件welcome
。
八、Link
Link
元件用於取代<a>
元素,生成一個連結,允許使用者點選後跳轉到另一個路由。它基本上就是<a>
元素的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');
九、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
屬性的包裝。
十、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)
十一、表單處理
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) }, })
十二、路由的鉤子
每個路由都有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
,將阻止路由的切換,否則就返回一個字串,提示使用者決定是否要切換。
(完)