Code Monkey home page Code Monkey logo

clean-frontend's Introduction

Clean Frontend Architecture:整洁前端架构

ToC

(Clean Architecture + MVP) with BFF

Clean MVP 组件化

(Clean Architecture + Component-based + MVP) without BFF

Clean MVP Component-based

前端的恶梦

在我最近的一个项目里,我使用了 Angular 和混合应用技术编写了一个实时聊天应用。为了方便这个应用直接修改,无缝地嵌入到其它应用程序中。我尽量减少了 Component 和 Service 的数量——然而,由于交互复杂 Component 的数量也不能减少。随后,当我们完成了这个项目的时候,主的组件的代码差不多有 1000 行。这差不多是一个复杂的应用的代码数。在我试图多次去重构代码时,我发现这并不是一件容易的事:太多的交互。导致了 UI 层的代码,很难被抽取出去。只是呢,我还能做的事情是将一些业务逻辑抽取出来,只是怎么去抽取了——这成了我的一个疑惑。

MVP 嘛,逻辑不都是放到 Presenter 里,还有其它的招吗?

AVR is evil

Angular、Vue 和 React 都是一些不错的框架,但是它们都是恶魔——因为我们绑定了框架。尽管我们可以很快地从一个 React 的框架,迁移应用到其它类 React 框架,诸如 Preact;我们可以从一个类似于 Vue 的框架,迁移应用到其它类 Vue 的应用。但是我们很难从 React 迁移到 Angular,又或者是 Vue 迁移到 Angular。万一有一天,某个框架的核心维护人员,健康状况不好,那么我们可能就得重写整个应用。这对于一个技术人员/Tech Lead/项目经验/业务人员来说,这种情况是不可接受的。

所以,为了应对这些框架带来的问题,我们选择 Web Components 技术,又或者是微前端技术,从架构上切开我们的业务。但是它们并不是银弹,它们反而是一个累赘,限定了高版本的浏览器,制定了更多的规范。与此同时,不论是微前端技术还是 Web Components,它们都没有解决一个问题:框架绑定应用

框架绑定应用,就是一种灾害。没有人希望哪一天因为 Java 需要高额的付费,而导致我们选择重写整个应用。

组件化及 Presenter 过重

应对页面逻辑过于重的问题,我们选择了组件化。将一个页面,拆分成一系列的业务组件,再进一步地对这些业务组件进行地次细分,形成更小的业务组件,最后它们都依赖于组件库。

可是呢,细化存在一个问题是:更难以摆脱的框架绑定。与此同时,我们大量的业务逻辑仍然放置在 Presenter 里。我们的 Presenter 充满了大量的业务逻辑和非业务逻辑:

  • 页面展示相应的逻辑。诸如点击事件、提交表单等等。
  • 状态管理。诸如是否展示,用户登录状态等等。
  • 业务逻辑。诸如某个字符串,要用怎样的形式展示。
  • 数据持续化。哪些数据需要存储在 LocalStorage,哪些数据存储在 IndexedDB 里?

为了应对 Presenter 过重的问题,我们使用了 Service 来处理某一块具体的业务,我们使用了 Utils、Helper 来处理一些公共的逻辑。哪怕是如此,我们使用 A 框架编写的业务逻辑,到了 B 框架中无法复用。

直到我最近重新接触了 Clean Architecture,我发现 Presenter 还是可以进一步拆分的。

整洁的前端架构

Clean Architecture 是由 Robert C. Martin 在 2012 年提出的(PS:时间真早)。最早,我只看到在 Android 应用上的使用,一来 Android 开发使用的是 Java,二来 Android 应用有很重的 View 层。与此同时,在 7 年的时间里,由于前后端的分离,UI 层已经从后端的部分消失了——当然了,你也可以说 JSON 也是一种 View(至少它是可见的)。尽管,还存在一定数量的后端渲染 Web 应用,但是新的应用几乎不采用这样的模式。

但是,在 9012 年的今天,前端应用走向了 MV* 的架构方案,也有了一层很重的 View 层——类似于过去的后端应用,或者后端应用。相似的架构,也可以在前端项目中使用。

整洁架构

