本文從屬於筆者的React入門與最佳實踐系列html
React Router是基於React的同時支持服務端路由與客戶端路由的強大易用的路由框架,能夠容許開發者方便地添加新頁面到應用中,保證頁面內容與頁面路由的一致性以及在頁面之間進行方便地參數傳遞。以前React Router做者沒有積極地開發與審覈Pull Request,結果有個rrtr一怒之下要建個獨立的分支,不事後來好像又迴歸到了React Router上。 目前React-Router的官方版本已經達到了2.6.0,其API也一直在發生變化,筆者在本文中所述內容也是基於2.6.0的官方文檔以及本身的實踐整理而來。同時,隨着React Router項目的更新本文文檔也會隨之更新,有須要的建議關注本項目。若是你是初學者但願快速搭建React的基本開發環境,那麼筆者建議參考Webpack-React-Redux-Boilerplate來迅速構建可應用於生產環境的自動化開發配置。首先,基本的React的路由配置以下所示:react
<Router history={appHistory}> <Route path = "/" component = {withRouter(App)}> //在2.4.0以後建議默認使用withRouter進行包裹 <IndexRoute component = {withRouter(ClusterTabPane)} /> //默認路由 <Route path = "cluster" component = {withRouter(ClusterTabPane)} /> </Route> <Route path="*" component={withRouter(ErrorPage)}/> //默認錯誤路由 </Router>
不過React-Router由於其與React的強綁定性也不可避免的帶來了一些缺陷,譬如在目前狀況下由於React存在的性能問題(筆者以爲在React-Fiber正式發佈以後能獲得有效解決),若是筆者打算使用Inferno來替換部分對性能要求較大的頁面,也是會存在問題。若是有興趣的話也能夠參考下你不必定須要React-Router這篇文章。webpack
React-Router的核心原理是將子組件根據選擇注入到{this.props.children}
中。在一個多頁面的應用程序中,若是咱們不使用React-Router,那麼總體的代碼可能以下所示:nginx
import React from 'react' import { render } from 'react-dom' const About = React.createClass({/*...*/}) const Inbox = React.createClass({/*...*/}) const Home = React.createClass({/*...*/}) const App = React.createClass({ getInitialState() { return { route: window.location.hash.substr(1) } }, componentDidMount() { window.addEventListener('hashchange', () => { this.setState({ route: window.location.hash.substr(1) }) }) }, render() { let Child switch (this.state.route) { case '/about': Child = About; break; case '/inbox': Child = Inbox; break; default: Child = Home; } return ( <div> <h1>App</h1> <ul> <li><a href="#/about">About</a></li> <li><a href="#/inbox">Inbox</a></li> </ul> <Child/> </div> ) } }) render(<App />, document.body)
能夠看出,在原始的多頁面程序配置下,咱們須要在render
函數中手動地根據傳入的Props來決定應該填充哪一個組件,這樣就致使了父子頁面之間的耦合度太高,而且這種命令式的方式可維護性也比較差,也不是很直觀。git
在React-Router的協助下,咱們的路由配置可能以下所示:github
import React from 'react' import { render } from 'react-dom' // First we import some modules... import { Router, Route, IndexRoute, Link, hashHistory } from 'react-router' // Then we delete a bunch of code from App and // add some <Link> elements... const App = React.createClass({ render() { return ( <div> <h1>App</h1> {/* change the <a>s to <Link>s */} <ul> <li><Link to="/about">About</Link></li> <li><Link to="/inbox">Inbox</Link></li> </ul> {/* next we replace `<Child>` with `this.props.children` the router will figure out the children for us */} {this.props.children} </div> ) } }) // Finally, we render a <Router> with some <Route>s. // It does all the fancy routing stuff for us. render(( <Router history={hashHistory}> <Route path="/" component={App}> <IndexRoute component={Home} /> <Route path="about" component={About} /> <Route path="inbox" component={Inbox} /> </Route> </Router> ), document.body)
React Router提供了統一的聲明式全局路由配置方案,使咱們在父組件內部不須要再去關係應該如何選擇子組件、應該如何控制組件間的跳轉等等。而若是你但願將路由配置獨立於應用程序,你也可使用簡單的JavaScript Object來進行配置:web
const routes = { path: '/', component: App, indexRoute: { component: Home }, childRoutes: [ { path: 'about', component: About }, { path: 'inbox', component: Inbox }, ] } render(<Router history={history} routes={routes} />, document.body)
在將React Router集成到項目中以後,咱們會使用Router
對象做爲根容器包裹數個Route配置,而Route也就意味着一系列用於指示Router應該如何匹配URL的規則。以簡單的TodoAPP爲例,其路由配置以下所示:
import React from 'react' import { render } from 'react-dom' 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> } }) 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)
根據以上的配置,Router可以智能地處理如下幾個路由跳轉:
URL | Components |
---|---|
/ |
App |
/about |
App -> About |
/inbox |
App -> Inbox |
/inbox/messages/:id |
App -> Inbox -> Message |
在上面的配置中,若是咱們默認訪問的/
地址,那麼根據React Router的原理此時並無選定任何的子組件進行注入,即此時的this.props.children
值爲undefined
。而React Router容許咱們使用<IndexRoute>
來配置默認路由。
import { IndexRoute } from 'react-router' const Dashboard = React.createClass({ render() { return <div>Welcome to the app!</div> } }) render(( <Router> <Route path="/" component={App}> {/* Show the dashboard at / */} <IndexRoute component={Dashboard} /> <Route path="about" component={About} /> <Route path="inbox" component={Inbox}> <Route path="messages/:id" component={Message} /> </Route> </Route> </Router> ), document.body)
此時總體路由的配置爲:
URL | Components |
---|---|
/ |
App -> Dashboard |
/about |
App -> About |
/inbox |
App -> Inbox |
/inbox/messages/:id |
App -> Inbox -> Message |
在上面的配置中,Message組件是Inbox的子組件,所以每次訪問Message組件都須要在路由上添加/inbox
,這樣會致使隨着應用層次的加深而部分路由過於冗長,所以React Router還容許將UI與URL的配置解耦,譬如對上述配置的重構方式就是:
render(( <Router> <Route path="/" component={App}> <IndexRoute component={Dashboard} /> <Route path="about" component={About} /> <Route path="inbox" component={Inbox} /> {/* Use /messages/:id instead of /inbox/messages/:id */} <Route component={Inbox}> <Route path="messages/:id" component={Message} /> </Route> </Route> </Router> ), document.body)
這樣近似於絕對路徑訪問的方式可以提升總體路由配置的可讀性,咱們不須要在URL中添加更多的Segments來訪問內部的組件,此時的總體路由配置爲:
URL | Components |
---|---|
/ |
App -> Dashboard |
/about |
App -> About |
/inbox |
App -> Inbox |
/messages/:id |
App -> Inbox -> Message |
注意,絕對路徑可能沒法使用在動態路由中。
React Router提供了<Redirect>
來容許咱們將某個路由重定向到其餘路由,譬如對於上面的配置中,當咱們將Message組件設置爲絕對路徑訪問而部分開發者仍然使用/inbox/message/:id
方式進行訪問時:
import { Redirect } from 'react-router' render(( <Router> <Route path="/" component={App}> <IndexRoute component={Dashboard} /> <Route path="about" component={About} /> <Route path="inbox" component={Inbox}> {/* Redirect /inbox/messages/:id to /messages/:id */} <Redirect from="messages/:id" to="/messages/:id" /> </Route> <Route component={Inbox}> <Route path="messages/:id" component={Message} /> </Route> </Route> </Router> ), document.body)
此時對於 /inbox/messages/5
會被自動重定向到/messages/5
。
當咱們使用JSX方式進行配置時,其嵌入式的層次結構有助於提升路由的可讀性,不一樣組件之間的關係也能較好地表現出來。不過不少時候咱們仍然但願使用單純的JS對象進行配置而避免使用JSX語法。注意,若是使用單純的JS對象進行配置的時候,咱們沒法再使用 <Redirect>
,所以你只可以在onEnter
鉤子中配置重定向。
const routes = { path: '/', component: App, indexRoute: { component: Dashboard }, childRoutes: [ { path: 'about', component: About }, { path: 'inbox', component: Inbox, childRoutes: [{ path: 'messages/:id', onEnter: ({ params }, replace) => replace(`/messages/${params.id}`) }] }, { component: Inbox, childRoutes: [{ path: 'messages/:id', component: Message }] } ] } render(<Router routes={routes} />, document.body)
路由主要依靠三個屬性來判斷其是否與某個URL相匹配:
嵌套的層級
路徑
優先級
React Router提供了嵌套式的路由聲明方案來表述組件之間的從屬關係,嵌套式的路由就好像樹形結構同樣,而React Router來對某個URL進行匹配的時候也會按照深度優先的搜索方案進行匹配搜索。
一個典型的路由路徑由如下幾個部分組成:
:paramName
– 匹配參數直到 /
, ?
, or #
.
()
– 匹配可選的路徑
*
– 非貪婪匹配全部的路徑
**
- 貪婪匹配全部字符直到 /
, ?
, or #
<Route path="/hello/:name"> // 匹配 /hello/michael and /hello/ryan <Route path="/hello(/:name)"> // 匹配 /hello, /hello/michael, and /hello/ryan <Route path="/files/*.*"> // 匹配 /files/hello.jpg and /files/hello.html <Route path="/**/*.jpg"> // 匹配 /files/hello.jpg and /files/path/to/file.jpg
路由算法自動根據路由的定義順序來決定其優先級,所以你在定義路由的時候須要注意前一個路由定義不能徹底覆蓋下一個路由的所有跳轉狀況:
<Route path="/comments" ... /> <Redirect from="/comments" ... />
React Router 是創建在 history 之上的。 簡而言之,一個 history 知道如何去監聽瀏覽器地址欄的變化, 並解析這個 URL 轉化爲 location
對象, 而後 router 使用它匹配到路由,最後正確地渲染對應的組件。經常使用的 history 有三種形式, 可是你也可使用 React Router 實現自定義的 history。
從 React Router 庫中獲取它們:
// JavaScript module import import { browserHistory } from 'react-router'
而後能夠傳入到<Router>
的配置中:
render( <Router history={browserHistory} routes={routes} />, document.getElementById('app') )
createHashHistory
:用於客戶端跳轉這是一個你會獲取到的默認 history ,若是你不指定某個 history (即 {/* your routes */}
)。它用到的是 URL 中的 hash(#
)部分去建立形如 example.com/#/some/path
的路由。
createHashHistory
嗎?Hash history 是默認的,由於它能夠在服務器中不做任何配置就能夠運行,而且它在所有經常使用的瀏覽器包括 IE8+ 均可以用。可是咱們不推薦在實際生產中用到它,由於每個 web 應用都應該有目的地去使用createBrowserHistory
。
?_k=ckuvup
沒用的在 URL 中是什麼?當一個 history 經過應用程序的 pushState
或 replaceState
跳轉時,它能夠在新的 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。你也能夠不使用這個特性 (更多內容點擊這裏):
// 選擇退出連續的 state, 不推薦使用 let history = createHistory({ queryKey: false });
createBrowserHistory
:用於服務端跳轉Browser history 是由 React Router 建立瀏覽器應用推薦的 history。它使用 History API 在瀏覽器中被建立用於處理 URL,新建一個像這樣真實的 URL example.com/some/path
。
首先服務器應該可以處理 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)
若是你的服務器是 nginx,請使用 try_files
directive:
server { ... location / { try_files $uri /index.html } }
當在服務器上找不到其餘文件時,這就會讓 nginx 服務器生成靜態文件和操做 index.html
文件。
若是咱們能使用瀏覽器自帶的 window.history
API,那麼咱們的特性就能夠被瀏覽器所檢測到。若是不能,那麼任何調用跳轉的應用就會致使 全頁面刷新,它容許在構建應用和更新瀏覽器時會有一個更好的用戶體驗,但仍然支持的是舊版的。
你可能會想爲何咱們不後退到 hash history,問題是這些 URL 是不肯定的。若是一個訪客在 hash history 和 browser history 上共享一個 URL,而後他們也共享同一個後退功能,最後咱們會以產生笛卡爾積數量級的、無限多的 URL 而崩潰。
createMemoryHistory
:非地址欄呈現Memory history 不會在地址欄被操做或讀取。這就解釋了咱們是如何實現服務器渲染的。同時它也很是適合測試和其餘的渲染環境(像 React Native )。
import React from 'react' import createBrowserHistory from 'history/lib/createBrowserHistory' import { 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' React.render( <Router history={createBrowserHistory()}> <Route path='/' component={App}> <IndexRoute component={Home} /> <Route path='about' component={About} /> <Route path='features' component={Features} /> </Route> </Router>, document.getElementById('app') )
在2.4.0版本以前,router
對象經過this.context
進行傳遞,不過這種方式每每會引發莫名的錯誤。所以在2.4.0版本以後推薦的是採起所謂的HOC模式進行router對象的訪問,React Router也提供了一個withRouter
函數來方便進行封裝:
import React from 'react' import { withRouter } from 'react-router' const Page = React.createClass({ componentDidMount() { this.props.router.setRouteLeaveHook(this.props.route, () => { if (this.state.unsaved) return 'You have unsaved information, are you sure you want to leave this page?' }) }, render() { return <div>Stuff</div> } }) export default withRouter(Page)
而後在某個具體的組件內部,可使用this.props.router
來獲取router
對象:
router.push('/users/12') // or with a location descriptor object router.push({ pathname: '/users/12', query: { modal: true }, state: { fromDashboard: true } })
router對象的常見方法有:
replace(pathOrLoc):Identical to push except replaces the current history entry with a new one.
go(n):Go forward or backward in the history by n or -n.
goBack():Go back one entry in the history.
goForward():Go forward one entry in the history.
React Router提供了鉤子函數以方便咱們在正式執行跳轉前進行確認:
const Home = withRouter( React.createClass({ componentDidMount() { this.props.router.setRouteLeaveHook(this.props.route, this.routerWillLeave) }, routerWillLeave(nextLocation) { // return false to prevent a transition w/o prompting the user, // or return a string to allow the user to decide: if (!this.state.isSaved) return 'Your work is not saved! Are you sure you want to leave?' }, // ... }) )
除了跳轉確認以外,Route也提供了鉤子函數以通知咱們當路由發生時的狀況,能夠有助於咱們進行譬如頁面權限認證等等操做:
onLeave
: 當咱們離開某個路由時
onEnter
: 當咱們進入某個路由時
若是咱們在React Component組件外,譬如Reducer或者Service中須要進行路由跳轉的時候,咱們能夠直接使用history
對象進行手動跳轉:
// your main file that renders a Router import { Router, browserHistory } from 'react-router' import routes from './app/routes' render(<Router history={browserHistory} routes={routes}/>, el) // somewhere like a redux/flux action file: import { browserHistory } from 'react-router' browserHistory.push('/some/path')
在介紹對於組件的異步加載以前,React Router也是支持對於路由配置文件的異步加載的。能夠參考huge apps以得到更詳細的信息。
const CourseRoute = { path: 'course/:courseId', getChildRoutes(partialNextState, callback) { require.ensure([], function (require) { callback(null, [ require('./routes/Announcements'), require('./routes/Assignments'), require('./routes/Grades'), ]) }) }, getIndexRoute(partialNextState, callback) { require.ensure([], function (require) { callback(null, { component: require('./components/Index'), }) }) }, getComponents(nextState, callback) { require.ensure([], function (require) { callback(null, require('./components/Course')) }) } }
React Router在其官方的huge apps介紹了一種基於Webpack的異步加載方案,不過其實徹底直接使用了Webpack的require.ensure
函數,這樣致使了大量的冗餘代碼,而且致使了路由的邏輯被分散到了多個子文件夾中,其樣例項目中的文件結構爲:
├── components ├── routes │ ├── Calendar │ │ ├── components │ │ │ └── Calendar.js │ │ └── index.js │ ├── Course │ │ ├── components │ │ │ ├── Course.js │ │ │ ├── Dashboard.js │ │ │ └── Nav.js │ │ └── routes │ │ ├── Announcements │ │ │ ├── components │ │ │ │ ├── Announcements.js │ │ │ │ ├── Sidebar.js │ │ │ ├── routes │ │ │ │ └── Announcement │ │ │ │ ├── components │ │ │ │ │ └── Announcement │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── Assignments │ │ │ ├── components │ │ │ │ ├── Assignments.js │ │ │ │ ├── Sidebar.js │ │ │ ├── routes │ │ │ │ └── Assignment │ │ │ │ ├── components │ │ │ │ │ └── Assignment │ │ │ │ └── index.js │ │ │ └── index.js │ │ └── Grades │ │ ├── components │ │ │ └── Grades.js │ │ └── index.js │ ├── Grades │ │ ├── components │ │ │ └── Grades.js │ │ └── index.js │ ├── Messages │ │ ├── components │ │ │ └── Messages.js │ │ └── index.js │ └── Profile │ ├── components │ │ └── Profile.js │ └── index.js ├── stubs └── app.js
這種結構下須要爲每一個組件寫一個單獨的index.js加載文件,毫無疑問會加大項目的冗餘度。筆者建議是使用bundle-loader
來替代require.ensure
,這樣能夠大大簡化目前的代碼。bundle-loader
是對於require.ensuire
的抽象,而且可以大大屏蔽底層的實現。若是某個模塊選擇使用Bundle Loader進行打包,那麼其會被打包到一個單獨的Chunk中,而且Webpack會自動地爲咱們生成一個加載函數,從而使得在須要時以異步請求方式進行加載。咱們能夠選擇刪除全部子目錄下的index.js
文件,而且將文件結構進行扁平化處理:
├── components ├── routes │ ├── Calendar.js │ ├── Course │ │ ├── components │ │ │ ├── Dashboard.js │ │ │ └── Nav.js │ │ ├── routes │ │ │ ├── Announcements │ │ │ │ ├── routes │ │ │ │ │ └── Announcement.js │ │ │ │ ├── Announcements.js │ │ │ │ └── Sidebar.js │ │ │ ├── Assignments │ │ │ │ ├── routes │ │ │ │ │ └── Assignment.js │ │ │ │ ├── Assignments.js │ │ │ │ └── Sidebar.js │ │ │ └── Grades.js │ │ └── Course.js │ ├── Grades.js │ ├── Messages.js │ └── Profile.js ├── stubs └── app.js
而後咱們須要在咱們的Webpack中配置以下專門的加載器:
// NOTE: this assumes you're on a Unix system. You will // need to update this regex and possibly some other config // to get this working on Windows (but it can still work!) var routeComponentRegex = /routes\/([^\/]+\/?[^\/]+).js$/ module.exports = { // ...rest of config... modules: { loaders: [ // make sure to exclude route components here { test: /\.js$/, include: path.resolve(__dirname, 'src'), exclude: routeComponentRegex, loader: 'babel' }, // run route components through bundle-loader { test: routeComponentRegex, include: path.resolve(__dirname, 'src'), loaders: ['bundle?lazy', 'babel'] } ] } // ...rest of config... }
上述配置中是會將routes
目錄下的全部文件都進行異步打包加載,即將其從主Chunk中移除,而若是你須要指定某個單獨的部分進行單獨的打包,建議是以下配置:
{ ...module: { loaders: [{ // use `test` to split a single file // or `include` to split a whole folder test: /.*/, include: [path.resolve(__dirname, 'pages/admin')], loader: 'bundle?lazy&name=admin' }] } ... }
然後在app.js
中,咱們只須要用正常的ES6的語法引入組件:
// Webpack is configured to create ajax wrappers around each of these modules. // Webpack will create a separate chunk for each of these imports (including // any dependencies) import Course from './routes/Course/Course' import AnnouncementsSidebar from './routes/Course/routes/Announcements/Sidebar' import Announcements from './routes/Course/routes/Announcements/Announcements' import Announcement from './routes/Course/routes/Announcements/routes/Announcement' import AssignmentsSidebar from './routes/Course/routes/Assignments/Sidebar' import Assignments from './routes/Course/routes/Assignments/Assignments' import Assignment from './routes/Course/routes/Assignments/routes/Assignment' import CourseGrades from './routes/Course/routes/Grades' import Calendar from './routes/Calendar' import Grades from './routes/Grades' import Messages from './routes/Messages'
須要注意的是,這裏引入的對象並非組件自己,而是Webpack爲咱們提供的一些封裝函數,當你真實地須要調用這些組件時,這些組件纔會被異步加載進來。而咱們在React Router中須要調用route.getComponent
函數來異步加載這些組件,咱們須要自定義封裝一個加載函數:
function lazyLoadComponents(lazyModules) { return (location, cb) => { const moduleKeys = Object.keys(lazyModules); const promises = moduleKeys.map(key => new Promise(resolve => lazyModules[key](resolve)) ) Promise.all(promises).then(modules => { cb(null, modules.reduce((obj, module, i) => { obj[moduleKeys[i]] = module; return obj; }, {})) }) } }
而最後的路由配置方案以下所示:
render( <Router history={ browserHistory }> <Route path="/" component={ App }> <Route path="calendar" getComponent={ lazyLoadComponent(Calendar) } /> <Route path="course/:courseId" getComponent={ lazyLoadComponent(Course) }> <Route path="announcements" getComponents={ lazyLoadComponents({ sidebar: AnnouncementsSidebar, main: Announcements }) }> <Route path=":announcementId" getComponent={ lazyLoadComponent(Announcement) } /> </Route> <Route path="assignments" getComponents={ lazyLoadComponents({ sidebar: AssignmentsSidebar, main: Assignments }) }> <Route path=":assignmentId" getComponent={ lazyLoadComponent(Assignment) } /> </Route> <Route path="grades" getComponent={ lazyLoadComponent(CourseGrades) } /> </Route> <Route path="grades" getComponent={ lazyLoadComponent(Grades) } /> <Route path="messages" getComponent={ lazyLoadComponent(Messages) } /> <Route path="profile" getComponent={ lazyLoadComponent(Calendar) } /> </Route> </Router>, document.getElementById('example') )
若是你須要支持服務端渲染,那麼須要進行下判斷:
function loadComponent(module) { return __CLIENT__ ? lazyLoadComponent(module) : (location, cb) => cb(null, module); }