深入理解react-router@4.0 使用和源码解析


Posted in Javascript onMay 23, 2017

如果你已经是一个正在开发中的react应用,想要引入更好的管理路由功能。那么,react-router是你最好的选择~

react-router版本现今已经到4.0.0了,而上一个稳定版本还是2.8.1。相信我,如果你的项目中已经在使用react-router之前的版本,那一定要慎重的更新,因为新的版本是一次非常大的改动,如果你要更新,工作量并不小。

这篇文章不讨论版本的变化,只是讨论一下React-router4.0的用法和源码。

源码在这里:https://github.com/ReactTraining/react-router

1.准备

只需要在你的react app中引入一个包yarn add react-router-dom@next

注:react-router-dom是对react-router的作了一些小升级的库,代码基于react-router

2.使用

我们直接上例子:

import React from 'react'
import {BrowserRouter as Router,Route,Link} from 'react-router-dom'

const Basic = () => (
 <Router>
 <div>
  <ul>
  <li><Link to="/">Home</Link></li>
  <li><Link to="/page1">Page1</Link></li>
  <li><Link to="/page2">Page2</Link></li>
  </ul>

  <hr/>

  <Route exact path="/" component={Home}/>
  <Route path="/page1" component={Page1}/>
  <Route path="/page2" component={Page2}/>
 </div>
 </Router>
)

跟之前的版本一样,Router这个组件还是一个容器,但是它的角色变了,4.0的Router下面可以放任意标签了,这意味着使用方式的转变,它更像redux中的provider了。通过上面的例子相信你也可以看到具体的变化。而真正的路由通过Route来定义。Link标签目前看来也没什么变化,依然可以理解为a标签,点击会改变浏览器Url的hash值,通过Route标签来捕获这个url并返回component属性中定义的组件,你可能注意到在为"/"写的路由中有一个exact关键字,这个关键字是将"/"做唯一匹配,否则"/"和"/xxx"都会匹配到path为"/"的路由,制定exact后,"/page1"就不会再匹配到"/"了。如果你不懂,动手试一下~

通过Route路由的组件,可以拿到一个match参数,这个参数是一个对象,其中包含几个数据:

  1. isExact:刚才已经说过这个关键字,表示是为作全等匹配
  2. params:path中包含的一些额外数据
  3. path:Route组件path属性的值
  4. url:实际url的hash值

我们来实现一下刚才的Page2组件:

const Page2 = ({ match }) => (
 <div>
 <h2>Page2</h2>
 <ul>
  <li>
  <Link to={`${match.url}/branch1`}>
   branch1
  </Link>
  </li>
  <li>
  <Link to={`${match.url}/branch2`}>
   branch2
  </Link>
  </li>
  <li>
  <Link to={`${match.url}/branch3`}>
   branch3
  </Link>
  </li>
 </ul>

 <Route path={`${match.url}/:branchId`} component={Branch} />
 <Route exact path={match.url} render={() => (
  <h3>Default Information</h3>
 )} />
 </div>
)

const Branch = ({ match }) => {
 console.log(match);
 return (
 <div>
  <h3>{match.params.branchId}</h3>
 </div>
 )
}

很简单,动手试一试。需要注意的就只有Route的path中冒号":"后的部分相当于通配符,而匹配到的url将会把匹配的部分作为match.param中的属性传递给组件,属性名就是冒号后的字符串。

3.Router标签

细心的朋友肯定注意到了上面的例子中我import的Router是BrowserRouter,这是什么东西呢?如果你用过老版本的react-router,你一定知道history。history是用来兼容不同浏览器或者环境下的历史记录管理的,当我跳转或者点击浏览器的后退按钮时,history就必须记录这些变化,而之前的react-router将history分为三类。

  1. hashHistory 老版本浏览器的history
  2. browserHistory h5的history
  3. memoryHistory node环境下的history,存储在memory中

4.0之前版本的react-router针对三者分别实现了createHashHistory、createBrowserHistory和create MemoryHistory三个方法来创建三种情况下的history,这里就不讨论他们不同的处理方式了,好奇的可以去了解一下~
到了4.0版本,在react-router-dom中直接将这三种history作了内置,于是我们看到了BrowserRouter、HashRouter、MemoryRouter这三种Router,当然,你依然可以使用React-router中的Router,然后自己通过createHistory来创建history来传入。