Robert C. Martin 总结了六边形架构(即端口与适配器架构)、DCI (Data-Context-Interactions,数据-场景-交互)架构、BCI(Boundary Control Entity)架构等多种架构,归纳出了这些架构的基本特点:

  • 框架无关性。系统不依赖于框架中的某个函数,框架只是一个工具,系统不能适应于框架
  • 可被测试。业务逻辑脱离于 UI、数据库等外部元素进行测试。
  • UI 无关性。不需要修改系统的其它部分,就可以变更 UI,诸如由 Web 界面替换成 CLI。
  • 数据库无关性。业务逻辑与数据库之间需要进行解耦,我们可以随意切换 LocalStroage、IndexedDB、Web SQL。
  • 外部机构(agency)无关性。系统的业务逻辑,不需要知道其它外部接口,诸如安全、调度、代理等。

如你所见,作为一个普通(不分前后端)的开发人员,我们关注于业务逻辑的抽离,让业务逻辑独立于框架。而在前端的实化,则是让前端的业务逻辑,可以独立于框架,只让 UI(即表现层)与框架绑定。一旦,我们更换框架的时候,只需要替换这部分的业务逻辑即可。

为此,基于这个概念 Robert C. Martin 绘制出了整洁架构的架构图:

Clean Architecture

如图所示 Clean Architecture 一共分为四个环,四个层级。环与环之间,存在一个依赖关系原则:源代码中的依赖关系,必须只指向同心圆的内层,即由低层机制指向高级策略。其类似于 SOLID 中的依赖倒置原则:

  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象
  • 抽象不应该依赖细节,细节应该依赖抽象

与此同时,四个环都存在各自核心的概念:

  • 实体 Entities (又称领域对象或业务对象,实体用于封装企业范围的业务规则)
  • 用例 Use Cases(交互器,用例是特定于应用的业务逻辑)
  • 接口适配器 Interface Adapters (接口适配器层的主要作用是转换数据)
  • 框架和驱动(Frameworks and Drivers),最外层由各种框架和工具组成,比如 Web 框架、数据库访问工具等

这个介绍可能有些简单,让我复制/粘贴一下更详细的解释:

实体(Entities),实体用于封装企业范围的业务规则。实体可以是拥有方法的对象,也可以是数据结构和函数的集合。如果没有企业,只是单个应用,那么实体就是应用里的业务对象。这些对象封装了最通用和高层的业务规则,极少会受到外部变化的影响。任何操作层面的改动都不会影响到这一层。

用例(Use Cases),用例是特定于应用的业务逻辑,一般用来完成用户的某个操作。用例协调数据流向或者流出实体层,并且在此过程中通过执行实体的业务规则来达成用例的目标。用例层的改动不会影响到内部的实体层,同时也不会受外层的改动影响,比如数据库、UI 和框架的变动。只有而且应当应用的操作发生变化的时候,用例层的代码才随之修改。

接口适配器(Interface Adapters)。接口适配器层的主要作用是转换数据,数据从最适合内部用例层和实体层的结构转换成适合外层(比如数据持久化框架)的结构。反之,来自于外部服务的数据也会在这层转换为内层需要的结构。

框架和驱动(Frameworks and Drivers)。最外层由各种框架和工具组成,比如 Web 框架、数据库访问工具等。通常在这层不需要写太多代码,大多是一些用来跟内层通信的胶水代码。这一层包含了所有实现细节,把实现细节锁定在这一层能够减少它们的改动对整个系统造成的伤害。

Done!

概念就这么扯到这里吧,然后看看相应的实现。

Clean Architecture 数据流

上图中的右侧部分表示的是相应的数据流,数据从 Controller 流出,经过 Use Case(用例)的输入端口,然后通过 Use Case 本身,最后通过 Use Case 输出端口返回给 Presenter。

让我们来看一个较为直观的例子:

Clean Architecture 数据流

上图(来源,见参考文章)是一个 Android 应用的数据流示意图。

对于只懂得前端的开发大致说明一下,Android 的 View 和 Presenter 的关系。在前端应用中,我们假设以使用 Component 来表示一个组件,如 Angular 中的 HomepageComponent。而这个 HomepageComponent 中,它必然充满了一些无关于页面显示的逻辑,比如从后端获取显示数据之类的。而 Java 的写法本身是比较臃肿的,所以在 Android 的 Activity 中就会充斥大量的代码。为此,Android 的开发人员们,采用了 MVP 架构,通过 Presenter 来将与显示无关的行为,从 View 中抽离出来。

优缺点

