写点什么

我的前端技术思考

  • 2022 年 3 月 27 日
  • 本文字数:11256 字

    阅读完需:约 37 分钟

又名 -- 《深入浅出 Angular 前端开发的几座大山》


前言

主要分享自己做前端 3 年多以来的技术思考,主题内容也是大概都是围绕我们比较熟悉的那一套东西,比如:组件化开发、状态管理、Dialog、Angular 服务等,框架上以 Angular 为主,但也会有一些和 React 在代码组织上的对比分析。


技术思考部分属于一些个人理解,每个人都应该有自己对技术的理解和思考,期待和大家达到共识。


免责声明 - 分享中涉及一些个人的理解,比如和 React 理念和思路的对比,仅代表个人看待技术的一个角度。


主题目录

1、技术体系

2、组件化

3、Angular 服务

4、状态管理

5、弹出层

技术体系

因为我们公司是 ToB 的业务,比较重前端,需要处理复杂交互、复杂状态管理以及多团队的开发管理,所以整个技术体系还是比较复杂的,对于开发者来说也是比较有挑战点、有意思的。


说难不难,说容易也不容易。


CDK 全称 Component Development Kit 是 Angular 官方提供组件开发包,前端组件开发的圣经。


个人建议:

  1. 首先基础方面是必须扎实的比如 JavaScript、Angular 框架,再者就是要对后面的几项精通 1~2 项。

  2. 另外就是做业务开发的过程中要敢于去深入技术细节,基于业务实践去熟悉技术链上的技术点、结构设计,针对不完善的部分提出改进意见。

  3. 珍惜这样的业务机会,让自己的技术往深度方向延伸。


组件化


组件

组件是前端交互逻辑封装的基本单元,根据复用程度可以分为:组件库、业务组件库、业务组件。


我们这边的开发大概也是这样,写业务组件、基础组件,业务组件中通用的部分抽取到基础组件中作为基础组件库或者基础业务组件库,以最大化实现代码复用,风格交互统一。


我们这边目前开发的一个难点也在于此,就是需要经验去确定哪些是可以复用的,组件库业务组件库都已经进行了那些封装,确定基础组件库是否满足业务需求等等。


组件本质

组件作为基础逻辑单元,本质上就是 DOM 结构设计、交互控制(MVC 中的 View 和 Control),从这个层面看来说三大框架都差不多,并且我们现在所讨论的组件通常是框架组件,因为现在浏览器虽然支持了原生组件(Web Component),但是现在基于 Web Component 去封装的组件库还很少,一方面是 Web Component 对比 Angular、React 等框架的在组件特性方面还有些落后,另以方面是 angular、react、vue 的生态已经很成熟,唯一不足的是组件库不能通用,但是公司内部可以选择统一的技术栈,那么这个问题也不是那么突出了。


Angular 组件/指令

Angular 组件有两种形态:组件和指令,这块简单的讲就是组件提供完成逻辑处理、DOM 结构的封装,指令通常是只提供逻辑封装,指令需要附加到宿主 HTML 元素上。


组件的话不用过多介绍,这里额外谈谈: 指令,Angular 指令是不允许提供模板的,它需要绑定到一个宿主上,对宿主元素进行额外的逻辑处理。


如果大家基于 Angular 写过新的组件库组件应该比较清楚,在实现一个基础组件的时候,我们通常有三种对外使用的方式:全局服务、组件、指令,组件设计阶段最先考虑的应该是组件的划分、是否提供指令、是否提供服务等。


比如通常 Angular 组件库中的 Tooltip 模块就是对外提供一个全局服务和一个指令,因为它是在已有 HTML 元素上进行的交互处理,这种场景下指令是一种更方便的使用方式,使用者只需在原有的 HTML 元素上绑定即可具备 tooltip 的功能。


tooltip 模块代码过于复杂,下面以一个简单的指令 thyStopPropagation 说明指令的特点:


它的功能就是阻止冒泡,形态上和组件基本一致,可以在构造函数获取到宿主元素(DOM 节点),然后监听事件,阻止事件默认的冒泡行为,且可以通过参数指定阻止冒泡的事件类型。


可以看出指令是纯粹的逻辑复用,它是很好的代码封装的一种方式。


大家可以考虑下,React 中只有组件的概念如果要实现类似于 Angular 中 thyStopPropagation 指令的这中需求该是怎么封装和和使用呢?


组件治理


大部分情况下组件是由框架管理的,我们开发者通常是按照框架规定的方式编写组件,然后使用组件其实就是交给框架去执行组件逻辑,框架承担了组件的大部分的管理工作,比如组件的创建、销毁以及其他声明周期函数,组件树的管理,组件状态的更新,组件对应的 DOM 的维护,我们现在写的前端是比较高级的代码,因为框架帮我们做了太多的事情。


有些情况下我们需要自己创建组件(Angular),比如弹出层场景,这个时候就需要开发者自己管理组件了,即是手动创建组件实例,但是这也不是完全脱离框架,一定程度上还要受框架的管理,比如开发者自己动态创建组件的时候使用的 viewContainerRef 对象,它其实就是 Angular 的组件上线文容器,组件实例通过 viewContainerRef 跟框架的组件树建立关联,运行状态、销毁等还是受框架控制。

基础组件树:



动态创建组件 D:





上图示意的是动态创建的组件与 Angular 组件树的关系,注意这里示意的只是与组件树的关系,组件对应的 DOM 结构是可以随意控制。


组件化的这部分到这里基本结束,主要谈谈对组件化的理解,以及 Angular 框架下组件的组织形态。

Angular 服务

Angular 服务是区别其它框架的一个特色点,我说的不是服务本身,而是包含了依赖注入、Rxjs 等的组合技术,它是组件、应用之间状态共享、消息通讯的桥梁,也是代码组织的一种重要方式。


应用场景

① Angular 状态管理:Angular 的状态管理技术相关技术基本都是以服务为基础实现的。

② 单例服务:注入组件库中的 ThyDialog、ThyPopover,这类的代码组织是处理函数和数据状态组合的一个形态,比如 ThyDialog 服务的实例可以打开弹框,那么这个弹框的实例状态就在服务中维护了,可以同时打开多个弹框,可以控制最大打开 3 个弹窗,就需要在服务中维护一个实例状态的列表。

③ 组件内配置服务:这类通常对应到我们的业务实现,比如 Wiki 产品中需要实现一个跨组件的事件或状态传递,层层传递参数或者层层抛出事件都太麻烦了,于是一个自然而然的方案出来了,通过一个服务存储共享状态,或者在服务中基于 Rxjs 做一个通知流,实现跨组件的通讯。


其中,[③ 组件内配置服务] 是一个一把双刃剑,使用服务它解决跨组件通信问题是一个利器,也存在被滥用或者不规范使用的问题:

  1. 需要明确服务是单例的还是组件注入 a.组件注入需要考虑注入在那个层级 b.组件注入的情况需要考虑组件的各种使用场景下是否都能取到服务实例

  2. 注意服务中的状态维护(严谨的维护数据状态)

  3. 对于数据流要注意取消订阅

  4. 注意服务中依赖注入的上线文,比如服务所依赖的服。


思辨点

服务在 Angular 中被认为是一个很好的组织业务逻辑的方式,但是在函数式编程中像服务这样的特性却被嗤之以鼻,因为它不符合纯函数的思想,服务中可以存储局部状态,那么服务中的方法就不再纯粹,这可能是代码坏味道的开始。


还有一点就是组件间的通讯增加了一层服务会使应用的数据流向、组件间通信方向变得不再直观,所以我也觉得服务是个好东西,但是要想清楚了才能用。


React 场景方案

