译者:这篇文章是在 medium 讲解前端架构分层系列的第一篇文章,分层和以前翻译的文章相似,相对普通项目多出来两层,领域层(从业务抽离出来领域实体)和用例层(实现应用业务逻辑)。另外在编程范式上,相对面对对象,做者更倾向于采用函数式,读者可根据项目特色选择适合本身的方式。html
文章首发于个人博客 github.com/mcuking/blo…node
这篇博客是《可扩展的前端》系列的一部分,你能够看到其余部分: #2 — Common Patterns 和 #3 — The State Layer。git
关于软件开发的可扩展性这一律念有两个最多见的的意义:代码的性能和可维护性。你能够同时兼顾这两点,可是专一于良好的可维护性会让一件事情变得容易,那就是提高性能且不影响应用的其他部分。更重要的是,前端与后端有一个重要的区别:本地状态。github
在这个系列博客中,咱们将会讨论如何经过实际的通过验证的方法,来开发和维护可扩展的前端应用。咱们大部分的例子将会使用 React 和 Redux,可是咱们会常常与其余的技术栈对比较,来展现你如何达到一样的结果。让咱们开始这个关于架构方面的系列讨论吧,这是你的软件中最重要的部分。数据库
那么架构究竟是什么?说架构是软件中的最重要的部分彷佛很自觉得是,但请耐新看下去。npm
架构是使软件的各个部分相互交互以突出必需要作出的最重要的决策,而且推迟次要的决策和实现细节的方式。设计一个软件的架构意味着将实际的应用从支持它的技术中分离开来。你的实际应用不知道数据库、AJAX 请求、或者 GUI;而是由用例和领域模型组成。这些用例和领域模型表明了你的软件所涵盖的概念,请忽略执行用例的角色或数据在哪里存储等。编程
关于架构还有一个重要的事情要说:那就是架构不意味着文件的组织,也不是如何命名你的文件和文件夹。 |
区分重要与次要的一种方式就是使用层,每一个层都有一些不一样且特定的职责。基于分层的架构中一种常见的方式是将它分红四个层:application 层、domain 层、infrastructure 层、input 层。这四个层在另外一个博客中有很好的解释,NodeJS and Good Practices。我推荐在继续阅读下面的文章以前,先看下这篇文章的第一部分。你不须要阅读第二部分,由于那已经具体到 NodeJS 了。redux
其中 domain 层和 application 层在前端和后端之间是没有什么不一样的,由于它们是与技术无关的,可是对于 input 层和 infrastructure 层咱们不能这么说。在 Web 浏览器中 input 层一般只有一个角色--view。因此咱们甚至能够称之为 view 层。一样在前端是没法操做数据库或队列引擎的,因此咱们没法在前端的 infrastructure 层中找到它们。相反咱们可以找的是封装 AJAX 请求、浏览器 cookie、LocalStorage,甚至是与 WebSocket 服务器交互的模块的抽象。后端
主要的区别是被抽象的内容,因此前端和后端的 Repository 甚至能够有彻底一致的接口而底层是不一样的技术。你能看到一个好的抽象有多棒了吗?
你使用 React,Vue,Angular 或其余任何工具来操做视图都没有关系,重要的是遵照没有任何逻辑的 view 层规则,将输入参数委托给下一层。关于基于前端分层的架构,还有另外一个重要规则:使 view 层始终与本地状态保持同步,你应该遵循单向数据流原则。这个概念是否听着很熟悉?咱们能够经过添加第五个层来达到这个目的:state ,或者称为 store。
当遵循单向数据流原则时,咱们永远不会在 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 层。
好的,分层很酷,可是它们如何互相通讯?咱们如何使一个层依赖另外一个层而不耦合它们?是否能够在不执委派给用例的状况下测试 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
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 };
复制代码
createUser.js
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;
}
};
复制代码
userRepository.js
export 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 层除外,它会在单独的文章中介绍。花一些时间来吸取这些概念。当咱们详细介绍这些模式时,它们将很是有用,一切都会变得更加有意义。到时候那里见!
Bob Martin — Architecture the Lost Years
Rebecca Wirfs-Brock — Why We Need Architects (and Architecture) on Agile Projects