说了,这么多,最后让我们看一下优缺点。优点吧,就这些——笑:

  • 框架无关性。
  • 可被测试。
  • UI 无关性。
  • 数据库无关性。
  • 外部机构(agency)无关性。

除此,还有:

  • 定义了特定功能的代码放在何处
  • 可以在多个项目共享业务逻辑

相应的它还有大量的缺点:

过于复杂。数据需要经过多层处理,Repository 转为 Entity,经过 Usecase 转为 Model,再交由 Presenter 处理,最后交由 View 显示。一个示例如下所示(源自Android-Clean-Boilerplate):

MainActivity ->MainPresenter -> WelcomingInteractor -> WelcomeMessageRepository -> WelcomingInteractor -> MainPresenter -> MainActivity

过度设计。事到如今,我们做了大量的设计,对于一个简单的工程来说,这样的模式可能是过度式的设计。

大量的模板式代码。Usecase、Model 等一系列重复的模板式代码。

陡峭的学习曲线。不用我多说,看这篇文章的长度。

所以,在采用之前,请再次考虑一下,你的应用是否足够的复杂——业务上的复杂度,代码上的复杂度等等。

前端 Clean 架构

说了,这么多,让我们来结合一下前端,设计一下新的前端架构。

客户端 Clean 架构 + MVP

与后端架构相比, Android 的 MVP 架构 + Clean 架构更与前端相似,为此我们再说看看它们结合的一个示例:

Android Clean Architecture

与上一个数据流的相比,这个数据流图更容易落地。其与传统的 MVP(Model-View-Presenter)架构相比:

MVP

基于 Clean Architecture 方案时,则多了一个领域层(图中的 Domain Layer,即业务层),在这一层领域层里,放置的是系统相关的用例(Usecase),而用例所包含的则是相应的业务逻辑。

Clean Architecture + MVP + 组件化

上述的 MVP + Clean Architecture 的架构方式,对于前端应用的架构设计来说,也是相当合适的。稍有不同的是,我们是否有必要将一个组件分为 Presenter + View。以我的角度来说,对于大部分前端应用来说,并没有这么复杂的情况,因为前端有组件化架构。

所以,最后对于我们的前端应用而言,架构如下图所示:

Clean MVP 组件化

这里,只是对于 Presenter 进行更细一步的细化,以真实的模式取代了 MVP 中的 Presenter。

实践

值得注意的是,我们在这里违反了依赖倒置原则。原因是,这里的注入带来了一定的前端复杂度,而这个注入并非是必须的——对于大部分的前端应用而言,只会有单一的数据源,那便是后端数据。

单体式分层架构

在我起初设计的版本里,参照的 Clean Angular 工程(Angular Clean Architecture)里,其采用的是单体式的 Clean Architecture 分层结构:

├── core
│   ├── base				// 基础函数,如 mapper 等
│   ├── domain				// 业务实体
│   ├── repositories        // repositories 模型
│   └── usecases			// 业务逻辑
├── data 					// 数据层
│   └── repository          // 数据源实现
└── presentation            // 表现层

这个实现还是相当不错的,就是过于重视理论——抽象相当的繁琐,导致有点不接地气。我的意思说,没有多少前端人员,愿意按照这个模式来写。

微服务式分层架构

考虑到 usecase 的业务相关性,及会存在大师的 usecase,我便将 usecase 移到了 data 目录——也存在一定的不合理性。后来,我的同事泽杭——一个有丰富的 React 经验前端开发,他提出了 Redux 中的相关结构。最后,我们探讨出了最后的目录结构:

├── core			    // 核心代码,包含基本服务和基础代码
├── domain				// 业务层代码,包含每个业务的单独 Clean 架构内容
│   └── elephant		// 某一具体的业务
├── features			// 公共页面组件
├── presentation  // 有权限的页面
├── pages 				// 公共页面
└── shared				// 共享目录

对应的 elephant 是某一个具体的业务,在该目录下包含了一个完整的 Clean Architecture,相应的目录和文件如下所示:

├── model
│   ├── elephant.entity.ts                    // 数据实体,简单的数据模型,用来表示核心的业务逻辑
│   └── elephant.model.ts                         // 核心业务模型
├── repository
│   ├── elephant.mapper.ts         // 映射层,用于核心实体层映射,或映射到核心实体层。
│   └── elephant.repository.ts                // Repository,用于读取和存储数据。
└── usecases
    └── get-elephant-by-id-usecase.usecase.ts     // 用例,构建在核心实体之上,并实现应用程序的整个业务逻辑。