React 不存在服务的概念,React 的核心就是组件,其它一些概念设计全部围绕组件开展,那么前面提到的 Angular 中使用服务处理的业务场景在 React 中它是如何处理的呢?


这也是 React 中的基础,我不是特别精通,这里算是卖弄一下:

① 父子级通讯 - 参数传递,事件回调

② context - 一个上下文用于在有层级关系的组件中共享状态,这个在 React 中用的非常多,实现了组件属性穿透。

③ 状态管理 - 毫无疑问,React 中状态管理是一种常见的跨组件通讯的解决方案

④ 自定义事件 - 用自定义事件来实现非嵌套组件结构下的消息通信问题,定义一个事件源,一个组件绑定事件处理函数,一个组件根据行为触发事件,思路大概是这样。


可以看出这类场景的处理本质上是一样的,状态共享、发布订阅,Angular 和 React 最大的差异是理念,React 推崇纯函数,所以在设计上不允许服务这样的概念存在,因为服务可以存储局部状态,违背了纯函数的理念,这是代码方式或者说是模式的引导,并不代表者 React 开发者不能使用类似服务的东西(也有人把依赖注入和服务引入到 React 中,但它注定主动不会被主流接纳),个人有兴趣可以拐个弯或者换个形态也是可以用的。


服务的限制

不知道大家在用 Angular 框架开发应用的过程中有没有遇到过服务使用的场景限制,就是有那些那些场景我用不了服务或者用服务很费劲?


① 组件注入场景 - 新手经常遇到的一个问题,就是服务在组件内配置,因为依赖注入上下文的关系,这个服务只能在该组件以及子组件去注入,同级组件或者父级组件(以及全局服务)是无法通过构造函数注入获取的。这种隐含关系的构建有它的灵活性,也增加了代码的复杂度,我觉得这是服务使用限制之一。

② 工具函数场景 - 大家都知道服务只能通过构造函数注入的方式获取实例,那么工具函数场景下使用服务就比较麻烦了,这个限制在编辑器中经常遇到,Wiki 编辑器的一些配置参数(只读状态、默认字号大小)获取或者是组件上下文服务,有时候需要在组件中用到、有时候需要在工具函数中用到,这个时候在工具函数中获取就比较麻烦。


可以给大家看下我们是如何解决这个问题的,就是工具函数中获取 Angular 服务的思路,下面是 Wiki 编辑器中设置字体大小工具函数的实现代码

核心是在编辑器初始化的时候给对象赋值一个依赖注入的 injector 对象,这个 injector 对象的依赖注上下文就是编辑器组件的上下文,有了这个 injector 对象在工具函数中就可以为所欲为了,就像开挂一样。


上面的例子用 injector 获取 Wiki 编辑器的上下文服务,除此之外还可以使用 injector 获取一些全局的 Angular 服务,比如我希望在工具函数处理中打开一个快捷菜单,那么我就可以通过 injector 获取全局的 ThyPopover 服务,去执行它的 open 函数打开菜单。


基于 Weakmap 的状态共享方案

前面主要是说用 injector 对象可以为工具函数和 Angular 服务之间的调用架起一组桥梁,其实在基于 Slate 开发富文本编辑器的过程中还有一部分的代码设计可以解决这方面的问题,可以理解为它也是一种状态共享方案,这里把它称为:基于 Weakmap 的状态共享方案。


理论上在应用中共享数据直接把数据存到一个全局变量中就可以了,然后就可以在工具函数中、Angular 组件中功能使用(回到了最原始的方式),但是这种情况下有一个问题,就是多编辑器情况下数据可能会相互影响,所以一般不建议将组件的数据存成全局数据,用 WeakMap 可以解决这个问题,可以把 Weakmap 的 key 指定为编辑器应用对象,这样每个编辑器的数据旧会起到隔离的作用,WeakMap 在 Wiki 编辑器中有很多应用:

从上图 Weakmap 的定义可以看出 Weakmap 的实际上单例的,key 的类型指定为 Editor 可以隔离多编辑器数据,这种数据设计方案非常简洁、实用。


到这里 Angular 服务的部分节本结束了,Angular 服务在 Angular 框架承担了桥梁的作用,让 Angular 框架的组织能力、代码设计能力大幅度增强。

状态管理

状态管理也是我个人的一个痛,因为在以前我一直搞不明白状态管理有什么作用,来到 Worktile 以后才算弄明白,所以这里核心说下我对状态管理的一个理解,当然也会结合 PingCode 中状态管理形态介绍下我们的应用场景:全局数据,业务数据,微前端数据等。


值得一提的是没有状态管理是绝对合理的,严格来说它都不能算作一种技术,因为状态管理就是数据的一种组织方式,而且它不是必须的。

状态管理的本质是规范数据的使用,包括数据的初始化、修改、页面更新,属于应用技术方案。


状态管理思想

状态管理核心体现的是管理的思想,提供数据修改的统一路径,让数据的修改规范化,数据修改本身可以被记录和统一处理


对于普通的应用状态管理不是必须的,但是随着 React 的发展以及状态管理相关开源库的普及,状态管理逐步变成了一种潮流,当然状态管理的思想本身也有它的进步性,所以现在大家通常按照状态管理的思想去处理数据。


基础理论

最简单的状态管理包含两个概念:不可变数据、Action。

① Immutable - 数据是不可以修改,如果修改只能重新创建一个新的对象,代表库:immutable.js、immer。

② Action - 描述数据修改,也可以理解为给每一次的数据需改增加一个类型标识。


而状态管理的鼻祖 Redux 还有一些更复杂的概念,这里再做过多介绍,核心关注这两个就可以了。


Redux Dev Tool

这是一个帮助开发者监控状态管理数据的浏览器插件,一般写过 React 的都用过,这是一个很不错的调试工具,它可以记录应用中的每一次数据修改,也可以对比出数据修改前后的数据变化,可以有效帮助开发者排查问题,梳理复杂数据的修改流程,我觉得这也是 Redux 成功的一个原因,就是配套工具做的很完善,开发者享受到了切切实实的便利,不枉学了那么多的概念。


思想延伸

状态管理作为一种优秀的数据管理的思想,在我们的富文本编辑器中也有用到,而且在富文本编辑器中这种对数据修改的设计和控制则是刚需,它是实现协同编辑、Redos/Undos 的基础。


下面是一个图片节点的数据示意:

[  {  	type: 'image',    url: 'https://altas.pingcode.com/xxx',    align: 'center'  }]
复制代码

现在用通过界面把对齐方式调整为居左,新的数据如下: 

[  {  	type: 'image',    url: 'https://altas.pingcode.com/xxx',    align: 'left'  }]
复制代码

大家想一想这个过程该怎么发生?,以及如何应对 协同编辑 和 Undo 场景?


简单的方式

直接修改数据,Undo 时直接恢复上一个的状态,协同编辑则需要把全量数据发送给协同方


管控的方式

  • 统一数据修改的方式

  • 给每一个种数据的修改设计一类数据操作类型


这种方式其实对应于 Slate 中的 Transforms 模块,无论数据结构变化的多复杂,数据的修改可以转化为一种基础操作类型(类似于前面说到的 Action 的概念),这样一来和前面的状态管理类似,我可以记录数据的修改操作,准确的知道本次数据修改的信息,包括数据修改的类型,变化前后的数据(对应 Slate 中的 Operation 对象)。


基于这个 Operation 对象实现协同和 Undo 操作:

  • Undo 时只需要执行这个 Operation 的反操作(一个插入字符的操作 insert_text 对应的反操作就是 remove_text,通过执行一个操作的反操作可以实现撤回的目的)。

  • 协同是只需要把这个 Operation 发送给协同方,协同方就可以基于这个 Operation 的信息实现数据的同步。


