热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

ReactRouterV4构建之道与源码分析

ReactRouter-V4构建之道与源码分析翻译自build-your-own-react-router-v4,从属于笔者的Web开发基础与工程实践系列。多年之后当我回想起初学客

ReactRouter-V4 构建之道与源码分析 翻译自build-your-own-react-router-v4,从属于笔者的 Web 开发基础与工程实践 系列。

《ReactRouter-V4 构建之道与源码分析》

多年之后当我回想起初学客户端路由的那个下午,满脑子里充斥着的只是对于单页应用的惊叹与浆糊。彼时我还是将应用代码与路由代码当做两个独立的部分进行处理,就好像同父异母的兄弟尽管不喜欢对方但是不得不在一起。幸而这些年里我能够和其他优秀的开发者进行交流,了解他们对于客户端路由的看法。尽管他们中的大部分与我“英雄所见略同”,但是我还是找到了合适的平衡路由的抽象程度与复杂程度的方法。本文即是我在构建 React Router V4 过程中的考虑以及所谓路由即组件思想的落地实践。首先我们来看下我们在构建路由过程中的测试代码,你可以用它来测试你的自定义路由:

const Home = () => (

Home


)
const About = () => (

About


)
const Topic = ({ topicId }) => (

{topicId}


)
const Topics = ({ match }) => {
const items = [
{ name: 'Rendering with React', slug: 'rendering' },
{ name: 'Components', slug: 'components' },
{ name: 'Props v. State', slug: 'props-v-state' },
]
return (

Topics



    {items.map(({ name, slug }) => (

  • {name}

  • ))}

{items.map(({ name, slug }) => (
(

)} />
))}
(

Please select a topic.


)}/>

)
}
const App = () => (


  • Home

  • About

  • Topics








)

如果你对于 React Router v4 尚不是完全了解,我们先对上述代码中涉及到的相关关键字进行解释。Route 会在当前 URL 与 path 属性值相符的时候渲染相关组件,而 Link 提供了声明式的,易使用的方式来在应用内进行跳转。换言之,Link 组件允许你更新当前 URL,而 Route 组件则是根据 URL 渲染组件。本文并不专注于讲解 RRV4 的基础概念,你可以前往官方文档了解更多知识;本文是希望介绍我在构建 React Router V4 过程中的思维考虑过程,值得一提的是,我很欣赏 React Router V4 中的 Just Components 概念,这一点就不同于 React Router 之前的版本中将路由与组件隔离来看,而允许了路由组件像普通组件一样自由组合。相信对于 React 组件相当熟悉的开发者绝不会陌生于如何将路由组件嵌入到正常的应用中。

Route

我们首先来考量下如何构建Route组件,包括其暴露的 API,即 Props。在我们上面的示例中,我们会发现Route组件包含三个 Props:exactpath 以及 component。这也就意味着我们的propTypes声明如下:

static propTypes = {
exact: PropTypes.bool,
path: PropTypes.string,
component: PropTypes.func,
}

这里有一些微妙的细节需要考虑,首先对于path并没有设置为必须参数,这是因为我们认为对于没有指定关联路径的Route组件应该自动默认渲染。而component参数也没有被设置为必须是因为我们提供了其他的方式进行渲染,譬如render函数:

{
return
}} />

render 函数允许你方便地使用内联函数来创建 UI 而不是创建新的组件,因此我们也需要将该函数设置为 propTypes:

static propTypes = {
exact: PropTypes.bool,
path: PropTypes.string,
component: PropTypes.func,
render: PropTypes.func,
}

在确定了 Route 需要接收的组件参数之后,我们需要来考量其实际功能;Route 核心的功能在于能够当 URL 与 path 属性相一致时执行渲染操作。基于这个论断,我们首先需要实现判断是否匹配的功能,如果判断为匹配则执行渲染否则返回空值。我们在这里将该函数命名为 matchPatch,那么此时整个 Route 组件的 render 函数定义如下:

class Route extends Component {
static propTypes = {
exact: PropTypes.bool,
path: PropTypes.string,
component: PropTypes.func,
render: PropTypes.func,
}
render () {
const {
path,
exact,
component,
render,
} = this.props
const match = matchPath(
location.pathname, // global DOM variable
{ path, exact }
)
if (!match) {
// Do nothing because the current
// location doesn't match the path prop.
return null
}
if (component) {
// The component prop takes precedent over the
// render method. If the current location matches
// the path prop, create a new element passing in
// match as the prop.
return React.createElement(component, { match })
}
if (render) {
// If there's a match but component
// was undefined, invoke the render
// prop passing in match as an argument.
return render({ match })
}
return null
}
}

现在的 Route 看起来已经相对明确了,当路径相匹配的时候才会执行界面渲染,否则返回为空。现在我们再回过头来考虑客户端路由中常见的跳转策略,一般来说用户只有两种方式会更新当前 URL。一种是用户点击了某个锚标签或者直接操作 history 对象的 replace/push 方法;另一种是用户点击前进/后退按钮。无论哪一种方式都要求我们的路由系统能够实时监听 URL 的变化,并且在 URL 发生变化时及时地做出响应,渲染出正确的页面。我们首先来考虑下如何处理用户点击前进/后退按钮。React Router 使用 History 的 .listen 方法来监听当前 URL 的变化,其本质上还是直接监听 HTML5 的 popstate 事件。popstate 事件会在用户点击某个前进/后退按钮的时候触发;而在每次重渲染的时候,每个 Route 组件都会重现检测当前 URL 是否匹配其预设的路径参数。

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
const match = matchPath(location.pathname, { path, exact })
if (!match)
return null
if (component)
return React.createElement(component, { match })
if (render)
return render({ match })
return null
}
}

你会发现上面的代码与之前的相比多了挂载与卸载 popstate 监听器的功能,其会在组件挂载时添加一个 popstate 监听器;当监听到 popstate 事件被触发时,我们会调用 forceUpdate 函数来强制进行重渲染。总结而言,无论我们在系统中设置了多少的路由组件,它们都会独立地监听 popstate 事件并且相应地执行重渲染操作。接下来我们继续讨论 matchPath 这个 Route 组件中至关重要的函数,它负责决定当前路由组件的 path 参数是否与当前 URL 相一致。这里还必须提下我们设置的另一个 Route 的参数 exact,其用于指明路径匹配策略;当 exact 值被设置为 true 时,仅当路径完全匹配于 location.pathname 才会被认为匹配成功:

pathlocation.pathnameexactmatches?
/one/one/twotrueno
/one/one/twofalseyes

让我们深度了解下 matchPath 函数的工作原理,该函数的签名如下:

const match = matchPath(location.pathname, { path, exact })

其中函数的返回值 match 应该根据路径是否匹配的情况返回为空或者一个对象。基于这些推导我们可以得出 matchPatch 的原型:

const matchPath = (pathname, options) => {
const { exact = false, path } = options
}

这里我们使用 ES6 的解构赋值,当某个属性未定义时我们使用预定义地默认值,即 false。我在上文提及的 path 非必要参数的具体支撑实现就在这里,我们首先进行空检测,当发现 path 为未定义或者为空时则直接返回匹配成功:

const matchPath = (pathname, options) => {
const { exact = false, path } = options
if (!path) {
return {
path: null,
url: pathname,
isExact: true,
}
}
}

接下来继续考虑具体执行匹配的部分,React Router 使用了 pathToRegex 来检测是否匹配,即可以用简单的正则表达式:

const matchPath = (pathname, options) => {
const { exact = false, path } = options
if (!path) {
return {
path: null,
url: pathname,
isExact: true,
}
}
const match = new RegExp(`^${path}`).exec(pathname)
}

这里使用的 .exec 函数,会在包含指定的文本时返回一个数组,否则返回空值;下表即是当我们的路由设置为 /topics/components 时具体的返回:

| path | location.pathname | return value |
| ----------------------- | -------------------- | ------------------------ |
| `/` | `/topics/components` | `['/']` |
| `/about` | `/topics/components` | `null` |
| `/topics` | `/topics/components` | `['/topics']` |
| `/topics/rendering` | `/topics/components` | `null` |
| `/topics/components` | `/topics/components` | `['/topics/components']` |
| `/topics/props-v-state` | `/topics/components` | `null` |
| `/topics` | `/topics/components` | `['/topics']` |