我一直思考这样的模式是否有问题,直到我看到我司大佬 Martin Fowler 写下的一篇文章《PresentationDomainDataLayering》——终于有人背锅了。文章中提到了这图:

分层

这个分层类似于微服务的概念,在我所熟悉的 Django 框架中也是这样的结构。也因此从理论和实践上不看,并不存在任何的问题。

其它

它不是一颗银弹。使用 MVP 并不妨碍开发人员将 UI 逻辑放在 View 中,使用 Clean Architecture 不会阻止业务逻辑泄漏到表示层。

我们仍然在优化相关的架构中,代码见:https://github.com/phodal/clean-angular

相关

相关文章

目录结构

来源: https://stackoverflow.com/questions/42779871/angular-core-feature-shared-modules-what-goes-where

  • app/shared - This is the module where I keep small stuff that every other module will need. I have 3 submodules there directives, components and pipes, just to keep things organized a little better. Examples: filesize.pipe, click-outside.directive, offline-status.component...
  • app/public - In this module I keep public routes and top-level components. Examples: about.component, contact.component, app-toolbar.component
  • app/core - Services that app needs (and cannot work without) go here. Examples: ui.service, auth.service, auth.guard, data.service, workers.service....
  • app/protected - Similar to public, only for authorized users. This module has protected routes and top-level components. Examples: user-profile.component, dashboard.component, dashboard-sidebar.component...
  • app/features - This is the module where app functionalities are. They are organized in several submodules. If you app plays music, this is where player, playlist, favoritessubmodules would go. If you look at the @angular/material2 this would be an equivalent to their MaterialModule and many submodules, like MdIconModule, MdSidenavModule etc.
  • app/dev - I use this module when developing, don't ship it in production.

相似项目

Clean Architecture 实施指南

在之前的那篇《整洁前端架构》的文章里, 我们介绍了如何在前端领域里使用 Clean Architecture。在过去的几个月里,我们实践了 Clean Architecture 架构,并且实践证明了 Clean Architecture 也可以在前端工作得非常好。

Clean Architecture + MVP + 组件化架构

开始之前,让我们先看一下使用 Clean Architecture 的 Angular 应用的最终架构:

Clean MVP + Componet-based + MVP

图中,我们将架构拆为了这部分来考虑:

  • 数据层。即前端与后端所有交互的处理层,从请求到返回结果,只返回前端需要的值与字段
  • MVP 层。MVP 不是本文的重点,不过有意思的是,在 Angular 应用中,module 层可以与后端 service 对应。而 module 层下的 page,也可以按此来拆分。
  • MVP 中的组件层。对于组件层的合理规划,会使我们的 componet 层也变得 clean,而不仅仅是 domain 层整洁。组件,难的地方是满足场景,而不是拆分、组合与封装。

在这里,我们少了样式层的部分。一来,使用各种 CSS 预处理器来组织代码已经很成熟了;二来,在基础设施完善的今天,CSS 已经没有那么痛了。

与我们旧的架构图相比,我们加入了更多的实施细节:

Clean Architecture

不过,前者是限定条件的,而后者是通用条件下的架构。

有利于实施的上下文

Clean Architecture 并不是银弹,它适合于我们,并不代表它就适合于你们。尤其是如果你习惯自由、自主地项目开发,那么强规范化的 Clean Architecture + Angular 并不一定适合你们。不过,与此同时,如果你的团队规模比较大,并且初级开发者比较多,那么我想规范化能帮助你们减少问题——易于维护。

所以,我先大介绍一些有利于我们的上下文环境:

  • 实施 DDD 的微服务后台架构。
  • 有志于实现全功能团队的成员。

还有其它诸如于初级开发者较多,采用适合于企业(追求规范化)的 Angular 框架——所以规范更多一点,反而更好维护。不过,剩下的这些因素,以于我们的架构来说:有帮助,但并不会太大。

实施 DDD 的微服务后台架构

DDD 只是一套软件开发方法,不同的人理解下的 DDD 自有差异。在特定的情形下,使用 DDD 进行微服务拆分时,每个子域都是一个特定的微服务。在这种模式之下,我们需要有一种命名模式来区分每一种服务,而其中的一种体现方式则是 URL。每个服务有自己特有的 URL 前缀/路径,对应的当这些服务暴露出来时,便可以产出对应的前端领域层——哪怕是没有参加过事件风暴,又或者是领域划分。

