前端架构的前世今生

架构是如何产生的?

刚开始时,前端是没有架构的。为什么会这么说呢?

因为 js 在刚开始诞生时,它的目的是为了让用户在 页面上的交互 会更加地流畅,仅仅只是作为一个工具来使用。通常情况下,那个时候的前端代码会内嵌到 后端的应用 中来使用。

随着时间的推移和前端的发展,在一个网页当中,js 的代码会越来越多,交互也变得越来越复杂。于是有了一些后端的架构,比如说:后端MVC框架。

MVC架构

所谓 MVC 架构,即将前端的渲染体系,从后端的服务体系中拆解出去。后端 MVC 架构具有以下特点:

  • 将视图层、数据层、控制层做分离,后端的服务主要在数据层和控制层
  • 缺点:重度依赖开发环境,代码混淆严重,复杂度比较高

前后端分离架构

所谓前后端分离架构,即将前端代码从后端环境中提炼出来( ajax 促进了 前后端分离架构 的发展),也就是我们所说的 多页面架构 。下面我们用一张图来了解前后端分离架构的特点:

前后端分离架构
前后端分离架构

即使出现了前后端分离架构,但它还是存在一定的缺点。比如:前端缺乏 独立部署能力 ,整体流程依赖后端环境。

针对存在的这些问题,后来也就有了 Nodejs 。

Nodejs

Nodejs 的广泛适应促进了前端技术的飞速发展,因为 Node 的出现,各种 打包构建工具 应运而生。同时,也诞生了 多元化 的前端开发方式,使得前端开发可以 脱离整体后端环境

Nodejs 的出现,使得前端产生了新的架构体系,即 单页面架构

单页面架构

(1) 现有单页面架构

市面上流行的单页面架构有以下几种类型:

  • 打包:gulp、rollup、webpack和vite……
  • 框架:vue/react/angular/……
  • ui库:antd/iview/elementui/mintui……

(2) 单页面架构的优势

  • 切换页面无刷新浏览器,用户体验号
  • 组件化开发方式,极大提升了代码复用率

(3) 单页面架构的劣势

单页面架构有一定的优势,但同时也存在一定的不足。具体如下:

  • 单页面架构所有的内容都是由 js 来渲染的,因此会不利于 SEO ,也不利于跨端搜索,因为它只有一个简单的 html 文件。
  • 且由于所有的渲染都是由 js 来控制的,因此首次渲染会出现较长时间的白屏。但比较好的一点是,这两个存在的问题都可以利用其他技术手段来解决。

说到这里,细心的同学可能会发现一个问题,所有的前端内容都还只停留在 浏览器阶段 ,只是跟用户在做一些简单的交互。那有没有可能有一种情况,我们的 js 也可以写一些服务端的逻辑呢?比如说:连接数据库、做一些增删改查的操作或者一些运维的工作。基于这样的背景,大前端时代到来了。

大前端时代

大前端时代的到来,为开发者们带来了一些服务端的框架。比如:

  • 后端框架:express、koa
  • 包管理工具:npm、yarn
  • node版本管理:nvm

综上,大前端时代给我们提供了一定的便利,但同时也带来了一定的弊端。比如:

  • 所有的代码基本都可以通过应用拆分来实现细颗粒度,但是呢,这样细颗粒度的拆分也会导致一些维护上的困难,即过于灵活的实现也导致了前端应用拆分过多,维护困难。
  • 往往一个功能或需求会跨两三个项目来进行开发,这样会增加很大的开发负担。

基于以上弊端的存在,现如今衍生出许多新型的架构,比如微前端等。

微前端架构

微前端等新型架构有以下特点:

  • 跟技术栈无关,可以使用vuereactangularhtml原生等技术栈;
  • 主框架不限制所接入应用的技术栈,微应用具备完全自主权;
  • 可以进行独立开发和独立部署。

微前端架构也是一样的。它具有以下优点:

  • 增量升级 - 不论是上线、发布还是开发,都可以使用增量的方式来升级;
  • 微前端是一种非常好的实施渐进式重构的手段和策略;
  • 所谓渐进式重构,即不断地对既有代码进行抽象、分离和组合。有时候在我们的大型应用里,我们需要把一部分页面通过重构的方式来进行重写或优化,以让代码后期可复用、无副作用和逻辑更单一,而这所使用的方式就是渐进式重构;
  • 微应用仓库独立,前后端可独立开发,主框架自动完成同步更新;
  • 微前端的每一个子应用,都是可以独立运行的;
  • 每个微应用之间的状态是隔离的,运行时状态不共享。