这里大家就会看出来,我们会为每个 实例创建一个 match 对象。在获取到 match 对象之后,我们需要再做如下判断是否匹配:

const matchPath = (pathname, options) => {
const { exact = false, path } = options
if (!path) {
return {
path: null,
url: pathname,
isExact: true,
}
}
const match = new RegExp(`^${path}`).exec(pathname)
if (!match) {
// There wasn't a match.
return null
}
const url = match[0]
const isExact = pathname === url
if (exact && !isExact) {
// There was a match, but it wasn't
// an exact match as specified by
// the exact prop.
return null
}
return {
path,
url,
isExact,
}
}
Link

上文我们已经提及通过监听 popstate 状态来响应用户点击前进/后退事件,现在我们来考虑通过构建 Link 组件来处理用户通过点击锚标签进行跳转的事件。Link 组件的 API 应该如下所示:

其中的 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()
// route here.
}
render() {
const { to, children} = this.props
return (

{children}

)
}
}

这里实际的跳转操作我们还是执行 History 中的抽象的 pushreplace 函数,在使用 browserHistory 的情况下我们本质上还是使用 HTML5 中的 pushStatereplaceState 函数。pushStatereplaceState 函数都要求输入三个参数,首先是一个与最新的历史记录相关的对象,在 React Router 中我们并不需要该对象,因此直接传入一个空对象;第二个参数是标题参数,我们同样不需要改变该值,因此直接传入空即可;最后第三个参数则是我们需要的,用于指明新的相对地址的字符串:

const historyPush = (path) => {
history.pushState({}, null, path)
}
const historyReplace = (path) => {
history.replaceState({}, null, path)
}

而后在 Link 组件内,我们会根据 replace 参数来调用 historyPush 或者 historyReplace 函数:

class Link extends Component {
static propTypes = {
to: PropTypes.string.isRequired,
replace: PropTypes.bool,
}
handleClick = (event) => {
const { replace, to } = this.props
event.preventDefault()
replace ? historyReplace(to) : historyPush(to)
}
render() {
const { to, children} = this.props
return (

{children}

)
}
}
组件注册

现在我们需要考虑如何保证用户点击了 Link 组件之后触发全部路由组件的检测与重渲染。在我们上面实现的 Link 组件中,用户执行跳转之后浏览器的显示地址会发生变化,但是页面尚不能重新渲染;我们声明的 Route 组件并不能收到相应的通知。为了解决这个问题,我们需要追踪那些显现在界面上实际被渲染的 Route 组件并且当路由变化时调用它们的 forceUpdate 方法。React Router 主要通过有机组合 setStatecontext 以及 history.listen 方法来实现该功能。每个 Route 组件被挂载时我们会将其加入到某个数组中,然后当位置变化时,我们可以遍历该数组然后对每个实例调用 forceUpdate 方法:

let instances = []
const register = (comp) => instances.push(comp)
const unregister = (comp) => instances.splice(instances.indexOf(comp), 1)

这里我们创建了两个函数,当 Route 挂载时调用 register 函数,而卸载时调用 unregister 函数。然后无论何时调用 historyPush 或者 historyReplace 函数时都会遍历实例数组中的对象的渲染方法,此时我们的 Route 组件就需要声明为如下样式:

class Route extends Component {
static propTypes: {
path: PropTypes.string,
exact: PropTypes.bool,
component: PropTypes.func,
render: PropTypes.func,
}
componentWillMount() {
addEventListener("popstate", this.handlePop)
register(this)
}
componentWillUnmount() {
unregister(this)
removeEventListener("popstate", this.handlePop)
}
...
}

然后我们需要更新 historyPushhistoryReplace 函数:

const historyPush = (path) => {
history.pushState({}, null, path)
instances.forEach(instance => instance.forceUpdate())
}
const historyReplace = (path) => {
history.replaceState({}, null, path)
instances.forEach(instance => instance.forceUpdate())
}

这样的话就保证了无论何时用户点击 组件之后,在位置显示变化的同时,所有的 组件都能够被通知到并且执行重匹配与重渲染。现在我们完整的路由解决方案就成形了:

