首页 >>

程序员透过现象看本质:常见的前端架构风格和案例

2019-12-13 11:12:18 点击:551 德甲中餐厅收官赵薇 复古蜂窝头

所谓软件架构风格,是指描述某个特定应用领域中系统组织方式的惯用模式。架构风格定义一个词汇表和一组约束,词汇表中包含一些组件及连接器,约束则指出系统如何将构建和连接器组合起来。软件架构风格反映了领域中众多系统所共有的结构和语义特性,并指导如何将系统中的各个模块和子系统有机的结合为一个完整的系统

没多少人能记住上面的定义,需要注意的是本文不是专业讨论系统架构的文章,笔者也还没到那个水平. 所以暂时没必要纠结于什么是架构模式、什么是架构风格。在这里尚且把它们都当成一个系统架构上的套路, 所谓的套路就是一些通用的、可复用的,用于应对某类问题的方式方法. 可以理解为类似“设计模式”的东西,只是解决问题的层次不一样。

透过现象看本质,本文将带你领略前端领域一些流行技术栈背后的架构思想。直接进入正题吧

文章大纲分层风格Virtual DOMTaro管道和过滤器中间件(Middleware)事件驱动MV*家喻户晓的MVCRedux复制风格微内核架构微前端组件化架构其他扩展阅读

分层风格

没有什么问题是分层解决不了,如果解决不了, 就再加一层 —— 鲁迅不不,原话是: Any problem in computer science can be solved by anther layer of indirection.

分层架构是最常见的软件架构,你要不知道用什么架构,或者不知道怎么解决问题,那就尝试加多一层。

一个分层系统是按照层次来组织的,每一层为在其之上的层提供服务,并且使用在其之下的层所提供的服务. 分层通常可以解决什么问题?是隔离业务复杂度与技术复杂度的利器. 典型的例子是网络协议, 越高层越面向人类,越底层越面向机器。一层一层往上,很多技术的细节都被隐藏了,比如我们使用HTTP时,不需要考虑TCP层的握手和包传输细节,TCP层不需要关心IP层的寻址和路由。

分离关注点和复用。减少跨越多层的耦合, 当一层变动时不会影响到其他层。例如我们前端项目建议拆分逻辑层和视图层,一方面可以降低逻辑和视图之间的耦合,当视图层元素变动时可以尽量减少对逻辑层的影响;另外一个好处是, 当逻辑抽取出去后,可以被不同平台的视图复用。

关注点分离之后,软件的结构会变得容易理解和开发, 每一层可以被复用, 容易被测试, 其他层的接口通过模拟解决. 但是分层架构,也不是全是优点,分层的抽象可能会丢失部分效率和灵活性, 比如编程语言就有'层次'(此例可能不太严谨),语言抽象的层次越高,一般运行效率可能会有所衰减:

分层架构在软件领域的案例实在太多太多了,咱讲讲前端的一些'分层'案例:

Virtual DOM

前端石器时代,我们页面交互和渲染,是通过服务端渲染或者直接操作DOM实现的, 有点像C/C++这类系统编程语言手动操纵内存. 那时候JQuery很火:

后来随着软硬件性能越来越好、Web应用也越来越复杂,前端开发者的生产力也要跟上,类似JQuery这种命令式的编程方式无疑是比较低效的. 尽管手动操作 DOM 可能可以达到更高的性能和灵活性,但是这样对大部分开发者来说太低效了,我们是可以接受牺牲一点性能换取更高的开发效率的.

怎么解决,再加一层吧,后来React就搞了一层VirtualDOM。我们可以声明式、组合式地构建一颗对象树, 然后交由React将它映射到DOM:

所以说 VirtualDOM 更大的意义在于开发方式的转变: 声明式、 数据驱动, 让开发者不需要关心 DOM 的操作细节(属性操作、事件绑定、DOM 节点变更),换句话说应用的开发方式变成了view=f(state), 这对生产力的解放是有很大推动作用的; 另外有了VirtualDOM这一层抽象层,使得多平台渲染成为可能。

