TinyVue:与 Vue 交往八年的组件库

本文由体验技术团队莫春辉老师原创~

去年因故停办的 VueConf,今年如约在深圳举行。作为东道主 & 上届 VueConf 讲师的我,没有理由不来凑个热闹。大会结束后,我见裕波在朋友圈转发 Jinjiang 的文章《我和 Vue.js 的十年》,我就在下面打趣道:“过两年我也写篇同名文章”,然后裕波回复:“先写一个我和 Vue 的八周年”。我寻思,我那十分钟的闪电演讲,有人吐槽没有干货,比如同时支持 Vue2 和 Vue3 的技术细节,不如就利用这个机会,给闪电演讲打个补丁。

与 Vue 结缘

QCon 2016 上海

时间先回到 2014 年,我刚加入华为公共技术平台部,参与 HAE 前端框架的研发。HAE 的全称是 Huawei Application Engine,即华为应用引擎。当时我们部门负责集团 IT 系统的基础设施建设。

作为云开发平台,HAE 需要支持全面的云化:云端开发、云端测试、云端部署、云端运营,以及应用实施的云化。其中,云端开发由 Web IDE 负责实现,这个 IDE 为用户提供基于配置的前端开发能力,因此需要支持可配置的 HAE 前端框架。

在 2014 年开始研发 HAE 时,主流的前端技术仍以 jQuery 为主。到了 2016 年,逐渐成熟的 HAE 进入第三个年头,已支撑内部多个关键领域的落地。作为前端架构负责人,我在考察 Angular、React、Vue 三个新兴框架,然后选择其中一个作为 HAE 后续的技术演进方向。于是 10 月份我来到上海,参加 QCon 全球软件开发大会。那时,尤雨溪给我的第一印象是这样的:

在这里插入图片描述

这张我用手机抓拍的照片堪称经典。当时的 Vue 就像他怀里没长大的娃,早早展露于舞台之上。选择 Vue 不是因为它有多强大,而是相信这位奶爸的亲和力,能把前端社区凝聚在一起共建 Vue 生态,让我相信 Vue 会有更亮眼的未来。

VueConf 2017 波兰

从 QCon 回来后不久,我们提交了 Vue 与 React 的对比分析报告并作了汇报。2017 年初,我们已经开始深入研究 Vue 替换 HAE 底层框架的可行性。因为在 HAE 我们已经实现了 Vue 的能力,比如数据双向绑定、生命周期管理、路由管理、模块化管理等等,然后在这个基础上用面向对象的方式构建了一套企业级组件库。

使用 Vue 进行重构,就是把底层框架的能力用 Vue 替换,然后上层的组件库再迁移到 Vue。这个过程相当磨人,中间遇到各种问题,包括 Webpack 构建工具对于当时的我们都是新鲜事物。我们急需专业指导,急需一个信心,然后,首届 VueConf 就来了。

后来我从裕波那里得知,首届 VueConf 是 2017 年 5 月在北京举办,而我去的波兰 VueConf 是 6 月份。但没关系,当我看到电影院现场座无虚席,一群来自全世界各个国家的 Vue 开发者汇聚一堂,就已经给了我很大的信心。当时的讲师阵容是这样的:

在这里插入图片描述

会议茶歇期间,我跟同事去找尤雨溪,当他得知我们从国内不远千里过来参会,很是惊讶。其实,当我了解到现场来参会的开发者,大部分都是个人名义自费参加,也让我惊叹 Vue 的魅力和影响力。

VueConf 2019 荷兰

经过近半年的研发,HAE 组件库成功迁移到 Vue 框架,并于 2017 年 12 月正式发布。在 2018 年,为统一用户体验遵循 Aurora 主题规范,我们对组件库进行升级改造,并改名为 AUI(后来又更名为 TinyVue)。在支撑了制造、采购、供应、财经等领域的大型项目后,到了 2019 年 AUI 进入成熟稳定期,我们才有时间去思考如何将 jQuery 的 30 万行代码重构为纯 Vue 的代码。

与此同时,Vue 在这两年里也得到长足的发展。2019 年 2 月 14 日情人节我再次参加海外的 VueConf,地点已经由波兰移至荷兰,从电影院升级成大剧院,舞台变得更豪华,但不变的是尤雨溪的幻灯片,一直保持朴素的风格:

在这里插入图片描述