react-router的history库依然使用的是 https://github.com/ReactTraining/history

4.Route标签

在例子中你可能注意到了Route的几个prop

  1. exact: propType.bool
  2. path: propType.string
  3. component: propType.func
  4. render: propType.func

他们都不是必填项,注意,如果path没有赋值,那么此Route就是默认渲染的。

Route的作用就是当url和Route中path属性的值匹配时,就渲染component中的组件或者render中的内容。

当然,Route其实还有几个属性,比如location,strict,chilren 希望你们自己去了解一下。

说到这,那么Route的内部是怎样实现这个机制的呢?不难猜测肯定是用一个匹配的方法来实现的,那么Route是怎么知道url更新了然后进行重新匹配并渲染的呢?

整理一下思路,在一个web 应用中,改变url无非是2种方式,一种是利用超链接进行跳转,另一种是使用浏览器的前进和回退功能。前者的在触发Link的跳转事件之后触发,而后者呢?Route利用的是我们上面说到过的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
   )
 )
 }
}

这里我只贴出了关键代码,如果你使用过React,相信你能看懂,Route在组件将要Mount的时候添加popState事件的监听,每当popState事件触发,就使用forceUpdate强制刷新,从而基于当前的location.pathname进行一次匹配,再根据结果渲染。

PS:现在最新的代码中,Route源码其实是通过componentWillReceiveProps中setState来实现重新渲染的,match属性是作为Route组件的state存在的.

那么这个关键的matchPath方法是怎么实现的呢?
Route引入了一个外部library:path-to-regexp。这个pathToRegexp方法用于返回一个满足要求的正则表达式,举个例子:

let keys = [],keys2=[]
let re = pathToRegexp('/foo/:bar', keys)
//re = /^\/foo\/([^\/]+?)\/?$/i keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }] 

let re2 = pathToRegexp('/foo/bar', keys2)
//re2 = /^\/foo\/bar(?:\/(?=$))?$/i keys2 = []

关于它的详细信息你可以看这里:https://github.com/pillarjs/path-to-regexp

值得一提的是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
 }, {})
}

5.Link

还记得上面说到的改变url的两种方式吗,我们来说说另一种,Link,看一下它的参数:

static propTypes = {
 onClick: PropTypes.func,
 target: PropTypes.string,
 replace: PropTypes.bool,
 to: PropTypes.oneOfType([
  PropTypes.string,
  PropTypes.object
 ]).isRequired
}

onClick就不说了,target属性就是a标签的target属性,to相当于href。

而replace的意思跳转的链接是否覆盖history中当前的url,若为true,新的url将会覆盖history中的当前值,而不是向其中添加一个新的。

handleClick = (event) => {
 if (this.props.onClick)
 this.props.onClick(event)

 if (
 !event.defaultPrevented && // 是否阻止了默认事件
 event.button === 0 && // 确定是鼠标左键点击
 !this.props.target && // 避免打开新窗口的情况
 !isModifiedEvent(event) // 无视特殊的key值,是否同时按下了ctrl、shift、alt、meta
 ) {
 event.preventDefault()

 const { history } = this.context.router
 const { replace, to } = this.props

 if (replace) {
  history.replace(to)
 } else {
  history.push(to)
 }
 }
}

需要注意的是,history.push和history.replace使用的是pushState方法和replaceState方法。

6.Redirect

我想单独再多说一下Redirect组件,源码很有意思:

class Redirect extends React.Component {
 //...省略一部分代码

 isStatic() {
 return this.context.router && this.context.router.staticContext
 }

 componentWillMount() {
 if (this.isStatic())
  this.perform()
 }

 componentDidMount() {
 if (!this.isStatic())
  this.perform()
 }

 perform() {
 const { history } = this.context.router
 const { push, to } = this.props

 if (push) {
  history.push(to)
 } else {
  history.replace(to)
 }
 }

 render() {
 return null
 }
}

很容易注意到这个组件并没有UI,render方法return了一个null。很容易产生这样一个疑问,既然没有UI为什么react-router的创造者依然选择将Redirect写成一个组件呢?

