引子
读了对于可扩大前端探讨的一些文章,翻译记录。
原文:Scalable Frontend #1 — Architecture Fundamentals
- Origin
- My GitHub
注释
对于软件开发中“可扩展性”一词最常见的两个含意,与随着时间推移代码库的性能和可维护性无关。你能够同时领有它们,然而重视良好的可维护性,能够让优化性能变的更容易,且不会影响应用程序其余部分。更重要的是在前端,与后端有一个重要的区别:本地状态。
在这个系列文章中,咱们将探讨如何应用通过理论测试的办法,开发和保护一个可扩大的前端应用程序。咱们大多数示例将应用 React 和 Redux ,但咱们将常常与其余技术栈进行比拟,展现如何达到雷同的成果。让咱们以探讨架构开始这个系列,这是你软件中最重要的局部。
软件架构是什么?
软件架构到底是什么?说它是软件中最重要的局部仿佛有些虚夸,请容我持续说上来。
架构是你如何让软件的各个单元彼此单干,以强调必须要做出的最重要决策,并推延主要决策和实现细节。设计软架构意味着将理论应用程序与其反对的技术脱离开来。你的理论应用程序不理解数据库、AJAX 申请或 GUI ;代替的是,由代表了你的软件所涵盖概念的用例和定义域单元组成,不思考执行用例的参与者或数据长久化的地位。
对于架构,还有一些重要的事件要说:它不意味着文件组织,也不意味着你是如何命名文件和文件夹。
前端开发中的档次
辨别什么是重要什么是主要的一种形式是应用分层,每一层都有不同具体的职责。基于分层的架构中,一种常见的形式是将其分为四层:应用层(application)、定义域层(domain)、基础设施层(infrastructure)和输出层(input)。这四层在文章 NodeJS and Good Practices 中有更好的解释。咱们倡议你在持续之前浏览该文章的第一局部。你不用浏览第二局部,因为它是对于 NodeJS 的。
定义域层和应用层在前端和后端之间没有什么不同,因为它们与技术无关,但咱们对于输出层和基础设施层不能这么说。在 web 浏览器中,通常在输出层(视图)有一个参与者,因而咱们甚至能够将其称为视图层 。另外,前端没有拜访数据库或队列引擎的权限,因而咱们在前端基础设施层中找不到这些。而咱们将发现的是封装 AJAX 申请、浏览器 cookie 、LocalStorage 甚至与 WebSocket 服务器交互单元的形象。次要的区别只是形象的内容,所以你甚至能够领有接口完全相同但底层技术不同的前端和后端存储库。你能设想一个好的形象是如许的棒吗?
无论你是应用 React、Vue、Angular 还是其它工具来创立你的视图都没有关系。重要的是要遵循输出层没有任何逻辑的规定,这样将输出参数传递给下一层。对于基于分层架构的前端,还有另一个重要规定:要让输出/视图层始终与本地状态放弃同步,你应该遵循单向数据流。这个词听下来相熟吗?咱们能够通过减少特定的第五层来实现这一点:状态,也称为存储。
状态层
当遵循单向数据流时,咱们从不更改或转换从视图中间接接管的数据。代替的是,咱们会从视图中散发“ actions ”。它是这样运行的:一个 action 向数据源发送一条音讯,数据源更新本人,而后用新数据从新渲染视图。请留神,绝不会有从视图到存储的间接通道,因而如果两个子视图应用雷同的数据,那么你就能够从其中任何一个子视图散发 action ,这都将导致它们用新数据从新渲染。看起来我是在专门探讨 React 和 Redux ,但并不是这样的;你能够用简直所有古代前端框架或库,实现雷同的成果,比方 React + context API、Vue + Vuex、Angular + NGXS ,甚至应用 Ember 的 data-down action-up 办法(又称 DDAU)。你甚至能够应用 jQuery 的事件零碎发送 actions !
这一层负责管理前端本地和一直变动的状态,如从后端获取的数据、在前端创立但未长久化的长期数据,或申请状态等长期信息。如果你还在推敲,这就是 actions 和它们对应负责更新状态的处理程序所在的层。
只管在 actions 中可能间接看到带有业务规定和用例定义的代码库是很常见,然而如果你仔细阅读其它层的形容,会发现咱们曾经有了搁置用例和业务规定的中央,而且并不是状态层。这是否意味着咱们的 actions 当初成了用例?不!那么咱们应该如何对待它们?
让咱们思考一下…咱们说过 actions 不是用例,咱们曾经有了一个层来搁置用例。视图应该散发 actions ,它们接管来自视图的信息,将其交给用例,依据响应散发新的 actions ,最初更新状态—更新视图并完结单向数据流。当初 actions 听起来难道不像是控制器吗?它们不就是一个从视图中获取参数,传递给用例,并依据用例的后果进行响应的中央吗?你就是应该这样对待他们。这里不应该有简单的逻辑或间接的 AJAX 调用,因为这些是另一层的职责。状态层应该只晓得如何治理本地存储,仅此而已。
还有一个重要因素在起作用。因为状态层治理视图层应用的本地存储,你将留神到这两个存储以某种形式产生了耦合。状态层中只有一些数据只用于视图,例如一个布尔标记,示意如果一个申请仍处于挂起状态,那么视图就能够显示一个加载中的旋转器,这齐全没有问题。不要因为这个而自责,你没必要适度概括状态层。
依赖注入(Dependency injection)
好的,分层很酷,但它们是如何互相通信的呢?咱们如何使一个层依赖于另一个层而不产生耦合?有没有可能测试一个 action 的所有可能输入,而不执行它所委托的用例?是否能够在不触发 AJAX 调用的状况下测试用例?当然能够,咱们能够通过依赖注入来实现。
依赖注入是一种在创立单元的过程中接管其耦合依赖项作为参数的技术。例如,在类的构造函数中接管类的依赖项,或者应用 React/Redux 将组件连贯到存储数据,并将所需的数据和 actions 作为 props 注入。实践并不简单,对吧?实际也不应该简单,所以让咱们以一个 React/Redux 应用程序作为例子。
咱们刚刚说过,应用 React/Redux 连贯是一种在视图和状态层之间实现依赖注入的办法,并且简单明了。然而咱们之前也说过,actions 将业务逻辑委托给用例,那么咱们如何将用例(应用层)注入到 actions(状态层)中呢?
让咱们构想一下,你有一个对象,其中蕴含应用程序的每个用例的办法。这个对象通常被称为依赖容器。是的,这看起来很奇怪,而且不能很好地扩大,但这并不意味着用例的实现就在这个对象外部。这些只是委托给用例的办法,而用例的定义是在其它中央。在应用程序中,所有用例集中在一个对象,比散布在整个代码库中很难找到它们要好得多。有了这个对象,咱们须要做的就是将它注入到 actions 中,让他们各自决定将触发什么用例,对吧?
如果你应用 redux-thunk ,那么应用 withExtraArgument 办法实现它非常简单,它容许你在每个 thunk 操作中将容器作为 getState
之后的第三个参数注入。如果你应用 redux-saga ,咱们将容器作为 run
办法的第二个参数传递,这种办法应该是简略的。如果你应用 Ember 或 Angular ,那么内置的依赖注入机制应该就足够了。
这样做将使 actions 与用例解耦,因为你不须要在定义 actions 时,在每个文件中手动导入用例。此外,当初将 action 从用例中离开来测试非常简单:只需注入一个完全符合你需要的模仿用例即可。如果用例失败,你想测试用例失败时将调度什么 action 吗?注入一个总是失败的模仿用例,而后测试 action 如何响应它。不须要思考理论用例是如何工作的。
好极了,咱们曾经将状态层注入到视图层,将应用程序层注入到状态层。剩下的呢?咱们如何将依赖注入到用例中来构建依赖容器?这是一个重要的问题,并且有很多办法能够解决。首先,不要忘了查看你应用的框架是否内置了依赖注入,例如 Angular 或 Ember 。如果已内置,你就不应该自造。如果没有,你能够用两种办法来实现:手动实现,或者在一个包的帮助下实现。
手动实现应该简单明了:
- 依照类或闭包定义你的单元,
- 先实例化那些没有依赖关系的,
- 实例化依赖于它们的对象,将它们作为参数传递,
- 复上述步骤,直到实例化了所有用例,
- 导出它们。
太形象了?看看几个代码示例:
import api from './infra/api'; // has no dependenciesimport { validateUser } from './domain/user'; // has no dependenciesimport 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; }});
你将留神到,重要的局部——用例,在文件开端被实例化,并作为一个独自对象被导出,因为它们将被注入到 actions 中。其余的代码不须要晓得存储库是如何创立和工作的。这并不重要,只是一个技术细节。对于用例来说,存储库是否发送 AJAX 申请或在 LocalStorage 中长久化某些内容并不重要;这些并不是用例的职责。如果你心愿在 API 仍处于开发阶段时应用 LocalStorage ,之后再切换到对在线 API 的调用,那么只有与 API 通信的代码,和与 LocalStorage 通信的代码遵循雷同的接口,就不须要更改用例。
你能够像下面所形容的那样,很好的手动执行注入,即便你有几十个用例、存储库、服务等等。如果构建所有依赖关系变得太凌乱,只有不减少耦合,你就能够始终应用依赖注入包。
测试你的 DI 包是否足够好的一个教训法令是,查看从手动办法切换到应用库是否只须要接触容器代码。如果不是,那么这个包就过于入侵,你应该抉择一个不同的包。如果你真的想应用包,咱们举荐 Awilix 。它的应用非常简单,脱离手动形式只须要接触容器文件。这里有由包的作者编写的一系列好文章介绍如何和为什么应用它。
接下来
好了,咱们曾经探讨了架构以及如何以一种好的形式连贯层!在下一篇文章中,咱们将展现一些真正的代码和刚刚谈到的层的通用模式,除了状态层,它将在另一篇文章中介绍。花些工夫来排汇这些概念;当咱们深刻理解这些模式时,它们会很有用,所有都会更有意义。再见!
举荐链接
- NodeJS and Good Practices
- Bob Martin — Architecture the Lost Years
- Rebecca Wirfs-Brock — Why We Need Architects (and Architecture) on Agile Projects
- Domain-Driven Design
参考资料
- Scalable Frontend #1 — Architecture Fundamentals