这个是我的一个类比,如果不是很理解也没有关系。


框架绑定

我认为状态管理技术可以大概分为三个部分:规范修改、修改通知、框架绑定


框架绑定是我抽取的一个阶段,我的理解是状态管理本身以及它和框架绑定应该是两个层,理论上状态管理中的规范修改、修改通知可以是框架无关的,只有框架绑定层才是与框架有关的。


因为状态数据最终是需要在界面上进行显示的,所以框架绑定就是结合框架的数据更新机制、把数据修改告诉框架,在框架的渲染机制内驱动界面刷新。


Angular 界面更新的核心机制是变化检测,如果组件不是 OnPush 模式,那么 Store 中的数据只要更新,界面就会自动更新,这很方便。但是如果组件是 OnPush 模式,那么组件需要订阅 Store 中数据的更新,手动执行 markForCheck 或者 detectChanges 去驱动界面刷新。


OnPush 模式是 Angular 对组件进行性能优化的一个方式,组件是 OnPush 模式意味着只有当组件的参数的引用更新后才会执行组件极其子组件的变化检测,参数无变化的情况下是会跳过变化检测的,就跟 React 的 useMemo 类似。


React - React 中驱动组件更新的方式主要靠 setState,那么 React 中可能会基于高阶组件去封装由状态管理数据到组件状态更新这块的逻辑,进而驱动数据更新时的界面刷新。


状态管理的基本形态

这里想要想对比 @tethys/store、Redux、Mobx 的基础使用、表现形态。


@tethys/store 我们的状态管理库,它是一种非常灵活的方式,Store 可以通过全局注入作为全局状态 ,也可以在组件内注入作为局部状态,主要是以 Angular 服务的方式存储和使用数据,只不过是数据的修改和存储是被组织化的。

Redux 完全遵循纯函数,是最 React 方式的状态,但是代码是比较离散。

Dva 国人封装的一个库,是 Redux、Thunk、Sagas 三种方式的结合,Redux 中不允许有异步,但是获取数据的 http 请求一定是异步的,所以提出一层专门处理异步请求,异步请求完成了再调用同步的数据修改,redux 因为太过离散,actions、reduces 等都是定义在独立的文件中所以,dva 对这种情况进行了封装,让状态定义、同步修改、异步修改等统一封装到一个 Model 中,不同业务模块的可以定义多个 Model。

Mobx 可能就是一步到位了,就是以 Store 的形态存储业务数据,在 Store 上提供数据修改方法。


我们的 Store (@tethys/store)形态上和 Mobx 类似,管什么纯函数,方便就完事,干就完了。


某种意义上 Redux 下的是一盘大棋,整出一套思想,整出一套巨复杂、变态的流程去约束你的数据的操作,然后为你提供了巨牛逼的调试工具,然后就搭载 React 一起火了起来,火起来之后开发者还是觉得它很麻烦,然后有了 Dva、Mobx 这类更易用的方式去状态管理。


其实我对这些东西向来不敏感,让我使用 Redux 那种写法我也觉得没啥,让我用我们的 @tethys/store 我也觉得 OK ,它就是一种代码架构方式,只要团队能够达成共识就行。


PingCode 前端状态数据

这块是我们 PingCode 产品中状态管理的一个典型场景的分析:全局数据、应用数据管理(封装在业务组件库实现逻辑复用)、复杂状态管理方案。


① 全局数据

AppRootContext - 个人信息、全局配置的信息管理,单例模式

GlobalUsersStore - 全局用户信息的管理,单例模式

GlobalUsersStore 数据获取流程大概如下:

  • Portal 应用通过 API 获取初始化的数据

  • 把初始化数据存放到 window 对象上

  • 子应用通过构造函数注入时在 window 读取这些数据或者对象