诸如于:/api/payment//api/manage/ 都可以清晰地拆分出前端的 domain data 层。

与此同时,若是后端再根据资源路径命名 Controller,诸如于 /api/blog/blog/:id/api/blog/blog/category/:id,那么前端便可以清晰地把它们划分到同一个 repository 之中。当然了,一旦后端设计有问题,前端也能明显地察觉出来。

全功能团队

过去,我在 ThoughtWorks 的某个团队里,采用的是全功能团队的模式——团队成员擅长某一领域,但是会其它领域,比如擅长前端,会点后端。它可以在某种程度上,降低沟通成本。而这意味着,我们有大量的 knowledge transfer 的成本。因此,我们采用了结对编程、让新人讲项目相关的 session。

所以,当你们决定成为一个全功能团队(前后端 + 业务分析都做),那么你们就会遇到这样的问题:

  • 找到应用的前端代码,怎么快速找?
  • 找到对应的后端代码,
  • 前后端模型对应
  • ……

让前后端尽量保持一致,便成为了一种新的挑战。

目录即分层

从某种意义上来说,Clean Architectute 是一种规范化模板化的实施方案。与此同时,它在数据层的三层机制,使得它存在两层防腐层,usecase 可以作为业务的缓冲层,repository 层可以隔离后端服务与模型。

在众多的架构设计之种,分层架构是最容易实施的,因为目录即分层。目录便是一种规范,一看就能看出哪放什么。一旦放错了,也能一眼看出来。

MVP 分层(目录划分)

从目录结果上来说,我们的划分方式和一般的 Angular 应用,并不会有太大的区别。

├── core          // 核心代码,包含基本服务和基础代码
├── domain        // 业务层代码,包含每个业务的单独 Clean 架构内容
  └── elephant   // 某一具体的业务
├── features         // 公共的业务组件
├── presentation  // 业务逻辑页面
├── pages            // 公共页面
└── shared          // 共享目录

我们将:

  1. 业务页面都放到了 presentation 目录
  2. 公共的页面(如 404)都放到了 pages 目录
  3. 业务页面共用的业务组件放到了 features 目录
  4. 剩余的通用部分都放到了 shared 目录,诸如于 pipesutilsservicescomponentsmodules

示例代码见:https://github.com/phodal/clean-frontend

domain + data 层:垂直 + 水平 分层

上述的目录中的 domain 层,示例结构如下所示:

├── model
   ├── elephant.entity.ts                         // 数据实体,简单的数据模型,用来表示核心的业务逻辑
   └── elephant.model.ts                         // 核心业务模型
├── repository
   ├── elephant.mapper.ts                      // 映射层,用于核心实体层映射,或映射到核心实体层。即进行模型转换
   └── elephant.repository.ts                  // Repository,用于读取和存储数据。
└── usecases
    └── get-elephant-by-id-usecase.usecase.ts     // 用例,构建在核心实体之上,并实现应用程序的整个业务逻辑。

相关的解释如上,这里就不 Ctrl + V / Ctrl + C 了。

值得注意的是,我们采用的是垂直 + 水平双分层的模式,垂直应对的是领域服务。它适用于没有 BFF 的微服务架构,尤其是采用 DDD 的微服务后端应用。

映射领域服务

在上一部分中,前端的 domain + layer 层,实际上已经映射了后端的服务。当前端发起一个请求时,它的流程一般是这样的:Component / Controller(前端) -> Usecase -> Repository -> Controller(后端)

对应的返回顺序便是:Controller(后端) -> Repository -> Usecase -> Component / Controller(前端)

为此,我们将 Repository 与后端的 Controller 对应。并且由于服务的简单化,我们的大部分 usecase 也与 repository 中的命名对应。

repository 命名:URL 命名

为了不看后端的代码就可以命名,我们使用 URL 来命名 repository 和 repository 中的方法。如有一个

URL 解释 抽象
/api/blog/blog/:id /API/微服务名/资源名称/资源 ID HTTP 动词 + 资源 + 行为