2019 年 5 月 16 日,VueConf 大会三个月后,美国商务部将华为列入出口管制“实体名单”,我们面临前所未有的困难,保证业务连续性成为我们首要任务。我们要做最坏的打算,如果有一天所有的主流前端框架 Angular、React、Vue 都不能再继续使用,那么重构后的 Vue 代码又将何去何从?

就在这时 2019 年 5 月 30 日尤雨溪发布了一个 Vue3 关于 Function API 的 RFC,也就是 Composition API 的前身。Function API RFC 这个链接我一直收藏至今,因为它启发了我如何重新设计组件架构。接下来的内容都是干货,想了解 TinyVue 同时支持 Vue2 和 Vue3 的技术细节请往下看。

全新架构的TinyVue组件库

有了 Function API 的支持,我们组件的核心代码就可以与前端框架解耦。经过不断的打磨和完善,拥有全新架构的 TinyVue 组件库逐渐浮出水面,以下就是 TinyVue 组件的架构图:

在这里插入图片描述

在这个架构下,TinyVue 组件有统一的 API 接口,开发人员只需写一份代码,组件就能支持不同终端的展现,比如 PC 端和 Mobile 端,而且还支持不同的 UX 交互规范。借助 React 框架的 Hooks API 或者 Vue 框架的 Composition API 可以实现组件的核心逻辑代码与前端框架解耦,甚至实现一套组件库代码,同时支持 Vue 的不同版本。

接下来,我们先分析开发组件库面临的问题,再来探讨面向逻辑编程与无渲染组件,最后以实现一个 TODO 组件为例,来阐述我们的解决方案,通过示例代码展现我们架构的四个特性:跨技术栈、跨技术栈版本、跨终端和跨 UX 规范。

其中,跨技术栈版本这个特性,已经为华为内部 IT 带来巨大的收益。由于 Vue 框架最新的 3.0 版本不能完全向下兼容 2.0 版本,而 2.0 版本又将于 2023 年 12 月 31 日到达生命周期终止(EOL)。于是华为内部 IT 所有基于 Vue 2.0 的应用都必须在这个日期之前升级到 3.0 版本,这涉及到几千万行代码的迁移整改,正因为我们的组件库同时支持 Vue 2.0 和 3.0,使得这个迁移整改的成本大大降低。

开发组件库面临的问题

目前业界的前端 UI 组件库,一般按其前端框架 Angular、React 和 Vue 的不同来分类,比如 React 组件库,Angular 组件库、Vue 组件库,也可以按面向的终端,比如 PC、Mobile 等不同来分类,比如 PC 组件库、Mobile 组件库、小程序组件库等。两种分类交叉后,又可分为 React PC 组件库、React Mobile 组件库、Angular PC 组件库、Angular Mobile 组件库、Vue PC 组件库、Vue Mobile 组件库等。

比如阿里的 Ant Design 分为 PC 端:Ant Design of ReactAnt Design of AngularAnt Design of Vue,Mobile 端:Ant Design Mobile of React(官方实现)Ant Design Mobile of Vue(社区实现)。

另外,由于前端框架 Angular、React 和 Vue 的大版本不能向下兼容,导致不同版本对应不同的组件库。以 Vue 为例,Vue 2.0 和 Vue 3.0 版本不能兼容,因此 Vue 2.0 的 UI 组件库跟 Vue 3.0 的 UI 组件库代码是不同的,即同一个技术栈也有不同版本的 UI 组件库。

比如阿里的 Ant Design of Vue 其 1.x 版本 for Vue 2.0,而 3.x 版本 for Vue 3.0。再比如饿了么的 Element 组件库,Element UI for Vue 2.0,而 Element Plus for Vue 3.0。

我们将上面不同分类的 UI 组件库汇总在一张图里,然后站在组件库使用者的角度上看,如果要开发一个应用,那么先要从以下组件库中挑选一个,然后再学习和掌握该组件库,可见当前多端多技术栈的组件库给使用者带来沉重的学习负担。

在这里插入图片描述

这些 UI 组件库由于前端框架不同、面向终端不同,常规的解决方案是:不同的开发人员来开发和维护不同的组件库,比如需要懂 Vue 的开发人员来开发和维护 Vue 组件库,需要懂 PC 端交互的开发人员来开发和维护 PC 组件库等等。

很明显,这种解决方案首先需要不同技术栈的开发人员,而市面上大多数开发人员只精通一种技术栈,其他技术栈则只是了解而已。这样每个技术栈就得独立安排一组人员进行开发和维护,成本自然比单一技术栈要高得多。另外,由于同一技术栈的版本升级导致的不兼容,也让该技术栈的开发人员必须开发和维护不同版本的代码,使得成本进一步攀升。

