举例来说,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
组件。
<Repos/>
App
组件要写成下面的样子。
export default React.createClass({
render() {
return <div>
{this.props.children}
上面代码中,
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>
现在,用户访问
/
的时候,加载的组件结构如下。
<Home/>
这种组件结构就很清晰了:
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>
如果希望当前的路由与其他路由有不同样式,这时可以使用
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">
</IndexLink>
上面代码中,根路由只会在精确匹配时,才具有
activeClassName
。
另一种方法是使用
Link
组件的
onlyActiveOnIndex
属性,也能达到同样效果。
<Link to="/" activeClassName="active" onlyActiveOnIndex={true}>
</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
,将阻止路由的切换,否则就返回一个字符串,提示用户决定是否要切换。
到现在还不知道react的威力强大在哪里? 而且感觉还更复杂,因为几乎要全部新学所有东西。可能没有接触过大数据量的场景,总之写了3,4个月了,只是感觉到它的复杂,在我看来其它框架遇到的问题,react,redux也不能完全解决,反而是增加了一种学习成本。最怀疑的,react这种火热还会持续多久?
想问下阮老师,对于想写nodejs(API)的职业规划有何建议?另外,koa和expressjs哪个比较稳定?看了一下网络上对koa的介绍和评价,虽然有些(也许)是es未来趋势的新特性,但是我心底里比较抵触es的新特性(除非它们事实上已经成为标准,并且至少有2-3年的存活历史,而不需要再依赖polyfill)。expressjs看起来有些年头,而且大致看了下它的api也比较简单。但是犹豫的是,如果后续换单位,是不是行业内更多的是已经使用koa?或还是其它框架?
最后一个“当用户离开一个路径的时候,跳出一个提示框”下边的代码写错了“this.props.router.setRouteLeaveHook” 应该是“this.context.router.setRouteLeaveHook”
即props应该改为是context
What makes you feel React is nothing but more complicated? The most stand out is its performance, this is well know. But sides this, it's so easy to write, especially it comes up with other frameworks. Just one example, if you use React + Relay + GraphQL, it is declarative instead of imperative -- so it handles all the async rendering for you, make your application fast and easy to develop.
您好,使用 browserHistory 时碰到个问题,路由的跳转是匹配根目录而不是项目目录。
例如目录为 localhost/login 正常,但目录为
http://localhost/path/login
就会报错了。
可以通过 useRouterHistory 定义 router 的根目录,但这样岂不是打包好的项目只能放在指定的目录下才能运行,若是放在别处,路径仍然是错误的。
难道这种需要多处放的项目只能使用 hashHistory 吗?
const requireAuth = (nextState, replace) => {
if (!auth.isAdmin()) {
// Redirect to Home page if not an Admin
replace({ pathname: '/' })
export const AdminRoutes = () => {
return (
请问里面的replace是自定义函数?
如果只是想去掉 ?_k=adseis 这样的字符串的话,可以使用外部的 history 模块。
import { createHashHistory } from 'history';
const appHistory = useRouterHistory(createHashHistory)({ queryKey: false });
ReactDOM.render(
<Router history={appHistory}>
{routes}
</Router>,
document.getElementById('app')
在window环境下命令窗口里应该如下使用: 这个应该先执行 set node_env=production 然后在执行 npm start 而不是一整行一次执行。这样就起作用了,你可以试试
我照着这个试来一次 没问题了 能看了。 这14和13的例子 是交大家怎么配合 EXPRESS设置路由是吗?
getComponent (nextState, cb) {
require.ensure([], (require) => {
const Route = require('./components/Route').default
cb(null, Route)
问:这种异步动态路由,是否比阮师 的写法更好一些呢?
Router history={createBrowserHistory()}
Route path="/" component={Index}
Route path="/test2" component={Test2}
Router
(为了方便显示简写的)
我这边会报:Uncaught Error: Amay have only one child element
阮老师,我不想打击您专研前端的积极性!但是我想说,React这一切的一切,缘起不都是因为html5规范考虑的不周全吗?我认为react领域里面有一些是http协议该考虑的问题,有一些是浏览器该考虑的问题!
http和html标准协议的制定和更新,需要您这样深耕的大牛去影响!而不是倒逼前端去营造N多小技巧!莫等到革新性的浏览器开发推广出来,这一切的一切纷纷扰扰,统统变为0。这样的努力是不是有点浪费?