Portal 属于 PingCode 产品中的基础应用,对应于微前端架构下的基座应用。


这块的代码是在业务组件库中:styx/module.ts 中

使用 useFactory 配置服务的提供商,保证微前端模式子应用使用服务的是单例的。


useFactory 定义如下:

可以看出背后的技术也很简单,就是把取到的数据挂到 window 对象上。


这块是通过业务组件库注入这些全局的数据的,因为要实现多应用共享,采用了相对传统的方式存储值,把数据挂载到 window 对象上了,感觉技术到了又回归到了不推荐的方式上,可能好一些的是我们这种方式是有节制的使用,没有滥用。


应用数据管理

PilotStore - 这是我们应用中比较典型的一个数据管理场景,首先它在各个子应用中有一定的通用性,API 一致(查询、搜索、收藏)、界面也比较类似,所以在业务组件库对 Pilot 有一定的封装,包括数据封装和组件封装。



Pilot 是我们内部抽象的一个领航的一个概念,代码每个应用的主体,比如:项目管理产品 Pilot 就代表项目、Wiki 产品 Pilot 就代表知识库。

有一个共识就是业务组件的封装更难一些,因为开发者需要平衡那些可以写死、那些不能写死、还要使用方尽可能的简单。


具体到 Pilot 数据管理的场景,每个子应用界面交互、数据结构基本相同,只有 API 不同,API 结构如下:

https://{sub_domain}.pingcode.com/api/wiki/pilot -> https://{sub_domain}.pingcode.com/api/{applicationName}/pilot

对于 Url 的不同业务组件可以有两种做法:

  • 子应用在使用这块的时候传递一个 Url

  • 业务组件库读取子应用的配置自动拼接这个 Url

两种方式都差不多,没有本质区别,因为我们每个子应用在初始化的时候都会增加一个全局的参数配置 StyxConfig 的实例,包含应用的标识名称,所以在 Pilot 封装的过程中直接读取这个标识就可以了,所以我们是用了第二种方案。


PingCode 前端状态管理技术

Angular 服务和 Rxjs 基本上是构成 PingCode 前端状态管理的核心技术,主要的使用场景是在组件中,通过依赖注入获取服务实例(Store),通过订阅 Store 中的数据(Rxjs 流)获取数据的更新推送,Store 本身也提供快照如果只需要读取一次可以直接通过快照获取无需订阅。

具体实现代码已经完全开源,这里不在做细节介绍,感兴趣的可以参考:

github 仓储: https://github.com/tethys-org/store 

文档地址: https://tethys-org.github.io/store/guides/intro 


复杂状态管理

Wiki 中有一块相对复杂的逻辑,就是页面数据更新的同步,以前总是出问题,经过去年的一次重构现在问题已经很少了,我们采用的是总线模式:整体思路和实现都非常直观简洁,这里在进行简单介绍,供大家参考:

modify origin : 页面数据修改源,可以是修改标题、修改内容、修改发布人等可能有无数个。

page event bus:全局的页面数据修改的 Bus ,基于发布订阅模式。

sync target:对应于要同步的修改的 store,也就是页面要同步数据修改的部分,也有可能有无数个。


数据修改源修改数据时 emit 一个数据修改事件,page-event-bus 接收到修改事件通知把它转发给订阅者,需要同步数据修改的地方订阅只需要订阅 page-event-bus 就可以了。整体的的关系是:多对 1、1 再对多

针对这个以前写了一篇技术文章: https://zhuanlan.zhihu.com/p/339607139 

弹出层

弹出层是我特别想讲的,也是我最熟悉的,感觉我们 PingCode 产品的交互皆是弹框。


弹出层交互

弹出层交互在 PingCode 中无处不在,大半的交互都是基于弹出层做的,Wiki 的页面详情弹框、页面编辑、Project 的工作项详情,Portal 的展开侧边栏,弹框详情中的内容选择、应用内的 Pilot 切换搜索,操作成功、失败提醒,提及选择、关联选择等等,总之弹出层很重要。