当然VirtualDOM或者React,不是唯一,也不是第一个这样的解决方案。其他前端框架,例如Vue、Angular基本都是这样一个发展历程。

上面说了,分层不是银弹。我们通过ReactNative可以开发跨平台的移动应用,但是众所周知,它运行效率或者灵活性暂时是无法与原生应用比拟的。

Taro

Taro 和React一样也采用分层架构风格,只不过他们解决的问题是相反的。React加上一个分层,可以渲染到不同的视图形态;而Taro则是为了统一多样的视图形态: 国内现如今市面上端的形态多种多样,Web、React-Native、微信小程序…… 针对不同的端去编写多套代码的成本非常高,这种需求催生了Taro这类框架的诞生. 使用 Taro,我们可以只书写一套代码, 通过编译工具可以输出到不同的端:

一开始VirtualDOM和DOM的关系比较暧昧,两者是耦合在一起的。后面有人想,我们有了VirtualDOM这个抽象层,那应该能多搞点别的,比如渲染到移动端原生组件、PDF、Canvas、终端UI等等。

后来VirtualDOM进行了更彻底的分层,有着这个抽象层我们可以将VirtualDOM映射到更多类似应用场景:

所以说 VirtualDOM 更大的意义在于开发方式的转变: 声明式、 数据驱动, 让开发者不需要关心 DOM 的操作细节(属性操作、事件绑定、DOM 节点变更),换句话说应用的开发方式变成了view=f(state), 这对生产力的解放是有很大推动作用的; 另外有了VirtualDOM这一层抽象层,使得多平台渲染成为可能。

当然VirtualDOM或者React,不是唯一,也不是第一个这样的解决方案。其他前端框架,例如Vue、Angular基本都是这样一个发展历程。

上面说了,分层不是银弹。我们通过ReactNative可以开发跨平台的移动应用,但是众所周知,它运行效率或者灵活性暂时是无法与原生应用比拟的。

Taro

Taro 和React一样也采用分层架构风格,只不过他们解决的问题是相反的。React加上一个分层,可以渲染到不同的视图形态;而Taro则是为了统一多样的视图形态: 国内现如今市面上端的形态多种多样,Web、React-Native、微信小程序…… 针对不同的端去编写多套代码的成本非常高,这种需求催生了Taro这类框架的诞生. 使用 Taro,我们可以只书写一套代码, 通过编译工具可以输出到不同的端:

(图片来源: 多端统一开发框架 - Taro)

管道和过滤器

在管道/过滤器架构风格中,每个组件都有一组输入和输出,每个组件职责都很单一, 数据输入组件,经过内部处理,然后将处理过的数据输出。所以这些组件也称为过滤器,连接器按照业务需求将组件连接起来,其形状就像‘管道’一样,这种架构风格由此得名。

这里面最经典的案例是*unix Shell命令,Unix的哲学就是“只做一件事,把它做好”,所以我们常用的Unix命令功能都非常单一,但是Unix Shell还有一件法宝就是管道,通过管道我们可以将命令通过标准输入输出串联起来实现复杂的功能:

# 获取网页,并进行拼写检查。代码来源于wiki

curl "http://en.wikipedia.org/wiki/Pipeline_(Unix)" |

sed 's// /g' |

tr 'A-Z ' 'a-z

' |

grep '' |

sort -u |

comm -23 - /usr/share/dict/words |

less

另一个和Unix管道相似的例子是ReactiveX, 例如RxJS. 很多教程将Rx比喻成河流,这个河流的开头就是一个事件源,这个事件源按照一定的频率发布事件。Rx真正强大的其实是它的操作符,有了这些操作符,你可以对这条河流做一切可以做的事情,例如分流、节流、建大坝、转换、统计、合并、产生河流的河流……

这些操作符和Unix的命令一样,职责都很单一,只干好一件事情。但我们管道将它们组合起来的时候,就迸发了无限的能力.

import { fromEvent } from'rxjs';

import { throttleTime, map, scan } from'rxjs/operators';

fromEvent(document, 'click')

.pipe(

throttleTime(1000),

map(event => event.clientX),

scan((count, clientX) => count + clientX, 0)

)

