Web UI体系结构:过去的木本,奇异的现在和光明的未来

图片

现在,现代开发人员社区比以往任何时候都更受时尚和潮流的影响,对于前端开发世界而言尤其如此。 我们的框架和新实践是主要价值,大多数简历,职位空缺和会议计划都将其列出。 并且尽管思想和工具的开发本身并不是负面的,但是由于开发人员不断追随难以捉摸的趋势的愿望,我们开始忘记关于应用程序体系结构的一般理论知识的重要性。

在理论和最佳实践知识之上进行价值调整的普遍性导致了这样一个事实,即当今大多数新项目的维护水平极低,从而给开发人员(研究和修改代码的持续高复杂性)和客户(低利率和低维护成本)带来了极大的不便。开发成本高)。

为了至少以某种方式影响当前状况,今天我想告诉您一个好的架构,它如何适用于Web界面,最重要的是,它随着时间的推移会如何发展。

注意 :作为本文的示例,仅使用作者直接处理的那些框架,此处将对React和Redux给予极大的关注。 但是,尽管如此,这里描述的许多思想和原则本质上都是通用的,可以或多或少地成功地投影到其他接口开发技术上。

傻瓜建筑


首先,让我们处理术语本身。 简而言之,任何系统的体系结构都是其组件的定义以及它们之间的交互方案。 这是一种概念基础,以后将在其基础上构建实现。

体系结构的任务是满足设计系统的外部要求。 这些要求因项目而异,并且可能非常具体,但是在一般情况下,它们是为了简化已开发解决方案的修改和扩展过程。

至于架构的质量,通常用以下属性表示:

- 伴奏 :已经提到的系统易于研究和修改的倾向(检测和纠正错误,扩展功能,使解决方案适应其他环境或条件的难度)
- 可替换性 :在不影响其他要素的情况下更改系统任何要素的实现的能力
- 可测试性 :验证元素正确操作的能力(控制元素并观察其状态的能力)
- 可移植性 :在其他系统中重用元素的能力
- 可用性 :由最终用户操作时系统的总体便利程度

还单独提到了构建质量体系结构的关键原则之一: 关注点分离的原则。 它包含以下事实:系统的任何元素都应专门负责一项任务(顺便适用于应用程序代码:请参见“ 单一责任原则” )。

既然我们已经了解了架构的概念,那么让我们看看在界面的上下文中哪些架构设计模式可以为我们提供。

三个最重要的词


接口开发的最著名模式之一是MVC(模型-视图-控制器),其关键概念是将接口逻辑分为三个独立的部分:

1. 模型 -负责接收,存储和处理数据
2. 视图 -负责数据可视化
3. 控制器 -控制模型和视图

该模式还包括它们之间的交互方案的描述,但是由于在一定时间后向公众展示了该模式的改进版本,称为MVP(Model-View-Presenter),该模式是原始模式,因此此处将省略此信息。交互大大简化:

图片

由于我们专门讨论Web界面,因此我们使用了通常伴随这些模式实现的另一个相当重要的元素-路由器。 它的任务是读取URL并调用与之关联的演示者。

以上方案的工作原理如下:

1.路由器读取URL并调用关联的Presenter
2-5。 演示者转向模型并从中获取必要的数据。
6. Presenter将数据从“模型”传输到“视图”,以实现其可视化。
7.在用户与界面交互期间,View将此通知给Presenter,这使我们回到第二点

如实践所示,MVC和MVP并不是理想的通用体系结构,但它们仍然做一件非常重要的事情-它们指出了三个关键责任领域,没有它们,就不能以一种或另一种形式实现任何接口。

注意:总体上讲,Controller和Presenter的概念含义相同,并且名称的区别仅是为了区分所提及的模式,仅在通信实现方面有所区别。

MVC和服务器渲染


尽管MVC是实现客户端的一种模式,但它也可以在服务器上找到其应用程序。 而且,在服务器的上下文中,最容易证明其操作原理。

在我们处理经典信息站点的情况下,Web服务器的任务是为用户生成HTML页面,MVC还允许我们组织一个相当简洁的应用程序体系结构:

-路由器从收到的HTTP请求(GET /用户配置文件/ 1)中读取数据,并调用关联的控制器(UsersController.getProfilePage(1))
-控制器调用模型以从数据库中获取必要的信息(UsersModel.get(1))
-控制器将接收到的数据传递给View (View.render(“用户/个人资料”,用户)),并从中接收HTML标记,然后将其传递回客户端

