React Router是一個基於 React 之上的強大路由庫,它可讓你嚮應用中快速地添加視圖和數據流,同時保持頁面與URL間的同步。在沒有react-router的時候,咱們須要對URL進行監聽,當URL的hash部分(指的是 # 後的部分)變化後,根據hash來渲染不一樣的組件。看起來很直接,但它很快就會變得複雜起來,當咱們的組件嵌套層級增長的時候,爲了讓咱們的URL解析變得更智能,咱們須要編寫不少代碼來實現指定URL應該渲染哪個嵌套的UI組件分支,因此咱們須要另一個解決方案,這個時候react-router出現了。html
路由配置是一組指令,用來告訴 router 如何匹配 URL以及匹配後如何執行代碼。咱們來經過一個簡單的例子解釋一下如何編寫路由配置。node
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:react
URL | 組件 |
---|---|
/ | App |
/about | App -> About |
/indox | App -> Inbox |
/inbox/message/:id | App -> Indox -> Message |
若是咱們能夠將 /inbox
從 /inbox/messages/:id
中去除,而且還可以讓 Message 嵌套在 App -> Inbox 中渲染,那會很是贊。絕對路徑可讓咱們作到這一點。webpack
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 對應關係以下:git
URL | 組件 |
---|---|
/ | App |
/about | App -> About |
/indox | App -> Inbox |
/message/:id | App -> Indox -> Message |
在Message組件中,咱們能夠經過如下方式獲取路由參數中的idgithub
const Message = React.createClass({ render(){ // 只適用於 /message/:id 形式的路由參數 // 若要獲取 ?id=yourId 形式的參數,使用this.props.location.query const { id } = this.props.params; return <div>參數id:{id}</div> } })
Route 能夠定義 onEnter 和 onLeave 兩個 hook ,這些hook會在頁面跳轉確認時觸發一次。這些 hook 對於一些狀況很是的有用,例如權限驗證或者在路由跳轉前將一些數據持久化保存起來。web
在路由跳轉過程當中,onLeave hook 會在全部將離開的路由中觸發,從最下層的子路由開始直到最外層父路由結束。而後onEnter hook會從最外層的父路由開始直到最下層子路由結束。算法
繼續咱們上面的例子,若是一個用戶點擊連接,從 /messages/5
跳轉到 /about
,下面是這些 hook 的執行順序:express
/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" ... />
React Router 是創建在 history
之上的。 簡而言之,一個 history 知道如何去監聽瀏覽器地址欄的變化, 並解析這個 URL 轉化爲 location 對象, 而後 router 使用它匹配到路由,最後正確地渲染對應的組件。
經常使用的 history 有三種形式, 可是你也可使用 React Router 實現自定義的 history。
browserHistory
hashHistory
createMemoryHistory
你能夠從 React Router 中引入它們:
// JavaScript 模塊導入(譯者注:ES6 形式) import { browserHistory } from 'react-router'
而後將它們傳遞給<Router>
:
render( <Router history={browserHistory} routes={routes} />, document.getElementById('app') )
Browser history 是使用 React Router 的應用推薦的 history。它使用瀏覽器中的 History API 用於處理 URL,建立一個像example.com/some/path這樣真實的 URL 。
服務器須要作好處理 URL 的準備。處理應用啓動最初的 /
這樣的請求應該沒問題,但當用戶來回跳轉並在 /accounts/123
刷新時,服務器就會收到來自 /accounts/123
的請求,這時你須要處理這個 URL 並在響應中包含 JavaScript 應用代碼。
一個 express 的應用可能看起來像這樣的:
const express = require('express') const path = require('path') const port = process.env.PORT || 8080 const app = express() // 一般用於加載靜態資源 app.use(express.static(__dirname + '/public')) // 在你應用 JavaScript 文件中包含了一個 script 標籤 // 的 index.html 中處理任何一個 route app.get('*', function (request, response){ response.sendFile(path.resolve(__dirname, 'public', 'index.html')) }) app.listen(port) console.log("server started on port " + port)
若是咱們能使用瀏覽器自帶的 window.history API,那麼咱們的特性就能夠被瀏覽器所檢測到。若是不能,那麼任何調用跳轉的應用就會致使 全頁面刷新,它容許在構建應用和更新瀏覽器時會有一個更好的用戶體驗,但仍然支持的是舊版的。
你可能會想爲何咱們不後退到 hash history,問題是這些 URL 是不肯定的。若是一個訪客在 hash history 和 browser history 上共享一個 URL,而後他們也共享同一個後退功能,最後咱們會以產生笛卡爾積數量級的、無限多的 URL 而崩潰。
Hash history 使用 URL 中的 hash(#)部分去建立形如 example.com/#/some/path
的路由。
createHashHistory
嗎?Hash history 不須要服務器任何配置就能夠運行,若是你剛剛入門,那就使用它吧。可是咱們不推薦在實際線上環境中用到它,由於每個 web 應用都應該渴望使用 browserHistory
。
?_k=ckuvup
沒用的在 URL 中是什麼?當一個 history 經過應用程序的 push 或 replace 跳轉時,它能夠在新的 location 中存儲 「location state」 而不顯示在 URL 中,這就像是在一個 HTML 中 post 的表單數據。
在 DOM API 中,這些 hash history 經過 window.location.hash = newHash 很簡單地被用於跳轉,且不用存儲它們的location state。但咱們想所有的 history 都可以使用location state,所以咱們要爲每個 location 建立一個惟一的 key,並把它們的狀態存儲在 session storage 中。當訪客點擊「後退」和「前進」時,咱們就會有一個機制去恢復這些 location state。
Memory history 不會在地址欄被操做或讀取。這就解釋了咱們是如何實現服務器渲染的。同時它也很是適合測試和其餘的渲染環境(像 React Native )。
和另外兩種history的一點不一樣是你必須建立它,這種方式便於測試。
const history = createMemoryHistory(location)
import React from 'react' import { render } from 'react-dom' import { browserHistory, Router, Route, IndexRoute } from 'react-router' import App from '../components/App' import Home from '../components/Home' import About from '../components/About' import Features from '../components/Features' render( <Router history={browserHistory}> <Route path='/' component={App}> <IndexRoute component={Home} /> <Route path='about' component={About} /> <Route path='features' component={Features} /> </Route> </Router>, document.getElementById('app') )
React Router 適用於小型網站,對於大型應用來講,一個首當其衝的問題就是所需加載的 JavaScript 的大小。程序應當只加載當前渲染頁所需的 JavaScript。有些開發者將這種方式稱之爲「代碼分拆」 —— 將全部的代碼分拆成多個小包,在用戶瀏覽過程當中按需加載。
對於底層細節的修改不該該須要它上面每一層級都進行修改。舉個例子,爲一個照片瀏覽頁添加一個路徑不該該影響到首頁加載的 JavaScript 的大小。也不能由於多個團隊共用一個大型的路由配置文件而形成合並時的衝突。
路由是個很是適於作代碼分拆的地方:它的責任就是配置好每一個 view。
React Router 裏的路徑匹配以及組件加載都是異步完成的,不只容許你延遲加載組件,而且能夠延遲加載路由配置。在首次加載包中你只須要有一個路徑定義,路由會自動解析剩下的路徑。
Route 能夠定義 getChildRoutes
,getIndexRoute
和 getComponents
這幾個函數。它們都是異步執行,而且只有在須要時才被調用。咱們將這種方式稱之爲 「逐漸匹配」。 React Router 會逐漸的匹配 URL 並只加載該 URL 對應頁面所需的路徑配置和組件。
若是配合 webpack
這類的代碼分拆工具使用的話,一個本來繁瑣的構架就會變得更簡潔明瞭。
const CourseRoute = { path: 'course/:courseId', getChildRoutes(location, callback) { require.ensure([], function (require) { callback(null, [ require('./routes/Announcements'), require('./routes/Assignments'), require('./routes/Grades'), ]) }) }, getIndexRoute(location, callback) { require.ensure([], function (require) { callback(null, require('./components/Index')) }) }, getComponents(location, callback) { require.ensure([], function (require) { callback(null, require('./components/Course')) }) } }
React Router 提供一個 routerWillLeave
生命週期鉤子,這使得 React 組件能夠攔截正在發生的跳轉,或在離開 route 前提示用戶。routerWillLeave
返回值有如下兩種:
return false
取消這次跳轉
return
返回提示信息,在離開 route 前提示用戶進行確認。
服務端渲染與客戶端渲染有些許不一樣,由於你須要:
發生錯誤時發送一個 500
的響應
須要重定向時發送一個 30x 的響應
在渲染以前得到數據 (用 router 幫你完成這點)
爲了迎合這一需求,你要在 Router API 下一層使用:
使用 match
在渲染以前根據 location
匹配 route
使用 RoutingContext
同步渲染 route 組件
它看起來像一個虛擬的 JavaScript 服務器:
import { renderToString } from 'react-dom/server' import { match, RoutingContext } from 'react-router' import routes from './routes' serve((req, res) => { // 注意!這裏的 req.url 應該是從初始請求中得到的 // 完整的 URL 路徑,包括查詢字符串。 match({ routes, location: req.url }, (error, redirectLocation, renderProps) => { if (error) { res.send(500, error.message) } else if (redirectLocation) { res.redirect(302, redirectLocation.pathname + redirectLocation.search) } else if (renderProps) { res.send(200, renderToString(<RoutingContext {...renderProps} />)) } else { res.send(404, 'Not found') } }) })
至於加載數據,你能夠用 renderProps
去構建任何你想要的形式——例如在 route 組件中添加一個靜態的 load
方法,或如在 route 中添加數據加載的方法——由你決定。