从另外一个角度考虑,弹出层是脱离路由交互的另外一种形态,它可以脱离当前的路由完成交互,是单页面应用交互的重要特色,脱离路由意味着它可以是跨应用、跨模块交互的一种形态,比如我们可以在一个应用中打开另外一个应用的页面详情。


弹出层技术

Angular 框架下应用程序的弹出层技术大多是基于 CDK Overlay 和 CDK Portals 实现的。


Overlay 负责弹出层的整体结构和交互处理,Protals 主要是对 Angular 动态创建组件的封装,因为弹出层是基于动态创建组件,所以一定程度上弹出层组件的声明周期是自主控制的,而且通过 Overlay 弹出的组件在 DOM 结构上也脱离了原始的文档结构。


Angular 框架下的弹出层技术是框架设计典范,弹出层技术和框架配合的非常巧妙,开发者使用时也非常顺手,好像代码本该如此。


使用场景

① Dialog - 打开 Wiki 页面详情



代码



② Popover - 打开排序菜单



代码



结构分析

① CDK Overlay 结构 - Dialog



① CDK Overlay 结构 - Popover



这个 popover 弹出层和 dialog 弹出层共用一个容器,因为他们都是基于 overlay 实现的


② Bootstrap 弹出层结构 - Modal

多个弹出层叠加需要累加 z-index ,新的弹出层在上一个弹出层的 z-index 基础上 +50 ,这个是在 bootstrap 底层维护。


③ Ant 结构 - Modal



③ Ant 结构 - Popover



Ant 弹出层的结构设计与 CDK Overlay 如出一辙,具体的实现也被抽取到了组件库之外: rc-dialog ,但是它的封装程度远不如 CDK Overlay。


弹出层独立性

在前面 [弹出层技术] 中说到了:Angular 的弹出层技术是更独立的。


① 组件独立 - 在前面 [使用场景] 中简单的介绍了 Overlay 弹出层组件是基于全局的服务打开的,用到了 Angular 的动态创建组件,说明组件的创建、销毁是开发者维护的,一定程度上脱离 Angular 框架的控制,说明了 Angular 的弹出层是组件独立的。

这个组件独立代表着弹出层组件不会在应用初始化时创建,而是在需要弹出的时候创建,这点是有别于 Ant Design 的,下面可以简单了解下 React 中是如何使用 Modal 和 Popover 的。


Modal 使用



Modal 提示



Popover 使用

可以看出 React 中针对弹出层这块的使用形态通常是组件,而 Angular 这块则被设计成了服务,我个人觉得基于服务的方式还是比较优雅的,当然 Modal 也提供了类似服务的全局调用的那种形式,但是限制了使用场景(info、success、error 等),而 Ant Desgin 对于 Popover 的使用则采用组件包裹的形式来组织,这个时候 Popover 组件则类似于一种虚拟组件,对包括的 DOM 进行交互增强处理(和 Angular 指令起的作用类似)。


② DOM 独立 - 与 Bootstrap 的结构组织相比,Overlay 实现的弹出层在 DOM 结构上脱离了原始的文档结构,布局不受弹出源的影响。


Bootstrap 的那种 DOM 结构理论上受布局的限制,比如因为布局或者滚动条的原因,可能会在页面中设置 overflow: hidden 样式,那么弹出层的显示就会受到影响,当然也这种问题 Bootstrap 应该也会有解决方案。


③ 位置策略、滚动策略 - 位置策略和滚动策略的起源我觉得应该都是 [② DOM 独立] 的产物。


