技术解析
The most incomprehensible thing about the world is that it is comprehensible.
世界上最不可理解的地方就是它竟然是可以理解的。-- 阿尔伯特·爱因斯坦
开源( Open-Source )造就了如今繁荣活跃的软件行业。开源让全世界的开发者都能够协力编写出优秀的工具类项目,也就是所谓的 "轮子",在造福大大小小的公司个人的同时,也可以展现创作者或贡献者的技术实力。如今很多开发者都在大量使用开源项目作为自己项目的第三方库或依赖,更快更高效的完成开发任务。
笔者也不例外。我最近在用 Vue 3 重构 Crawlab 前端的时候,用到了 Element 团队开发的升级版的 ElementUI,也就是 Vue 3 重构的新 UI 框架 Element Plus。Element 团队在 Element Plus 中将该项目用 Vue 3 完全重构,全面拥抱了 TypeScript;而且相比于之前的 Vue 2 版本丰富了部分组件;而整体风格和使用方式跟之前的版本一致;一些 API 在使用上还变得更精简了。因此,笔者在重构 Crawlab 前端初期过程中没有遇到太大的障碍,再加上之前的编写经验,开发过程中显得驾轻就熟。然而,好景不长,随着项目的不断开发,笔者遭遇到一些技术上的困难。更准确的说,在实现一些复杂功能时遇到了来自于 Element Plus 框架本身的限制。虽然最终想方设法将问题解决了,但是我也深刻体会到了硬啃( Hacking )开源项目源代码的困难。因此,也希望借此机会将自己驾驭开源代码的经验分享给读者。
本篇文章将以解决 Element Plus 问题的经历开始,循序渐进讨论开源项目或开源框架的问题,进一步讨论驾驭开源项目源代码的方法和技巧,分享自己阅读、理解和更改源代码的思路。本篇文章主要是方法论的讨论,不涉及太多技术细节,任何专业背景的读者都可阅读。
如果你是使用过 Vue 的前端工程师,你肯定听说过 ElementUI 。这是一个 UI 框架,也就是说它是帮助你构建 Web 项目的工具类框架,其中包含很多常用的组件( Component )、布局( Layout )以及主题( Theme )等。早期的 ElementUI 是用 Vue 2 写的,是 Vue 中最受欢迎的 UI 框架,在 Github 上有 49k 标星,是第二名 iView( 24k )的两倍多。随着 Vue 作者尤雨溪发布 Vue 3 版本,宣告全面拥抱 TS 之后,原 Element 团队在 Vue 3 的基础上开发了新版本 Element Plus,也就是这个故事的主角。如果对 Vue 3 甚至 Vue 不了解,可以阅读本博客之前的技术文章《 TS 加持的 Vue 3,如何帮你轻松构建企业级前端应用》。
Element Plus 的使用方法跟之前的 Vue 2 版本一样简单易用,文档风格也跟老版本一致,非常全面。按照官方文档鼓励的,笔者也尝试用组件化的方式来优化之前的 Crawlab 老组件,例如表格( Table )。除了将列表( Column )、数据( Data )、分页( Pagination )等进行简单的封装以外,笔者还希望加入一些新的实用功能,例如自定义表格的筛选( Filter )和排序( Sort ),以及表格能够允许用户自定义( Customize )要展示的列和调整列的顺序,等等。虽然 Element Plus 框架有排序和筛选功能,但笔者个人觉得太基础了,通常用户需要既简单又好用的 UI 组件。
下图是筛选表格的前端截图,比较类似 Excel 的展示和操作方式。
实现这个功能的问题不是非常大,按照官方的文档都能解决。其中主要利用了列组件中的 el-table-column
的 header
插槽( Slot ),在其中加入弹出框( Popover )。很好,So far so good !接下来就是加入列的自定义功能了,看上去很快就要大功告成了嘛。
要实现列的自定义,最完美的实现方法就是让用户点击操作按钮,弹出对话框( Dialog ),里面可以选择要展现的列,用拖拽的方式将其排序,然后点击确认。然后我在 Element Plus 官网上快速找到了一个新组件,穿梭框( Transfer ),效果如下图。理论上,只要我在对话框中引用该弹出框来控制展现列的数组就可以了,例如将要展示的列放到 "列表 2" 中。锵锵!目前看起来似乎一切非常顺利。
不过,在仔细阅读文档之后我发现该组件并不支持拖拽,因此暂时无法实现列排序的功能。虽然有点沮丧,但这个影响不大,我们主要想实现的是列选择功能。排序虽然也重要,但暂时先搁置一下,先实现组件选择再说!
然而,现实总是出乎意料。笔者在进一步试验中发现,这个组件似乎存在一个重大 Bug:当穿梭框中全选候选元素时,竟然无法选择或取消选择了(向左移动或向右移动)!赶紧仔细调试查找原因,反复确认是不是我自己写的代码有问题。然后发现是 Vue 在更新组件时在 runtime-core.esm-builder.js
中的 patchBlockChildren
方法下出现了 oldChildren
为 null
的情况。进一步阅读 Element Plus 中穿梭框组件的相关源代码之后,怀疑是 transfer-panel.vue
这个子组件出了问题,它更新渲染的时候 el-checkbox-group
不存在,因此导致更新时出错。随后我在 Element Plus 的 Github 仓库中提交了 Bug Issue。不过维护者并不认为我反映的问题是 Bug,因为在 CodePen 上似乎没有问题。但这个 Bug 确实在我的项目中客观存在,可以在 crawlab-frontend 的 历史提交 中复现。
无奈之下,自行琢磨半天无果之后,果断决定自己造轮子。很快从头写了一个类似穿梭框的组件出来,组件效果如下图。新写的组件在外观上与之前的非常类似,不过自由度很高,因此也顺便支持了拖拽功能。看上去问题似乎快要解决了。
不过,当我满怀期望准备测试拖拽排序功能时,意想不到的事情发生了:列顺序数据改变之后,表格列竟然没有任何改变!改变列组件 el-table-column
的顺序对界面展示的竟然没有作用!而在官方文档的定义中,el-table-column
的顺序决定着列的实际展示顺序。经过重复多次试验之后,笔者只好承认原先的实现方式有问题。简单的改变模版中的列组件顺序并不会影响界面上的展示。
笔者当时想到一个 "聪( yu )明( chun )" 的办法。我试图用强制刷新表格组件的方式让其数据重新渲染,达到改变列顺序的目的。但经过测试,发现这样的暴力方式存在很大的性能问题,每次点击 "Apply" 之后会卡顿近一秒。
于是,笔者又陷入了绝望的境地。
在进退两难的情况下,笔者停下来仔细思考。是不是我对 Element Plus 的框架还不够了解?表格组件本身是如何实现的?它有什么限制和缺点?这些问题都推动我进一步来拆解 Element Plus 框架本身,也就是去阅读它的源代码,理解组件本身的代码逻辑和工作原理。于是,笔者克隆了 Element Plus 代码仓库 到本地。
非常幸运,Element Plus 项目的代码质量相当高,代码组织结构和命名方式都非常清晰。虽然注释相对来说偏少,但它清晰的逻辑结构和良好的命名规范让可读性变得很强。在惊叹于大厂工程师职业素养的同时,我也快速定位到了表格组件的源码位置 packages/table
。整个 Element Plus 项目是用 MonoRepo 的方式管理的,简单来说是一个 Git 仓库里有很多个 NPM 项目。管理工具使用了 Lerna。下面是 el-table
组件 NPM 项目的代码组织结构。
.
├── __tests__
│ └── table.spec.ts
├── index.ts
├── package.json
└── src
├── config.ts
├── filter-panel.vue
├── h-helper.ts
├── layout-observer.ts
├── store
│ ├── current.ts
│ ├── expand.ts
│ ├── helper.ts
│ ├── index.ts
│ ├── tree.ts
│ └── watcher.ts
├── table
│ ├── style-helper.ts
│ └── utils-helper.ts
├── table-body
│ ├── events-helper.ts
│ ├── index.ts
│ ├── render-helper.ts
│ ├── styles-helper.ts
│ └── table-body.d.ts
├── table-column
│ ├── index.ts
│ ├── render-helper.ts
│ └── watcher-helper.ts
├── table-footer
│ ├── index.ts
│ ├── mapState-helper.ts
│ └── style-helper.ts
├── table-header
│ ├── event-helper.ts
│ ├── index.ts
│ ├── style.helper.ts
│ ├── table-header.d.ts
│ └── utils-helper.ts
├── table-layout.ts
├── table.type.ts
├── table.vue
├── tableColumn.ts
└── util.ts
从这个结构可以看到,整个 el-table
表格组件由几个子组件组成,例如 table-body
、table-column
、table-footer
等。而整个项目只有 table.vue
这个一个组件。根据 Vue 开发的经验,笔者很快意识到这就是整个组件的入口。咱们先去瞅瞅看吧!
整个 table.vue
文件有 469 行代码,算是比较大的文件,限于篇幅原因就不在这里详细解释了。其中,最重要的发现是一个叫 store
的变量。仔细研究后发现这是由 Vuex 创建的状态管理器。好家伙!原来表格组件是用 Vuex 来管理数据的啊。这下清楚了,只要能搞定 Vuex 的部分,剩下的问题应该就可以迎刃而解了。
话不多说,盘它!
我开始不断尝试查找跟列相关的代码,开始全局搜索 "columns" 等类似的关键字。在发挥了名侦探柯南的洞察能力之后,我逐渐注意到 src/store/index.ts
这个文件中的 useStore
方法,这就是整个问题的关键!好了,整个问题原因找到了,关键在于store.states._columns
这个内部状态变量,它是渲染列数据的关键。但它只有初始化或添加删除列的时候才会被赋值,调整 el-table-column
的顺序根本不会改变这个变量!
找到原因,解决办法就简单了。笔者添加了 setColumns
这个 Mutation 方法,以更方便的设置列数组。具体实现过程限于篇幅原因就不详述了。感兴趣的朋友可以看 Crawlab Frontend 的 源码。
下面是完整的表格自定义列的效果图。大功告成!
世界上没有完美的东西,优秀开源框架也不例外。而这一次硬啃( Hacking )源码的经历让笔者深刻体会到这个道理。笔者认为非常有必要讨论关于使用和开发开源框架或开源项目的相关问题。首先我们来看看它的优势。
很多知名优秀的开源项目,例如 Nginx 和 Redis,都成为了软件开发的核心技术。那么,我们为什么要使用优秀的开源框架?笔者认为开源框架主要有以下几个优势:
虽然开源项目有不少突出优势,但在使用开源项目的时候却经常会遇到各种各样的问题。笔者认为使用开源框架存在一定的风险。笔者认为开源框架主要有以下几个劣势:
就笔者前面提到的解决开源框架问题的经历来说,未知风险和学习成本尤为突出。笔者在采用 Element Plus 的穿梭框( Transfer )组件时,想当然的认为它可以有效满足我的需求。然而,没想到的是它既不能支持我计划的列排序需求,又存在重大 Bug 导致无法使用。这些问题都让我放弃使用该组件,从而寻求重新造轮子的高成本解决方案。
在之后硬啃 Element Plus 表格组件的过程中,我没有放弃使用该组件,而是通过阅读该组件的源代码,根据逻辑和经验判断出了核心代码的位置以及其可能产生问题的原因,并针对性的做了调整,最终顺利的解决了问题。这次阅读源代码而进行调整的决定,给我节省了大量的时间和精力,因为要重复造表格组件这样的复杂轮子要做大量的工作,这是非常不划算的。
上图是官方文档其中一个组件的截图。Element Plus 的文档其实已经非常完善了,关于组件的使用方法、例子、API 等都写得很详细,每当开发时遇到不确定的直接去官方文档查看即可。不过,ElementUI 最早是诞生于饿了么,因此其中的组件主要是为了支持饿了么的外卖业务,因此它有一定的局限性。作为 ElementUI 的 升级版,Element Plus 虽然是同一个团队的产品,但它的功能以及 API 相比于老版本并没有太大的变化。作为对比,诞生于蚂蚁金服的 UI 框架 Ant Design 相对来说就要灵活自由很多,因为它支持的业务线很广,而阿里推行的中台系统也要求 UI 框架需要设计得很通用。因此,Element Plus 的局限性很可能来自于它的开发团队背景。Crawlab 前端框架选择 Element Plus 主要是因为老版本是 ElementUI,迁移成本相对来说比较低,但因此也带来了一些因框架不成熟导致的问题。甚至,我们也必须要知道即使是优秀的开源项目也会存在这样那样的问题。从这方面来看,了解开源框架本身的优点和缺点,对于如何正确使用来说非常重要。为了让开源框架能为我所用,我们需要知道如何正确驾驭开源项目的源代码。
接下来,笔者将结合自己的项目经验,为读者介绍如何有效驾驭开源项目中的源代码。
如果一个古代的人穿越到现代,当他看到各式各样的科技产品时,一定会惊呼它们为魔法。然而,他们不知道的是,这些所谓的 "魔法" 都是利用科学技术创造出来的,它们背后的原理是通过科学实验总结出来的,并不是什么神秘的巫术。这个道理同样适用于软件行业。
程序员用代码创造了魔法般的互联网社会,这对于非技术人员来说非常神秘。他们知道那不是真正的魔法,但他们完全不知道这些是如何实现的,背后的原理是什么。对于程序员来说,利用一些优秀的开源项目,可以快速让他们开发出产品原型、验证产品可行性、甚至开发出生产可用的系统。不过,如果程序员只是把开源产品当作工具,完全不理解里面的原理是什么,那就等于是在使用魔法,而你也不知道它为何会生效。这样的做法会给你带来麻烦。要正确使用一个开源框架,你必须掌握它;要掌握它,你必须理解其工作原理;要理解其工作原理,你必须阅读其源代码;要让源代码听命于你,你必须学会如何驾驭它,也就是如何修改和优化源代码。
接下来笔者将介绍几个有效方式来帮助你驾驭源代码。
读者可能会惊讶于这个看上去没什么用的建议。但是,我看到很多朋友想学习一个开源项目时,经常因为项目过于复杂而中途放弃了,包括很多年前的自己。其中就是挫败感在作祟。我承认,要短期理解一个大型开源项目是不太现实的。这导致了很多朋友被玲琅满目的技术难点给吓跑了,留下一句 "只要会用就行",然后继续搬砖拧螺丝。其实,他们都太低估自己的潜力了。笔者并不反对程序员的实用主义,但要注意过于实用主义会导致功利主义,最终原地踏步的将是自己。
开源框架最棒的一点就是它对来说完全是透明的,任何人都可以看到其中的代码,包括底层实现逻辑、代码组织结构、项目部署方式等等。这不是提升自己驾驭源代码能力的绝佳机会么?通过阅读开源框架源代码,你不仅可以了解其中的工作原理,还可以学习更好的编程方式,从而提升自己的职业素养。我真的不提倡 "从入门到放弃系列" 这句戏言,你至少应该这样暗示自己,**"我目前看不懂是因为我的基础知识不足,等我提升基础实力之后一定能驾驭它"**。笔者在硬啃完一些复杂的开源框架源代码之后,发现阅读源码其实也没那么难。就像锻炼身体一样,撑过这一个山坡、这一个冲刺,之后就非常轻松了,而且身体素质也提高了。
因此,克服挫败情绪对于驾驭开源项目源代码来说非常重要,而且你要暗示自己阅读源码并不难。
人们不愿意阅读源代码的其中一个原因就是不了解。就像你不了解一个心仪的的时候,你会认为她是完美无瑕的女神;但当你真正追求她,跟她一起约会交流生活的时候,你会发现她不过是一个普通女孩儿。因此,你可以尝试动手开始自己写一个开源项目。这对你理解开源项目作者的思路会非常有帮助。这同样不是一个有任何实际操作性的建议,但如果你真的开始动手做,就一定会提升得很快。俗话说:Learn by doing !
当然,要开始写开源项目并不是一件容易的事情。你首先可能就会陷入写什么的困难中。这里笔者推荐之前写的关于开发开源项目的两篇文章《如何打造一个上千 Star 的 Github 项目》以及《收获人生第一个 5k Star 开源项目,经验教训分享给大家》,里面详细讲解了笔者开发、维护、推广开源爬虫管理平台 Crawlab 的心得体会,会介绍如何定位痛点、调研用户、推广产品、项目管理等等。有兴趣的读者可以深入阅读。
终于,这一条是一个稍微有点干货的建议了。在阅读源代码的时候,你必须首先找到该项目的入口文件( Entry File )。所谓的入口文件,就是这个项目模块或系统暴露给外部系统的一个公共通道,整个项目的运行、调用、执行都是从这个文件开始的。 而其他实现具体逻辑的代码就在入口文件中被引用,或者在其他文件中被引用。对于前端项目,一般就是 main.ts
; Python 项目一般是 app.py
; Golang 项目一般就是 main.go
,等等。如果把看似不可驾驭的大型开源项目比做战无不克的战神阿喀琉斯,那么入口文件就是他的致命之踵。
其实,不光是阅读开源项目,就是我们阅读工作中其他同事写的代码时,也是先从入口文件开始研究。掌握了入口文件的内部逻辑,你可以看到它引用的其他子模块,然后就可以顺藤摸瓜找到其他核心模块,接着继续下去。当整个项目通过入口文件遍历一遍之后,你会更容易理解代码的逻辑结构,从而帮助你深入理解具体的工作原理。
在之前解决 Element Plus 表格问题的例子中,笔者定位到了 table.vue
这个入口文件,然后进一步找到了列数据的关键位置,最后轻松的解决了问题。看!驾驭源码其实并不是那么难,对么?
这又是一个技术性建议。由于大型项目通常来说要拆分成很多文件,因此对于这种情况来看,你如果穷举式的遍历所有文件会花大量的时间。因此,你可以尝试全局搜索可能的关键字,从而快速定位到核心代码部分。全局搜索在主流 IDE 中功能非常强大,通常支持正则表达式、模糊匹配、精确匹配、大小写等等。下图是 JetBrains 旗下 WebStorm IDE 中的全局搜索结果截图。
对于像 TS 这样的静态类型语言来说,还可以像 Java 、C#、Golang 那样查找方法或变量的调用位置,在 WebStorms 里是 "Find Usages",在其他 IDE 中可能名称不一样,但它们都是一个意思。这些技巧都能有效帮助你阅读理解源码。
好的文档包括项目的架构、原理、概念等,可以帮助开发者快速理解框架的代码结构。不过,并不是所有开源框架都有详细的文档,这种方式可遇不可求。不过,如果开源项目中一旦包含了技术文档,请一定花时间浏览一下。
当然,你也可以设法联系到开源项目作者。NPM 项目信息中通常会包含作者的邮箱地址;而且如果你看的是 Github 上的开源项目,还可以主动向其提 Issue,在 Issue 中表达你的疑问。
本篇文章通过介绍笔者折腾开源 Vue 3 UI 框架 Element Plus 的开发经历,讨论了开源项目的优势和劣势,得出优秀开源框国外服务器架也存在问题的结论。然后,笔者结合自己的项目经验提出了 5 个驾驭开源项目的技巧和方法,包括克服挫败情绪、自己写开源项目、定位入口文件、使用全局搜索以及阅读技术文档。现在的软件开发离不开优秀的开源项目,但是我们也必须意识到开源项目并不是专门为你自己服务的,因此它存在一定的局限性。为了能更好的使用开源框架,让它能兼容你的复杂需求,很多情况下你必须仔细阅读它的源代码,这就少不了会产生一定的学习成本。
我们通常说程序员要提升自己的技术实力,要会 "写" 出好代码;但是,在团队合作越来越重要的趋势下,笔者认为程序员更重要的能力是要会 "读" 别人的代码,不管好与坏。在编写代码的过程中保证正确合理的命名规范,尽量添加上注释以方便他人理解,这是写代码的能力;跟他人合作时,仔细阅读别人写的代码,尽可能理解他们的思路,思考实现方式是否正确,等等,这是读代码的能力。当然,写和读只是非常基础的部分,驾驭代码的能力还不止 "写" 和 "读",应该还包括思维、逻辑、规划等能力,例如系统架构、算法原理、可扩展性设计等等。
因此,当我们在抱怨同事代码写得差的时候,千万别懊恼或生气,因为那可能是因为你阅读代码的能力不够,没有理解清楚同事的思路;同样,可能在同事看来,我们的代码也糟糕透了;或者,这是传说中的五十步笑百步?
如果您对笔者的文章感兴趣,可以加笔者微信 tikazyq1 并注明 "码之道",笔者会将你拉入 "码之道" 交流群。