微前端架构劣势如下:

  • 接入难度高 - 微前端的整体理念跟我们以往的开发方式还略有不同,初步理解时会稍有困难,因此会有较长一段时间的接受程度。
  • 应用场景 - 在移动端较少、而在 PC 端 和 后台管理端 应用较多。

软件设计原则与分层

软件设计原则

(1)单一职责原则

概念: 永远不应该有多于一个原因来改变某个类。

理解: 对于一个类而言,应该仅有一个引起它变化的原因。

应用: 如果一个类拥有了两种职责,那就可以将这个类分为两个类。

相信看上面的概念可能很多小伙伴会觉得很枯燥,接下来我们用一个例子来进行阐述。具体如下:

  • 比如说我们现在在做 登录页面 ,这个页面会有 两个验证功能 。第一个是验证用户的用户名,第二个是验证用户的密码。那假设说我们把这两个功能放在同一个方法里,这不仅仅会不利于查找错误的原因,而且在一个方法里面,我们会写很多的逻辑判断来验证我们当前所验证的字段是用户名还是密码。
  • 这样做,不仅仅会加重我们开发的负担,而且在我们后期的开发过程中,维护起来也是比较困难的。因此在这个时候,我们就可以将验证用户名和验证密码这两个功能,拆分成两个方法去进行实现。

(2)开放封闭原则

概念: 软件实体扩展应该是开放的,但对于修改应该是封闭的。

理解: 在我们整体的开发过程中,封装的类或者方法,应该是便于扩展而不便于修改的。也就是说:“可以去扩展某个类或者某个方法,但是尽量不要去对它进行修改”。

应用:当需求有改动,尽量用继承或组合的方式来扩展类的功能,而不是直接修改类的代码。

同样我们用一个例子来阐述开放封闭原则,具体如下:

  • 在上面验证用户名和验证密码功能的基础上,我们继续添加一个核对图片验证码的功能。在输入了用户名和密码之后,需要让用户再输入验证码,只有验证码输入正确后,才可以进行正常登录。
  • 那这个时候可能有的小伙伴会想去修改验证密码这个函数的功能,那么我们不仅仅要付出大量的开发时间,还有多出很多的测试内容要去测试。同时,上线了之后,我们也不能保证是否会对验证密码这个功能造成影响。所以这样做,后期的维护成本是相当大的。
  • 因此,我们应该先去封装一个新的函数,来验证图片验证码的功能。这样处理,即使是线上环境出了问题,我们也可以更好地追溯到出现问题的源头。

(3)里氏替换原则

概念: 父类在使用的过程中,一定是能够被子类替换的。

理解:

  • 那么也就是说,我们在使用父类方法的时候,如果说将父类的引用化为子类的引用,那么对于整体的程序设计来说,是没有任何影响的。里氏替换可能在我们适应面向对象的时候,会不断的注意到。而对于现在大部分在使用函数式编程时,对里氏替换原则的关注度,可能就没有那么高了。
  • 对于架构设计而言,里氏替换原则是一个比较重要的原则,我们需要对一些整体的程序做一些可替换的操作。

(4)最少知识原则

概念: 只与你最直接的对象交流(即与你直接关联的对象)。

理解: 低耦合,高内聚。

应用: 做系统设计时,尽量减少依赖关系。

(5)接口隔离原则

概念: 一个类与另一个类之间的依赖性,应该依赖于尽可能小的接口。

理解: 不要对外暴露没有实际意义的接口,用户不应该依赖它不需要的接口。

应用: 当需要对外暴露接口时,如果是非必要对外提供,尽量删除。

(6)依赖导致原则

概念:高层模块不应该依赖于低层模块,他们应该依赖于抽象。抽象不应该依赖于细节,而细节应该依赖于抽象。

理解:应该面向接口编程,不应该面向实现类编程。

注意点:并不是说,所有的类都要有一个对应的接口,而是说,如果有接口,那就尽量使用接口来编程。