于是乎,对应的 repository 的名字应该是 blog.repository.ts。对应的 repository 的名字也是 get-blog-by-id。相似的,还有 URL /api/blog/blog/:id/comment 对应的 repository 便是 get-comment-by-blog-id

嗯,是的,和数据库的存取保持一致。

usecase 命名

由于,我们还不涉及复杂的 API,所以常见的行为如下:

  • 常规动词:get / create / update / delete / patch
  • 非常规:search, submit

哈哈,是不是和 repository 相似了。

Clean Architecture 的 MVP 层实践

实际上这里的 MVP 层, 主要内容便是组件化架构。这部分的内容已经在之前的文章(《【架构拾集】组件设计原则》)介绍过了,这里就不详细介绍了。简单的介绍一下就是:

  • 基础组件库,如 Material Design
  • 二次封装组件。额外的三方组件库,如 Infinite Scroller,务必在封装后使用。
  • 自制基础组件。
  • 领域特定组件。

上述的四部分,构建了整个系统的通用组件库模块。随后,便是业务相关组件和页面级组件。

  • 业务相关组件。在不同的模块、页面之间,共享逻辑的组件。
  • 页面级组件。在不同的模块、路由之间,共享页面。

嗯,这部分的东西就这么多了。

Clean Architecture 的 Domain + Data 层实践

嗯,其它部分和正常的项目开发,并没有太大的区别。于是,我们便可以把注意力集中在 Domain + Data 层上。

DDD ApplicationService vs 多个 Usecases

在 DDD 实践中,自然应该采用自顶向下的实现方式。ApplicationService 的实现遵循一个很简单的原则,即一个业务用例对应ApplicationService上的一个业务方法。1

稍微有点不同的是,我们采用的 Clean Architecture 推荐的方式是:一个业务用例(usecase)对应于一个业务类。即,同样的业务场景下,前端是一堆 usecase 文件,而后端是一个applicationService。所以,在这种场景之下,前端有:

  • change-production-count.usecase.ts
  • delete-product.usecase.ts

后端便是 OrderApplicationService.java,其中有多个方法。

usecases + repository vs services

如果我们的 usecases + repository 做的功能,和一个 services 是一样的,那么只使用 serivce 不好吗?只使用 service 会有这样的问题:

  • 存在 API 重复调用的问题
  • 调用划分不清晰

那么,使用 usecase 呢:

  • 更多的模板化代码
  • 更多的分层

Usecases 的复用率极低,项目会因而急剧的增加类和重复代码。因此,我们试图以更多的代码量,来提升架构的可维护性。PS:更多的代码,还可能降低代码的可维护性,不过在 IDE 智能化的今天,这应该不是问题。

Usecases 作为逻辑层/防腐层

不过,Usecases 在带来更多代码的同时,也带来了防腐层。它负责了以下的职责:

  • 业务逻辑处理。在数据传给后端之前,对一些必要的内容进行处理。
  • 返回数据管理。从后端返回的数据里,构建出前端所需要的结果。当需要调用多个 API 时,可以在 usecase 里做这样的工作。
  • 输入参数管理。

也因此,当前端被赋予过重的业务逻辑时,Usecases 层就非常有用。反之,如果逻辑被放置在 BFF 层时,那么 Usecases 层就变得有些鸡肋。但是它仍然是一个非常不错的防腐层。

模型管理

在我们处理 Usecase 的同时,我们就需要解决前端的模型问题。后端,有多个微服务,有多个工程,每个工程有自己的模型。而如果前端只有一个工程,那么前端的模型管理就变成一个痛点。因为在不同的限界上下文里,后端的模型是不一样的。即在不同的 API 里,其模型是不一样的,而这些根据业务定制的模型,最后在前端都聚合到一起。

在这个时候,同个资源有可能有多种不同的模型。因此,要么:

  • 前端拥有一一对应的模型。管理起来比较麻烦。
  • 使用同一个模型。不能使用类型检查来减少 bug。

当前,我也想不到一个更好的解决方法。我们采用的主要是第二种方式,毕竟管理起来方便一些。

以下是我们的几种类型的分类和管理方式:

  • Request Model / Response Model。即请求参数和返回模型(修改过,适用于前端展示),都放在服务的对应的 .model.ts 目录下。
  • Response Entity。直接使用后端返回结果时,名字上带 entity,否则就使用 model
  • View Model / Component Model。适用于业务组件的封装时,传入参数用 model 传入。

