React Router知识体系
React Router基本用法
import React from 'react';
import { render, findDOMNode } from 'react-dom';
import { Router, Route, Link, IndexRoute, Redirect } from 'react-router';
import { createHistory, createHashHistory, useBasename } from 'history';
// 此处用于添加根路径
const history = useBasename(createHashHistory)({
queryKey: '_key',
basename: '/blog-app',
});
React.render((
<Router history={history}>
<Route path="/" component={BlogApp}>
<IndexRoute component={SignIn}/>
<Route path="signIn" component={SignIn}/>
<Route path="signOut" component={SignOut}/>
<Redirect from="/archives" to="/archives/posts"/>
<Route onEnter={requireAuth} path="archives" component={Archives}>
<Route path="posts" components={{
original: Original,
reproduce: Reproduce,
}}/>
</Route>
<Route path="article/:id" component={Article}/>
<Route path="about" component={About}/>
</Route>
</Router>
), document.getElementById('app'));
从上面代码中,我们可以发现:
- Router 与 Route 一样都是 react 组件,它的 history 对象是整个路由系统的核心,它暴露了很多属性和方法在路由系统中使用;
- Router不会被渲染,只是创建内部路由规则的配置对象
- Route 的 path 属性表示路由组件所对应的路径,可以是绝对或相对路径,绝对路径可以忽略嵌套,相对路径由祖先节点和自身的路径拼接组成;
- Route组件的path属性是可以省略的,这样的话,不管路径是否匹配,总是会加载指定组件。
- Redirect 是一个重定向组件,有 from 和 to 两个属性;
- Route 的 onEnter 钩子将用于在渲染对象的组件前做拦截操作,比如验证权限;
- 在 Route 中,可以使用 component 指定单个组件,或者通过 components 指定多个组件集合;
- Route组件还可以嵌套使用。
path属性可以使用通配符
<Route path="/hello/:name">
// 匹配 /hello/tom
<Route path="/hello(/:name)">
// 匹配 /hello
// 匹配 /hello/tom
// 匹配 /hello/liyan
<Route path="/files/*.*">
// 匹配 /files/signin.jpg
// 匹配 /files/signout.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)*
*匹配任意字符直到名中下一个字符或者整个URL的末尾,并创建一个splat参数
(4) ***
** 匹配任意字符,直到下一个/、?、#为止。
路由原理
无论是传统的后端 MVC 主导的应用,还是在当下最流行的单页面应用中,路由的职责都很重要,但原理并不复杂,即保证视图和 URL 的同步,而视图可以看成是资源的一种表现。当用户在页面中进行操作时,应用会在若干个交互状态中切换,路由则可以记录下某些重要的状态,比如在一个购物系统中用户是否登录、购物车中有哪些物品。而这些变化同样会被记录在浏览器的历史中,用户可以通过浏览器的前进、后退按钮切换状态,同样可以将 URL 分享给好友。简而言之,用户可以通过手动输入或者与页面进行交互来改变 URL,然后通过同步或者异步的方式向服务端发送请求获取资源(当然,资源也可能存在于本地),成功后重新绘制 UI,原理如下图所示:
Route标签的原理
从上面的例子可以看出Route拥有下面的几个props:
- exact:propType.bool
- path:propType.string
- component:propType.func
- render:propType.func
他们都不是必填项,注意,如果path没有赋值,那么此Route就是默认渲染的。
Route的作用就是当url和Route中path属性的值匹配时,就渲染component中的组件或者render中的内容。
Route利用的是history|(history是用来兼容不同浏览器或者环境下的历史记录管理的,当我跳转或者点击浏览器的后退按钮时,history就必须记录这些变化)的listen方法来监听url的变化。为了防止引入新的库,Route的创作者选择了使用html5中的popState事件,只要点击了浏览器的前进或者后退按钮,这个事件就会触发,我们来看一下Route的代码:
class Route extends Component {
static propTypes: {
path: PropTypes.string,
exact: PropTypes.bool,
component: PropTypes.func,
render: PropTypes.func,
}
componentWillMount() {
addEventListener("popstate", this.handlePop)
}
componentWillUnmount() {
removeEventListener("popstate", this.handlePop)
}
handlePop = () => {
this.forceUpdate()
}
render() {
const {
path,
exact,
component,
render,
} = this.props
//location是一个全局变量
const match = matchPath(location.pathname, { path, exact })
return (
//有趣的是从这里我们可以看出各属性渲染的优先级,component第一
component ? (
match ? React.createElement(component, props) : null
) : render ? ( // render prop is next, only called if there's a match
match ? render(props) : null
) : children ? ( // children come last, always called
typeof children === 'function' ? (
children(props)
) : !Array.isArray(children) || children.length ? ( // Preact defaults to empty children array
React.Children.only(children)
) : (
null
)
) : (
null
)
)
}
}
Route在组件将要Mount的时候添加popState事件的监听,每当popState事件触发,就使用forceUpdate强制刷新,从而基于当前的location.pathname进行一次匹配,再根据结果渲染。
那么,matchPath方法是如何实现的呢?
Route引入了一个外部library:path-to-regexp。这个pathToRegexp方法用于返回一个满足要求的正则表达式,举个例子:
let keys = [],keys2=[]
let re = pathToRegexp('/home/:login', keys)
//re = /^\/home\/([^\/]+?)\/?$/i keys = [{ name: 'login', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
let re2 = pathToRegexp('/home/login', keys2)
//re2 = /^\/home\/login(?:\/(?=$))?$/i keys2 = []
值得一提的是matchPath方法中对匹配结果作了缓存,如果是已经匹配过的字符串,就不用再进行一次pathToRegexp了。
随后的代码就清晰了:
const match = re.exec(pathname)
if (!match)
return null
const [ url, ...values ] = match
const isExact = pathname === url
//如果exact为true,需要pathname===url
if (exact && !isExact)
return null
return {
path,
url: path === '/' && url === '' ? '/' : url,
isExact,
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index]
return memo
}, {})
}
Link
Link相当与html中的a标签,通过点击锚来实现页面的跳转,Link 组件的 API 应该如下所示:
<Link to='/path' replace={false} />
其中的 to 是一个指向跳转目标地址的字符串,而 replace 则是布尔变量来指定当用户点击跳转时是替换 history 栈中的记录还是插入新的记录。基于上述的 API 设计,我们可以得到如下的组件声明:
class Link extends Component {
static propTypes = {
to: PropTypes.string.isRequired,
replace: PropTypes.bool,
}
}
现在我们已经知道 Link 组件的渲染函数中需要返回一个锚标签,因此我们需要为每个锚标签添加一个点击事件的处理器:
class Link extends Component {
static propTypes = {
to: PropTypes.string.isRequired,
replace: PropTypes.bool,
}
handleClick = (event) => {
const { replace, to } = this.props
event.preventDefault()
const { history } = this.context.router
replace ? history.peplace(to) : history.push(to)
}
render() {
const { to, children} = this.props
return (
<a href={to} onClick={this.handleClick}>
{children}
</a>
)
}
}
需要注意的是,history.push和history.replace使用的是pushState方法和replaceState方法。