管道和过滤器架构风格:从 Unix Shell 命令到 ReactiveX 的应用

2024-11-14
来源:网络整理

管道和过滤器

在管道/过滤器架构风格中,每个组件都有一组输入和输出。每个组件都有单一的职责。数据输入到组件中,在内部进行处理,然后输出处理后的数据。所以这些组件也称为过滤器。连接器根据业务需求连接组件,其形状像“管道”,因此这种架构风格的名称。

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

# 获取网页,并进行拼写检查。代码来源于wiki
curl "http://en.wikipedia.org/wiki/Pipeline_(Unix)" | \
sed 's/[^a-zA-Z ]/ /g' | \
tr 'A-Z ' 'a-z\n' | \
grep '[a-z]' | \
sort -u | \
comm -23 - /usr/share/dict/words | \
less

另一个类似于 Unix 管道的例子是 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的影响,但现代打包工具也使用相同的模型来处理文件,即用于转换模块的源代码。通过组合,可以实现复杂的文件翻译需求。

// webpack.config.jsmodule.exports = {
...
module: {
rules: [{
test: /\.scss$/,
use: [{
loader: "style-loader"// 将 JS 字符串生成为 style 节点
}, {
loader: "css-loader"// 将 CSS 转化成 CommonJS 模块
}, {
loader: "sass-loader"// 将 Sass 编译成 CSS
}]
}]
}
};

中间件()

如果你开发过Koa或者Koa,你可能会发现中间件模式和上面提到的管道模式有一定的相似之处,如上图所示。与管道相比,中间件模式可以使用洋葱配置文件来描述。但与管道相比,通用中间件实现具有以下特点:

我在上搜索了很长时间的中间件,但无法得到满意的中间件定义。让我们将其视为管道模式的一种特殊形式。这种模式通常用在后端,以清晰地分离请求的不同阶段,即单独的关注点。例如我们可以创建这些中间件:

使用中间件,我们不需要在每个响应处理方法中都包含这些逻辑,而专注于我们应该做什么。下面是 Koa 的示例代码:

const Koa = require('koa');
const app = new Koa();

// logger

