乐趣区

关于前端:Midway-后端代码的设计建议

Midway 是阿里巴巴外部开发的基于 TypeScript 的 Node.js 研发框架,在团体外部,因为其集成了外部的各类根底服务与稳定性监控,同时反对 FaaS 函数部署,所以是外部 Node.js 利用研发的首选框架。

尽管 Midway 联合了面向对象(OOP + Class + IoC)与函数式(FP + Function + Hooks)两种编程范式,但思考到个别我的项目大多采纳面向对象的开发方式,所以本文也重点论述针对面向对象这种范式,在工程开发中能够参考的代码设计。

基于 MVC 的工程目录设计

在 Midway 工程开发中,个别倡议采纳如下工作目录组织业务代码。config 目录中的代码内容,依据本身须要,联合官网的配置文档即可正确规范的实现配置,在本文中不做过多解说。

- config  配置文件目录,寄存不同环境的差别配置信息
- constant 常量寄存目录,寄存业务常量及国际化业务文案
- controller 控制器寄存目录,寄存外围业务逻辑代码
- dto 数据传输对象目录,寄存内部数据的校验规定
- entity 数据实体目录,寄存数据库汇合对应的数据实体
- middleware 中间件目录,寄存我的项目中间件代码
- service 服务逻辑寄存目录,存放数据存储、部分通用逻辑代码
- util 工具代码寄存目录,寄存业务通用工具办法

当申请进入时,各目录对应的代码施展了如下的性能作用:

  1. Middleware 作为起始逻辑进行通用性逻辑执行。
  2. 接着 DTO 层对参数进行校验。
  3. 参数校验无异样进入 Controller 执行整体业务逻辑。
  4. 数据库的调用,或者整体性比拟强的通用业务逻辑会被封装到 Service 层不便复用。
  5. 工具办法、常量、配置和数据库实体则作为工程的底层撑持,向 Service 或 Controller 返回数据。
  6. Controller 吐出响应后果,如果响应异样,Middleware 进行逻辑兜底。
  7. 最终吐出响应数据返回给用户。

整顿一下,你能够这么分类,在 MVC 中,C 层对应为 Middleware + DTO + ControllerM 层对应为 Service;V 层 因为个别后端只提供对外的接口,不会有太多动态页面透出,所以 临时能够疏忽。当然,Service 层有肯定的边界混同,它不仅仅只蕴含 Model 模型层,否则咱们就间接起名成 Model 层好了,在 Service 中,我也会把一些可形象、可复用的逻辑放入其中,来缓解一下简单业务中 Controller 逻辑过于繁琐的问题。

理解上述 Midway 代码目录的设计思考后,就别离对每一个局部开展代码设计上的一些教训分享。

Middleware 层的代码倡议

在开发中,业务中间件能够自行设计开发,这依赖于你的业务诉求。然而,代码执行异样,能够通过下述计划比拟优雅的实现解决。

异样兜底容错中间件

代码执行异样,是指在执行业务代码过程中,可能产生的执行谬误。一般来说,为了解决这种潜在的危险,咱们能够在逻辑外层减少 try catch 语句进行解决。在很多工程中,因为为了做异样解决,减少了大量的 try catch 语句;还有很多工程中,没有思考异样解决的问题,基本就没有做 try catch 的兜底容错。

// 以下代码短少异样兜底冗错
@Get('/findOne')@Validate()
async findOne(@Query(ALL) appParams: AppFindDTO) {const { id} = appParams;
  const app = await this.appService.findOneAppById(id);
  return getReturnValue(true, app);
}

// 以下代码每个函数都要有一个 try catch 包裹
@Get('/findOne')@Validate()
async findOne(@Query(ALL) appParams: AppFindDTO) {
  try {const { id} = appParams;
    const app = await this.appService.findOneAppById(id);
    return getReturnValue(true, app);
  } catch(e) {return getReturnValue(false, e.message);
  }
}

应用中间件,就能够解决下面的两个问题,你能够编写如下中间件:

@Provide('errorResponse')
@Scope(ScopeEnum.Singleton)
export class ErrorResponseMiddleware {resolve() {return async (ctx: FaaSContext, next: () => Promise<any>) => {
      try {await next();
      } catch (error) {
        ctx.body = getReturnValue(
          false,
          null,
          error.message || '零碎产生谬误,请分割管理员'
        );
      }
    };
  }
}