你们呢,是否有更好的实践?

相关问题

真的 Clean 吗?还没有

框架依赖的表单验证

由于 Angular 框架本身提供了强大的 Reactive Form 功能,我们在大部分的表单设计时,采用了 Reactive Form,而不是通过 Entity 来验证的方式。这使得我们在这部分的 UI 交互,依赖于 Angular 框架,而非自己实现。

如果采用了诸如 DDD 的 Entity 模式,又或者是采用 validator 的方式。随后,我们还需要开发自己的表单验证模式,类似于此:

{
    validator: RegExp,
    errorMessage: string
}

而它意味着大量的开发成本。不过,好在我们可以尽量将它通用化。

下一步

  • Clean 表单。如上所述。
  • 代码生成。尽管,我们已经在项目中,采用了 Angular Schematics 来生成模板代码。但是相信,下一步我们可以使用工具来生成页面。
  • 架构守护。有了分层结构之后, 要判定层级关系变得更加简单。
  • 其它框架尝试

License

Phodal's Idea

@ 2019 A Phodal Huang's Idea. This code is distributed under the MIT license. See LICENSE in this directory.

Footnotes

  1. https://insights.thoughtworks.cn/backend-development-ddd/

clean-frontend's People

Contributors

0xflotus avatar buptsteve avatar chaslui avatar dependabot[bot] avatar phodal avatar zsy-cn avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

clean-frontend's Issues

请教翻页通信区应该放在哪层

我们一般在做列表查询时,没查询所有的数据,每次只查一段数据,另外会带翻页通信区,请问这种翻页通信区、还有一些通用通信区,一般放在哪一层?谢谢

clean-architecture+domain 垂直分层问题

你好,有个问题请教下:

当使用domain垂直分层的时候,每个domain 相当于redux的结构。对于单个usecase的执行是没问题。但是当用户的一个操作涉及到多个usecase组合的时候,这部分逻辑放在哪里进行呢,例如:用户点击加购物车,弹出领券,领券后,加到用户中心,然后自动将商品加到购物车。这里面涉及劵,用户,购物车多个domain。在哪层完成这些逻辑的组合呢

自己研究的一套微架构,在Angular项目里用,拿出来探讨探讨

目前在写一个大型ERP项目,因为一直用Angular,所以对架构这块也是非常感兴趣的。

你有一个观点是,这套理论很难被开发者应用起来,其实不然,你提出的这种方式确实复杂了一些(个人感觉有点回到了写Java的时代),但让开发大规模用起来也不难,就是用脚本(CLI)生成整套代码。

说出来你可能不信,我现在页面结构,路由,乃至整个项目,都是由一堆脚本来维护的。如果我将来继续从事Angular,这将是我着力要做的事,就是前端自动化运维——把重复的复杂的烦人的工作都交给脚本一键生成/处理。

自研微架构

好了,说回我的设计。说起来也惭愧,本人所在的地区Angular人才极少,所以我也很难与同事探讨一些架构方面的问题,所以闭门造车是免不了的,我的想法也许有问题,但我不怕丢人,说出来我自己反而好受一些。

Store

从 Redux / Vuex 中吸取了一些精华,感觉Angular也不是不能搞这种东西,而且感觉跟Rxjs很像,所以就用Rxjs实现了一套简单的 Store + Status + Mapper 模型:

  • 有一个公共的 Store 池,任何实体都可以从中注入,抽取,观察里面的值。
export class Store {
    // 池子里都是用BehaviorSubject装起来的值,一个键只能对应一个值
    static pool: {[key: string]: BehaviorSubject<any>} = {};
    
    // 用来注入键值
    static set(key, value) {}

    // 可以直接获取值,一般用来不需要观察的业务逻辑里
    static get<T = any>(key): T {}

    // 用来追踪某一个值
    static track<T = any>(key): BehaviorSubject<T> {}

    // 创建一个状态,返回 T 是希望能够保留它的类型,code的时候很是方便
    static createStatus<T = any>(prefix: string, obj: T): T {}
}
  • 每个实体都拥有自己的 Status
export const AppStatus = Store.createStatus('app', // 前缀,因为直接拿key去存,所以需要用前缀做区分
{
    fullscreen: false, // 默认状态
    loading: false ,
    users: [],
});
  • 每个 Status 也可以有自己的 mapper。其实我项目里没有加 Mapper,因为这个 Mapper 可以在 track 的时候写在属性后面,这样就比较灵活,只针对有需求的加 Mapper。(全局的也能加上,容我想想怎么设计)
