一、行云2.0的开篇
话说天下大势,合久必分,分久必合。
在行云2.0时代,一个原本平平无奇的业务工程,宛如一颗迅速膨胀的种子,短短两三个月,便摇身一变,成为容纳百十来个子应用的庞大“生态系统”。这些子应用来自五湖四海,各自施展浑身解数,为JDer们提供琳琅满目的产品功能,无论是与产研紧密相关,还是关联性稍弱的功能,皆涵盖其中。
作为基于Vue搭建的平台,不仅能够跨技术栈加载非Vue技术栈的应用,对于vue技术栈的应用还无私地奉献出了全局共享的Vue实例、router、vuex等等,同时连带全局组件库以及axios实例的分发,为依赖Vue技术栈的子应用提供全方位的支持,尽心哺育着有全局依赖需要的子子孙孙们。
二、回溯往昔:架构困境初现
不过,不管您用没用过这些出色的产品工具们,咱今天都不打算讨论子应用们,而是转回头审视行云前端工程最初的架构模样,着实还是有些惨不忍睹的。
惨在何处呢,我们粗略地说来:
- 代码结构混沌:平台与业务代码纠葛不清
虽设有/modules
目录用以存放不同业务域的代码,然而,文件夹之间组件引用关系错综复杂。以实际场景为例,在A业务模块开发过程中,开发人员可能因便捷性,直接引用了B业务模块中的某个组件,但并未遵循清晰的业务分层或模块划分原则。如此一来,当B业务模块进行调整时,极有可能影响到A业务模块的正常运行,使得代码的依赖关系犹如一团乱麻,难以梳理清晰。
以下是一个简化的模块依赖关系图,可以看出来此时复杂依赖情况:
实线箭头表示确定的依赖关系,虚线箭头表示可能存在的相互依赖或引用。实际情况也许更糟糕。
2. 业务杂糅:单体应用的沉重负担
这个单体应用身兼数职,既承担平台基础功能搭建,又涉足协作域业务开发,甚至还包含一些并非严格意义上子应用,却拥有顶级路由的特殊页面,如开放平台、开发者后台、应用商店等。不同业务逻辑在同一代码库中交织缠绕,业务之间耦合度极高。任何一处业务逻辑的修改,都可能像推倒多米诺骨牌一样,引发一系列难以预估的问题,极大增加了代码维护与扩展的难度。
3. 频繁发版之殇:全量构建的弊端
协作域作为业务迭代的“主战场”,业务更新极为频繁,每两到三周便可能经历多次发版。但当时采用的全量构建方式,虽操作相对简单,却存在严重弊端。每次发版时,不仅协作域代码会更新,连平台业务也会被一并更新。此时最可怕的事情莫过于听到的是:行云,又双叒叕白屏了……
4. 商业化困境:业务切割难题凸显
当行云迈向商业化进程,业务切割不清的问题愈发棘手。客户需求千差万别,例如有些客户只希望购买平台上的代码库,明确表示不要协作域功能,仅保留工作台,同时又要求配备开放平台和帮助中心。面对此类需求,开发团队由于原有代码结构缺乏清晰的业务边界划分,只能通过大量的手动删改代码来满足客户要求。这一过程不仅耗时费力,而且极易因人为失误引入新的问题,OMG…只能一把鼻涕一把泪地删删改改日日到天明。
5. 内部迭代困境:新旧代码鸿沟难越
随着行云内部不断迭代新功能,交付团队面临着巨大挑战。新功能开发过程中,技术栈逐渐演进,从JS到TypeScript的变革、Vue版本从2.6升级到2.7,这期间代码兼容性出现诸多问题,简单的代码拷贝已无法实现。同时,包依赖也发生了显著变化,以前交付的标品工程结构已经逐渐演变为多个multi - repo,与内部迭代的版本相差甚远。这些因素叠加,使得新功能迁移到现有工程变得异常艰难。开发人员需要深入研究不同版本之间的差异,解决各种依赖冲突,其难度不亚于完成一项高难度的拼图任务。
6. 静态资源臃肿:公共依赖的枷锁
平台积累了大量臃肿的静态资源,尽管进行了代码分割,能够做到非必要业务不加载,但作为支撑Vue子应用的平台,必要的公共依赖无法轻易翦除。而且平台自身的架构也对这些公共设施有依赖需求,导致静态资源占用空间始终居高不下,影响应用的加载性能。
7. 废弃组件堆积:代码库的隐形负担
在数年的开发过程中,虽然沉淀了不少全局复用的组件,但随着业务迭代,部分组件已被废弃,或者虽在UI层面不再使用,但仍在一些边角业务中少量存在。这些废弃组件依旧躺在代码库的components目录里,白白占据代码体积,增加了代码维护的复杂性。
8. 原子样式乱象:无序使用的代价
行云前端很早就实践了原子样式,在摸索阶段,由于缺乏统一的使用规范,不同同学采用不同的使用姿势。例如,对于同样的样式效果,有人用w-24px,有人用w-6。同时,有些开发者在组件中既采用BEM命名风格设置样式,又在各个class里使用@apply引入原子样式,这种做法既无法享受原子样式无需费劲命名class的优势,又不能有效重用class以减少构建体积,还浪费了构建时的资源。直到有一天我们发现,windicss停止维护了。。。这是上天给我们悔过的机会啊,是时候把乱象丛生的杂草整顿干净了。
9. 新业务扩展难题:平衡之术的挑战
当计划开展新业务时,面临着诸多挑战。一方面要保证新业务与现有业务在UI风格、交互方式以及与后端API交流方式等方面保持一致;另一方面,要确保新业务的加入不会增加现有业务的构建时间(当时构建时间动辄八分钟),且不会引入更复杂的依赖关系。简单来说,就是要实现工程的易扩展性,同时满足开闭原则,这对工程架构提出了极高的要求。
10. 构建方式单一:Vue CLI的局限性
整个工程所有业务均统一采用Vue CLI进行构建。Vue CLI虽具有上手容易、配置简单等优点,但随着业务规模的不断扩大和复杂度的提升,启动慢、构建慢、构建过程中也会有不必要地编译和打包一些无需更新的代码,延长了构建时间,降低了开发效率。
三、困境中的挣扎与坚持
对于一个服务上万JDer研发的业务而言,产品迭代如同奔腾不息的河流,不能有片刻停滞。业务在飞跑,我们如何停下来好好思考呢?可以的,不要停,一边跑一边搞。
我们幸运的(苦涩地🤷♀️)迎来了一个完美的契机:
给你个机会实现一下内外同源。
于是,故事就从这里开始了。
故事开始前再唠叨两句掏心窝子的废话……本段可以跳过。
为什么说是血泪史呢?对于一个服务着上万JDer研发的业务来说,产品迭代不能停,人力资源有限,寥寥三五个小可爱,还有同学是兼职来共建的,再加上,这是一个背负着十多万行代码的仓库,想要短时间内完成治理几乎可以说是天方夜谭。哪怕是在我们确定了改造方向并且开始实施到中途时,都无数次的怀疑过自己,怀疑过是否决策正确,无数次的质疑过这样的努力是否真的是有价值的,痛苦的日子真的想过还是放弃算了。是的,真的很难。
但是现在回想起来,只要我们在做正确的事情,那就是值得的。
四、内外同源:破局的曙光
废话不多说,我们几个老可爱加上小可爱经过了多次头脑风暴(反复折腾和分析),确定了几个改造重点方向(主要是解决以上问题),确定了改造原则(模块化实践原则),盘了我们能动用的人力和投入的资源配比,制定了《内外同源绞杀计划》,该计划将分步骤将任务分别拆分到几个不同的迭代内完成,改造任务和业务story的迭代人力资源配比大概是1:3,保障功能迭代稳健推进。
“绞杀者模式”的启示
为什么叫绞杀计划呢?这个idea源于老司机一次聊天时提到的"绞杀者模式"。概括来说就是:
绞杀者模式是一种软件设计模式,用于逐步替换现有的大型系统或应用,通过新系统逐渐接管旧系统的功能,最终完全取代旧系统,而无需一次性完成整个系统的重写。这种方式可以减少风险,确保平稳过渡。
这在当时一筹莫展、选择艰难的我看来,仿佛醍醐灌顶一般看到了曙光。
改造的主要目标
在这个绞杀计划里,主要目标在于:
1. 模块切割:把平台和业务代码分家,公共依赖和业务应用实现分离。
2. 构建瘦身:避免全量构建了,发布哪个应用就只构建这个应用相关的依赖,平台别再背锅白屏了。
3. 代码断舍离:废弃组件、冗余代码,统统清理掉。
4. tw样式规范:原子样式别再乱写了,按照同一份选择实践。
5. 扩展性与可维护性升级:采用分层架构,保持良好的独立性和可扩展性。
6. 绞杀者模式上线:制定详细的过渡计划,分阶段逐步替换旧系统的功能模块。
总之,既要让行云前端轻装上阵,又要让它跑得更快、更稳、更优雅!
五、架构演进路线
基于以上目标,我们制定了以下具体的改造路线:
阶段1:主工程准备(基于 pnpm workspace把现有工程改造为monorepo)
- 创建目录结构
- 工具引入:pnpm workspace, nx
- 更新CI/CD配置
详情请移步:行云前端重构之路-Monorepo实践篇
阶段2:monorepo架构实施
- 业务模块分离:划分几个相对独立的,有自己的路由、状态管理的子应用,均可独立开发运行和发布,放在apps/下
- 轻量的新平台实现:
- Radix vue在vue3中的实践(待完善,文章在路上)
- 使用JModule快速实现宿主平台
- 移除Windi CSS,全工程迁移到Tailwind CSS,集中管理Tailwind CSS配置
- 公共pkg抽离:common/common-service/utils/tailwind-config/components
- 搭建一个vue-ts ui组件库总共分几步(待完善,文章在路上)
- 缓存策略优化(待完善,文章在路上)
阶段3:自动化构建流程
- 构建方案和工具的选用:业务工程使用vue-cli/vite;基建pkg使用vite/tsup
- 构建流程优化:依赖关系处理,哪些包需要前置构建等
- 发布脚本
六、改造后的工程新面貌
主业务的新依赖关系图
工程结构对比(图)
在新的工程结构中,主要包含以下部分:
- apps/: 包含多个独立的应用
- admin/: 管理后台应用,用于系统管理和配置
- ai-assistant/: AI助手应用,提供智能交互和辅助功能
- jacp/: 主要的协作域应用,包含核心业务逻辑
- open/: 开放平台应用,用于对外提供开放平台、文档等功能
- platform/: 平台应用,用于统一管理和集成其他应用
- packages/: 包含可复用的模块和库
- common/: 提供vue共享的环境和资源
- common-service/: 通用服务,如API调用、数据处理等
- components/: 可复用的UI组件
- tailwind-config/: Tailwind CSS配置,用于统一样式
- utils/: 工具函数集合
- xingyun-elements/: 存放web-component组件
技术栈
- 前端框架:Vue.js
- Monorepo管理工具:Nx (提供工作区管理、依赖图分析、增量构建等功能)
- 包管理器:pnpm
- UI框架:自定义组件库 + Tailwind CSS
- 类型检查和代码质量工具:
- 构建工具:Vue CLI/Vite (用于Vue应用的开发和构建)
架构设计原则
- 模块化:通过packages目录中的不同模块实现代码复用和关注点分离。
- 微前端:多个独立应用可以独立开发、部署和扩展。
- 组件驱动开发:首先关注于创建和完善单个组件,然后将这些组件组合成更复杂的结构,最终形成完整的用户界面。
- 主题定制:使用Tailwind CSS实现灵活的样式定制。
- 增量构建:利用Nx的依赖图分析,只构建产生变化的部分,提高开发效率。
- 代码质量保证:使用TypeScript和ESLint确保代码质量和一致性。
模块职责
应用模块 (apps/)
每个应用模块都是相对独立的,有自己的路由、状态管理和UI组件,均可独立开发运行和发布。它们可以共享packages中的代码。
- jacp: 核心业务应用,实现主要的协作域业务逻辑。
- platform: 独立的平台应用,用于加载其他子应用,提供统一平台入口。
- admin: 负责系统配置、用户管理等后台管理功能。
- open: 负责行云开放平台及应用商店、开发者后台、文档管理等业务功能。
- ai-assistant: 提供调用(Autobots)AI-Chat的用户交互界面,在平台中使用iframe加载其页面。
共享模块 (packages/)
共享模块提供了可以在多个应用中复用的功能和组件。
- common: 为共享vue实例的子应用导入并配置了必要的依赖,提供了一个共享的环境和资源(这个pkg只为兼容历史发展中与平台有紧密联系的部分子应用,在未来计划逐步改造为可独立管理独立运行的模块)
- common-service: 封装通用的后端服务调用和数据处理逻辑。
- components: 包含可在多个应用中使用的UI组件,可独立安装使用。
- tailwind-config: 集中管理Tailwind CSS配置,确保样式的一致性。
- utils: 提供各种通用的工具函数。
- xingyun-elements: 存放web-component组件。
模块化开发原则与代码结构
- 模块化开发原则:构建高内聚低耦合的独立模块,确保单一职责、清晰接口和可复用性,以提升代码的可维护性、可测试性和可扩展性。
- 从 monolith repo 到 monorepo:重构带来的代码结构优化。monorepo 实现了更好的代码共享、版本一致性和构建效率,同时保持了清晰的模块边界。
- 开发体验优化:
- 部分项目迁移到Vite,利用其快速的开发服务器和构建能力。
- 利用缓存提高启动速度,减少资源消耗,以提高开发效率。
- 构建配置添加并行处理以提高构建速度。
- Webpack构建的应用使用硬件缓存。
- 减少低频变化包的构建:此处有坑。
- 微前端小工具:进一步简化子应用的调试步骤。
云端构建优化与缓存策略
- 代码分割:部分业务应用使用Vue的异步组件和Webpack的动态导入实现按需加载。
- 缓存优化:通过利用Webpack的文件级缓存和Nx的缓存和增量构建功能提高构建速度。
- 在行云部署缓存实现:行云部署的缓存基于构建镜像存储,通过将源码拷贝到缓存目录下,可以显著提高缓存读取效率。
- Monorepo的结构:应用独立化后,可按需构建,提高单个应用构建速度和资源利用率。
扩展性
- 添加新应用:在apps/目录下创建新的应用目录,配置Nx工作区。
- 添加新共享模块:在packages/目录下创建新的模块,并在需要使用的应用中引入。
- 扩展现有模块:遵循开闭原则,通过扩展现有类或组件来添加新功能。
总结
行云前端工程从单体应用发展到微前端架构的过程中,虽实现功能集成但面临诸多困境。通过 “内外同源” 的契机,秉持模块化实践原则,制定 “内外同源绞杀计划” 进行改造。
改造前,工程存在平台与业务代码混淆、业务混杂、构建不合理、业务切割困难、代码臃肿、样式混乱、扩展艰难等问题。改造聚焦模块切割、构建瘦身、代码清理、样式规范、提升扩展性与可维护性等目标。
改造后的工程采用 monorepo框架,借助 Nx 与 pnpm 管理,遵循模块化、微前端等原则,明确 apps/ 应用模块与 packages/ 共享模块职责。在开发体验优化上,部分项目迁移至 Vite,利用缓存提升效率;云端构建通过代码分割、缓存优化等提高构建速度与资源利用率,整体具备良好扩展性,为业务发展提供更坚实的架构支撑。
这次改造,我们实现了模块化、微前端、组件驱动开发和主题定制等架构设计原则,使得行云前端小工程更加轻量化、可扩展性和易于维护。
写这篇文章是为了分享我们的经验和教训,希望能帮助其他人在面对类似问题时有所启发。同时,这也是一次自我反思和总结的过程,帮助我们更好地理解和改进我们的工作流程和技术选择。