前端分层架构实践心得(附开源代码)

最近笔者在使用 DDD / Clean Architecture 思想开发公司内部使用的 CRM,以为这种分层架构能够解决目前遇到的问题,因此决定对目前开源的移动端最佳实践项目进行重构,下面是该项目关于分层架构方面的说明。想了解更多内容请查看源码: github.com/mcuking/mob…html

应用介绍

首先介绍下本项目的应用,是一个交互简洁的 Todo 应用,应用取名叫 Memo,Memory 的简写,参考了微软的 To Do 以及 Listify、Trello 等应用。不过最大的不一样是,项目并不依赖后端,而是使用浏览器提供的 indexDB 进行数据的存储,能够保证数据的绝对安全。另外更新应用也不会清除原来的数据,除非将应用卸载。效果图以下:前端

体验平台 二维码 连接
Web 点击体验
Android 点击体验

架构分层

目前前端开发主要是以单页应用为主,当应用的业务逻辑足够复杂的时候,总会遇到相似下面的问题:vue

  • 业务逻辑过于集中在视图层,致使多平台没法共用本应该与平台无关的业务逻辑,例如一个产品须要维护 Mobile 和 PC 两端,或者同一个产品有 Web 和 React Native 两端;react

  • 产品须要多人协做时,每一个人的代码风格和对业务的理解不一样,致使业务逻辑分布杂乱无章;git

  • 对产品的理解停留在页面驱动层面,致使实现的技术模型与实际业务模型出入较大,当业务需求变更时,技术模型很容易被摧毁;github

  • 过于依赖前端框架,致使若是重构进行框架切换时,须要重写全部业务逻辑并进行回归测试。web

针对上面所遇到的问题,笔者学习了一些关于 DDD(领域驱动设计)、Clean Architecture 等知识,并收集了相似思想在前端方面的实践资料,造成了下面这种前端分层架构:后端

其中 View 层想必你们都很了解,就不在这里介绍了,重点介绍下下面三个层的含义:浏览器

Services 层

Services 层是用来对底层技术进行操做的,例如封装 AJAX 请求,操做浏览器 cookie、locaStorage、indexDB,操做 native 提供的能力(如调用摄像头等),以及创建 Websocket 与后端进行交互等。安全

其中 Services 层又可细分出 request 层和 translator 层, request 层主要是实现 Services 的大部分功能。而 translator 层主要用于清洗从服务端或客户端接口返回的数据:删除部分数据、修改属性名、转化部分数据等,通常可定义成纯函数形式。下面以本项目实际代码为例进行讲解。

从后端获取 quote 数据:

export class CommonService implements ICommonService {
  @m({ maxAge: 60 * 1000 })
  public async getQuoteList(): Promise<IQuote[]> {
    const {
      data: { list }
    } = await http({
      method: 'post',
      url: '/quote/getList',
      data: {}
    });

    return list;
  }
}
复制代码

向客户端日历中同步 Note 数据:

export class NativeService implements INativeService {
  // 同步到日历
  @p()
  public syncCalendar(params: SyncCalendarParams, onSuccess: () => void): void {
    const cb = async (errCode: number) => {
      const msg = NATIVE_ERROR_CODE_MAP[errCode];

      Vue.prototype.$toast(msg);

      if (errCode !== 6000) {
        this.errorReport(msg, 'syncCalendar', params);
      } else {
        await onSuccess();
      }
    };

    dsbridge.call('syncCalendar', params, cb);
  }
  ...
}
复制代码

从 indexDB 读取某个 Note 详情数据:

import { noteTranslator } from './translators';

export class NoteService implements INoteService {
  public async get(id: number): Promise<INotebook | undefined> {
    const db = await createDB();

    const notebook = await db.getFromIndex('notebooks', 'id', id);
    return noteTranslator(notebook!);
  }
}
复制代码

其中,noteTranslator 就属于 translator 层,用于订正接口返回的 note 数据,定义以下:

export function noteTranslator(item: INotebook) {
  // item.themeColor = item.color;
  return item;
}
复制代码

另外咱们能够拓宽下思路,当后端 API 仍在开发的时候,咱们可使用 indexDB 等本地存储技术进行模拟,创建一个 note-indexDB 服务,先提供给上层 Interactors 层进行调用,当后端 API 开发好后,就能够建立一个 note-server 服务,来替换以前的服务。只要保证先后两个服务对外暴露的接口一致,另外与上层的 Interactors 层没有过分耦合,便可实现快速切换。

Entities 层

实体 Entity 是领域驱动设计的核心概念,它是领域服务的载体,它定义了业务中某个个体的属性和方法。例如本项目中 Note 和 Notebook 都是实体。区分一个对象是不是实体,主要是看他是否有惟一的标志符(例如 id)。下面是本项目的实体 Note:

export default class Note {
  public id: number;
  public name: string;
  public deadline: Date | undefined;
  ...

  constructor(note: INote) {
    this.id = note.id;
    this.name = note.name;
    this.deadline = note.deadline;
    ...
  }

  public get isExpire() {
    if (this.deadline) {
      return this.deadline.getTime() < new Date().getTime();
    }
  }

  public get deadlineStr() {
    if (this.deadline) {
      return formatTime(this.deadline);
    }
  }
}
复制代码