app.use(async (ctx, next) => {
await next();
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method}${ctx.url} - ${rt}`);
});

// x-response-time

app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});

// response

app.use(async ctx => {
ctx.body = 'Hello World';
});

app.listen(3000);

事件驱动

事件驱动,或者说发布-订阅风格,是前端开发中一个熟悉的概念。它定义了一对多的依赖关系。在事件驱动系统风格中,组件不直接调用另一个组件。 ,但触发或广播一个或多个事件。系统中的其他组件注册一个或多个事件。当一个事件被触发时,系统会自动通知所有在该事件中注册的组件。

这将关注点分开。订阅者依赖于事件而不是发布者,发布者不需要关心订阅者。两者是脱钩的。

生活中存在很多发布订阅的例子,比如微信公众号信息订阅。当添加新的订阅者时,发布者无需进行任何调整。同样,当发布者调整时,也不会影响订阅者。只要协议不发生任何变化。我们可以发现,发布者和订阅者之间的动态关系实际上是弱化的。

脱钩的目的是一方面,但另一方面也可能是基因决定的。有些东西自然不适合或者不支持同步方式调用,或者这些行为是异步触发的。

这个基因决定了事件驱动模型在前端领域的广泛使用。它在浏览器和 Node 中如何工作?直观的讲解简单介绍了执行原理 在应用场景中,一个线程根本就忙不过来,事件驱动的异步方法就是一根救命稻草。

就浏览器而言,浏览器是一个GUI程序。 GUI程序就是一个循环(更专业的名字是事件循环)。它接收用户输入,程序对其进行处理然后反馈给页面,然后接收用户输入……用户的输入是异步的。将用户输入抽象为事件是最简洁、自然、灵活的方式。

需要注意的是,事件驱动和异步不能等同。异步!==事件驱动,事件驱动!==异步

扩大:

MV*

MV* 架构风格也被广泛使用。我认为MV*本质上是一个分层架构,也强调职责分离。最经典的就是MVC架构风格。此外,还有各种派生样式,如MVP、MVVM、MVI(View)。还有一些相关的 Flux 或 。

著名的MVC

顾名思义,MVC 将应用程序分为三层,即:

目前,很少有前端应用程序纯粹使用MVC。要么视图层和控制器层混在一起,要么模型和控制器混在一起,要么根本就没有所谓的控制器。但有一点是肯定的,很多应用程序总是分离的。 “逻辑层”和“视图层”。

以下是典型代码,视图层:

Todo</h2>{{todoList.remaining()}} of {{todoList.todos.length}} remainingspan>
[ <ahref=""ng-click="todoList.archive()">archivea> ]
"unstyled"
><ling-repeat="todo in todoList.todos"><labelclass="checkbox"><inputtype="checkbox"ng-model="todo.done"><spanclass="done-{{todo.done}}">{{todo.text}}span>label>li>ul><formng-submit="todoList.addTodo()"><inputtype="text"ng-model="todoList.todoText"size="30"placeholder="add new todo here"><inputclass="btn-primary"type="submit"value="add">form>div>

逻辑层:

angular.module('todoApp', [])
.controller('TodoListController', function() {
var todoList = this;
todoList.todos = [
{text:'learn AngularJS', done:true},
{text:'build an AngularJS app', done:false}];

todoList.addTodo = function() {
todoList.todos.push({text:todoList.todoText, done:false});
todoList.todoText = '';
};

todoList.remaining = function() {
var count = 0;
angular.forEach(todoList.todos, function(todo) {
count += todo.done ? 0 : 1;
});
return count;
};

todoList.archive = function() {
var oldTodos = todoList.todos;
todoList.todos = [];
angular.forEach(oldTodos, function(todo) {
if (!todo.done) todoList.todos.push(todo);
});
};
});

至于MVP、MVVM,以及这些MVC模型的扩展或者升级,网上有很多资源,这里不再赘述。

它是 Flux 架构的改进,融合了 Elm 语言中的函数式思想。下面是架构图:

从上图可以看出,该架构有以下几个关键点:

单一数据源首先解决了传统MVC架构中多模型数据流混乱的问题(如下图)。单一数据源使应用程序状态可预测和可调试。此外,单一数据源也方便实现数据镜像、undo/redo、数据持久化等功能。

单向数据流用于辅助单一数据源。主要目的是防止应用程序代码直接修改数据源。这不仅简化了数据流,而且使应用程序状态变化可预测。

以上两个特征是该建筑风格的核心。至于强调不可变数据、使用中间件封装副作用、规范化状态树,只是一种最佳实践。框架也有很多类型,比如Vuex、ngrx,它们在架构思想上是一致的:

复制风格

基于()风格的系统将使用多个实例来提供相同的服务,以提高服务的可访问性、可扩展性和性能。这种架构风格可以改善用户感知的性能和简单服务响应的延迟。

这种风格在后台经常使用。举一个熟悉的前端例子,.是单线程的。为了利用多核资源,标准库提供了一个可以根据CPU数量创建多个进程的模块。这些流程可以共享。服务器端口向外界提供同质服务,进程会将资源分配给:

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);

// Fork workers.for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}

cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`);
});
} else {
// Workers可以共享任意的TCP连接 // 比如共享HTTP服务器
http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world\n');
}).listen(8000);

console.log(`Worker ${process.pid} started`);
}

利用多核功能可以提高应用程序性能和可靠性。我们还可以使用PM2等进程管理工具来简化Node集群的管理。它支持许多有用的功能,例如集群节点重启、日志收集、性能监控等。

复制方式通常用于 Web 服务器。浏览器和 Node 都有相同的概念,但一般建议仅在 CPU 密集型场景中使用它们,因为浏览器或内置的异步操作已经非常高效。事实上,前端应用的CPU密集型场景并不多,或者说现阶段并不是特别实用。此外,你还必须权衡进程间通信的效率、管理复杂度、异常处理等。

