pyqt 获取 UI 中组件_如何更优雅的设计前端组件

组件是大多数现代前端框架的基本概念之一,在 React 和 Vue 以及 Ember 和 Mithril 等框架中均有所体现。组件通常是由标记语言、逻辑和样式组成的集合。它们被创建的目的就是作为可复用的模块去构建我们的应用程序。

cf4177fbb7090822c2a6fbc0d7b1c2d4.png

类似于传统 OOP 语言中 class 的设计,在设计组件的时候需要考虑到很多方面,以便它们可以很好的复用,组合,分离和低耦合,但是功能可以比较稳定的实现,即使是在超出实际测试用例范围的情况下。这样的设计说起来容易做起来却很难,因为现实中我们往往没有足够的时间按照最优的方式去做。

设计思路

在本文中,我想介绍一些组件相关的设计概念,在进行前端开发时应该考虑这些概念。我认为最好的方法是给每个概念一个简洁精炼的名字,然后逐一解释每个概念是什么以及为什么重要,对于比较抽象概念的会举一些例子来帮助理解。

以下这个列表并不是不全面也不完整,但我注意到的只有 8 件事情值得一提,对于那些已经可以编写基本组件但想要提高他们的技术设计技能的人来说。所以这是列表:

以下列举的这个列表仅仅是是我注意到的 8 个方面,当然组件设计还有其他一些方面。在此我只是列举出来我认为值得一提的。

对于已经掌握基本的组件设计并且想要提高自身的组件设计能力的开发者,我认为以下 8 项是我认为值得去注意的,当然这并不是组件设计的全部。

  • 层次结构和 UML 类图
  • 扁平化、面向数据的 state/props
  • 更加纯粹的 State 变化
  • 低耦合
  • 辅助代码分离
  • 提炼精华
  • 及时模块化
  • 集中/统一的状态管理

请注意,代码示例可能有一些小问题或有点人为设计。但是它们并不复杂,只是想通过这些例子来帮助更好的理解概念。

层次结构和类图

应用内的组件共同形成组件树, 而在设计过程中将组件树可视化展示可以帮助你全面了解应用程序的布局。一个比较好的展示这些的办法就是组件图。

UML 中有一个在 OOP 类设计中经常使用的类型,称为 UML 类图。类图中显示了类属性、方法、访问修饰符、类与其他类的关系等。虽然 OOP 类设计和前端组件设计差异很大,但是通过图解辅助设计的方法值得参考。对于前端组件,该图表可以显示:

  • State
  • Props
  • Methods
  • 与其他组件的关系( Relationship to other components )

因此,让我们看一下下面这个基础表组件的组件层次图,该组件的渲染对象是一个数组。该组件的功能包括显示总行数、标题行和一些数据行,以及在单击其单元格标题格时对该列进行排序。在它的 props 中,它将传递列列表(具有属性名称和该属性的人类可读版本),然后传递数据数组。我们可以添加一个可选的’on row click’功能来进行测试。

5805103c307278a5778da88e8545b110.png

虽然这样的事情可能看起来有点多,但是它具有许多优点,并且在大型应用程序开发设计中所需要的。这样会带来的一个比较重要的问题是它会需要你在开始 codeing 之前就需要考虑到具体细节的实现,例如每个组件需要什么类型的数据,需要实现哪些方法,所需的状态属性等等。

一旦你对如何构建一个组件(或一组组件)的整体有大概的思路,就会很容易认为当自己真正开始编码实现时,它会如自己所期望的按部就班的完成,但事实上往往会出现一些预料之外的事情, 当然你肯定不希望因此去重构之前的某些部分,或者忍受初始设想中的缺点并因此扰乱你的代码思路。而这些类图的以下优点可以帮助你有效的规避以上问题,优点如下:

  • 一个易于理解的组件组成和关联视图
  • 一个易于理解的应用程序 UI 层次结构的概述
  • 一个结构数据层次及其流动方式的视图
  • 一个组件功能职责的快照
  • 便于使用图表软件创建

顺带一提,上图并不是基于某些官方标准,比如 UML 类图,它是我创建的一套表达规则。例如,在 props 、方法的参数和返回值的数据类型定义声明都是基于 Typescript 语法。我还没有找到书写前端组件类图的官方标准,可能是由于前端 Javascript 开发的相对较新且生态系统不够完善所致,但如果有人知道主流标准,请在回复中告诉我!

扁平的,面向数据的 state/props

在 state 和 props 频繁被 watch 和 update 的情况下,如果你有使用嵌套数据,那么你的性能可能会受到影响,尤其是在以下场景中,例如一些因为浅对于而触发的重新渲染;在涉及 immutability 的库中,比如 React,你必须创建状态的副本而不是像在 Vue 中那样直接更改它们,并且使用嵌套数据这样做可能会创建笨拙,丑陋的代码。

//Flat, data-oriented state/propsconst state = { clients: { allClients, firstClient, lastClient: { name: 'John', phone: 'Doe', address: { number: 5, street: 'Estin', suburb: 'Parrama', city: 'Sydney' } } }}// 倘若我们需要去修改 address number时需要怎么办?const test = { clients: { ...state.clients, lastClient: { ...state.clients.lastClient, address: { ...state.clients.lastClient.address, number: 10 } } }}