经过上面的代码能够看到,这里主要是以实体自己的属性以及派生属性为主,固然实体自己也能够具备方法,用于实现属于实体自身的业务逻辑(笔者认为业务逻辑能够分为两部分,一部分业务逻辑属于跟实体强相关的,应该经过在实体类中的方法实现。另外一部分业务逻辑则更多的是实体之间的业务,则能够放在 Interactors 层中实现)。只是本项目中尚未涉及,在这里就不做更多说明了,有兴趣的能够参考下面列出来的笔者翻译的文章:可扩展的前端#2--常见模式(译)

另外笔者认为并非全部的实体都应该按上面那样封装成一个类,若是某个实体自己业务逻辑很简单,就没有必要进行封装,例如本项目中 Notebook 实体就没有作任何封装,而是直接在 Interactors 层调用 Services 层提供的 API。毕竟咱们作这些分层最终的目的就是理顺业务逻辑,提高开发效率,因此没有必要过于死板。

Interactors 层

Interactors 层是负责处理业务逻辑的层,主要是由业务用例组成。通常状况下 Interactor 是一个单例,它使咱们可以存储一些状态并避免没必要要的 HTTP 调用,提供一种重置应用程序状态属性的方法(例如:在失去修改记录时恢复数据),决定何时应该加载新的数据。

下面是本项目中 Common 的 Interactors 层提供的公共调用的业务:

class CommonInteractor {
  public static getInstance() {
    return this._instance;
  }

  private static _instance = new CommonInteractor(new CommonService());

  private _quotes: any;

  constructor(private _service: ICommonService) {}

  public async getQuoteList() {
    // 单例模式下,将一些基本固定不变的接口数据保存在内存中,避免重复调用
    // 但要注意避免内存泄露
    if (this._quotes !== undefined) {
      return this._quotes;
    }

    let response;

    try {
      response = await this._service.getQuoteList();
    } catch (error) {
      throw error;
    }

    this._quotes = response;
    return this._quotes;
  }
}
复制代码

经过上面的代码能够看到,Sevices 层提供的类的实例主要是经过 Interactors 层的类的构造函数获取到,这样就能够达到两层之间解耦,实现快速切换 service 的目的了,固然这个和依赖注入 DI 仍是有些差距的,不过已经知足了咱们的需求。

另外 Interactors 层还能够获取 Entities 层提供的实体类,将实体类提供的与实体强相关的业务逻辑和 Interactors 层的业务逻辑融合到一块儿提供给 View 层,例如 Note 的 Interactors 层部分代码以下:

class NoteInteractor {
  public static getInstance() {
    return this._instance;
  }

  private static _instance = new NoteInteractor(
    new NoteService(),
    new NativeService()
  );

  constructor( private _service: INoteService, private _service2: INativeService ) {}

  public async getNote(notebookId: number, id: number) {
    try {
      const note = await this._service.get(notebookId, id);
      if (note) {
        return new Note(note);
      }
    } catch (error) {
      throw error;
    }
  }
}
复制代码

固然这种分层架构并非银弹,其主要适用的场景是:实体关系复杂,而交互相对模式化,例如企业软件领域。相反实体关系简单而交互复杂多变就不适合这种分层架构了。

在具体业务开发实践中,这种领域模型以及实体通常都是有后端同窗肯定的,咱们须要作的是,和后端的领域模型保持一致,但不是同样。例如同一个功能,在前端只是一个简单的按钮,而在后端则可能至关复杂。

另外须要明确的是,架构和项目文件结构并非等同的,文件结构是你从视觉上分离应用程序各部分的方式,而架构是从概念上分离应用程序的方式。你能够在很好地保持相同架构的同时,选择不一样的文件结构方式。没有完美的文件结构,所以请根据项目的不一样选择适合你的文件结构。

最后引用蚂蚁金服数据体验技术的《前端开发-领域驱动设计》文章中的总结做为结尾:

要明白,驱动领域层分离的目的并非页面被复用,这一点在思想上必定要转化过来。领域层并非由于被多个地方复用而被抽离。它被抽离的缘由是:

  • 领域层是稳定的(页面以及与页面绑定的模块都是不稳定的)
  • 领域层是解耦的(页面是会耦合的,页面的数据会来自多个接口,多个领域)
  • 领域层具备极高复杂度,值得单独管理(view 层处理页面渲染以及页面逻辑控制,复杂度已经够高,领域层解耦能够轻 view 层。view 层尽量轻量是咱们架构师 cnfi 主推的思路)
  • 领域层以层为单位是能够被复用的(你的代码可能会抛弃某个技术体系,从 vue 转成 react,或者可能会推出一个移动版,在这些状况下,领域层这一层都是能够直接复用)
  • 为了领域模型的持续衍进(模型存在的目的是让人们聚焦,聚焦的好处是增强了前端团队对于业务的理解,思考业务的过程才能让业务前进)

推荐几个相关的类库:

react-clean-architecture

business-rules-package

ddd-fe-demo

推荐几篇相关文章:

前端架构-让重构不那么痛苦(译)

可扩展的前端#1--架构基础(译)

可扩展的前端#2--常见模式(译)

领域驱动设计在互联网业务开发中的实践

前端开发-领域驱动设计

领域驱动设计在前端中的应用

PS:

移动 web 最佳实践项目接下来的计划:

实践 APP 离线包技术,即将前端静态资源提早集成到客户端中,能够将网页的网络加载时间变为 0,极大提高应用的用户体验。

计划在今年年末以前完成,因这个方案会涉及到前端、客户端以及后端,尤为是客户端工做量较大,因此会花费较长周期,届时会开源整个方案的全部端代码,敬请期待。

相关文章
相关标签/搜索