将这段中间件代码退出到程序的执行逻辑中,编写代码时,你就无需再关注代码执行异样的问题,中间件会帮你捕捉程序执行异样并标准化返回。同时,你也能够在这里对立做异样的日志收集或实时预警,扩大更多的性能。所以这个中间件设计,强烈推荐在工程中对立应用。

DTO 层的代码倡议

DTO 层,也就是数据传输对象层,在 Midway 中,次要是通过它来对 POST、GET 等申请的申请参数进行校验。在实际的过程中,有两方面的问题须要在设计中着重关注:正当的代码复用、明确的代码职责划分。

正当的代码复用

首先咱们看一下不合理的 DTO 层的代码设计:

// 第一种问题:// 分页的校验,看起来很难懂,将来很多中央都要用,这么写无奈复用
export class AppsPageFindDTO {@Rule(RuleType.string().required())
  siteId: number;
  
  @Rule(RuleType.number().integer().empty('').default(1).greater(0))
  pageNum: number;

  @Rule(RuleType.number().integer().empty('').default(20).greater(0))
  pageSize: number;
}

// 第二种问题
// 对参数的校验,自身应该是 DTO 层面校验的,放到业务中不合理
// 同时,对逗号距离的 id 进行校验,这是常见性能,放在这难以复用
@Get('/findMany')@Validate()
async findMany(@Query(ALL) appParams: AppsFindDTO) {const { ids} = appParams;
  const newIds = ids.split(',');
  if(!Array.isArray(newIds)) {return getReturnValue(false, null, 'ids 参数不符合要求');
  }
  const app = await this.appService.findOneAppByIds(newIds);
  return getReturnValue(true, app);
}

倡议应用如下的形式进行 DTO 层的代码编写,首先对可复用的常见规定进行封装:

// 必填字符串规定
export const requiredStringRule = RuleType.string().required();
// 页码校验规定
export const pageNoRule = RuleType.number().integer().default(1).greater(0);
// 单页显示内容数量校验规定
export const pageSizeRule = RuleType.number().integer().default(20).greater(0);

// 逗号距离的 id 进行校验的规定扩大,起名为 stringArray
RuleType.extend(joi => ({base: joi.array(),
  type: 'stringArray',
  coerce: value => ({value: value.split ? value.split(',') : value,
  }),
}));

接着在你的 DTO 定义文件中,代码就能够精简为:

// 分页的校验的逻辑能够精简为这种写法
export class AppsPageFindDTO {@Rule(requiredStringRule)
  siteId: number;
  @Rule(pageNoRule)
  pageNum: number;
  @Rule(pageSizeRule)
  pageSize: number;
}

// 逗号距离的 id 字符串校验,能够改为如下写法
export class AppsFindDTO {@Rule(RuleType.stringArray())
  ids: number;
}

@Get('/findMany')@Validate()
async findMany(@Query(ALL) appParams: AppsFindDTO) {const { ids} = appParams;
    const app = await this.appService.findOneAppByIds(ids);
    return getReturnValue(true, app);
}

比起初始的代码,要精简十分多,而且所有的校验规定,都能够将来复用,这是比拟举荐的 DTO 层代码设计。

明确的职责划分

DTO 的外围职责是对入参进行校验,它的职责仅限于此,然而很多时候,咱们能看到这样的代码:

// Controller 层代码逻辑
@Post('/createRelatedService')
@Validate()
async createRelatedService(@Body(ALL) requestBody: AppRelationSaveDTO) {
  // 判断以后站点和利用的关联是否存在
  const appRelation = new AppRelation();
  const saveResult = await this.appServiceService.saveAppRelation(
    appRelation,
    requestBody,
  );
  return getReturnValue(true, saveResult);
}

// Service 层的代码逻辑
async saveAppRelation(
  relation: AppRelation,
  params: AppRelationSaveDTO,
) {const { appId, serviceId} = params;
  appId && (relation.appId = appId);
  serviceId && (relation.serviceId = serviceId);
  const result = await this.appServiceRelation.save(relation);
  return result;
}

在 Service 层中的办法中,应用了 AppRelationSaveDTO 这个 DTO 作为 Typescript 的类型来帮忙做代码类型校验。这段代码问题在于,让 DTO 层承当了数据校验外的额定职责,自身 Service 层关注数据怎么存,当初 Service 层还要关注内部数据怎么传,很显然代码职责就比拟凌乱。

优化的形式也很简略,咱们能够改良一下代码:

// Controller 层代码逻辑
@Post('/createRelatedService')
@Validate()
async createRelatedService(@Body(ALL) requestBody: AppRelationSaveDTO) {
  // 判断以后站点和利用的关联是否存在
  const {appId, serviceId} = requestBody;
  const appRelation = new AppRelation();
  const saveResult = await this.appServiceService.saveAppRelation(
    appRelation,
    appId,
    serviceId
  );
  return getReturnValue(true, saveResult);
}

// Service 层的代码逻辑
async saveAppRelation(
  relation: AppRelation,
  appId: string,
  serviceId: stirng
) {const { appId, serviceId} = params;
  appId && (relation.appId = appId);
  serviceId && (relation.serviceId = serviceId);
  const result = await this.appServiceRelation.save(relation);
  return result;
}

Service 层的参数类型,不再应用 DTO 进行形容,代码逻辑很清晰:Controller 层负责摘取必要数据;Service 层,负责拿到必要的数据进行增删改查即可;而 DTO 层,也只承当数据校验的职责。

管制层和服务层的代码倡议

Controller 和 Service 层的设计倡议可能会有比拟大的争议,这里仅表白一下集体的观点:Controller 是控制器,所以业务逻辑都应该放在 Controller 中进行编写,Service 层作为服务层,应该把形象积淀的逻辑放在其中(比如说数据库操作,或者复用性代码)。也就是说,Controller 层应该寄存业务定制的一次性逻辑,而 Service 层则寄存可复用性的业务逻辑

管制层和服务层的职责明确

围绕这个思路,给一个优化代码的设计例子供参考:

// 控制器层代码
@Post('/create')
@Validate()
async update(@Body(ALL) appBody: AppSaveDTO) {const { code, name, description} = appBody;
  const saveResult = await this.appService.saveApp(code, name, description);
  return getReturnValue(true, saveResult);
}

// 服务层代码
async saveApp(code: sting, name: string, description: string) {const app = await this.findOneAppByCode(code);
  app.code = code;
  app.name = name;
  app.description = description;
  const result = await this.appModel.save(app);
  return result;
}

这段代码,其实是要更新一条信息,而且一下子必须更新 code,name 和 description,这样做 Service 层其实是和 Controller 有耦合的,到底怎么存实际上是业务逻辑,应该由 Controller 来决定,所以倡议批改成如下代码:

// 控制器层代码
@Post('/create')
@Validate()
async update(@Body(ALL) appBody: AppSaveDTO) {const { code, name, description} = appBody;
  const app = await this.appService.findOneAppByCode(code);
  app.code = code;
  app.name = name;
  app.description = description;
  const saveResult = await this.appService.saveApp(app);
  return getReturnValue(true, saveResult);
}

// 服务层代码
async saveApp(app: App) {const result = await this.appModel.save(app);
  return result;
}

这样写,绝对于之前的代码,Controller 更聚焦业务;Service 更聚焦服务,而且可能失去更好的复用。这是在控制器和服务层写代码时能够参考的设计思路。

控制器层和服务层一对一匹配

在编写 Midway 代码的时候,存在这样的一种灵活性:控制器能够调用多个服务,而服务之间也能够相互调用。也就是说,服务层的一段代码,可能在任何的控制器中被调用,也可能在任何的服务层被调用。这种比拟强的灵便度,最终肯定会导致代码的层次结构不清晰,编码方式不对立,最终导致系统可维护性削弱。

为了躲避适度灵便可能带来的问题,咱们能够从标准上进行肯定的束缚。目前我的想法是,控制器只调用本人的服务层,如果须要其余服务层的能力,在本人的服务层进行转发。这样做后,一个服务层的代码,只能被本人的控制器调用,或者被其余的服务层调用,调用的灵便度从 N2 升高到 N,代码也就绝对更可控。

仍然通过代码举例来说:

// 控制器中的函数办法
@Post('/create')
@Validate()
async create(@Body(ALL) siteBody: SiteCreateDTO) {
  // 用到一个 ACL 的服务层
  const hasPermission = await this.aclService.checkManagePermission('site');
  if (!hasPermission) {return getReturnValue(false, null, '您无权限,无奈创立');
  }
  const {name, code} = siteBody;
  const site = new Site();
  site.name = name;
  site.code = code;
  // 用到本身的服务层
  const result = await this.siteService.createSite(site);
  return getReturnValue(true, result);
}

如果代码这样设计,业务代码中,用到 ACL 的服务,校验权限,那么随着业务的倒退,aclService 层可能会耦合越来越多的定制逻辑,因为所有的权限校验都由着一个办法提供,如果调用场景多,必定会存在定制化需要。

