关于前端:前端分层架构实践心得

38次阅读

共计 5020 个字符,预计需要花费 13 分钟才能阅读完成。

文章首发于我的博客 https://github.com/mcuking/bl…
最近笔者在应用 DDD / Clean Architecture 思维开发公司外部应用的 CRM,感觉这种分层架构能够解决目前遇到的问题,所以决定对目前开源的挪动端最佳实际我的项目进行重构,上面是该我的项目对于分层架构方面的阐明。想理解更多内容请查看源码:
https://github.com/mcuking/mo…

架构分层

目前前端开发次要是以单页利用为主,当利用的业务逻辑足够简单的时候,总会遇到相似上面的问题:

  • 业务逻辑过于集中在视图层,导致多平台无奈共用本应该与平台无关的业务逻辑,例如一个产品须要保护 Mobile 和 PC 两端,或者同一个产品有 Web 和 React Native 两端;
  • 产品须要多人合作时,每个人的代码格调和对业务的了解不同,导致业务逻辑散布横七竖八;
  • 对产品的了解停留在页面驱动层面,导致实现的技术模型与理论业务模型出入较大,当业务需要变动时,技术模型很容易被捣毁;
  • 过于依赖前端框架,导致如果重构进行框架切换时,须要重写所有业务逻辑并进行回归测试。

针对下面所遇到的问题,笔者学习了一些对于 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– 常见模式(译)

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

前端开发 - 畛域驱动设计

畛域驱动设计在前端中的利用

正文完
 0