一. 前言
react-route作为react框架下的单页面应用最普及的路由,依赖histroy包提供的histroy对象作为其核心,具有使用简单、使用范围广的特点。本文主要分析了一些常用组件的使用方法以及实现实现原理,欢迎大家讨论、斧正。
二. 组件详解
1. 依赖版本
| 依赖 | 版本 |
|---|---|
| react-router-dom | 5.2.0 |
| react-router | 5.2.0 |
| react | 16.12.0 |
| react-dom | 16.12.0 |
2. 上代码
<HashRouter>
<Route path="/about">
<About />
</Route>
<Route path="/users">
<Users />
</Route>
<Route path="/topics">
<Topics />
</Route>
<Redirect path="/">
<Home />
</Route>
</HashRouter>
这是我们比较常见的写法,最外层HashRouter代表了页面的路由协议。除了HashRouter外,我们还可以选择BrowserRouter和MemoryRouter。
Router和Redirect作为二级节点,是路由协议可以匹配并且展示的组件,上面示例中的写法会导致和同时被匹配并展示,可以通过在Route上添加exact={true}属性值 或者在外层包裹Switch组件避免。
如下:
<HashRouter>
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/users">
<Users />
</Route>
<Route path="/topics">
<Topics />
</Route>
</Switch>
</HashRouter>
3.1 HashRouter
1) 顾名思义,HashRouter工作原理就是监听hashchange来实现路由功能。
假定四个组件在hash路由下完整路径如下:
<About/>: http://localhost:3000/#/test/about
<Users/>: http://localhost:3000/#/test/users
<Topics/>: http://localhost:3000/#/test/topics
<Home/>: http://localhost:3000/#/test/
2) 属性:
basename
hash头部通用路径,如上所有的组件链接对应的hash的头部路径都是/test。我们在写Route时候,需要每个Route都的path属性都写成"/test/xxx”。如果basename属性写为"test", 则所有Route的path属性都可以写成"/xxx", histroy自动就帮我们把test拼上。hashType
hash标识符的样式,有3种样式可以选择,功能上无任何区别'slash' : http://localhost:3000/#/test/about
'noslash': http://localhost:3000/#test/about
'hashbang': http://localhost:3000/#!/test/about
3) 优点:
浏览器链接的hash变化并不会导致浏览器重新请求服务器,在服务端还不支持h5 histroy的情况下,是个非常不错的选择。
3.2 BrowerHistroyRouter
1) BrowerHistroyRouter,通过监听popstate变化来实现路由功能,切换路由时调用window.histroy.pushState()或者window.histroy.replaceState()来实现浏览器地址栏变化。
需要服务端支持,不然强制刷新页面可能会导致请求不到资源。
假定四个组件在hash路由下完整路径如下:
<About/>: http://localhost:3000/test/about
<Users/>: http://localhost:3000/test/users
<Topics/>: http://localhost:3000/test/topics
<Home/>: http://localhost:3000/test/
2) 属性:
basename: 同HashRouter
getUserConfirmation: 同HashRouter
forceRefresh: 是否强制刷新页面,默认false
keyLength: key长度,默认6
3) 优点: 链接路径看上去会优雅点。
3.3 MemoryRouter
1) MemoryRouter 一般用在不存在浏览器的环境中,比如单元测试、react-native,底层自定义了栈来管理路由切换。
2) 属性:
initialEntries: 初始栈;
initialIndex: 初始页面在栈中位置;
getUserConfirmation: 同HashRouter;
keyLength: 同BrowerHistroy;
3) 优点:
适用于无浏览器环境,即无法使用window.location/window.histroy等api的情形,可以自定义一套无需http开头的scheme路由协议,可扩展性强,使用范围广。
3.4 Switch
<Switch>组件会在child中从上往下查找path和location.pathname匹配的<Route>,找到后就会设置它的computedMatch值,用来告诉<Route/>你就是天选之子,并且终止查找并返回。
3.5 Route
1) Route的render的方法执行的时候,它还需要自己判断下自己是否被match, 以及如果match上,它该渲染children/component/render属性中的哪一个。一般情况下只写componet属性值就可以了,大家可以根据实际情况取舍。核心代码如下:
render() {
//第一步: 判断是否匹配上
const match = this.props.computedMatch // 父组件<Switch>会设置此值,如果父组件不是<Swtich>,则此值是undefined
? this.props.computedMatch
: this.props.path
? matchPath(location.pathname, this.props) //通过matchPath()计算是否匹配
: context.match;//兜底逻辑,不写path属性,会被match上
//第二步: 根据第一步结果以及children/component/render的优先级先后的关系选择子节点
return (
<RouterContext.Provider value={props}>
{props.match
? children // children优先级最高
? typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: children
: component // 其次是component
? React.createElement(component, props)
: render // 最后才是render
? render(props)
: null
: typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: null // 不匹配返回null
}
</RouterContext.Provider>
);
}
2) exact、sensitive、strict 属性会影响匹配结果,具体规则如下:
exact: 为 true 时,则要求路径与 location.pathname 必须完全匹配
| exact | path | URL | 是否匹配 |
|---|---|---|---|
| true | /a | /a/b | false |
| true | /a | /a | true |
| true | /a | /A | true |
| false | /a | /a/b | true |
sensitive: 路径与 location.pathname 匹配时是否区分URL大小写
| sensitive | path | URL | 是否匹配 |
|---|---|---|---|
| true | /a | /A | false |
| false | /a | /A | true |
strict: 为true时,有斜线的路径只能匹配有斜线的location.pathname
| sensitive | path | URL | 是否匹配 |
|---|---|---|---|
| true | /a/ | /a | false |
| true | /a/ | /a/b | true |
| true | /a | /a/b | true |
| false | /a/ | /a/ | true |
3.6 withRouter
众所周知,react推崇的是组件化的思想,一个界面的可能由许许多多的component组成,如果你想从某个component中在不使用路由组件(Link/NavLink)的情况下进行路由跳转怎么办?答案之一就是使用withRouter。withRouter是一个高阶组件,原理就是使用RouterContext.Consumer为我们的组件提供RouterContext,我们可以从RouterContext拿到histroy,从而进行跳转。
3.7 Link、 NavLink
都是基于<a/>组件的封装,都提供了导航到某个路由的功能。NavLink相当于Link的加强版,支持点击后样式变化.
3.8 Redirect
Redirect, 类似于http的code 304的作用, 当location.pathname 和from属性值匹配时,重定向到to属性值对应的地址。
三. 路由跳转(区分window.histroy)
1) 原理: 直接或间接调用histroy.push()/histroy.replace()方法。
2) <Link/>或<NavLink/>或<Redirect/>组件天然支持。
3) 在不能使用如上组件的情况下,可以设法通过得到histroy对象。那么有哪些途径能拿到histroy了?
| 途径 | 获得方法 |
|---|---|
| Route 的component组件 | component组件内部直接获得: let histroy = this.props.histroy |
| withRouter(component) | component组件内部直接获得: let histroy = this.props.histroy |
| withHistroy() | react-router-dom提供的hook方法,let histroy = useHistroy() |
四. location对象(区别window.location)
HashRouter下location包含pathname、search、hash三个属性。
BrowerRouter下location包含pathname、search、hash、key四个属性。
例如: histroy.push('/about?size=10&location=1#hahs'),在不同路由下拼装出来的完整链接尽管是不同的:
| path | BrowerRouter | HashRouter |
|---|---|---|
| '/about?size=10&location=1#hahs' | 'http://localhost:3000/#/about?size=10&location=1#hahs' | 'http://localhost:3000/about?size=10&location=1#hahs' |
但是这并不影响location。在BrowerRouter和HashRouter都能得到下面一致的输出:
| router | path | location.pathname | location.search | location.hash | window.location.pathname | window.location.search | window.location.hash |
|---|---|---|---|---|---|---|---|
| BrowerRouter | '/about?size=10&location=1#hahs' | '/about' | '?size=10&location=1' | '#hahs' | '/about' | '?size=10&location=1' | '#hahs' |
| HashRouter | '/about?size=10&location=1#hahs' | '/about' | '?size=10&location=1' | '#hahs' | '/' | '' | '#/about?size=10&location=1#hahs' |
显而易见,histroy提供的location磨平了路由差异,让上层的Router组件逻辑更加清晰。
react-router-dom也提供了个useLocation这个hook方法,方便我们在子组件中获取到当前location对象。