面对上述组件开发和维护成本高的问题,业界还有一种解决方案,即以原生 JavaScript 或 Web Component 技术为基础,构建一套与任何开发框架都无关的组件库,然后再根据当前开发框架流行的程度,去适配不同的前端框架。比如 Webix 用一套代码适配任何前端框架,既提供原生 JavaScript 版本的组件库,也提供 Angular、React 和 Vue 版本的组件库。

这种解决方案,其实开发难度更大、维护成本更高,因为这相当于先要自研一套前端框架,类似于我们以前的 HAE 框架,然后再用不同的前端框架进行套壳封装。显然,套壳封装势必影响组件的性能,而且封闭自研的框架其学习门槛、人力成本要高于主流的开源框架。

面向逻辑编程与无渲染组件

当前主流的前端框架为 Angular、React 和 Vue,它们提供两种不同的开发范式:一种是面向生命周期编程,另一种是面向业务逻辑编程。基于这些前端框架开发应用,页面上的每个部分都是一个 UI 组件或者实例,而这些实例都是由 JavaScript 创造出来的,都具有创建、挂载、更新、销毁的生命周期。

所谓面向生命周期编程,是指基于前端框架开发一个 UI 组件时,按照该框架定义的生命周期,将 UI 组件的相关逻辑代码注册到指定的生命周期钩子函数里。以 Vue 框架的生命周期为例,一个 UI 组件的逻辑代码可能被拆分到 beforeCreate、created、beforeMount、mounted、beforeUnmount、unmounted 等钩子函数里。

所谓面向逻辑编程,是指在前端开发的过程中,尤其在开发大型应用时,为解决面向生命周期编程所引发的问题,提出新的开发范式。以一个文件浏览器的 UI 组件为例,这个组件具备以下功能:

  • 追踪当前文件夹的状态,展示其内容
  • 处理文件夹的相关操作 (打开、关闭和刷新)
  • 支持创建新文件夹
  • 可以切换到只展示收藏的文件夹
  • 可以开启对隐藏文件夹的展示
  • 处理当前工作目录中的变更

假设这个组件按照面向生命周期的方式开发,如果为相同功能的逻辑代码标上一种颜色,那将会是下图左边所示。可以看到,处理相同功能的逻辑代码被强制拆分在了不同的选项中,位于文件的不同部分。在一个几百行的大组件中,要读懂代码中一个功能的逻辑,需要在文件中反复上下滚动。另外,如果我们想要将一个功能的逻辑代码抽取重构到一个可复用的函数中,需要从文件的多个不同部分找到所需的正确片段。

在这里插入图片描述

如果用面向逻辑编程重构这个组件,将会变成上图右边所示。可以看到,与同一个功能相关的逻辑代码被归为了一组:我们无需再为了一个功能的逻辑代码在不同的选项块间来回滚动切换。此外,我们可以很轻松地将这一组代码移动到一个外部文件中,不再需要为了抽象而重新组织代码,从而大大降低重构成本。

早在 2018 年 10 月,React 推出了 Hooks API,这是一个重要的里程碑,对前端开发人员乃至社区生态都产生了深远的影响,它改变了前端开发的传统模式,使得函数式组件成为构建复杂 UI 的首选方式。到了 2019 年初,Vue 在研发 3.0 版本的过程中也参考了 React 的 Hooks API,并且为 Vue 2.0 版本添加了类似功能的 Composition API

当时我们正在规划新的组件架构,在了解 Vue 的 Composition API 后,意识到这个 API 的重要性,它就是我们一直寻找的面向逻辑编程。同时,我们也发现业界有一种新的设计模式 —— 无渲染组件,当我们尝试将两者结合在一起,之前面临的问题随即迎刃而解。