(7) 总结

将以上六大原则的英文首字母拼在一起就是SOLID(稳定的),所以也称之为SOLID原则。

只有满足了这六大原则之后,才能设计出稳定的软件架构!

补充设计原则

组合/聚合复用原则

  • 当要扩展类的功能时,优先考虑使用组合,而不是继承。
  • 该原则在 23 种经典设计模式中频繁使用。
  • 如:代理模式、装饰模式、适配器模式等等。

无依赖原则

  • A模块依赖于B模块,B模块依赖于C模块,C模块依赖于A模块,此时将出现循环依赖。
  • 在设计中避免该问题,可通过引入 “中介者模式” 解决。

共同封装原则

  • 应该将易变的类放在同一个包里,将变化隔离出来。
  • 该原则是 “开放-封闭原则” 的延伸。

共同重用原则

  • 如果我们重用了某个包中的一个类,那么这个包又还需要依赖于它的一个父类。
  • 这样就相当于重用了包中的所有类,因此我们要尽可能减少包的大小。

好莱坞原则

  • 秉承Don't call me, I‘ll call you.的原则;
  • 控制反转(或称为“依赖注入”);
  • 不需要主动创建对象,而是由容器帮我们来创建并管理这些对象。

其他设计原则

  • 不要重复你自己 —— 不要让重复的代码到处都是,要让它们足够的重用,所以要尽可能地封装。
  • 保持它简单与傻瓜 —— 保持系统界面简洁,功能实用,操作方便。
  • 高内聚与低耦合 —— 模块内部需要做到内聚度高,模块之间需要做到耦合度低。
  • 关注点分离 —— ①将一个复杂的问题分离为多个简单的问题,然后逐个解决。②难点:如何进行分离。
  • 你不需要它 —— ①不要一开始就把问题设计的非常复杂,不要陷入“过渡设计”的深渊;②让系统足够简单,而又不失去扩展性。

软件分层设计

软件的设计分层可以分为4个部分,分别是: 系统级架构应用级架构代码级架构和模块级别架构 。具体如下图所示:

软件设计分层
软件设计分层

系统级架构

  • 应用在整个系统内,如与后台服务如何通信,与第三方系统如何集成。
  • 设计前端首要条件:了解前端系统与其他系统之间的关系。其中关系包括:业务关系和协同机制。
  • 设计后端首要条件:只需要规定与后台数据传递的机制。
  • 包括:api 设计规则,访问授权的一个开放标准(OAuth)跳转 token 的验证,数据传递 cookie 等。
  • 前端与后端的关系考虑的主要因素是:前后端分离的架构设计。
  • 前后端分离架构其实是如何实施技术决策,包括用户鉴权、API接口管理和设计、API文档管理、Mock的使用、BFF(服务于前端的后端,nodejs),以及是否需要服务端渲染等。

微前端

  • 在一个系统内微前端是应用间的架构方案。
  • 在多个应用之间,微前端则是一种系统间等架构方案。
  • 微前端是将多个前端应用以某种形式结合在一起进行应用。
  • 旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(FrontEnd Monolith)后,随之而来的是应用不可维护的问题。
  • 单实例:即同一时刻,只有一个子应用被展示,子应用具备一个完整的应用生命周期。
  • 多实例:通常基于 url 的变化来做子应用的切换,值得注意的是,同一时刻可展示多个子应用。
  • 通常使用 Web Components 方案来做子应用封装,子应用更像是一个业务组件而不是应用。

应用级架构

  • 应用级架构可以看作是系统级别架构的细化。
  • 单个应用与其他外部应用的关系,微服务架构下多个应用的协作、数据交换等。
  • 应用级架构举例:脚手架、模式库和设计系统

模块级架构

这部分内容是我们开始业务编码之前进行设计,我们称之为迭代。

代码级架构

主要谈论:规范与原则。

上面我们谈到软件的 4 种设计分层,同时对于这 4 种设计分层来说,它的难度是 逐级递减 的。
在开发中,要注意 可维护性可扩展性
简单的代码可维护性高;越是写得抽象的代码越难维护。

如何保证架构的质量

系统的稳定性和健壮性

稳定性

