深入理解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 相关文章推荐
一个cssQuery对象 javascript脚本实现代码
Jul 21 Javascript
jQuery LigerUI 使用教程入门篇
Jan 18 Javascript
js生成缩略图后上传并利用canvas重绘
May 15 Javascript
js使用split函数按照多个字符对字符串进行分割的方法
Mar 20 Javascript
js跨域请求数据的3种常用的方法
Dec 01 Javascript
JavaScript闭包实例详解
Jun 03 Javascript
用jquery获取自定义的标签属性的值简单实例
Sep 17 Javascript
基于jQuery实现中英文切换导航条效果
Sep 18 Javascript
使用vue和datatables进行表格的服务器端分页实例代码
Jun 07 Javascript
ES6 迭代器(Iterator)和 for.of循环使用方法学习(总结)
Feb 08 Javascript
vue下使用nginx刷新页面404的问题解决
Aug 02 Javascript
基于vue项目设置resolves.alias: '@'路径并适配webstorm
Dec 02 Vue.js
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中foreach结合curl实现多线程的方法分析
2016/09/22 PHP
PHP合并数组的2种方法小结
2016/11/24 PHP
PHP实现正则表达式分组捕获操作示例
2018/02/03 PHP
ThinkPHP5框架中使用JWT的方法示例
2020/06/03 PHP
javascript中对对层的控制
2006/12/29 Javascript
jQuery EasyUI API 中文文档 - Documentation 文档
2011/09/29 Javascript
jquery+css+ul模拟列表菜单具体实现思路
2013/04/15 Javascript
我的Node.js学习之路(四)--单元测试
2014/07/06 Javascript
分享10个原生JavaScript技巧
2015/04/20 Javascript
利用JavaScript脚本实现滚屏效果的方法
2015/07/07 Javascript
jQuery实现图片文字淡入淡出效果
2015/12/21 Javascript
基于JS实现的笛卡尔乘积之商品发布
2016/05/13 Javascript
JS加载iFrame出现空白问题的解决办法
2016/05/13 Javascript
谈谈JavaScript中浏览器兼容问题的写法小议
2016/12/17 Javascript
Ajax跨域实现代码(后台jsp)
2017/01/21 Javascript
JS表单数据验证的正则表达式(常用)
2017/02/18 Javascript
js动态添加表格逐行添加、删除、遍历取值的实例代码
2018/01/25 Javascript
微信小程序scroll-view仿拼多多横向滑动滚动条
2020/04/21 Javascript
详解vue 数组和对象渲染问题
2018/09/21 Javascript
Python对数据库操作
2016/03/28 Python
Django实现简单分页功能的方法详解
2017/12/05 Python
python开启debug模式的方法
2019/06/27 Python
Python3环境安装Scrapy爬虫框架过程及常见错误
2019/07/12 Python
Django自带日志 settings.py文件配置方法
2019/08/30 Python
Python全局变量与global关键字常见错误解决方案
2020/10/05 Python
python爬虫scrapy图书分类实例讲解
2020/11/23 Python
css3背景_动力节点Java学院整理
2017/07/11 HTML / CSS
阿联酋电子产品购物网站:Menakart
2017/09/15 全球购物
TripAdvisor西班牙官方网站:全球领先的旅游网站
2018/01/10 全球购物
生物科学专业个人求职信范文
2013/12/07 职场文书
房地产项目策划书
2014/02/05 职场文书
大学班级文化建设方案
2014/05/06 职场文书
环境卫生整治简报
2015/07/20 职场文书
女方家长婚礼致辞
2015/07/27 职场文书
2019学校请假条格式及范文
2019/06/25 职场文书
详解Nginx 被动检查服务器的存活状态
2021/10/16 Servers