这是第 109 篇不掺水的原创,想获取更多原创好文,请搜索公众号关注咱们吧~ 本文首发于政采云前端博客:React 中的一些 Router 必备知识点javascript
每次开发新页面的时候,都免不了要去设计一个新的 URL,也就是咱们的路由。其实路由在设计的时候不只仅是一个由几个简单词汇和斜杠分隔符组成的连接,偶尔也能够去考虑有没有更“优雅”的设计方式和技巧。而在这背后,路由和组件之间的协做关系是怎样的呢?因而我以 React 中的 Router 使用方法为例,整理了一些知识点小记和你们分享~html
一般咱们使用 React-Router 来实现 React 单页应用的路由控制,它经过管理 URL,实现组件的切换,进而呈现页面的切换效果。前端
其最基本用法以下:java
import { Router, Route } from 'react-router';
render((
<Router> <Route path="/" component={App}/> </Router>
), document.getElementById('app'));
复制代码
亦或是嵌套路由:react
在 React-Router V4 版本以前能够直接嵌套,方法以下:git
<Router>
<Route path="/" render={() => <div>外层</div>}> <Route path="/in" render={() => <div>内层</div>} /> </Route>
</Router>
复制代码
上面代码中,理论上,用户访问 /in
时,会先加载 <div>外层</div>
,而后在它的内部再加载 <div>内层</div>
。github
然而实际运行上述代码却发现它只渲染出了根目录中的内容。后续对比 React-Router 版本发现,是由于在 V4 版本中变动了其渲染逻辑,缘由听说是为了践行 React 的组件化理念,不能让 Route 标签看起来只是一个标签(奇怪的知识又增长了)。数组
如今较新的版本中,可使用 Render 方法实现嵌套。浏览器
<Route
path="/"
render={() => (
<div> <Route path="/" render={() => <div>外层</div>} /> <Route path="/in" render={() => <div>内层</div>} /> <Route path="/others" render={() => <div>其余</div>} /> </div>
)}
/>
复制代码
此时访问 /in
时,会将“外层”和“内层”一块儿展现出来,相似地,访问 /others
时,会将“外层”和“其余”一块儿展现出来。微信
在实际开发中,每每在页面切换时须要传递一些参数,有些参数适合放在 Redux 中做为全局数据,或者经过上下文传递,好比业务的一些共享数据,但有些参数则适合放在 URL 中传递,好比页面类型或详情页中单据的惟一标识 id
。在处理 URL 时,除了问号带参数的方式,React-Router 能帮咱们作什么呢?在这其中,Route 组件的 path
属性即可用于指定路由的匹配规则。
描述:就想让普普统统的 URL 带个平平无奇的参数
那么,接下来咱们能够这样干:
Case A:路由参数
path="/book/:id"
复制代码
咱们能够用冒号 + 参数名字的方式,将想要传递的参数添加到 URL 上,此时,当参数名字(本 Case 中是 id)对应的值改变时,将被认为是不一样 URL。
Case B:查询参数
path="/book"
复制代码
若是想要在页面跳转的时候问号带参数,那么 path 能够直接设计成既定的样子,参数由跳转方拼接。 在跳转时,有两种形式带上参数。其一是在 Link 组件的 to 参数中经过配置字符串并用问号带参数,其二是 to 参数能够接受一个对象,其中能够在 search 字段中配置想要传递的参数。
<Link to="/book?id=111" />
// 或者
<Link to={{ pathname: '/book', search: '?id=111', }}/>
复制代码
此时,假设当前页面 URL中的 id 由111 修改成 222 时,该路由对应的组件(在上述例子中就是 React-Route 配置时 path="/book"
对应的页面/组件 )会更新,即执行 componentDidUpdate 方法,但不会被卸载,也就是说,不会执行 componentDidMount 方法。
Case C:查询参数隐身式带法
path="/book"
复制代码
path 依旧设计成既定的样子,而在跳转时,能够经过 Link 中的 state 将参数传递给对应路由的页面。
<Link to={{
pathname: '/book',
state: { id: 111 }
}}/>
复制代码
但必定要注意的是,尽管这种方式下查询参数不会明文传递了,但此时页面刷新会致使参数丢失(存储在 state 中的通病),So,灰常不推荐~~(其实不想明文能够进行加密处理,但通常状况下敏感信息是不建议放在 URL 中传递的~)
描述:编辑/详情页,想要共用一个页面,URL 由不一样的参数区分,此时咱们但愿,参数必须为 edit、detail、add 中的 1 个,否则须要跳转到 404 Not Found 页面。
path='/book/:pageType(edit|detail|add)'
复制代码
若是不加括号中的内容 (edit|detail|add)
,当传入错误的参数(好比用户误操做、随便拼接 URL 的状况),则页面不会被 404 拦截,而是继续走下去开始渲染页面或调用接口,但此时颇有可能致使接口传参错误或页面出错。
描述:新增页和编辑页辣么像,个人新增页也想和编辑/详情共用一个页面。可是新增页不须要 id,编辑/详情页须要 id,使用同一个页面怎么办?
path='/book/:pageType(edit|detail|add)/:id?'
复制代码
别急,能够用 ?
来解决,它意味着 id 不是一个必要参数,可传可不传。
描述:个人 id 只能是数字,不想要字符串怎么办?
path='/book/:id(\\\d+)'
复制代码
此时 id 不是数字时,会跳转 404,被认为 URL 对应的页面找不到啦。
有了这么多场景,那 Router 是怎样实现的呢?其实它底层是依赖了 path-to-regexp 方法。
var pathToRegexp = require('path-to-regexp')
// pathToRegexp(path, keys, options)
// 示例
var keys = []
var re = pathToRegexp('/foo/:bar', keys)
// re = /^\/foo\/([^\/]+?)\/?$/i
// keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
复制代码
delimiter:重复参数的定界符,默认是 '/',可配置
一些其余经常使用的路由正则通配符:
? 可选参数
* 匹配 0 次或屡次
+ 匹配 1 次或屡次
若是忘记写参数名字,而只写了路由规则,好比下述代码中 /:foo
后面的参数:
var re = pathToRegexp('/:foo/(.*)', keys)
// 匹配除“\n”以外的任何字符
// keys = [{ name: 'foo', ... }, { name: 0, ...}]
re.exec('/test/route')
//=> ['/test/route', 'test', 'route']
复制代码
它也会被正确解析,只不过在方法处理的内部,未命名的参数名会被替换成数组下标。
path 带的参数,能够经过 this.props.match
获取
例如:
// url 为 /book/:pageType(edit|detail|add)
const { match } = this.props;
const { pageType } = match.params;
复制代码
因为有 #,# 以后的全部内容都会被认为是 hash 的一部分,window.location.search 是取不到问号带的参数的。
那么在 React-Router 中,问号带的参数,能够经过 this.props.location
(官方墙推 👍)获取。我的理解是由于 React-Router 帮咱们作了处理,经过路由和 hash 值(window.location.hash)作了解析的封装。
例如:
// url 为 /book?pageType=edit
const { location } = this.props;
const searchParams = location.search; // ?pageType=edit
复制代码
实际打印 props 参数发现,this.props.history.location
也能够取到问号参数,但不建议使用,由于 React 的生命周期(componentWillReceiveProps、componentDidUpdate)可能使它变得不可靠。(缘由可参考:blog.csdn.net/zrq1210/art…
在早期的 React-Router 2.0 版本是能够用 location.query.pageType 来获取参数的,可是 V4.0 去掉了(有人认为查询参数不是 URL 的一部分,有人认为如今有不少第三方库,交给开发者本身去解析会更好,有个对此讨论的 Issue,有兴趣的能够自行获取 😊 github.com/ReactTraini…
针对上一节中场景 1 的 Case C,查询参数隐身式带法时(从 state 里带过去的),在 this.props.location.state
里能够取到(不推荐不推荐不推荐,刷新会没~)
<div>
<Route path="/router/:type" render={() => <div>影像</div>} />
<Route path="/router/book" render={() => <div>图书</div>} />
</div>
复制代码
若是 <Route />
是平铺的(用 div
包裹是由于 Router 下只能有一个元素),输入 /router/book
则影像和图书都会被渲染出来,若是想要只精确渲染其中一个,则须要 Switch
<Switch>
<Route path="/router/:type" render={() => <div>影像</div>} />
<Route path="/router/book" render={() => <div>图书</div>} />
</Switch>
复制代码
Switch 的意思即是精准的根据不一样的 path 渲染不一样 Route 下的组件。 可是,加了 Switch 以后路由匹配规则是从上到下执行,一旦发现匹配,就再也不匹配其他的规则了。所以在使用的时候必定要“百般当心”。
上面代码中,用户访问 /router/book
时,不会触发第二个路由规则(不会 展现“图书”),由于它会匹配 /router/:type
这个规则。所以,带参数的路径通常要写在路由规则的底部。
路由作的事情:管控 URL 变化,改变浏览器中的地址。
Router 作的事情:URL 改变时,触发渲染,渲染对应的组件。
URL 有两种,一种不带 #,一种带 #,分别对应 Browse 模式和 Hash 模式。
通常单页应用中,改变 URL,可是不从新加载页面的方式有两类:
Case 1(会触发路由监听事件):点击 前进、后退,或者调用的 history.back( )、history.forward( )
Case 2(不会触发路由监听事件):组件中调用 history.push( ) 和 history.replace( )
因而参考 「源码解析 」这一次完全弄懂 React-Router 路由原理 一文,针对上述两种 Case,以及这两种 Case 分别对应的两种模式,做出以下总结。
图片来源:「源码解析 」这一次完全弄懂 React-Router 路由原理
Case 1:
URL 改变,触发路由的监听事件 popstate
,then,监听事件的回调函数 handlePopState
在回调中触发 history 的 setState
方法,产生新的 location 对象。state 改变,通知 Router 组件更新 location
并经过 context 上下文传递,匹配出符合的 Route 组件,最后由 <Route />
组件取出对应内容,传递给渲染页面,渲染更新。
/* 简化版的 handlePopState (监听事件的回调) */
const handlePopState = (event)=>{
/* 获取当前location对象 */
const location = getDOMLocation(event.state)
const action = 'POP'
/* transitionManager 处理路由转换 */
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
if (ok) {
setState({ action, location })
} else {
revertPop(location)
}
})
}
复制代码
Case 2: 以 history.push 为例,首先依据你要跳转的 path 建立一个新的 location
对象,而后经过 window.history.pushState
(H5 提供的 API )方法改变浏览器当前路由(即当前的 url),最后经过 setState
方法通知 Router,触发组件更新。
const push = (path, state) => {
const action = 'PUSH'
/* 建立location对象 */
const location = createLocation(path, state, createKey(), history.location)
/* 肯定是否能进行路由转换 */
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
... // 此处省略部分代码
const href = createHref(location)
const { key, state } = location
if (canUseHistory) {
/* 改变 url */
globalHistory.pushState({ key, state }, null, href)
if (forceRefresh) {
window.location.href = href
} else {
/* 改变 react-router location对象, 建立更新环境 */
setState({ action, location })
}
} else {
window.location.href = href
}
})
}
复制代码
Case 1:
增长监听,当 URL 的 Hash 发生变化时,触发 hashChange 注册的回调,回调中去进行相相似的操做,进而展现不一样的内容。
window.addEventListener('hashchange',function(e){
/* 监听改变 */
})
复制代码
Case 2: history.push
底层调用 window.location.hash
来改变路由。history.replace
底层是调用 window.location.replace
改变路由。而后 setState 通知改变。
从一些参考资料中显示,出于兼容性的考虑(H5 的方法 IE10 如下不兼容),路由系统内部将 Hash 模式做为建立 History 对象的默认方法。(此处如有疑议,欢迎指正~)
在实际项目中发现,Link,Route 都是从 dva/router
中引进来的,那么,Dva 在这之中作了什么呢?
答案:貌似没有作特殊处理,Dva 在 React-Router 上作了上层封装,会默认输出 React-Router 接口。
Case 1:
项目代码的 src 目录下,无论有多少文件夹,路由通常会放在同一个 router.js 文件中维护,但这样会致使页面太多时,文件内容会愈来愈长,不便于查找和修改。
所以咱们能够作一些小改造,在 src 下的每一个文件夹中,建立本身的路由配置文件,以便管理各自的路由。但这种状况下 React-Router 是不能识别的,因而咱们写了一个 Plugin 放在 Webpack 中,目的是将各个文件夹下的路由汇总,并生成 router-config.js 文件。以后,将该文件中的内容解析成组件须要的相关内容。插件实现方式可了解本团队另外一篇文章: 手把手带你入门 Webpack Plugin。
Case 2:
路由的 Hash 模式虽然兼容性好,可是也存在一些问题:
所以公司内部作了一次 Hash 路由转 Browser 路由的改造。
如原有连接为:aaa.bbb.com/book-center…
改造方案为:
经过新增如下配置代码去掉 #
import createHistory from 'history/createBrowserHistroy';
const app = dva({
history: createHistory({
basename: '/book-center',
}),
onError,
});
复制代码
同时,为了不用户访问旧页面出现 404 的状况,前端须要在 Redirect 中配置重定向以及在 Nginx 中配置旧的 Hash 页面转发。
Case 3:
在实际项目中,其实咱们也会去考虑用户未受权时路由跳转、页面 404 时路由跳转等不一样状况,如下 Case 和代码仅供读者参考~
<Switch>
{
getRoutes(match.path, routerData).map(item =>
(
// 用户未受权处理,AuthorizedRoute 为项目中本身实现的处理组件
<AuthorizedRoute {...item} redirectPath="/exception/403" />
)
)
}
// 默认跳转页面
<Redirect from="/" exact to="/list" />
// 页面 404 处理
<Route render={props => <NotFound {...props} />} />
</Switch>
复制代码
「源码解析 」这一次完全弄懂react-router路由原理
开源地址 www.zoo.team/openweekly/ (小报官网首页有微信交流群)
政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在平常的业务对接以外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推进并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。
若是你想改变一直被事折腾,但愿开始能折腾事;若是你想改变一直被告诫须要多些想法,却无从破局;若是你想改变你有能力去作成那个结果,却不须要你;若是你想改变你想作成的事须要一个团队去支撑,但没你带人的位置;若是你想改变既定的节奏,将会是“5 年工做时间 3 年工做经验”;若是你想改变原本悟性不错,但老是有那一层窗户纸的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但愿参与到随着业务腾飞的过程,亲手推进一个有着深刻的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我以为咱们该聊聊。任什么时候间,等着你写点什么,发给 ZooTeam@cai-inc.com