定义:当一个实际的系统处于一个平衡的状态时,如果受到外来作用的影响时,系统经过一个过渡过程仍然能够回到原来的平衡状态,我们称这个系统就是稳定的,否则称系统不稳定。

特点:

  • 架构设计的基石
  • 可以更好地实现自我修复

健壮性

定义:计算机软件在输入错误、磁盘故障、网络过载或有意攻击情况下,能否不死机、不崩溃,就是该软件健壮性的具体表现。

解释:一个系统容错能力强,运行不易被干扰,安全性好。

度量标准:

  • 一个软件可以从错误的输入中推断出正确合理的输入;
  • 一个软件可以正确的运行在不同环境下;
  • 一个软件能够检测到自己的内部设计或者编码错误,并得到正确的结果。

总结

  • 健壮性和稳定性是特定软件自身的要求;
  • 健壮性和稳定性是软件处理的一部分;
  • 软件架构的健壮性和稳定性是该软件规划时所确定的目标;
  • 若软件的实现未能达到原定的目标,则该软件的健壮性和稳定性不够或不好。

架构质量的衡量

对于日常开发过程中的软件架构来说,总会有好的架构和不好的架构设计。那对于架构质量来说,有以下几点衡量指标:

  • 理解难度
  • 崩溃率和错误率的指标
  • 接入依赖的成本
  • 开发效率
  • 错误上报和信息收集等功能

架构尹始

架构前期准备

都说正确的选择是良好架构的开端,也就是说,如果前期我们把所有需要准备的内容准备好,那么在做架构设计时,也将会让架构设计更加的得心应手。下面我们就来谈论几点架构前期需要准备的内容。

架构师分类

系统架构师👇:

  • 从系统的维度出发,负责整体系统的架构设计;
  • 主要是基础服务和各系统间协调,着眼全局;
  • 比如关注负载、可靠性、伸缩、扩展、整体项目切分、缓存应用等方面的基础架构设计。

应用架构师👇:

  • 从应用程序的维度出发,负责某个应用的技术架构,主要偏业务系统。
  • 关注和理解业务,梳理模型、设计模式、接口和数据交互等方面。

业务架构师👇:

  • 从业务流程的维度出发,对某一个行业、业务的领域做相应的分析,获取领域模型,最终获得系统的模型。
  • 也可以称为是领域专家、行业专家、产品咨询师和资深顾问。

技术前期贮备

  • 技术选型: 社区氛围、发展规模、未来发展趋势、与当前团队的契合度、执行成本、维护和迁移成本、执行效率等内容的调研和报告。想
  • 充分调研每一项技术可能带来的利与弊。
  • 最大程度上预测架构设计中的缺陷,以防重大问题的发生。

技术优化

在架构发展过程中,可能会存在一些有悖于当前架构设计的实现,造成了架构发展阻塞,所以需要进行架构优化,使得架构设计的适应性更高。

架构优化

  • 架构不是一蹴而就的,即不是一成不变的。在业务发展的过程中,架构也在不断地演进。
  • 因此要对架构设计进行实时调优,使架构优化成为常态化。
  • 通过不断的调整架构实现,改进初始架构中设计的不足来补足短板。

技术填补

编码方案问题

在开发中,有时候会因为开发时间紧急的原因,让我们写的代码有些粗糙。如果每次都不让自己去追求完美,而是一味的妥协现有不好用的编码方案。久而久之,很容易使得整个软件架构零散成一地,像是东拼西凑的,没有任何支撑点可言。常见问题的存在形式有以下几大方面:

  • 开发过程中因为时间紧迫导致的实现不合理 —— 比如:查找10000以内的质数,利用循环或筛选法等方式来进行实现。
  • 暂时没有想到更好的实现方式而妥协的版本 —— 刚开始使用 if……else 实现,慢慢地演变成责任链的模式。
  • 架构设计前期没有考虑到的细节 —— ①交互细节→ props 传递参数(交互冗余,流程较长);②使用全局状态管理实现参数传递。
  • 不合理的交互设计,导致技术实现复杂。
  • 旧功能文档缺失,无正确拓展、修改和兼容旧功能,导致上线后问题剧增。

技术填补会引发的后果