我想我们可以从作者口中的"Just Components API"中窥得原因吧。

希望这篇文章可以帮助你更好的创建你的React应用.

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
js字符串转换成xml对象并使用技巧解读
Apr 18 Javascript
js跳转页面方法总结
Jan 29 Javascript
浅谈JS日期(Date)处理函数
Dec 07 Javascript
纯JS实现旋转图片3D展示效果
Apr 12 Javascript
js实现表单多按钮提交action的处理方法
Oct 24 Javascript
js实现跨域的多种方法
Dec 25 Javascript
简单几步实现返回顶部效果
Dec 05 Javascript
微信小程序实战之运维小项目
Jan 17 Javascript
Vue-resource实现ajax请求和跨域请求示例
Feb 23 Javascript
整理关于Bootstrap模态弹出框的慕课笔记
Mar 29 Javascript
vue.js的状态管理vuex中store的使用详解
Nov 08 Javascript
微信小程序实现签到弹窗动画
Sep 21 Javascript
angularjs中ng-bind-html的用法总结
May 23 #Javascript
vue.js实现价格格式化的方法
May 23 #Javascript
js编写选项卡效果
May 23 #Javascript
jQuery日期范围选择器附源码下载
May 23 #jQuery
详解JavaScript数组过滤相同元素的5种方法
May 23 #Javascript
强大的 Angular 表单验证功能详细介绍
May 23 #Javascript
微信小程序 侧滑删除(左滑删除)
May 23 #Javascript
You might like
php实现数组按指定KEY排序的方法
2015/03/30 PHP
Laravel 实现添加多语言提示信息
2019/10/25 PHP
Laravel 验证码认证学习记录小结
2019/12/20 PHP
php如何实现数据库的备份和恢复
2020/11/30 PHP
window.parent与window.openner区别介绍
2012/04/12 Javascript
JavaScript子窗口ModalDialog中操作父窗口对像
2012/12/11 Javascript
JS中使用Array函数shift和pop创建可忽略参数的例子
2014/05/28 Javascript
改变状态栏文字的js代码
2014/06/13 Javascript
js实现点击链接后延迟3秒再跳转的方法
2015/06/05 Javascript
jQuery解析与处理服务器端返回xml格式数据的方法详解
2016/07/04 Javascript
浅析Javascript的自动分号插入(ASI)机制
2016/09/29 Javascript
Angular.JS学习之依赖注入$injector详析
2016/10/20 Javascript
如何解决hover在ie6中的兼容性问题
2016/12/15 Javascript
AngularJS中的promise用法分析
2017/05/19 Javascript
weui框架实现上传、预览和删除图片功能代码
2017/08/24 Javascript
解决在Bootstrap模糊框中使用WebUploader的问题
2018/03/22 Javascript
vue路由拦截及页面跳转的设置方法
2018/05/24 Javascript
vue使用i18n实现国际化的方法详解
2019/09/05 Javascript
如何在 Vue 中使用 JSX
2021/02/14 Vue.js
JavaScript实现H5接金币功能(实例代码)
2021/02/22 Javascript
在Python中关于中文编码问题的处理建议
2015/04/08 Python
Django框架中处理URLconf中特定的URL的方法
2015/07/20 Python
python使用Matplotlib改变坐标轴的默认位置
2019/10/18 Python
python中删除某个元素的方法解析
2019/11/05 Python
python用quad、dblquad实现一维二维积分的实例详解
2019/11/20 Python
Pygame的程序开始示例代码
2020/05/07 Python
纯CSS实现预加载动画效果
2017/09/06 HTML / CSS
html标签之Object和EMBED标签详解
2013/07/04 HTML / CSS
HTML5超文本标记语言的实现方法
2020/09/24 HTML / CSS
美国领先的低折扣旅行网站:Hotwire
2019/01/19 全球购物
网络专业学生个人的自我评价
2013/12/16 职场文书
岗位职责说明书模板
2014/07/30 职场文书
合作经营协议书范本
2014/09/16 职场文书
如何使用php生成zip压缩包
2021/04/21 PHP
详解 TypeScript 枚举类型
2021/11/02 Javascript
pytorch中的 .view()函数的用法介绍
2022/03/17 Python