文章首发于我的博客 https://github.com/mcuking/bl...

译者:这篇文章是在 medium 解说前端架构分层系列的第一篇文章,分层和之前翻译的文章相似,绝对一般我的项目多进去两层,畛域层(从业务抽离进去畛域实体)和用例层(实现利用业务逻辑)。另外在编程范式上,绝对面对对象,作者更偏向于采纳函数式,读者可依据我的项目特点抉择适宜本人的形式。

原文链接 https://blog.codeminer42.com/...

这篇博客是《可扩大的前端》系列的一部分,你能够看到其余局部:
#2 — Common Patterns 和 #3 — The State Layer。

对于软件开发的可扩展性这一概念有两个最常见的的意义:代码的性能和可维护性。你能够同时兼顾这两点,然而专一于良好的可维护性会让一件事件变得容易,那就是晋升性能且不影响利用的其余部分。更重要的是,前端与后端有一个重要的区别:本地状态。

在这个系列博客中,咱们将会探讨如何通过理论的通过验证的办法,来开发和保护可扩大的前端利用。咱们大部分的例子将会应用 React 和 Redux,然而咱们会常常与其余的技术栈对比拟,来展现你如何达到同样的后果。让咱们开始这个对于架构方面的系列探讨吧,这是你的软件中最重要的局部。

什么是软件架构?

那么架构到底是什么?说架构是软件中的最重要的局部仿佛很自以为是,但请耐新看上来。

架构是使软件的各个局部互相交互以突出必须要做出的最重要的决策,并且推延主要的决策和实现细节的形式。设计一个软件的架构意味着将理论的利用从反对它的技术中拆散开来。你的理论利用不晓得数据库、AJAX 申请、或者 GUI;而是由用例和畛域模型组成。这些用例和畛域模型代表了你的软件所涵盖的概念,请疏忽执行用例的角色或数据在哪里存储等。

