什么是可扩展前端架构?

关于软件开发,可扩展性这个词的两个最常见的含义与代码库的性能和长期可维护性有关。你可以同时拥有这两点,但专注于良好的可维护性会使你更容易调整性能而不影响应用程序的其他部分。在前台更是如此,在这里我们有一个与后台的重要区别:本地状态。

在这一系列的文章中,我们将谈论如何用现实生活中经过测试的方法来开发和维护一个可扩展的前端应用程序。我们的大多数例子将使用React和Redux,但我们会经常与其他技术栈进行比较,以说明你如何能达到同样的效果。让我们开始讨论这个系列的架构,这是你的软件中最重要的部分。

什么是软件架构?
架构到底是什么?说架构是你的软件中最重要的部分似乎很自命不凡,但请听我说。

架构是你如何使你的软件的各个单元相互作用,以突出你必须做出的最重要的决定,并推迟次要决定和实施细节。设计一个软件的架构意味着将实际的应用程序与它的支持技术分开。你的实际应用不知道数据库、AJAX请求或GUI;相反,它是由代表你的软件所涵盖的概念的用例和领域单元组成的,而不考虑执行用例的角色或数据被持久化的地方。

关于架构,还有一些重要的事情要谈:它并不意味着文件组织,也不是你如何命名文件和文件夹。


前端开发中的分层
将重要的东西和次要的东西分开的一种方法是使用层,每个层都有不同的和特定的责任。在基于层的架构中,一个常见的方法是把它分成四层:应用、领域、基础设施和输入。这四层在另一篇文章《NodeJS和良好实践》中得到了更好的解释。我们建议你在继续之前阅读关于它们的帖子的第一部分。你不需要阅读第二部分,因为它是专门针对NodeJS的。

前端和后端之间的领域和应用层并没有什么不同,因为它们是技术无关的,但我们不能对输入和基础设施层说同样的话。在网络浏览器中,输入层通常只有一个角色,即视图,所以我们甚至可以把它称为视图层。另外,前端没有访问数据库或队列引擎的权限,所以我们不会在我们的前端基础设施层中找到这些。我们将找到的是封装AJAX请求、浏览器cookies、LocalStorage的抽象,甚至是与WebSocket服务器交互的单元。主要的区别只是被抽象出来的东西,所以你甚至可以拥有界面完全相同但下面有不同技术的前端和后端仓库。你能看到一个好的抽象可以有多棒吗?

如果你使用React、Vue、Angular或其他工具来创建你的视图,这并不重要。重要的是要遵循输入层的规则,不要有任何逻辑,因此要把输入参数委托给下一层。关于基于前端层的架构,还有一个重要的规则:为了让输入/视图层始终与本地状态保持同步,你应该遵循单向数据流。这个术语听起来很熟悉吗?我们可以通过添加一个专门的第五层来实现:状态,也称为存储。


状态层
当遵循单向数据流时,我们永远不会直接在视图中改变或变异视图所接收的数据。相反,我们从视图中调度我们称之为 "行动 "的东西。它是这样的:一个动作向数据源发送一个消息,数据源更新自己,然后用新的数据重新提交给视图。请注意,从视图到存储空间从来没有一个直接的通道,所以如果两个子视图使用相同的数据,你可以从任何一个子视图中派发动作,这将导致两个子视图用新数据重新渲染。看起来我是在专门谈论React和Redux,但事实并非如此;你可以用几乎所有的现代前端框架或库来实现同样的结果,比如React + context API,Vue + Vuex,Angular + NGXS,甚至Ember,使用数据下行动上的方法(又称DDAU)。你甚至可以用jQuery来做,用它的事件系统来发送动作!

这一层负责管理你的前端的本地和不断变化的状态,比如从后端获取的数据,在前端创建但尚未持久化的临时数据,或者像请求的状态这样的瞬时信息。如果你想知道,这就是负责更新状态的动作和处理程序所在的层。

尽管我们经常看到代码库中的业务规则和用例定义直接放在动作中,但如果你仔细阅读其他层的描述,你会发现我们已经有一个地方可以放置我们的用例和业务规则,而不是状态层。这是否意味着我们的行动现在就是用例?不是!是的。那么我们应该如何对待它们呢?

让我们想一想......我们说行动Action不是用例,而且我们已经有一个层来放置我们的用例。视图应该调度行动Action,行动Action接收来自视图的信息,将其交给用例,根据响应调度新的行动,最后更新状态--更新视图并关闭单向数据流。这些动作现在听起来是不是像控制器?它们不是从视图中获取参数,委托给用例,并根据用例的结果进行响应的地方吗?这正是你应该对待它们的方式。这里不应该有复杂的逻辑或直接的AJAX调用,因为这些是另一个层的职责。状态层应该只知道如何管理本地存储,仅此而已。

还有一个重要的因素在起作用。由于状态层管理着视图层所消耗的本地存储,你会注意到这两者在某种程度上是耦合的。在状态层中会有一些只针对视图的数据,比如一个布尔标志,表示一个请求是否还在等待中,以便视图可以显示一个旋转器,这完全没有问题。

代码:

import api from './infra/api'; // has no dependencies
import { validateUser } from './domain/user'; // has no dependencies
import makeUserRepository from './infra/user/userRepository';
import makeArticleRepository from './infra/article/articleRepository';
import makeCreateUser from './app/user/createUser';
import makeGetArticle from './app/article/getArticle';

const userRepository = makeUserRepository({
  api
});

const articleRepository = makeArticleRepository({
  api
});

const createUser = makeCreateUser({
  userRepository,
  validateUser
});

const getArticle = makeGetArticle({
  userRepository,
  articleRepository
});

export {
  createUser,
  getArticle
};
export default ({ validateUser, userRepository }) => async (userData) => {
  if(!validateUser(userData)) {
    throw new Error('Invalid user');
  }

  try {
    const user = await userRepository.add(userData);
    return user;
  } catch(error) {
    throw error;
  }
};
export default ({ api }) => ({
  async add(userData) {
    const user = await api.post('/users', userData);

    return user;
  }
});

你会注意到重要的部分,即用例createUser 等,在文件的最后被实例化了,并且是唯一被导出的对象,因为它们将被注入到动作Action中。你的其他代码不需要知道存储库是如何创建的,以及它是如何工作的。这并不重要,这只是一个技术细节。
对于用例来说,版本库是否发送AJAX请求或在LocalStorage中持久化一些东西并不重要;用例没有责任知道这些。如果你想在你的API还在开发中时使用LocalStorage,然后转而使用对API的线上调用,只要与API交互的代码与与LocalStorage交互的代码遵循相同的接口,你就不需要改变用例。

即使你有几十个用例、存储库、服务等,你也可以像上面描述的那样,手动进行注入。如果建立所有的依赖关系太乱,你可以使用依赖注入包,只要它不增加耦合度。

测试你的DI包是否足够好的一个经验法则是,检查从手动方法到使用该库是否不需要接触比容器代码更多的东西。如果是这样,那么这个包就太麻烦了,你应该选择一个不同的包。如果你真的想使用一个包,我们推荐Awilix。它的使用相当简单,而且脱离了手动方式,只需要触碰容器文件即可。关于如何使用它和为什么使用它,有一个非常好的系列,由包的作者写的。 

猜你喜欢

转载自blog.csdn.net/weixin_44786530/article/details/130194702