无渲染组件其实是一种设计模式。假设我们开发一个 Vue 组件,无渲染组件是指这个组件本身并没有自己的模板(template)以及样式。它装载的是各种业务逻辑和状态,是一个将功能和样式拆开并针对功能去做封装的设计模式。这种设计模式的优势在于:

  • 逻辑与 UI 分离:将逻辑和 UI 分离,使得代码更易于理解和维护。通过将逻辑处理和数据转换等任务抽象成无渲染组件,可以将关注点分离,提高代码的可读性和可维护性。
  • 提高可重用性:组件的逻辑可以在多个场景中重用。这些组件不依赖于特定的 UI 组件或前端框架,可以独立于界面进行测试和使用,从而提高代码的可重用性和可测试性。
  • 符合单一职责原则:这种设计鼓励遵循单一职责原则,每个组件只负责特定的逻辑或数据处理任务。这样的设计使得代码更加模块化、可扩展和可维护,减少了组件之间的耦合度。
  • 更好的可测试性:由于无渲染组件独立于 UI 进行测试,可以更容易地编写单元测试和集成测试。测试可以专注于组件的逻辑和数据转换,而无需关注界面的渲染和交互细节,提高了测试的效率和可靠性。
  • 提高开发效率:开发人员可以更加专注于业务逻辑和数据处理,而无需关心具体的 UI 渲染细节。这样可以提高开发效率,减少重复的代码编写,同时也为团队协作提供了更好的可能性。

比如下图的示例,两个组件 TagsInput ATagInput B 都有相似的功能,即提供 Tags 标签录入、删除已有标签两种能力。虽然它们的外观截然不同,但是录入标签和删除标签的业务逻辑是相同的,是可以复用的。无渲染组件的设计模式将组件的逻辑和行为与其外观展现分离。当组件的逻辑足够复杂并与它的外观展现解耦时,这种模式非常有效。

在这里插入图片描述

单纯使用面向逻辑的开发范式,仅仅只能让相同的业务逻辑从原本散落到生命周期各个阶段的部分汇聚到一起。无渲染组件的设计模式的实现方式有很多种,比如 React 中可以使用 HOC 高阶函数,Vue 中可以使用 scopedSlot 作用域插槽,但当组件业务逻辑日趋复杂时,高阶函数和作用域插槽会让代码变得难以理解和维护。

要实现组件的核心逻辑代码与前端框架解耦,实现跨端跨技术栈,需要同时结合面向逻辑的开发范式与无渲染组件的设计模式。首先,按照面向逻辑的开发范式,通过 React 的 Hooks API,或者 Vue 的 Composition API,将与前端框架无关的业务逻辑和状态拆离成相对独立的代码。接着,再使用无渲染组件的设计模式,将组件不同终端的外观展现,统一连接到已经拆离相对独立的业务逻辑。

跨端跨技术栈 TODO 组件示例

接下来,我们以开发一个 TODO 组件为例,讲解基于新架构的组件如何实现跨端跨技术栈。假设该组件 PC 端的展示效果如下图所示:

在这里插入图片描述

对应 Mobile 端的展示效果如下图所示:

在这里插入图片描述

该组件的功能如下:

  • 添加待办事项:在输入框输入待办事项信息,点击右边的 Add 按钮后,下面待办事项列表将新增一项刚输入的事项信息。
  • 删除待办事项:在待办事项列表里,选择其中一个事项,点击右边的 X 按钮后,该待办事项将从列表里清除。
  • 移动端展示:当屏幕宽度缩小时,组件将自动切换成如下 Mobile 的展示形式,功能仍然保持不变,即输入内容直接按回车键添加事项,点击 X 删除事项。

这个 TODO 组件的实现分为 Vue 版本和 React 版本,即支持两个不同的技术栈。以上特性都复用一套 TODO 组件的逻辑代码。这套 TODO 组件的逻辑代码以柯里化函数形式编写。柯里化(英文叫 Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。举一个简单的例子:

// 普通函数var add = function (x, y) {
   
  return x + y}add(3, 4) // 返回 7// 柯里化函数var foo = function (x) {
   
  return function (y) {
   
    return x + y
  }}foo(3)(4) // 返回 7

本来应该一次传入两个参数的 add 函数,柯里化函数变成先传入 x 参数,返回一个包含 y 参数的函数,最终执行两次函数调用后返回相同的结果。一般而言,柯里化函数都是返回函数的函数。

回到 TODO 组件,按照无渲染组件的设计模式,首先写出不包含渲染实现代码,只包含纯业务逻辑代码的函数,以 TODO 组件的添加和删除两个功能为例,如下两个柯里化函数:

/**
 * 添加一个标签,给定一个 tag 内容,往已有标签集合里添加该 tag
 * 
 * @param {object} text - 输入框控件绑定数据
 * @param {object} props - 组件属性对象
 * @param {object} refs - 引用元素的集合
 * @param {function} emit - 抛出事件的方法
 * @param {object} api - 暴露的API对象
 * @returns {boolean} 标签是否添加成功
 */
const addTag = ({
    </
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值