前端技术架构-业务架构 应用架构 技术架构
有的同学可能会觉得前端比较简单,不需要架构,或者因为前端交互细节复杂混乱,很难统一抽象,所以没办法设计架构. 这种认识是片面的。 虽然有些前端项目是在没有仔细考虑架构的情况下堆砌的,但这并不代表不需要架构设计。 任何业务程序都可以通过代码堆叠来实现功能,但其背后的可维护性和可扩展性自然千差万别。
为什么前端项目也要考虑架构设计? 有几个原因:
从必然性上看,虽然操作系统和各种基础库屏蔽了底层实现,让业务只关心业务逻辑,极大地解放了生产力,但一个应用必须要和底层操作系统和业务层协同运行代码。 应用程序具有逻辑和组织良好的层次结构设计。 如果业务层没有好的架构设计,技术抽象就会一团糟。 很难想象这样形成的整体运行环境是健康的。
业务模块的架构设计应该类似于基于计算机的架构设计。 从需求分析出发,设计业务子模块,定义这些子模块的职责和子模块之间的关系。 子模块的设计取决于业务的特点,子模块之间的分层取决于业务的扩展能力。
例如,在设计绘图软件时,只要需要组件子系统和布局子系统,它们就相互独立,可以无缝组合。 对于BI软件,由于增加了筛选联动和通用数据查询的概念,因此也增加了相应的筛选联动模型、数据模型、图形语法子模块,并按功能关系上下分层:
如果分层清晰准确,可以看出两个业务上层具有相同的抽象,即最上层是组件和布局的组合,以及过滤联动和数据查询,以及映射function from data model 到 graph element relationship 都是附加项,移除这些项不会影响系统的运行。 如果不这样设计,系统之间的异同可能不明确,造成功能耦合。 为了维护一个大型系统,可能需要了解模块之间的相互影响。 这样的系统既不清晰也不充分。 可扩展性,关键是理解其维护成本高。
从可行性上来说,前端的特点是用户输入的触点较多,但这并不妨碍我们对标准输入接口进行抽象。 比如用户点击按钮或者输入框输入,快捷键也是一种输入法,URL参数也是一种输入法,业务前面的表单配置也是一种输入法。 如果输入法很多,那么对标准输入的抽象就变得重要了,这样业务代码的实际复杂度就不会真的膨胀到用户使用的复杂度那么高了。
不仅输入触点多,前端系统的功能组合也很多,比如图形绘图软件,画布可以容纳任意数量的组件,每个组件有任意数量的配置,组件也可以影响每一个其他。 这种系统是一个开放的系统,用户可以很容易地尝试开发者从未想过的功能组合。 有时开发人员会惊讶于这些新组合可以协同工作! 用户会感叹软件的强大功能,但开发者并不能真正将这些功能一一组合起来,试图解决冲突。 必须使用合理的分层抽象来保证功能组合的稳定性。
事实上,这种挑战也是计算机面临的问题。 如何设计一台具有通用架构的计算机,让任何开发者的软件都可以在上面运行,并且软件之间可以相互独立或相互调用,系统不易出现BUG。 从这个角度来看,计算机的底层架构设计对前端架构设计具有借鉴意义。 一般来说,计算机通过硬件、操作系统和软件三个层次来解决所有的计算问题。
冯诺依曼系统在硬件层面解决了这个问题。 为了保证软件层的可扩展性,通过对CPU、存储、输入输出设备的抽象来解决计算、存储、扩展三种基本能力。 在细分方面,CPU只支持三种基本能力:数学计算、条件控制、子函数。 这使得计算机的底层设计稳定,设计因素也可枚举,扩展能力强。
操作系统也是如此。 它不需要知道软件是如何执行的。 只需要为软件提供一个安全的运行环境,使软件不会受到其他软件的干扰; 它提供了一些基本范例来统一软件的行为,例如多窗口系统。 ,防止软件同时在一个区域绘制,相互影响; 为上层语言提供一些基础的系统调用包进行二次封装,考虑到这些系统调用包可能会随需求扩展,然后采用动态链接库的方式实现,等等。 为了使自身的功能稳定和可枚举,操作系统在自身和软件之间定义了明确的界限。 无论软件如何扩展,操作系统都不需要扩展。
回到前端业务,如果要保证一个复杂绘图软件代码的清晰度和良好的可维护性,同样需要从网上最底层的稳定模块开始,一步步构建模块之间的依赖关系。 只有这样,模块中的逻辑才能可枚举,比如只敢将模块与模块组合起来,设计自己的扩展点,这样整个系统最终会有很强的扩展能力,但是每个子模块都是一个简单的,清晰,可枚举和可测试的代码逻辑。
以BI系统为例,如果分为组件、筛选、布局、数据模型四个子系统:
从广义上讲,前端业务代码早就处于一系列的架构层,即编程语言和前端框架。 编程语言和前端框架都会有一些设计模式来减少混合代码范式带来的沟通成本。 其实架构设计本身也需要解决代码一致性的问题,所以这些内容都是架构设计的一部分。
前端框架本身带来的数据驱动特性,很大程度上解决了前端代码在复杂应用中的可维护性问题,大大降低了流程代码的复杂度。 React或Vue框架本身也像一个操作系统,即定义上层组件的规范(软件规范),平滑浏览器对组件渲染和事件响应的差异(硬件差异),并提供组件渲染调度功能(软件调度)。 同时,还提供组件间的变量传递(进程通信),使组件间的通信符合统一的接口。
但是没有必要将每个组件都类比为一个流程来设计,也就是说组件之间没有必要进行通信。 比较合适的类比粒度是模块,将一个大的模块抽象成一个组件,模块之间互不依赖,采用数据通信的方式进行通信。 小粒度的组件被做成状态无关的组件。 注意,功能相似的组件的接口尽量保持一致,这样才能体验到多态的好处。
所以话说回来,遵循前端框架的代码规范并不是可有可无的。 业务架构设计已经从编程语言和前端框架开始。 如果一个组件不遵循框架的最佳实践,它就无法参与到更高层的业务架构规划中,最终可能会导致项目混乱,或者根本就没有架构。 因此,关注架构设计,从代码规范开始。
那么前端架构设计是很有必要的,那么如何做好前端架构设计呢? 这个话题太大了。 本次借用操作系统的一些灵感,先说说对分层和抽象的理解。
没有绝对的分层
分层是架构设计的重点,但是一个模块的分层位置可能会随着业务迭代而变化,类比操作系统举两个例子:
语音输入现在是各个软件自己提供的,背后的语音识别和NLP能力可能来自各大公司的AI中台,或者一些提供AI能力的云服务。 但是,语音输入能力成熟之后,很可能会成为操作系统的内置能力,因为语音输入和键盘输入都是标准输入,只是语音输入难度更大,操作系统更难短期内要内置,所以目前在各种上层应用中开发。
Go语言的协程是在编程语言层实现的,而它的目标线程是在操作系统层实现的。 协程运行在用户态,而线程运行在内核态。 但是如果有一天操作系统提供了更高效的线程,内存占用也采用了动态增加的逻辑,也许协程就没有那么必要了。
按理说,语音输入是标准输入的一部分,应该在操作系统的通用输入层实现。 协程也是多任务的一部分,应该在操作系统的多任务层实现,但是现在都更高了。 有的在编程语言层,有的在业务服务层。 造成这些事故的原因是通用输入输出层和多任务处理层的需求并没有想象中那么稳定。 随着技术的迭代,当需要扩展它的时候,因为它是建立在底层而不方便扩展的,所以只能在更高层实现扩展。
当然,我们还要注意,即使这些扩展点是在更高层次上实现的,但它们对软件工程师并没有特别侵入性的影响,比如goroutine,程序员不接触操作系统提供的API,所以编程语言层对操作系统能力的扩展对程序员来说是透明的; 语音输入有一点影响。 如果由操作系统来实现,可能会变成一个与键盘输出一致的事件结构,但是是由业务层来实现的。 API格式不计其数,业务流程可能会更复杂,比如添加鉴权。
我们可以从计算机操作系统示例中学到两件事:
从层次合理性的角度,对输入进行进一步的抽象和整合。 例如,语音识别被封装到标准输入事件中,使其在逻辑上成为标准输入层。
业务架构设计难免会遇到分层不满足业务扩展性的场景。
业务分层与硬件和操作系统的区别在于,在业务分层中,几乎所有的层都易于修改和扩展。 因此,如果遇到不合理的分层设计,最好将它移到它应该属于的层中。 操作系统和硬件层之所以不方便随意扩展,是因为版本更新的频率与软件更新的频率不匹配。
同时,也要认识到分层需要一个进化的过程。 等新模块稳定下来再移动到它所属的层可能会更好,因为从上层移动到下层意味着更多的模块被共享和使用,就像我们不会轻易的那样软件层中某个包提供的功能内置于编程语言中,编程语言中实现的功能不会随意内置于操作系统的内置系统调用中。
前端领域的一个例子是,如果一个平台项目中已经有了一套组件元信息描述,那么最好让它在业务代码中运行一段时间,观察元定义了哪些属性。哪些信息是缺失的,哪些是不需要的,在业务稳定一段时间后,再把这套元信息运行时代码抽取出来,打包成一个通用的包,提供给这个业务,甚至其他业务。 但即使这种能力被沉淀到通用包中,也不代表它永远无法迭代。 操作系统的多任务管理都有协程来挑战前端技术架构,更何况前端抽象包的能力? 因此,我们对抽象要谨慎,但抽象后也要敢于质疑、敢于挑战。
没有绝对的抽象
抽象的粒度一直是架构设计中的难题。
计算机将一切理解为数据。 计算结果就是数据,执行程序的代码也是数据,所以CPU只要专注于数据的计算前端技术架构,再加上存储和输入输出,就可以完成所有的工作。 想想这样一个抽象的伟大之处:所有的程序对于计算机来说归根结底就是这三个概念,CPU在计算的时候不需要关心任何业务意义,这也让它可以计算任何业务。
另一个有争议的抽象是Unix中一切皆文件的抽象。 这种抽象使得对文件、进程、线程、套接字等的管理抽象成文件API,都有具体的“文件路径”。 例如,你甚至可以通过 /proc 访问进程文件夹,ls 可以看到所有正在运行的进程。 当然,进程不是文件。 这恰恰说明了Unix的一个抽象哲学,即“文件”本身就是一个抽象。 开发和开发可以像文件一样理解一切,这带来了理解成本的巨大降低,也使得许多代码模式可以与特定的资源类型无关。 但是这个有争议的一点是并不是所有的资源都适合抽象成文件,比如输入输出中的显示,作为呈现彩色像素的载体,真的很难用文件系统来统一描述。
计算机设计和操作系统设计给了我们明显的启发,就是凡是可以抽象的东西都应该尽可能抽象,这样才能提高系统各个模块的稳定性。 但是从Unix中一切皆文件的抽象来看,有时技术抽象难免受限于当时的业务需求。 当输入和输出设备的类型增加时,这种极端的抽象可能并不总是合适的。 但始终相信抽象,因为如果所有的资源都可以用文件抽象来描述,而且使用上没有不便,那为什么还要创造其他的抽象概念呢? 除非必要,否则不要添加实体。
比如BI场景的筛选、联动、下钻场景是否可以抽象为组件之间的联动关系? 如果一个标准的联动设计能够解决这三种场景,那么自然就没有必要针对具体的场景引入概念了。 从原有场景来看,无论是筛选、联动、下钻场景,都是通过修改组件的访问参数来改变查询条件。 我们可以为组件之间的联动抽象一个规范,从而驱动访问参数的变化。 但是,未来的需求可能会引入更多的可能性,比如在筛选时触发一些额外的附加分析查询。 这时候,之前的抽象就受到了挑战。 我们需要权衡保持统一的好处,通用接口不适合特殊场景。 成本之间的平衡。
抽象的方式有无数种。 哪一个更好取决于业务如何变化。 不要太担心完美的抽象。 连Unix中一切皆文件这种最基本的抽象都存在争议,业务抽象的稳定性肯定会更差。 需要随着需求的变化进行调整。
总结
我们从计算机和操作系统的架构设计出发,讨论了前端架构设计的必要性,并从分层和抽象的角度分析了架构设计的注意事项。 我们希望当你在架构设计中遇到不确定的问题时,可以深入到计算机的架构设计中去寻求一些启发或支持。
讨论地址为:精读《前端架构的理解——分层与抽象》·Issue #436·dt-fe/weekly
Node 社群
我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。
如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:
1. 点个「在看」,让更多人也能看到这篇文章2. 订阅官方博客 www.inode.club 让我们一起成长
点赞和在看就是最大的支持❤️