有一个典型的CPU密集型场景,就是源文件翻译。一个典型的例子是它利用浏览器机制来提高源文件的翻译性能:

除了处理CPU密集型任务外,它也是浏览器的重要安全机制,用于隔离不安全代码的执行或限制对浏览器DOM相关事物的访问。小程序分离逻辑进程的原因之一是安全

其他例子:

微内核架构

微内核架构()也称为“插件架构”,是指软件的核心比较小,主要功能和业务逻辑都以插件的形式实现。内核只包含系统运行所需的最少功能。插件之间是相互独立的,应尽量减少插件之间的通信,以减少相互依赖。

微内核结构的难点在于建立一套适当粒度的插件协议,并实现插件之间适当的隔离和解耦。这保证了良好的可扩展性、灵活性和可移植性。

前端领域的典型例子有:

举个例子。核心是一个。这个主要功能是集成插件系统和维护模块对象图。模块代码的具体编译、模块打包、优化、分析、聚合都是基于外部插件完成的。

前面提到,使用管道模式来翻译源文件;那么该行为就可以被注入到整个运行生命周期的钩子中,并完全访问当前状态。

肖恩有一句话:是一个!从外面

这里还有一篇文章专门写了前端微内核架构模型的一些应用。建议阅读。

微前端

前几天,我在Code Time上听了左耳鼠标的一个节目。他介绍说,亚马逊内部有很多小团队。亚马逊网站上一块豆腐块大小的区域可能需要一个团队来维护,比如地址选择器、购物车、配送时间计算……这样一个大工厂的超级项目是如何协调和维护的?这可能是微前端或微服务出现的原因。

微前端旨在将整体前端分解为更小、更简单的模块,这些模块可以由独立团队开发、测试和部署,最终组合成一个大的整体。

微前端下的各个应用模块独立运行、独立开发、独立部署。相应的,它会配备一个更加自主的团队(一个团队可以做好一件事)。微前端的实现还需要扎实的前端基础设施和研发体系的支持。

如果想了解更多微前端架构,建议阅读相关文章,还有他的新书《前端架构:从入门到微前端》

组件架构

如今,组件开发对我们来说就像水之于鱼一样自然。以至于我们忘记了组件化也是一个非常重要的架构思想,它的中心思想就是分而治之。根据Wiki上面的定义:组件化是基于可复用性的目的,以关注点分离的形式将一个大型软件系统拆分成多个独立的组件。主要目的是减少耦合。

具体从前端角度来看,如下图,石器时代(右侧)和组件时代(左侧)的开发方式:

(图片来源:……)据Vue官网介绍:组件系统是Vue的另一个重要概念,因为它是一种抽象,允许我们使用小型、独立且经常可重用的组件来构建大型应用程序。如果仔细思考一下,几乎任何类型的应用程序接口都可以抽象为组件树:

根据我的理解,组件和函数是同一个东西,这就是函数式编程思想如此自然地应用在.几个简单的功能可以组合成复杂的功能,然后复杂的功能可以组合成复杂的应用程序。对于前端来说,页面也是如此。复杂的页面是由不同粒度的组件组成的。

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

其他

我终于无法弥补了!架构风格有很多种,受限于文章篇幅,而且这些风格主要用在后端领域,这里就不一一解释了。您可以通过进一步阅读来了解这些模式

通过以上内容,你可能会觉得架构风格比设计模式或算法更容易理解。俗话说‘最大的简单’,但是‘简单却不简单’!大多数项目的架构一开始并不是这样的。他们或许经历了长期的迭代,踩在巨人的肩膀上,才成为今天的样子。

希望这篇文章能给你一些启发。对于我们前端工程师来说,不应该只追求能做出多少炫酷的页面,能掌握多少API。我们要学会透过现象看本质,举一反三,综合全面。这就是前进之道。

分享