本文从属于笔者的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); }