即使使用展开运算符,这种写法也并不够优雅。扁平 props 也可以很好地清除组件正在使用的数据值。如果你传给组件一个对象但是你并不能清楚的知道对象内部的属性值,所以找出实际需要的数据值是来自组件具体的属性值则是额外的工作。但如果 props 足够扁平化,那么起码会方便使用和维护。

// 我们无法得知 customer 这个对象里面拥有什么属性// 这个组件需要使用这个对象所有的属性值或者只是需要其中的一部分?// 如果我想要将这个组件在别处使用,我应该传入什么样的对象// 下面的这个组件接收的属性就一目了然

state / props 还应该只包含组件渲染所需的数据。You shouldn’t store entire components in the state/props and render straight from there.(此外,对于数据繁重的应用程序,数据规范化可以带来巨大的好处,除了扁平化之外,你可能还需要考虑一些别的优化方法)。

更加纯粹的 State 变化

对 state 的更改通常应该响应某种事件,例如用户单击按钮或 API 的响应。此外它们不应该因为别的 state 的变化而做出响应,因为 state 之间这种关联可能会导致难以理解和维护的组件行为。state 变化应该没有副作用。

如果你滥用watch而不是有限考虑以上原则,那么在 Vue 的使用中就可能由此引发的问题。我们来看一个基本的 Vue 示例。我正在研究一个从 API 获取一些数据并将其呈现给表的组件,其中排序,过滤等功能都是后端完成的,因此前端需要做的就是 watch 所有搜索参数,并在其变化时触发 API 调用。其中一个需要 watch 的值是“zone”,这是一个过滤器。当更改时,我们想要使用过滤后的值重新获取服务端数据。watcher 如下:

//State change purityzone:{ handler() { // 重置页码 if(this.pagination.page > 1){ this.pagination.page = 1 return; } this.getDataFromApi() }}

你会发现一些奇怪的东西。如果他们超出了结果的第一页,我们重置页码然后结束?这似乎不对,如果它们不在第一页上,我们应该重置分页并触发 API 调用,对吧?为什么我们只在第 1 页上重新获取数据?实际上原因是这样,让我们来看下完整的 watch:

watch: { pagination() { this.getDataFromApi() }},zone: { handler() { // 重置页码 if(this.pagination.page > 1) { this.pagination.page = 1 return; } this.getDataFromApi() }}

当分页改变时,应用首先会通过 pagination 的处理函数重新获取数据。因此,如果我们改变了分页,我们并不需要去关注数据更新这段逻辑。

让我们一下来考虑以下流程:如果当前页面超出了第 1 页并且更改了 zone,而这个变化会触发另一个状态(pagination)发生变化,进而触发 pagination 的观察者重新请求数据。这样并不是预料之中的行为,而且产生的代码也不够直观。

解决方案是改变页码这个行为的事件处理函数(不是观察者,用户更改页面的实际处理函数)应该更改页面值并触发 API 调用请求数据。这也将消除对观察者的需求。通过这样的设置,直接从其他地方改变分页状态也不会导致重新获取数据的副作用。

虽然这个例子非常简单,但不难看出将更复杂的状态更改关联在一起会产生令人难以理解的代码,这些代码不仅不可扩展并且是调试的噩梦。

松耦合

组件的核心思想是它们是可复用的,为此要求它们必须具有功能性和完整性。“耦合”是指实体彼此依赖的术语。松散耦合的实体应该能够独立运行,而不依赖于其他模块。就前端组件而言,耦合的主要部分是组件的功能依赖于其父级及其传递的 props 的多少,以及内部使用的子组件(当然还有引用的部分,如第三方模块或用户脚本)。

紧密耦合的组件往往更不容易被复用,当它们作为特定父组件的子项时,就很难正常工作,当父组件的一个子组件或一系列子组件只能在该父组件才能够正常发挥作用时,就会使得代码写的很冗余。因为父子组件别过度的关联在一起了。

在设计组件时,你应该考虑到更加通用的使用场景,而不仅仅只是为了满足最开始某个特定场景的需求。虽然一般来说组件最初都是出于特定目的进行设计,但没关系,如果在设计它们站在更高的角度去看待,那么很多组件将具有更好的适用性。

让我们看一个简单的 React 示例,你想在写出一个带有一个 logo 的链接列表,通过连接可以访问特定的网站。最开始的设计可能是并没有跟内容合理的进行解耦。下面是最初的版本:

const Links = ()=>( 
)

虽然这这样会满足预期的使用场景,但却很难被复用。如果你想要更改链接地址该怎么办?你必须重新复制一份相同代码,并且手动去替换链接地址。而且, 如果你要去实现一个用户可以更改连接的功能,那么意味着不可能将代码写“死”,也不能期望用户去手动修改代码,那么让我们来看一下复用性更高的组件应该如何设计:

const DEFAULT_LINKS = [ {route: "/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值