在上面中我们举例了各种软件编码过程中修修补补的方案,那这样不断的技术填补后,会导致什么样的后果么?具体表现有:

  • 修复变重构 —— 小的技术债务不做偿还,最后会演变成一场大规模的重构工作,导致产出不高;
  • 影响开发速度 —— 技术债务的存在导致整体开发需要减容的点过多,影响开发效率。极大影响上线速度,导致整体项目迭代缓慢,失去核心竞争力;
  • 开发死循环 —— 容易陷入维护旧功能→开发新功能→兼容旧功能→维护旧功能→开发新功能……这样的恶性循环。

技术填补解决方案

上面说到了技术填补会引发的后果,那后果产生了,自然也就需要有对应的解决方案来弥补。

具体解决方案有以下几种:

  • 优秀的架构设计是基础 —— ①必须能够有效处理当前需求中可预见的情况,而对于未知的、可能出现的特殊情况,一般很小的改动就能解决问题;②根据当前的业务,进行合理的项目拆分,尽量地进行代码解耦,减少高耦合度的情况发生;③必须有日志模块,操作日志,错误日志,业务日志等等。
    良好的技术培训和传帮带能力 —— ①让每一位开发者可以从更深一层次理解自己所需要实现的功能;②从最开始的代码规范、到熟悉业务、最后再到编写文档。(传帮带)
  • 充分的技术方案可避免一部分技术债务的产生 —— 技术方案是充分理解需求之后所能产出的对需求理想的实现方式,必要性不言而喻。
  • 不同工程师之间可以相互review —— CodeReview 是非常重要的,同时也是对自身的一个提高。
  • 提升对修复技术债务重要性的认知 —— 工程师如果能预见一个债务可能导致的问题,自然愿意花时间去处理。
  • 善于发现和定期处理一些技术债务 —— 勇于发现系统中的技术债务,让自己为系统负责。

总结来说就是:

等产品上线后,开发就没有那么紧张了,这个时候就可以找个时间来处理下遗留下来的技术债务,在解决技术债务中不断去突破自己的上限,更好地提高自己的技术能力和软件架构设计的能力。

奔溃预防

架构奔溃是严重的架构设计事故,也是我们需要预防的关键所在。在软件奔溃的时候,首先要分析原因:①系统奔溃的产生;②日志记录,如:操作日志,错误日志,业务日志等。

分析完成原因之后,接下来要讲的是,如何预防架构奔溃。主要有以下几点:

  • 用户行为抓取→争取在最新时间获取到用户操作链条
  • 解决存量问题→技术债务
  • 遏制新增→减少新增问题的概率
  • 对脏数据进行兜底和校验
  • 单元测试
  • 奔溃预警
  • 自动化测试
  • 更广的灰度触达
  • 性能优化体系
  • ……

系统重构

架构并不是永恒不变的,架构也是具有生命周期的,也会经历初生、发展、巅峰、衰弱、消亡的过程。那下面就来了解软件重构的相关内容。

基础概念

什么是重构:

  • 对软件内部结构的一种调整。
  • 目的是在不改变可观察行为的前提下,提高其可理解性,降低其修改成本。

实现方式:

  • 使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
    重构理念:

  • 运用大量微小且保持软件行为的步骤,一步步达成大规模的修改。

早期和晚期的系统

早期系统特点:

  • 开发速度快
  • 代码复杂度低
  • 代码规范都保持完好
  • 严格注重开发规范,不会允许危及架构设计的代码产生
  • 以上因素导致添加功能难度低,成本低

晚期系统特点:

  • 具备所有早期系统的劣势
  • 代码复杂度高
  • 代码规范不完善
  • 很多需求或功能会出现为了愉悦架构设计的情况
  • 添加新功能兼顾较多,涉及较多模块,牵一发而动全身

什么时候需要重构

  • 当发现现有系统体系已经不能满足当前迭代速度的时候,就需要进行重构工作。
  • 对【坏味道】的代码通过一些重构手段进行微重构。

重构流程

重构一般具有以下流程:

  • 确定问题点,确定重构功能和范围;
  • 根据旧架构中已有的设计,梳理重构后软件应有的逻辑走向;
  • 确保重构后的软件具有一定的稳定性和性能上的优化;
  • 解决此前在需求过程中存在的冲突问题。