import React, { PropTypes, Component } from 'react'
let instances = []
const register = (comp) => instances.push(comp)
const unregister = (comp) => instances.splice(instances.indexOf(comp), 1)
const historyPush = (path) => {
history.pushState({}, null, path)
instances.forEach(instance => instance.forceUpdate())
}
const historyReplace = (path) => {
history.replaceState({}, null, path)
instances.forEach(instance => instance.forceUpdate())
}
const matchPath = (pathname, options) => {
const { exact = false, path } = options
if (!path) {
return {
path: null,
url: pathname,
isExact: true
}
}
const match = new RegExp(`^${path}`).exec(pathname)
if (!match)
return null
const url = match[0]
const isExact = pathname === url
if (exact && !isExact)
return null
return {
path,
url,
isExact,
}
}
class Route extends Component {
static propTypes: {
path: PropTypes.string,
exact: PropTypes.bool,
component: PropTypes.func,
render: PropTypes.func,
}
componentWillMount() {
addEventListener("popstate", this.handlePop)
register(this)
}
componentWillUnmount() {
unregister(this)
removeEventListener("popstate", this.handlePop)
}
handlePop = () => {
this.forceUpdate()
}
render() {
const {
path,
exact,
component,
render,
} = this.props
const match = matchPath(location.pathname, { path, exact })
if (!match)
return null
if (component)
return React.createElement(component, { match })
if (render)
return render({ match })
return null
}
}
class Link extends Component {
static propTypes = {
to: PropTypes.string.isRequired,
replace: PropTypes.bool,
}
handleClick = (event) => {
const { replace, to } = this.props
event.preventDefault()
replace ? historyReplace(to) : historyPush(to)
}
render() {
const { to, children} = this.props
return (

{children}

)
}
}

另外,React Router API 中提供了所谓 组件,允许执行路由跳转操作:

class Redirect extends Component {
static defaultProps = {
push: false
}
static propTypes = {
to: PropTypes.string.isRequired,
push: PropTypes.bool.isRequired,
}
componentDidMount() {
const { to, push } = this.props
push ? historyPush(to) : historyReplace(to)
}
render() {
return null
}
}

注意这个组件并没有真实地进行界面渲染,而是仅仅进行了简单的跳转操作。到这里本文也就告一段落了,希望能够帮助你去了解 React Router V4 的设计思想以及 Just Component 的接口理念。我一直说 React 会让你成为更加优秀地开发者,而 React Router 则会是你不小的助力。