对于架构还有一个重要的事件要说:那就是架构不意味着文件的组织,也不是如何命名你的文件和文件夹。
## 前端开发中的层辨别重要与主要的一种形式就是应用层,每个层都有一些不同且特定的职责。基于分层的架构中一种常见的形式是将它分成四个层:application 层、domain 层、infrastructure 层、input 层。这四个层在另一个博客中有很好的解释,NodeJS and Good Practices。我举荐在持续浏览上面的文章之前,先看下这篇文章的第一局部。你不须要浏览第二局部,因为那曾经具体到 NodeJS 了。其中 domain 层和 application 层在前端和后端之间是没有什么不同的,因为它们是与技术无关的,然而对于 input 层和 infrastructure 层咱们不能这么说。在 Web 浏览器中 input 层通常只有一个角色--view。所以咱们甚至能够称之为 view 层。同样在前端是无奈操作数据库或队列引擎的,所以咱们无奈在前端的 infrastructure 层中找到它们。相同咱们可能找的是封装 AJAX 申请、浏览器 cookie、LocalStorage,甚至是与 WebSocket 服务器交互的模块的形象。次要的区别是被形象的内容,所以前端和后端的 Repository 甚至能够有完全一致的接口而底层是不同的技术。你能看到一个好的形象有多棒了吗?你应用 React,Vue,Angular 或其余任何工具来操作视图都没有关系,重要的是恪守没有任何逻辑的 view 层规定,将输出参数委托给下一层。对于基于前端分层的架构,还有另一个重要规定:使 view 层始终与本地状态放弃同步,你应该遵循单向数据流准则。这个概念是否听着很相熟?咱们能够通过增加第五个层来达到这个目标:state ,或者称为 store。## State 层当遵循单向数据流准则时,咱们永远不会在 view 外部间接更改 view 接管的数据。相同,咱们从 view 中 dispatch 咱们所谓的 “action”。它是这样的:一个 action 将音讯发送到数据源,该数据源将更新本身,而后应用新数据从新渲染 view。须要留神的是,从 view 到 store 没有间接通道,因而如果两个子 view 应用了雷同的数据,则能够从任何一个 view 中 dispatch 一个 action,这会导致两个子 view 都会用新据渲染。仿佛我是在专门议论 React 和 Redux,但事实并非如此;简直能够通过所有古代的前端框架或库取得雷同的后果,例如 React + context API, Vue + Vuex, Angular + NGXS, 甚至应用 data-down action-up 形式的 Ember。你甚至能够应用 jQuery 的事件零碎来实现发送 action up!该层负责管理前端的本地和一直变动的状态,例如从后端获取的数据,在前端操作但尚未长久化的长期数据,或者是刹时信息,例如申请状态。即便在 actions 外部,也会常常看到带有业务规定和用例定义的代码,如果你仔细阅读其它层的形容,你会看到咱们曾经有搁置咱们的用例和业务逻辑的中央了,而且不是 state 层。这是否意味着咱们的 actions 当初是用例?没有!那么咱们应该如何看待它们呢?让咱们考虑一下……咱们说 action 不是用例,并且咱们曾经有了搁置用例的层。view 应该 dispatch 一个 action,该 action 从视图中获取信息,将其交给用例,依据响应 dispatch 新 action,最初更新 state -- 更新 view 并敞开单向数据流。这些 action 当初看起来不像 controller 吗?他们不是一个从 view 中获取参数,委派给用例并依据用例后果进行响应的中央吗?那就是你应该对待它们的形式。不应有简单的逻辑或间接的 AJAX 调用,因为这是另一层的职责。state 层应该只晓得如何治理本地存储,仅此而已。其中还有另一个重要因素,因为 state 层治理着 view 层依赖的本地存储,因而你会留神到这两者是以某种形式耦合在一起的。state 层中只会有一些数据供 view 应用,例如一个布尔类型的标记,批示申请是否仍在期待解决,以便视图能够显示 spinner,这齐全能够。不要为此而懊恼,你不须要适度概括 state 层。## 依赖注入(Dependency injection)好的,分层很酷,然而它们如何相互通信?咱们如何使一个层依赖另一个层而不耦合它们?是否能够在不执委派给用例的状况下测试 action 的所有可能输入?是否能够在不触发 AJAX 调用的状况下测试用例?能够必定的是,咱们能够通过依赖注入来做到这一点。依赖注入是一种技术,该技术包含在创立一个模块的过程中接管另一个模块的耦合依赖关系作为参数。例如,在其构造函数中接管类的依赖项,或应用 React / Redux 将组件连贯到 store 并注入必要的数据和 action 作为参数。这个实践并不简单,对吧?相干的实际也不应该简单,所以让咱们以 React / Redux 应用程序为例。咱们刚刚说过,应用 React / Redux 的 connect 是一种在 view 和 state 层之间实现依赖注入的办法,而且它变得非常简单。然而咱们之前也说过,action 将业务逻辑委托给用例,那么咱们如何将用例(application 层)注入到 actions(state 层)中呢?让咱们设想一下,你有一个对象,其中蕴含针对你的应用程序的每个用例的办法。该对象通常称为 dependency container。是的,看起来很奇怪,而且扩展性不好,但这并不意味着用例的实现就在该对象内。这些只是委托给用例的办法,这些用例在其余中央定义。应用程序的所有用例一起应用一个对象比将它们散布在整个代码库中要好得多,后者会使它们很难找到。有了这个对象,咱们要做的就是将其注入到 actions 中,让每个 action 决定将触发什么用例,对吗?如果你应用的是 redux-thunk,则应用 withExtraArgument 办法能够很容易地实现它,该办法容许你将容器中的每个 thunk 动作作为 getState 之后的第三个参数注入。如果你应用的是 redux-saga,则该办法应该很简略,在该办法中,咱们将容器作为 run 办法的第二个参数进行传递。如果你应用的是 Ember 或 Angular,则内置的依赖项注入机制就足够了。这样做会使 action 与用例解耦,因为你无需在定义 action 的每个文件中手动导入用例。而且将 actions 与用例离开进行测试当初变得非常简单:只需注入一个伪造的用例实现即可,该实现的行为完全符合你想要的形式。你是否想测试如果用例失败,将 dispatch 什么 action?注入一个总是失败的模仿用例,而后测试 action 如何对此做出响应。无需思考理论用例如何工作。太好了,咱们将 state 层注入了 view 层,并将 application 层注入了 state 层。其余的呢?咱们如何将依赖项注入用例来构建 dependency container?这是一个重要的问题,有很多办法能够解决。首先,不要遗记查看你应用的框架是否内置了依赖项注入,例如 Angular 或 Ember。如果的确如此,则你不应该本人结构。如果没有,你能够通过两种形式来做到这一点:手动或在软件包的帮忙下。手动进行操作应该很简略:- 将你的模块定义为类或闭包,- 首先实例化没有依赖性的模块,- 而后再实例化有依赖的的模块,将它们作为参数传递,- 反复上述步骤,直到实例化所有用例为止,- 导出它们。太形象了?看一些代码示例:container.js`jsimport 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 };`createUser.js`jsexport 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; }};`userRepository.js`jsexport default ({ api }) => ({ async add(userData) { const user = await api.post('/users', userData); return user; }});`你会留神到,重要局部(用例)已在文件开端实例化,并且是惟一导出的对象,因为它们将被注入到 actions 中。你的其余代码无需理解 repository 的操作形式和工作形式。这并不重要,而只是技术细节。对于用例,repository 是发送 AJAX 申请还是在 LocalStorage 中保留某些内容都没有关系;用例没有职责须要晓得。如果你想在 API 仍在开发中时应用 LocalStorage,而后切换为应用通过网络 API 的调用,只有与 API 交互的代码遵循与 LocalStorage 交互的接口,而无需更改用例。即便你有数十个 use cases(用例), repositories, services 等,也能够如上所述手动实现注入。如果太麻烦而无奈构建所有依赖关系,则能够始终应用依赖注入的库,只有它不会减少耦合。测验你的 DI(Dependency injection) 库是否足够好的一条教训法令是,查看从手动办法转移到应用库是否只须要操作 container 代码即可。如果不是这样,则阐明库太过侵入,你应该抉择其余库。如果你的确要应用库,咱们倡议你应用 Awilix。它非常简单易用,无需手动操作,只需操作 container 文件即可。这个库的作者撰写了一系列无关如何应用以及为什么应用它的很好的文章,点击查看。## 接下来好的,咱们曾经探讨了架构以及如何以一种很好的形式连贯各层!在下一篇文章中,咱们将为方才探讨的层展现一些理论的代码和通用模式,但 state 层除外,它会在独自的文章中介绍。花一些工夫来排汇这些概念。当咱们具体介绍这些模式时,它们将十分有用,所有都会变得更加有意义。到时候那里见!## 举荐浏览链接NodeJS and Good PracticesBob Martin — Architecture the Lost YearsRebecca Wirfs-Brock — Why We Need Architects (and Architecture) on Agile ProjectsDomain-Driven Design