const AppUsersMapper = map(item => {});
...
users = Store.track<User[]>(AppStatus.users).pipe(AppUsersMapper);

这样就算转起来了,在项目里用着也是相当巴适。

DAO 层

这块是重点,我觉得前后端啊,还是不能完全脱离,必须得有一个东西可以把他们有机得结合起来。

所以我就自研了一个DAO层。

首选说一下组成:

  • DaoInterfaces

    • 这个需要第一个说,为什么呢,这里面存放了与后端数据库表完全相同的接口定义。
    • 这些定义怎么来的呢?问得好。这些Interface都是我用脚本分析转化后端代码生成出来的。
    • 这意味着什么呢?在模型上,我可以完全跟后端保持同步。
    • 这也是我为什么不加 Mapper 的原因,因为没有必要,我直接用后端的结构就可以了。
    • 后端来的数据,结构都在前端定义好了,如果这个数据是全局性的,直接扔Store里就好了,类型一传,美滋滋。
    • 这些 interface 就像原材料,会扔到下面的工厂里进行加工。
  • DaoFactory

    • 这是用来生成Dao对象的工厂。你想,一千个模型,它们与后端交互的方式是相同的(因为我们用的是restfull API),所以只要抽象出一个Dao模型,再套上不同类型的壳,那它不就能适用于所有模型了吗。
export class Dao<T = any> {
    changed = new EventEmitter(); // 每当某些对数据产生影响的行为触发时 会关联触发这个事件

    // 创建对象
    create(data: T) {}
    
    // 删除对象
    remove(id) {}

    // 更新对象
    update(data: T) {}
    
    // 获取单个对象
    get(id) {}

    // 获取列表,DaoQuery 是架构无关的查询条件格式
    list(criteria?: DaoQuery) {}
}

这样一来,一套模型,就可以复刻出千千万万个子模型

  • DaoService
    • 这里面就是存放那千千万万个子模型的地方~
    • 代码类似于下面这个样
export class DaoService {
  private dao: any = {};

  ...

  // dao start 这里也是脚本自动生成的代码

  get User() {
    this.preInit('User');
    return this.dao.User as Dao<User>;
  }
  
  ...

  // dao end

  private preInit(name: string) {
    if (!this.dao[name]) this.dao[name] = this.factory.create(name);
  }
}

// 在视图层就可以直接这样调用
class HomePage {
    // 这样写可能会造成接口重复调用,不推荐
    // users$ = this.dao.User.list();
    users$ = Store.track<User[]>(AppStatus.users);
    users: User[];
    
    constructor(private dao: DaoService) {}

    ngOnInit() {
        this.dao.User.changed.subscribe(params => {
            this.dao.User.list(params).subscribe(resp => {
                Store.set(AppStatus.users, resp.results);
                // 或者直接赋值
                this.users = resp.results;
            });
        });
        this.dao.User.changed.emit();
    }

    addUser() {
        // 添加用户
        this.dao.User.create({...}).subscribe(resp => {...})
    }
}
  • DaoStatus
    • 全局Dao的状态,可以存一些全局的数据

小结

通篇来看,发挥作用的有两个重要的点

  • 前端与后端的约定 > 前端自己再造新的模型(好的后端才能有好的前端)
  • 重复的工作交给脚本去做,这样你能省掉大量的时间

嗯,目前就这些,非常欢迎大家能一起探讨一下~ 献丑了~

关于Clean Architecture + MVP + 组件化

Clean Architecture + MVP + 组件化
这章节没看懂, 所谓组件化只是把presenter层划分成了若干组件?
而每个组件里又有controller 和model,model这块不应该属于data层吗

可能文字有误

写的很好,我们前端引入框架,越来越复杂,现在也在找处理办法

https://github.com/phodal/clean-frontend#全功能团队

过去,我在 ThoughtWorks 的某个团队里,采用的是全功能团队的模式——团队成员擅长某一领域,但是会其它领域,比如擅长前端,后点后端。它可以在某种程度上,降低沟通成本。而这意味着,我们有大量的 knowledge transfer 的成本。因此,我们采用了结对编程、让新人讲项目相关的 session。

后点后端 是不是 会点后端

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.