推荐阅读
  • 深入解析C#中app.config文件的配置与修改方法
    在C#开发过程中,经常需要对系统的配置文件进行读写操作,如系统初始化参数的修改或运行时参数的更新。本文将详细介绍如何在C#中正确配置和修改app.config文件,包括其结构、常见用法以及最佳实践。此外,还将探讨exe.config文件的生成机制及其在不同环境下的应用,帮助开发者更好地管理和维护应用程序的配置信息。 ... [详细]
  • Spring框架中枚举参数的正确使用方法与技巧
    本文详细阐述了在Spring Boot框架中正确使用枚举参数的方法与技巧,旨在帮助开发者更高效地掌握和应用枚举类型的数据传递,适合对Spring Boot感兴趣的读者深入学习。 ... [详细]
  • 深入解析 Android 中 EditText 的 getLayoutParams 方法及其代码应用实例 ... [详细]
  • 在Android应用开发中,实现与MySQL数据库的连接是一项重要的技术任务。本文详细介绍了Android连接MySQL数据库的操作流程和技术要点。首先,Android平台提供了SQLiteOpenHelper类作为数据库辅助工具,用于创建或打开数据库。开发者可以通过继承并扩展该类,实现对数据库的初始化和版本管理。此外,文章还探讨了使用第三方库如Retrofit或Volley进行网络请求,以及如何通过JSON格式交换数据,确保与MySQL服务器的高效通信。 ... [详细]
  • Squaretest:自动生成功能测试代码的高效插件
    本文将介绍一款名为Squaretest的高效插件,该工具能够自动生成功能测试代码。使用这款插件的主要原因是公司近期加强了代码质量的管控,对各项目进行了严格的单元测试评估。Squaretest不仅提高了测试代码的生成效率,还显著提升了代码的质量和可靠性。 ... [详细]
  • JavaScript XML操作实用工具类:XmlUtilsJS技巧与应用 ... [详细]
  • 在C#编程中,设计流畅的用户界面是一项重要的任务。本文分享了实现Fluent界面设计的技巧与方法,特别是通过编写领域特定语言(DSL)来简化字符串操作。我们探讨了如何在不使用`+`符号的情况下,通过方法链式调用来组合字符串,从而提高代码的可读性和维护性。文章还介绍了如何利用静态方法和扩展方法来实现这一目标,并提供了一些实用的示例代码。 ... [详细]
  • 为了确保iOS应用能够安全地访问网站数据,本文介绍了如何在Nginx服务器上轻松配置CertBot以实现SSL证书的自动化管理。通过这一过程,可以确保应用始终使用HTTPS协议,从而提升数据传输的安全性和可靠性。文章详细阐述了配置步骤和常见问题的解决方法,帮助读者快速上手并成功部署SSL证书。 ... [详细]
  • 本指南介绍了如何在ASP.NET Web应用程序中利用C#和JavaScript实现基于指纹识别的登录系统。通过集成指纹识别技术,用户无需输入传统的登录ID即可完成身份验证,从而提升用户体验和安全性。我们将详细探讨如何配置和部署这一功能,确保系统的稳定性和可靠性。 ... [详细]
  • 本文详细介绍了 Java 中遍历 Map 对象的几种常见方法及其应用场景。首先,通过 `entrySet` 方法结合增强型 for 循环进行遍历是最常用的方式,适用于需要同时访问键和值的场景。此外,还探讨了使用 `keySet` 和 `values` 方法分别遍历键和值的技巧,以及使用迭代器(Iterator)进行更灵活的遍历操作。每种方法都附有示例代码和具体的应用实例,帮助开发者更好地理解和选择合适的遍历策略。 ... [详细]
  • 本文详细介绍了在 Oracle 数据库中使用 MyBatis 实现增删改查操作的方法。针对查询操作,文章解释了如何通过创建字段映射来处理数据库字段风格与 Java 对象之间的差异,确保查询结果能够正确映射到持久层对象。此外,还探讨了插入、更新和删除操作的具体实现及其最佳实践,帮助开发者高效地管理和操作 Oracle 数据库中的数据。 ... [详细]
  • 分享一款基于Java开发的经典贪吃蛇游戏实现
    本文介绍了一款使用Java语言开发的经典贪吃蛇游戏的实现。游戏主要由两个核心类组成:`GameFrame` 和 `GamePanel`。`GameFrame` 类负责设置游戏窗口的标题、关闭按钮以及是否允许调整窗口大小,并初始化数据模型以支持绘制操作。`GamePanel` 类则负责管理游戏中的蛇和苹果的逻辑与渲染,确保游戏的流畅运行和良好的用户体验。 ... [详细]
  • 在本文中,我们将深入探讨C#中的构造函数及其应用场景。通过引入构造函数,可以有效解决在访问类属性时反复赋值导致的代码冗余问题,提高代码的可读性和维护性。此外,还将介绍构造函数的不同类型及其在实际开发中的最佳实践。 ... [详细]
  • 本文详细探讨了在ASP.NET环境中通过加密数据库连接字符串来提升数据安全性的方法。加密技术不仅能够有效防止敏感信息泄露,还能增强应用程序的整体安全性。文中介绍了多种加密手段及其实施步骤,帮助开发者在日常开发过程中更好地保护数据库连接信息,确保数据传输的安全可靠。 ... [详细]
  • 深入解析:React与Webpack配置进阶指南(第二部分)
    在本篇进阶指南的第二部分中,我们将继续探讨 React 与 Webpack 的高级配置技巧。通过实际案例,我们将展示如何使用 React 和 Webpack 构建一个简单的 Todo 应用程序,具体包括 `TodoApp.js` 文件中的代码实现,如导入 React 和自定义组件 `TodoList`。此外,我们还将深入讲解 Webpack 配置文件的优化方法,以提升开发效率和应用性能。 ... [详细]
author-avatar
mobiledu2502886217
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有