在这种情况下,View通常按以下方式实现:

图片

const templates = { 'users/profile': ` <div class="user-profile"> <h2>{{ name}}</h2> <p>E-mail: {{ email }}</p> <p> Projects: {{#each projects}} <a href="/projects/{{id}}">{{name}}</a> {{/each}} </p> <a href=/user-profile/1/edit>Edit</a> </div> ` }; class View { render(templateName, data) { const htmlMarkup = TemplateEngine.render(templates[templateName], data); return htmlMarkup; } } 

注意:上面的代码被故意简化为示例。 在实际项目中,模板会导出到单独的文件中,并在使用前经过编译阶段(请参阅Handlebars.compile()_.template() )。

在这里,使用了所谓的模板引擎,它们为我们提供了方便地描述文本模板的工具以及在其中替换真实数据的机制。

这种实现View的方法不仅证明了理想的职责分离,而且还提供了高度的可测试性:要检查显示的正确性,对于我们而言,将参考线与从模板引擎获得的线进行比较就足够了。

因此,使用MVC,我们得到了几乎完美的体系结构,其中的每个元素都具有非常特定的用途,最小的连接性以及高度的可测试性和可移植性。

至于使用服务器工具生成HTML标记的方法,由于UX较低,因此这种方法逐渐开始被SPA取代。

骨干和MVP


将显示逻辑完全带给客户端的第一个框架之一是Backbone.js 。 其中的Router,Presenter和Model的实现是相当标准的,但是View的新实现值得我们注意:

图片

 const UserProfile = Backbone.View.extend({ tagName: 'div', className: 'user-profile', events: { 'click .button.edit': 'openEditDialog', }, openEditDialog: function(event) { // ... }, initialize: function() { this.listenTo(this.model, 'change', this.render); }, template: _.template(` <h2><%= name %></h2> <p>E-mail: <%= email %></p> <p> Projects: <% _.each(projects, project => { %> <a href="/projects/<%= project.id %>"><%= project.name %></a> <% }) %> </p> <button class="edit">Edit</button> `), render: function() { this.$el.html(this.template(this.model.attributes)); } }); 

显然,映射的实现变得更加复杂-基本模型中增加了侦听来自模型和DOM的事件及其处理逻辑的功能。 此外,要显示界面中的更改,非常希望不要完全重新渲染View,而要对特定的DOM元素(通常使用jQuery)做更精细的工作,这需要编写许多其他代码。

由于View实现的一般复杂性,它的测试变得更加复杂-因为现在我们直接与DOM树一起工作,为了进行测试,我们需要使用其他提供或模拟浏览器环境的工具。

新的View实现的问题并没有就此结束:

除了上述之外,使用嵌套在彼此的View中相当困难。 随着时间的流逝,这个问题在Marionette.js中Regions的帮助下得以解决,但是在此之前,开发人员不得不发明自己的技巧来解决这个相当简单且经常出现的问题。

还有最后一个。 以这种方式开发的接口易受数据不同步的影响-由于所有模型都在不同的演示者级别隔离存在,因此在接口的某个部分更改数据时,它们通常不会在另一部分进行更新。

但是,尽管存在这些问题,这种方法仍然可行,并且前面提到的以木偶形式出现的Backbone的开发仍然可以成功地用于SPA的开发。

反应和虚空


很难相信,但是在最初发布时, React.js在开发人员社区中引起了很多怀疑。 这种怀疑是如此之大,以至于很长一段时间以来,以下文字已在官方网站上发布:

给它五分钟
React挑战了许多传统智慧,乍一看,其中一些想法似乎很疯狂。

而且尽管有这样一个事实,与大多数竞争者和前任者不同,React并不是一个成熟的框架,而只是一个小型库,可方便在DOM中显示数据:

React是一个JavaScript库,用于通过Facebook和Instagram创建用户界面。 许多人选择将React视为MVC中的V。

React提供给我们的主要概念是组件的概念,它实际上为我们提供了一种新的实现View的方法:

 class User extends React.Component { handleEdit() { // .. } render() { const { name, email, projects } = this.props; return ( <div className="user-profile"> <h2>{name}</h2> <p>E-mail: {email}</p> <p> Projects: { projects.map(project => <a href="/projects/{project.id}">{project.name}</a>) } </p> <button onClick={this.handleEdit}>Edit</button> </div> ); } } 

React非常令人愉悦地使用。 迄今为止,其不可否认的优势包括:

1) 声明和反应性 。 更改显示的数据时,不再需要手动更新DOM。

2) 组成成分 。 建立和探索View树已成为一项完全基本的操作。

但是,不幸的是,React有很多问题。 最重要的一个事实是,React不是一个完善的框架,因此没有为我们提供任何类型的应用程序架构或成熟的工具来实现它。

为什么将其写入缺陷? 是的,因为现在React是用于开发Web应用程序的最流行的解决方案( 证明另一个证明 和另一个证明 ),它是新前端开发人员的切入点,但同时不提供或推广任何体系结构,也没有用于构建完整应用程序的任何方法和最佳实践。 此外,他发明并推广了自己的自定义方法,例如HOCHooks ,这些方法并未在React生态系统之外使用。 结果,每个React应用程序都以自己的方式解决了典型的问题,并且通常不会以最正确的方式来解决。

可以借助React开发人员最常见的错误之一来证明这个问题,其中包括滥用组件:

如果您仅有的工具是锤子,那么一切都会看起来像钉子。

在他们的帮助下,开发人员解决了完全无法想象的任务,远远超出了数据可视化的范围。 实际上,借助组件,他们可以实现几乎所有内容-从CSS的媒体查询路由

React和Redux


Redux的出现和普及极大地方便了React应用程序结构的恢复顺序。 如果React是MVP的视图,那么Redux为我们提供了一个相当方便的Model变体。

Redux的主要思想是数据的传输以及与它们一起工作到单个集中式数据仓库(即所谓的存储)中的逻辑。 这种方法完全解决了我们稍早谈到的数据复制和不同步问题,还提供了许多其他便利,其中包括轻松研究应用程序中数据的当前状态。

另一个同等重要的功能是商店与应用程序其他部分之间的通信方式。 我们提供了使用所谓的动作(描述事件或命令的简单对象)代替直接访问商店或其数据的方法,这些动作在商店和事件源之间的松散耦合程度很弱,从而大大提高了项目的可维护性。 因此,Redux不仅迫使开发人员使用更正确的架构方法,而且还使您能够利用事件源的各种优势-现在,在调试过程中,我们可以轻松查看应用程序中操作的历史记录,它们对数据的影响,并在必要时可以导出所有这些信息。 ,这在分析生产中的错误时也非常有用。

使用React / Redux的应用程序的一般方案可以表示如下:

图片

React组件仍然负责显示数据。 理想情况下,这些组件应该是干净的且可以使用,但是,如果有必要,它们可以具有局部状态和关联的逻辑(例如,实现隐藏/显示特定元素或对用户操作进行基本预处理)。

当用户在界面中执行操作时,该组件将简单地调用相应的处理程序功能,该功能将从外部与显示的数据一起接收。

所谓的容器组件对我们来说是演示者-它们是对显示组件及其与数据交互进行控制的人。 它们是使用connect函数创建的,该函数扩展了传递给它的组件的功能,添加了用于更改Store中数据的订阅,并让我们确定应将哪些数据和事件处理程序传递给它。

而且,如果这里的数据一切都清楚了(我们只是将数据从存储映射到预期的“道具”),那么我想更详细地介绍事件处理程序-它们不仅将动作发送到商店,而且很可能包含用于处理事件的其他逻辑-例如,包括分支,执行自动重定向以及执行特定于演示者的任何其他工作。

关于容器组件的另一个重要点:由于它们是通过HOC创建的,因此开发人员经常在单个模块中描述显示组件和容器组件,并且仅导出容器。 这不是正确的方法,因为为了测试和重新使用显示组件,应将其与容器完全分开,最好从单独的文件中取出。

好吧,我们尚未考虑的最后一件事是商店。 它充当我们模型的一个相当具体的实现,它由几个组件组成:状态(包含我们所有数据的对象),中间件(预处理所有接收到的动作的一组函数),Reducer(在State中修改数据的函数)和一些组件或负责执行异步操作(访问外部系统等)的副作用处理程序。

这里最常见的问题是我们国家的形式。 正式地,Redux不会对我们施加任何限制,也不会就该对象的用途提出建议。 开发人员可以在其中存储任何数据(包括表单状态来自路由器的信息 ),这些数据可以是任何类型( 不禁止存储甚至函数和对象实例)并且具有任何嵌套级别。 实际上,这再次导致以下事实:从一个项目到另一个项目,我们使用一种完全不同的方法来使用状态,这再次引起了一些困惑。

首先,我们同意我们不必将所有应用程序数据都绝对保留在State中- 文档中已明确指出 。 尽管在调试过程中浏览动作历史记录时(将组件的内部状态始终保持不变),将部分数据存储在组件的状态内会带来某些不便(将组件的内部状态始终保持不变),但将数据传输到State会带来更大的困难-这会极大地增加其大小,并需要创建更多的内容。动作和减速器。

至于在State中存储任何其他本地数据,我们通常会处理一些常规接口配置,即一组键值对。 在这种情况下,我们可以轻松地使用一个简单的对象及其简化程序。

而且,如果我们正在谈论存储来自外部源的数据,那么基于这样的事实,即在绝大多数情况下,接口的开发中我们都在处理经典的CRUD,那么对于存储来自服务器的数据,将State视为RDBMS是有意义的:密钥是名称资源,并在它们后面存储着已加载对象的数组( 不带nesting )和它们的可选信息(例如,服务器上用于创建分页的记录总数)。 此数据的一般形式应尽可能统一-这将使我们能够简化每种资源类型的化简器的创建:

 const getModelReducer = modelName => (models = [], action) => { const isModelAction = modelActionTypes.includes(action.type); if (isModelAction && action.modelName === modelName) { switch (action.type) { case 'ADD_MODELS': return collection.add(action.models, models); case 'CHANGE_MODEL': return collection.change(action.model, models); case 'REMOVE_MODEL': return collection.remove(action.model, models); case 'RESET_STATE': return []; } } return models; }; 

好吧,我想在使用Redux的上下文中讨论的另一点是副作用的实现。

首先,完全忘掉Redux Thunk-他提出的将Actions转换为具有副作用的功能的方法,尽管这是一个可行的解决方案,但它融合了我们体系结构的基本概念,并将其优势减为零。 Redux Saga为我们提供了一种更正确的方法来实现副作用,尽管有关其技术实现方面存在一些疑问。

接下来-尝试尽可能统一访问服务器的副作用。 像State表单和reducers一样,我们几乎总是可以使用一个处理程序来实现向服务器创建请求的逻辑。 例如,在RESTful API的情况下,这可以通过侦听通用动作来实现,例如:

 { type: 'CREATE_MODEL', payload: { model: 'reviews', attributes: { title: '...', text: '...' } } } 

...并在其上创建相同的通用HTTP请求:

 POST /api/reviews { title: '...', text: '...' } 

通过有意识地遵循上述所有技巧,即使不是理想的体系结构,也至少可以接近它。

美好的未来


Web界面的现代开发确实向前迈出了重要的一步,现在我们正处在一个主要问题的重要部分已经以一种或另一种方式解决的时代。 但这根本不意味着未来将不会有新的革命。

如果您展望未来,那么很可能我们会看到以下内容:

1.不使用JSX的组件方法

组件的概念已被证明是非常成功的,并且很可能我们将看到它们的更大普及。 但是JSX本身可以并且必须死亡。 是的,使用起来确实很方便,但是,它既不是公认的标准,也不是有效的JS代码。 用于实现接口的库,无论它们有多好,都不应发明新标准,而新标准则必须在每个可能的开发工具包中一次又一次地实现。

2.声明没有Redux的容器

Redux提出的使用集中式数据仓库也是一种非常成功的解决方案,将来应该成为接口开发中的一种标准,但是其内部架构和实现可能会发生某些变化和简化。

3.增强库的互换性

我相信,随着时间的流逝,前端开发人员社区将意识到最大程度地实现库互换性的好处,并将不再将自己锁定在其小型生态系统中。 应用程序的所有组件-路由器,状态容器等-应该非常通用,并且对其进行替换不应要求进行大规模重构或从头开始重写应用程序。

为什么要这样?


如果我们尝试概括以上介绍的信息并将其简化为更简单和简短的形式,那么我们将获得一些相当普遍的观点:

-对于成功的应用程序开发,对语言和框架的了解还不够,应注意一般的理论知识:应用程序体系结构,最佳实践和设计模式。

“唯一不变的是变化。” 耕作和开发方法将继续发生变化,因此大型且长期存在的项目必须对架构给予适当的关注-如果没有架构,引入新工具和实践将非常困难。

这可能就是我的全部。 非常感谢每个能读完这篇文章的人。 如果您有任何问题或意见,我邀请您发表评论。

Source: https://habr.com/ru/post/zh-CN456794/


All Articles