.subscribe(count =>console.log(count));

除了上述的RxJS,管道模式在前端领域也有很多应用,主要集中在前端工程化领域。例如'老牌'的项目构建工具Gulp, Gulp使用管道化模式来处理各种文件类型,管道中的每一个步骤称为Transpiler(转译器), 它们以 NodeJS 的Stream 作为输入输出。整个过程高效而简单。

不确定是否受到Gulp的影响,现代的Webpack打包工具,也使用同样的模式来实现对文件的处理, 即Loader, Loader 用于对模块的源代码进行转换, 通过Loader的组合,可以实现复杂的文件转译需求.

// webpack.config.jsmodule.exports = {

...

module: {

rules:

}]

}

};

复制代码

中间件(Middleware)

如果开发过Express、Koa或者Redux, 你可能会发现中间件模式和上述的管道模式有一定的相似性,如上图。相比管道,中间件模式可以使用一个洋葱剖面来形容。但和管道相比,一般的中间件实现有以下特点:中间件没有显式的输入输出。这些中间件之间通常通过集中式的上下文对象来共享状态有一个循环的过程。管道中,数据处理完毕后交给下游了,后面就不管了。而中间件还有一个回归的过程,当下游处理完毕后会进行回溯,所以有机会干预下游的处理结果。

我在谷歌上搜了老半天中间件,对于中间件都没有得到一个令我满意的定义. 暂且把它当作一个特殊形式的管道模式吧。这种模式通常用于后端,它可以干净地分离出请求的不同阶段,也就是分离关注点。比如我们可以创建这些中间件:日志:记录开始时间 ⏸ 计算响应时间,输出请求日志认证:验证用户是否登录授权:验证用户是否有执行该操作的权限缓存:是否有缓存结果,有的话就直接返回 ⏸ 当下游响应完成后,再判断一下响应是否可以被缓存执行:执行实际的请求处理 ⏸ 响应

有了中间件之后,我们不需要在每个响应处理方法中都包含这些逻辑,关注好自己该做的事情。下面是Koa的示例代码:

const Koa = require('koa');

按照Vue官网的说法: 组件系统是 Vue 的另一个重要概念,因为它是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。仔细想想,几乎任意类型的应用界面都可以抽象为一个组件树:

按照我的理解组件跟函数是一样的东西,这就是为什么函数式编程思想在React中会应用的如此自然。若干个简单函数,可以复合成复杂的函数,复杂的函数再复合成复杂的应用。对于前端来说,页面也是这么来的,一个复杂的页面就是有不同粒度的组件复合而成的。

组件另外一个重要的特征就是内聚性,它是一个独立的单元,自包含了所有需要的资源。例如一个前端组件较包含样式、视图结构、组件逻辑:

其他

我终于编不下去了!还有很多架构风格,限于文章篇幅, 且这些风格主要应用于后端领域,这里就不一一阐述了。你可以通过扩展阅读了解这些模式面向对象风格: 将应用或系统任务分割为单独、可复用、可自给的对象,每个对象都包含数据、以及对象相关的行为C/S 客户端/服务器风格面向服务架构(SOA): 指那些利用契约和消息将功能暴露为服务、消费功能服务的应用N层/三层: 和分层架构差不多,侧重物理层. 例如C/S风格就是一个典型的N层架构点对点风格

通过上文,你估计会觉得架构风格比设计模式或者算法好理解多的,正所谓‘大道至简’,但是‘简洁而不简单’!大部分项目的架构不是一开始就是这样的,它们可能经过长期的迭代,踩着巨人的肩膀,一路走过来才成为今天的样子。

希望本文可以给你一点启发,对于我们前端工程师来说,不应该只追求能做多酷的页面、掌握多少API,要学会通过现象看本质,举一反三融会贯通,这才是进阶之道。

文章来源:向世界亮明中国态度

标签:全球最富有城市排行,中国建成首个志愿军烈士DNA数据库,泰晤士河螃蟹胃里塞满卫生巾塑料,李湘王岳伦为女庆生,大学生交白卷得满分