位置策略 - 最新的 CDK 代码只包含 global 和 flexible 两种位置策略,global 比较简单对应全局弹出 Dialog 的场景(这种场景下 DOM 本来就应该是独立的,因为弹框的位置是全局固定的,不是基于某一个已经存在的 DOM 的相对位置),flexible 则复杂一些,它的位置应当是基于已经存在的 DOM 的相对位置,那么它的核心就是解决基于位置源弹出的问题,因为弹出层 DOM 脱离了原始的文档结构,所以弹出位置需要基于原始位置去计算,flexible 就对应这种场景的代码封装。


滚动策略 - 因为弹出层是 [DOM 独立] 的,默认页面滚动时弹出层是不会自动跟随的,这点跟 Bootstrap 那种结构有本质区别。所以 Overlay 专门设计了滚动策略处理这个场景,可以配置滚动策略:滚动跟随、滚动阻塞、滚动关闭。


可以看出基于 Angular 的 CDK Overlay 是一套完整的解决方案,它把弹出层的 DOM 结构从原始的文档中提出来,解放了弹出层组件 DOM 与原始文档结构的耦合,然后为此设计了一套这种结构下所带来的问题的处理方法,并且实现了与全局弹框的统一,除了 Dialog、Popover 的弹出层场景,它的应用还包括了 Select 组件、Tootip 组件、Dropdown 组件等,实现底层技术的大统一,这也太强了。


设计的艺术

其实 Overlay 的本质体现的是代码的设计,包含:DOM 结构设计、组件结构设计、实例 Ref 结构设计、场景策略设计、样式设计等。


从我们的实践来看,基于 Overlay 的弹出层技术方案整体是更优雅、问题更少的,也更容易写出松耦合的代码。DOM 结构上的独立性解除了对原始文档布局的依赖,以服务的形式使用也让弹出层的调用更灵活,是非常完美的设计。

总结

零零散散分享了一些不太有体系的东西吧,算是自己的一些拙见,希望可以借此引起大家的一些思考或者共鸣。

总的来说还是希望大家在实际的开发的过程中,除了要关注基础框架(库)的使用、还要多一些思考,多去看一些优秀开源库的架构方式,遇到问题追根溯源,把这些架构、库为自己所用,而不是陷入框架或者逻辑的泥潭。

现在的前端开发已经不在像纯 JavaScript、HTML 时代那样简单了,现在的技术体系一般都会很复杂,比如说我们公司有自研的组件库、业务组件库、状态管理、微前端,这样一来对公司的每一个前端开发者都有一定的挑战,产品出现一个缺陷它的问题链排查起来就很长,但这也是机遇,越是复杂的场景越能扩展开发人员的技术深度。

我印象最深的就是我以前刚学 React 那会无论如何都搞不懂状态管理是咋会事,面试经常被问住,来到我们公司才算真正搞懂,我觉得最重要的一个点就是实际开发中没有遇到真正匹配的场景,那时候遇到的都是一些可用、可不用的场景,自然理解不到精髓。


最后,推荐我们的智能化研发管理工具 PingCode 给大家。


PingCode官网


关于 PingCode


PingCode 是由国内老牌 SaaS 厂商 Worktile 打造的智能化研发管理工具,围绕企业研发管理需求推出了 Agile(敏捷开发)、Testhub(测试管理)、Wiki(知识库)、Plan(项目集)、Goals(目标管理)、Flow(自动化管理)、Access (目录管理)七大子产品以及应用市场,实现了对项目、任务、需求、缺陷、迭代规划、测试、目标管理等研发管理全流程的覆盖以及代码托管工具、CI/CD 流水线、自动化测试等众多主流开发工具的打通。


自正式发布以来,以酷狗音乐、商汤科技、电银信息、51 社保、万国数据、金鹰卡通、用友、国汽智控、智齿客服、易快报等知名企业为代表,已经有超过 13 个行业的众多企业选择 PingCode 落地研发管理。

用户头像

还未添加个人签名 2021.02.01 加入

还未添加个人简介

评论

发布
暂无评论
我的前端技术思考_Worktile_PingCode研发中心_InfoQ写作平台