所以更正当、更可扩大的代码能够扭转成上面的样子:

// 控制器中的函数办法
@Post('/create')
@Validate()
async create(@Body(ALL) siteBody: SiteCreateDTO) {
  // 用到本身的服务层
  const hasPermission = await this.siteService.checkManagePermission();
  if (!hasPermission) {return getReturnValue(false, null, '您无权限,无奈创立');
  }
  const {name, code} = siteBody;
  const site = new Site();
  site.name = name;
  site.code = code;
  // 用到本身的服务层
  const result = await this.siteService.createSite(site);
  return getReturnValue(true, result);
}

// 本身服务层的代码
async checkManagePermission(): Promise<boolean> {const hasPermission = await this.aclService.checkUserPermission('site');
  return hasPermission;
}

在本身的服务层,减少一层转发代码,不仅能够束缚代码的灵便度,当定制性逻辑减少的时候,也能够间接在这里扩大,所以是一种更正当的代码设计。

数据库查问的代码设计

应用逻辑表关联

在 Midway 中,集成的 TypeORM 的数据库框架,外面提供了 OneToOne,OneToMany 这样的数据库操作语法,帮忙你主动生成 Join 语句,治理表之间的关联。

但在业务零碎中,我不倡议应用这种间接的表连贯语句,因为这很容易产生慢 SQL,影响零碎的性能,所以倡议在数据库操作中,对立采纳逻辑表关联的形式进行关联数据查问,这里间接给出代码例子:

@Get('/findRelatedServices')
  @Validate()
  async findRelatedServices(@Query(ALL) appParams: AppServicesFindDTO) {const { id} = appParams;
    // 寻找关联关系内容
    const relations = await this.appService.findAppRelatedServices(id);
    // 从关联关系中找到另一张表关联的 id 合集
    const serviceIds = relations.map(item => item.serviceId);
    // 去另一张表取数据拼装
    const services = await this.appService.findServicesByIds(serviceIds);
    // 返回最终数据
    return getReturnValue(true, {services});
  }

尽管这种查问,绝对于 Join,代码更多,然而逻辑全副在代码中体现,而且性能很好,所以在开发中,举荐应用这种数据库操作的设计。

常量的用法

常量在服务端开发中十分罕用,通过常量语义化的表述一些枚举,这种根底内容不再累述,次要讲一下应用常量治理业务提醒的想法。

业务提醒文案抽离

简单的我的项目,最终有可能走向国际化的路线,如果在代码中,写死的文字提醒太多,最初做国际化,还是要投入精力批改,所以不如在开发开始,就对我的项目做一个提前准备,很简略,你只有把所有的文字提醒抽离到常量文件里治理就能够了。

// 不举荐这种写法,文字和业务耦合
@Post('/create')
@Validate()
async create(@Body(ALL) appBody: AppSaveDTO) {const { code, name} = appBody;
  const appWithSameCode = await this.appService.findOneAppByCode(code);
  if (appWithSameCode) {
    // 文字和业务耦合在一起
    return getReturnValue(false, null, 'code 已存在,无奈反复创立!');
  }
  const app = new App();
  const saveResult = await this.appService.saveApp(app, name);
  return getReturnValue(true, saveResult);
}

// 举荐这种写法,文字和业务解耦
@Post('/create')
@Validate()
async create(@Body(ALL) appBody: AppSaveDTO) {const { code, name} = appBody;
  const appWithSameCode = await this.appService.findOneAppByCode(code);
  if (appWithSameCode) {
    // 文字拆离到常量中治理,实现解耦
    return getReturnValue(false, null, APP_MESSAGES.CODE_EXIST);
  }
  const app = new App();
  const saveResult = await this.appService.saveApp(app, name);
  return getReturnValue(true, saveResult);
}

很小的一个改变,就能够让你的代码看起来有很大的变动,十分倡议应用这个技巧。

总结

在简单的我的项目开发中,抉择好开发框架只是第一步,真正把代码写好才是最艰难的事件,本篇文章总结了过来一年在应用 Midway 框架开发过程中,我对如何写好服务端代码本人的一些思考和编码技巧,心愿可能对你有肯定的启发,如果有挑战获疑难,欢送留言探讨。

作者:ES2049 | Dell

文章可随便转载,但请保留原文链接。
十分欢送有激情的你退出 ES2049 Studio,简历请发送至 caijun.hcj@alibaba-inc.com。

退出移动版