组件化架构漫谈
2017.06.01
架构之大,分而治之。
在项目趋于稳定迭代的情况下,上周与团队分享了我的架构设计,反响很好,梳理自己的知识结构总是好的。
组件化的定义
在传统的项目开发中,如果项目比较小,普通的单工程 + MVC 架构就可以满足大多数需求了;
但是随着程序代码量和业务的发展,公司业务的发展速度随着时间和市场需求加快,原有的单工程架构就不足以满足架构需求了,很多都面临着重构的问题,特别是在多人协作的团队;这时候组件化架构无疑就是最好的解决方案。
组件化这个概念在后端早已盛行很多年,随着在近几年前端的发展,被频繁的提及和使用,组件化其实就是一个对业务界面的各个元素定义和归类的过程。
在分层和分模块后,每一个业务组件由三层各自存在的部署包组成,包本身是一个包含了技术组件和服务组件的一个结合体。
由数据层,逻辑层,界面层三层的三个业务包可以构成一个完整的具备独立功能的业务组件。
组件化分成文件和业务逻辑的组件化(标签化),其一是文件形式的,将各个单元从业务界面中拆分出来,存成一个个树状结构的文件,使用时通过路径去一一引用;其次是业务逻辑上的组件化,同样是把单元功能从业务界面中拆分出来,对同一类型的单元存入文件中,进而自定义标签,使用组件时只需引用标签即可。
这两种方式可以相辅相成,相互应用,其中怎么去使用,需要根据业务所设计的架构去权衡。
我将“组件化”理解为以下几要素:
- 组件是对逻辑的封装,不限于图形元素。
- 组件具备单个可移植性,即“随加载随用”,加载组件便加载所有资源。
- 组件是声明式定义的,而非命令式。
为什么组件化
目的
归根结底,组件化的最终的目的分为以下几种:
- 尽可能把设计与开发中的元素独立化,使它具备完整的局部功能。
- 组件之间通过自由组合来构成整个产品。
- 组件化后,每层的职责更专一了,只需对组件进行单元测试的覆盖。
- 提高生产的效率,不写重复的代码,从而提高项目的可维护性,降低维护成本。
- 按需分配和引用组件,性能调优掌握在自己手中。
- 形成组件库,下次新项目开发时,能不能”偷个懒”把之前项目的组件拿来使用。
举个例子,在做中后台管理系统时,会遇到修改某个公用部分的情况,典型的有顶部导航、底部导航,如果项目小,工作量看起来不大,但一旦处于大型项目且业务比较复杂时,需要每个业务界面都要去一一修改,这无疑是增加了可维护的成本。
组件化就是最好的解决方案,将公用的部分从业务界面中解耦出来,如今前端都在这么做,将公共的部分抽成一个文件,在使用时只需修改一个文件的代码就够了。
所谓组件化,核心意义莫过于提取真正有复用价值的东西,其实不外乎下面四种:
- 控件(常用的表格、按钮、下拉框)
- 基础逻辑功能
- 公共样式
- 稳定的业务逻辑
职责转换
早些年的架构设计比较多关注的是横向的分层,即数据层,逻辑层和UI层,也就是我们常说的 MVC(模型,视图,控制器)。
而组件化架构必须同时关注纵向的隔离和解耦,同样也引入了 MVC 的概念,但这是随应时代发展对结果导向。
由于 “用户体验” 时代的到来,人人都在谈体验,从而产品会更偏向与交互性的体验,从而使前端在现如今的发展中,从整个产品的架构中获得更多的控制权,典型的有:后端的路由控制交给了前端,从 前端(V)- 后端(MC)到 前端(VC)- 后端(M),让后端更专注于内存的使用和当好一个数据提供商。
如何组件化
组件化主要分为两种,全局组件化和局部组件化,对于一个有一定规模的 Web 应用来说,把所有东西都全局 “组件化” ,在管理上会有较大的便利性,但是带来的问题是,单元过于零散,需要详细的架构说明和文档,否则团队很难使用组件去组织新的应用体系;
局部组件化面临的是在架构方面生命周期和非生命周期权重的理解去拆分,如果生命周期比非生命周期的权重大时,则推荐局部组件化的方式。
HTML 片段化
举个简单例子,你新接手的项目,如果在多个页面中存在一个公共的部分:导航(这里指视图 HTML),就可以进行复用。
上述的代码中将顶部导航和底部导航片段化成 HTML 文件,以方便用户通过 JS 引用路径去多次复用(大部分前端框架都集成了这种做法)
CSS 单元化
传统的项目开发中,为了达到项目运行时,减少去服务器上下载的数量,我们往往会把一整个项目的 CSS 写在一个样式表文件中;
但是在项目运营的工程中,会接连不断有新的需求或者改进,如果想改个东西,需要的成本会比较大,特别是在多人协作的过程中,会发生代码冲突的情况,这是最为致命的。
而且现在业界内对于 CSS 开发并没有很成熟的最佳实践或者约定可循,这时候,CSS 模块化的优势就体现出来了!
将之前的一个样式表拆成多个样式表,可以理解成多个组件或者模块,为了利于多人协作或者模块管理,看起来结构清晰,条理清楚,可一人负责一个模块(依团队而定),大大的提高效率和降低协作成本。
服务器拉去静态资源请求增多了?
此时,可以使用构建工具指定出口后,进行打包压缩,生成一个 CSS 文件即可,这样你就可以无痛的在生产环节下处理好你的组件或者模块的样式。
谨慎打包库或者第三方框架的代码,防止打包后的文件体积过大;可以采用 CDN 的形式进行加载。
JS 模块化
视图层模块化 视图(view)是对结构(structure)中具有代表意义的部分元素和关系进行抽取,也可以说,视图是对结构的精简。
引用张云龙博客中一图。
数据层实体化 作为应用数据链路的最下游,前端的 Model 层与后端的 Model 层其实有着很大的区别。其中最核心的就是,相较于后端 Model,前端 Model 并不能起到定义数据结构的目的,而更像是一个容器,用于存放后端接口返回的数据。
数据层的建立有益于视图间的数据共享,同一份数据被多处视图使用,并且要保持一定程度的同步;如果一个业务场景中,不存在视图之间的数据复用,可以考虑使用端到端组件。
业务认知
什么叫业务?简而言之就是行业常识和经验,比如说,一个有经验的仓库保管员,可能文化程度不高,理解不了软件的运行原理之类,但一定对产品出库入库的流程非常熟悉,包括各种审批过程和异常状况;
但这些,程序员是不懂的。那如果要促进这个领域的信息化,程序员必然要学习业务,从理解刀耕火种的时代的业务流程,用计算机的编程的方式去实现这些业务流程。
设计架构 ≠ 套用技术选型
业务是项目架构的核心,项目架构是从业务到 IT 转换的第一步,应用是对(系统)能力的分组,实现业务功能并且管理数据,依据具体的业务场景给出解决方案,其中的解决方案包括技术细节,团队能力,和开发规范;绝大部分的项目和需求都是以业务为基石的,所以要想设计出一个好的架构必须洞悉业务而不是在对业务不了解的情况下套一大堆技术选型来去做架构。
组件切分
这个时代,人们的主要关注点是数据驱动的界面,而现如今流行的的组件化方式主要有以下几种:
- shadow DOM 封装组件的内部结构
- Custom Element 对外提供组件的标签
- Template Element 定义组件的HTML模板
- HTML imports 控制组件的依赖加载
将业务界面拆分成单元组件,无非是为了不写重复代码,希望达到最大程度的代码复用的效果,但这仅仅只是代码和控件的的拆分,难点在于业务逻辑的拆分和业务逻辑上的架构,所以前端架构往往是业务和代码上的架构,这需要一个对业务理解透彻的代码的老手。
所以,组件化的本质目的并不一定是要为了可复用,而是提升可维护性。这一点正如面向对象语言,Java 要比 C++ 纯粹,因为它不允许例外情况的出现,连 main 函数都必须写到某个类里,所以 Java 是纯面向对象语言,而 C++ 不是。
组件化难点
关于组件化和模块化,在切分方式上,模块粒度和组件粒度无疑是一个比较模糊的点。
模块有时可以很大,也可以很小,小到一个函数独立成成一个模块,组件也是一样的道理,它可以是一个函数独立成组件、单模块成组件、多模块集成一个组件,粒度很大的组件,比如常见的单页应用中每个页面都可以作为一个组件,每个页面对外的接口只有 show 和 hide两个状态,页面的状态内部维护。
同样,粒度小的组件也只有简单的 API,状态内部维护;而这里我们常说的组件除了 HTML片段,JS 模块外,其实组件化应该不仅仅局限于 UI 功能组件,甚至某些特殊场景下整个缓存数据结构都可以作为组件,所以模块应该主要是通用工具、api、类的封装,而组件更多的是业务功能、UI 组件的封装、缓存数据的复用。
组件管理
组件加载策略
现如今已经没有哪个前端组件化框架可以不考虑异步加载问题了,因为,在前端这个领域,加载就是一个绕不过去的坎,必须有了加载,才能有执行过程。
每个组件化框架都不能阻止自己的使用者规模膨胀,因此也应当在框架层面提出解决方案。
我们可能会动态配置路由,也可能在动态加载的路由中又引入新的组件,如何控制这些东西的生命周期,值得仔细斟酌,如果在框架层面全异步化,对于编程体验的一致性是有好处的。
回到 GUI 的本质问题,我们要面临一下问题:
- 页面由大量模块组成
- 每个模块部分由首页自主维护,部分由业务方通过插件维护
- 所有模块是同时进行加载
- 模块中媒体资源(图片、视频)较多
- 每个模块的依赖资源较多(包括 js 文件、接口文件、css 文件等)
所以在项目开发前,应在团队中约定好每个模块之间的依赖,最大程度的降低耦合,将相关的逻辑(比如请求接口、请求相关的依赖资源)都封装在内部,在项目中尽量落实成组件的形式。
- 将各模块拆分为组件粒度
- 将组件依赖的资源全部封装在组件内部进行调用
在完成了组件化的拆分,确保模块之间不会互相影响和产生耦合之后,我们可以方面地调整加载策略。加载的策略是根据可见性来处理优先级问题。
- 优先加载首屏可见模块
- 其余不可见模块懒加载,按需加载(用啥加载啥),精确定位组件。
组件就近管理
这里我们推荐一个模块(组件)一个目录的做法,尽可能的模块内的资源就近管理,在维护和修改时只需定位到目的文件夹进行相应的操作,如果不再使用,可以将整个文件夹删除。
组件间通信
组件会发生三种通信。
- 向子组件发消息
- 向父组件发消息
- 向其他组件发消息
在通信过程中我们可能会面临以下问题:
- 组件之间依赖公共的数据源
- 组件之间可能存在输入输出关系
- UI 组件存在联动关系
- 组件之间需要数据一致性和同步性
解决方法:
使用消息总线的方式,组件根据依赖关系采用订阅和发布方式同步数据。
架构的基础构建
自动化构建工具的出现有效地解决了前端开发中的效率问题,至于工具的选择,前端架构需要调研每种工具的特点,评估其优点和缺点,根据项目的需求选择更加适合的工具。
要知道,模块边界的划分,不只是一个人的事情,在架构实施时,团队中每个代码的贡献者都有义务参与架构和义务去提建议。
现如今,规范的前端架构流程主要分为以下几种:
- 选用哪种基础框架
- 业务代码如何规划
- UI 组件如何规划
- 样式和主题如何规划
- 构建方案怎样
- 人员如何协作
特别是基础框架这个环节,如果把一个项目的软件架构看成一棵大树的话,那么基础框架就是树干,重要性显而易见,使用一种框架,首先要考虑团队的整体水平,其次它能不能把业务解耦的很好,进而在框架版本更新时如何迁移,这些都要在基础架构设计时必须要考虑的。
而对于大型的 Web 应用,最关键的两点在于:
可控,组件化是一种解决方案,我们要的是两种东西可控,一种是组件之间的关系,一种是组件内部的数据,这两者,不同框架有不同策略去做,并无高下之分,主要还是取决于使用者的水平。
高效,目前从开发效率讲,带双向绑定的框架无疑为开发者节省了大量的成本。
技术选型
同样,框架选型也是如此,Angular、React、Vue等 MVVM 框架一时炙手可热,在项目的技术选型时,必须对整个技术架构考虑周全,包括可扩展性、可测试性等等。所谓框架,只是帮助我们解决问题的工具,提高开发效率,我们需要的是学习框架的思想,不要过度依赖框架。
框架的选择要根据适当的业务场景,如果 Jquery 最适合当前的业务,那就是最好的选择;前端架构师的工作就是不断探索和评估新的技术、平台、方法和框架。
永远要记住一点,当你身为软件架构师去做架构设计的时候,要根据业务场景、团队水平和未来项目架构伸缩性去选择合适的框架,而不是一味的推崇框架门派而去选择框架,你是使用框架,而不是被框架使用。
目录层面
利用操作系统与生俱来的目录结构,合理规划目录结构,是好的架构实现基础。
页面层面
页面层面,顾名思义就是用来组织视图的,也就是我们常说的 HTML。
功能层面
这个层面比较特殊,打个比方,最简单的网站,可以是单个文件或者多个相关功能的聚合,而当系统逻辑和业务过于复杂时,这一点就比较考验架构师的组织能力和对业务架构的前瞻性。
组件化和设计模式层面
上面提到,当一个逻辑需要被多次使用时,我们就要开始组件化和抽象。
人员如何协作
- 新人负责 UI 开发
- 有经验的工程师负责业务逻辑开发
架构的落地
部署与发布
前端代码部署按照现软件行业界的分工标准,虽然不是前端的工作范畴,但它对性能优化有直接的影响,其中包括静态资源缓存、cdn、非覆盖式发布等问题;合理的静态资源资源部署可以为前端性能带来较大的优化空间。
架构的维护
从流程管理来讲,前端架构的职责是明确前端开发的各个环节,从需求分析到原型设计,到具体的代码提交和测试,再到最终的部署和维护。
在自动化和工程化日益完善的前端领域,你可以采用更先进的方法,通过设计更加完善细致的自动化流程,才能构建出更加高效、更加健壮和可扩展的应用。具体来说,包括工作流设计、团队协作工具、构建工具、持续集成等等。
代码层面
Code Review & 代码规范
架构层面
这方面考察架构的可扩展性是否受到制约,比如考察未来半年内的产品需求或者功能能否快速的上线就是一个标准。
优化
- 性能(缓存)
- 内存管理
相关文档
通常来说,作为软件项目,我们需要有这几类文档:
- 需求说明文档
- 功能设计文档
- 系统架构说明书
- 模块概要设计文档
- 模块详细设计文档
- 系统架构设计是一个非常依赖于经验的设计过程。需要根据软件项目的特定功能需求和非功能性需求进行取舍,最终获得一个满足各方要求的系统架构。系统架构的不同,将很大程度上决定系统开发和维护是否能够较为容易的适应需求变化,以及适应业务规模扩张。
架构设计工作中,用户参与程度很低,软件开发团队中的需求人员参与程度很低,但团队中的所有核心设计和开发人员都应该参与其中,并达成一致意见。