路由就是Router,这里的路由并不是我们在计算机网路中所学的路由的概念,而是前端的‘路由’,简单的来说就是通过URL的变化,使用路由来跳转到相对应的路径来显示不一样的页面,虽然只是这么一个小功能,但是它却是web应用中一个非常非常非常重要的一步。一个只会写页面的前端工程师不是工程师,顶多就是个工人,工程师的目标就是去架构,去优化,去理解,去运用。所以ReactRouter是必须要迈过的一个坎,为了方便写文章,ReactRouter的知识得再从头学一遍。
import React from 'react'
import { render } from 'react-dom'
//这是创建React组件的ES5的写法,ES6的写法是export default class extends React.component{}
//可以看到我们一共创建了4个页面组件,
const About = React.createClass({/*...*/})
const Inbox = React.createClass({/*...*/})
const Home = React.createClass({/*...*/})
//当前渲染的是App页面
const App = React.createClass({
//初始化路由的状态为'#'后面的字符串
getInitialState() {
return {
route: window.location.hash.substr(1)
}
},
//在页面第一次渲染后随时监听URL上'#'后面的字符串来控制跳转
componentDidMount() {
window.addEventListener('hashchange', () => {
this.setState({
route: window.location.hash.substr(1)
})
})
},
//一旦state重新赋值后会触发当前页面重新渲染,这个时候APP页面的子组件会发生改变,改变的页面自然就是路由的值决定的
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>
)
}
})
React.render(<App />, document.body)
根据上面的方法,如果在About页面中还有其他子页面,那么case的状况就会变多,并且这个嵌套的结构也会变得更加的复杂。我们再来看一下ReactRouter解决这种路径定向的方式:
import React from 'react'
import { render } from 'react-dom'
// 首先我们需要导入一些组件...
import { Router, Route, Link } from 'react-router'
// 然后我们从应用中删除一堆代码和
// 增加一些 <Link> 元素...
const App = React.createClass({
render() {
return (
<div>
<h1>App</h1>
{/* 把 <a> 变成 <Link> */}
<ul>
<li><Link to="/about">About</Link></li>
<li><Link to="/inbox">Inbox</Link></li>
</ul>
//这里我们注意一下,this.props.children指的该组件的所有子节点
{/*
接着用 `this.props.children` 替换 `<Child>`
router 会帮我们找到这个 children
*/}
{this.props.children}
</div>
)
}
})
// 最后,我们用一些 <Route> 来渲染 <Router>。
// 这些就是路由提供的我们想要的东西。
React.render((
<Router>
<Route path="/" component={App}>
<Route path="about" component={About} />
<Route path="inbox" component={Inbox} />
</Route>
</Router>
), document.body)
这里面Router组件是个容器,里面的Route组件才是关键,Route组件包含path和component两个参数,path是路径,component是我们要渲染的页面,所有的Route组件组成了一个树形结构,Router会先加载外层的Route组件再加载内层的Router组件。所以当我们的路径是/about,最后渲染出的结构就是:
<App>
<about />
</App>
这个时候,如果about页面如果还有子页面,我们就可以把子组件包裹在about页面中:
React.render((
<Router>
<Route path="/" component={App}>
<Route path="about" component={About}>
<Route path="messages/:id" component={Message} />
</ Route>
<Route path="inbox" component={Inbox} />
</Route>
</Router>
), document.body)
现在访问 URL about/messages/Jkei3c32 将会匹配到一个新的路由,并且它成功指向了 App -> About -> Message 这个 UI 的分支。
仔细观察一下message组件,它后面还跟了一个/:id的字样,这就是我们这个页面接受某个id,就像我们访问CDN的个人博客,就需要带一个个人独有的id,没毛病,这就是URL中的参数。
为了从服务器获取 message 数据,我们首先需要知道它的信息。当渲染组件时,React Router 会自动向 Route 组件中注入一些有用的信息,尤其是路径中动态部分的参数。
const Message = React.createClass({
componentDidMount() {
// 来自于路径 `/about/messages/:id`
const id = this.props.params.id
fetchMessage(id, function (err, message) {
this.setState({ message: message })
})
},
// ...
})
Router会根据URL路径按照深度优先遍历自上而下一层层找相应组件,如果没有那些乱七八糟的通配符,路由的匹配相当简单,顺着路径在对应的组件下面找符合路径的子组件,这个真的没啥好说的。
(1) :paramName
:paramName匹配URL的一个部分,直到遇到下一个/、?、#为止。这个路径参数可以通过this.props.params.paramName取出。
(2) ()
()表示URL的这个部分是可选的。
(3) *
*匹配任意字符,直到模式里面的下一个字符为止。匹配方式是非贪婪模式。
(4) **
** 匹配任意字符,直到下一个/、?、#为止。匹配方式是贪婪模式。
<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
路由匹配规则是从上到下执行,一旦发现匹配,就不再其余的规则了。
//先匹配第一个,第二个就不会匹配了
<Route path="/comments" ... />
<Route path="/comments" ... />
//同样,为了防止以下这种情况,我们一般把带参数的放在底部
<Router>
<Route path="/:userName/:id" component={UserPage}/>
<Route path="/about/me" component={About}/>
</Router>
对于history这里的解释我一直很困惑:React Router 是建立在 history 之上的。 简而言之,一个 history 知道如何去监听浏览器地址栏的变化, 并解析这个 URL 转化为 location 对象, 然后 router 使用它匹配到路由,最后正确地渲染对应的组件。
前面的实例命名没有写明history,不还是能根据url进行跳转吗?其实Router组件在不注明history参数时,默认是history={hashHistory},在reactRouter中共有3种history:
import { hashHistory } from 'react-router'
render(
<Router history={hashHistory} routes={routes} />,
document.getElementById('app')
)
当然你也可以不配置,hashHistory的作用就是监听URL中#后的值的变化,就像前面的例子,对应的URL应该是这种形式: ‘http://example.com/#/some/path‘。同时history会把历史网页push进内存,这样我们就可以实现回退,跳转等操作。
import { browserHistory } from 'react-router'
render(
<Router history={browserHistory} routes={routes} />,
document.getElementById('app')
)
如果设为browserHistory,浏览器的路由就不再通过Hash完成了,而显示正常的路径’example.com/some/path’,背后调用的是浏览器的History API。所以我们建议的history应该是browerHistory。
Memory history 不会在地址栏被操作或读取。这就解释了我们是如何实现服务器渲染的。同时它也非常适合测试和其他的渲染环境(像 React Native )。
const history = createMemoryHistory(location)
关于history,ReactRouterV4已经有了非常大的改动,这个我们会在V4版本学习笔记中在在探讨。
看最上面的实例我们会发现我们似乎漏掉了一个Home页面,Home页面就是我们在URL中只有’/’加载的App下的默认组件,那么我们如何使Home显示呢?
{this.props.children || }这种写法实现的就是在this.props.children是undefined的情况下(即’/’)加载Home组件,但是因为Home和About,Inbox是同级组件,所以这种结构并不太好。
IndexRoute就是解决这个问题,显式指定Home是根路由的子组件,即指定默认情况下加载的子组件。
<Router>
<Route path="/" component={App}>
<IndexRoute component={Home}/>
<Route path="accounts" component={Accounts}/>
<Route path="statements" component={Statements}/>
</Route>
</Router>
现在,用户访问/的时候,加载的组件结构如下:
<App>
<Home/>
</App>
Link的作用类似于a标签,生成一个链接,用户点击跳转。
<ul>
<li><Link to="/about">About</Link></li>
<li><Link to="/inbox">Inbox</Link></li>
</ul>
样式:
<Link to="/about" activeStyle={{color: 'red'}}>About</Link>
<Link to="/repos" activeStyle={{color: 'red'}}>Repos</Link>
或者
<Link to="/about" activeClassName="active">About</Link>
<Link to="/repos" activeClassName="active">Repos</Link>
在Router组件之外,导航到路由页面,可以使用浏览器的History API,像下面这样写。
import { browserHistory } from 'react-router';
browserHistory.push('/some/path');
IndexLink是我们在链接到根路由/时使用的组件,为什么要使用IndexLink,因为对于根路由,我们设置样式是就会总匹配,所以我们就需要一个只匹配根路由样式的组件。
<IndexLink to="/" activeClassName="active">
Home
</IndexLink>
本来是想一口气把ReactRouterV2的知识都学完的,包括之后的ReactRouter高级用法,但是我看了一下高级用法,虽然每小节内容不多,但是却需要花些功夫去理解实践,因此,本篇的内容写到这里,再写也显得冗长。想看更多的直接去看ReactRouter中文文档。
另外,ReactRouterV4早就更新了,里面有非常多的改动,因此,写完ReactRouterV2之后,我们会再去研究ReactRouterV4的内容。