一套 UI 两套接口,如何利用设计模式优雅设计

2,083 阅读7分钟

前言

哎,最近摸鱼比较多,导致需求给延期了 0.5d,下次摸鱼之前,一定先把需求拆分好,并且看看你的队友(前后端)的设计以及实现。否则到时候全是坑,埋的是自己。。

下次还要起摸鱼的开头,比较适合我,哈哈哈哈哈哈。进入正题!!!

业务背景

最近做的这个需求,类似于图片、视频(素材)的后台管理模块,实现起来很简单,筛选列表以及数据展示列表。But...问题不在于页面的业务逻辑,而是后端接口设计问题。。下文称为素材代表图片和视频页面的统称

FBI Warning!

这个需求就是我通过自己的奇淫妙想想到的那个毛玻璃实现图片预览图的需求,你说巧了不是?一个需求,搞到两个知识点并且和大家分享出来了~~

那么下面,咳咳,我继续用了之前给大家画的图,需求原型类似下面这样!!

image

比如目前有素材的上传、下载、筛选项、列表、导出、下载等。

对应到后端就有了 12 个接口

  • 图片上传
  • 图片下载
  • 图片的筛选项
  • 图片的列表
  • 图片的导出
  • 图片的下载
  • 视频上传
  • 视频下载
  • 视频的筛选项
  • 视频的列表
  • 视频的导出
  • 视频的下载

以上。。。可是前端页面的 UI 几乎完全一致,只需要区分视频图片的文案以及在请求对应的接口时,需要对应模块的前缀,例如图片是 image ,视频是 video 等等

实现之前

由于这个需求是之前一个系统里类似的模块,后端直接拿来使用,前端的 UI 改变了,而且之前代码写的比较臃肿且技术栈比较落后(先甩个锅,我来公司之前就有的模块)。难以维护,基于这两点,我肯定是需要重写的。

但是当我看到后端给我的接口文档时,我当时是拒绝的。。我希望后端可以统一接口,对前端暴露的字段以及接口最简单化(不然的话,你数据库给我,我直接增删改查数据库不也行吗?),可是后端老大哥给我的拒绝拒绝了。已经实现了(之前系统代码拿过来用的),不能改了。

image

设计前端中间层

那既然后端老大哥不给改,咱就当锻炼咱们自己的能力了,你说是不?那就 nèng 吧!

在前一段时间还看了曾探大佬的《JavaScript 设计模式与开发实践》,受益良多。推荐!!!

首先为了复用以及组件的灵活性,将和其他模块类似的公共组件抽离出去,这里就不多说了。一个 SearchBar 组件以及一个 CardList 组件,供各个页面级组件来调用就好,自己传入对应的选项即可。主要来设计一下如何实现一套 UI 两套接口的中间层组件

废话不多说,直接上代码以及对应思路

思路

  1. 页面根据不同的类型来区分视频和图片就好,扩展性较好,例如之后加了音频那就在加一个类型而已。通过枚举来将类型枚举出来,例如需要改枚举,只需要更改一个地方,整个系统都会变更
  2. 需要用到中介者模式,来将控制两个不同模块的展现以及对应的封装
  3. 利用原型技术实现职责链模式,动态统一封装接口
  4. 动态拼接(重写)参数来实现后端的不同字段参数传递

实现

  1. 页面根据不同的类型来区分比较好实现,你是通过 Props 或者是 URL 的 query 都可以。目前我是通过组件的 Props 来传递的
export default function ImageManagement() {
  return <MaterialManagement type={MATERIAL_TYPE.IMAGE} />
}

  1. 利用中介者模式,封装统一的行为

中介者模式:中介者模式的作用就是解除对象与对象之间的紧耦合关系。 《JavaScript 设计模式与实践》 -- 曾探

function MediatorPattern(this: any, type: number): any {
  this.type = type
  // 先区分 image 例如后期加入其他类型,可以改成枚举形式,进行添加
  // 这里只有 image 和 video 所以先区分一个即可
  this.isImage = this.type === MATERIAL_TYPE.IMAGE
  this.prefix = this.isImage ? 'image' : 'video'
  this.text = this.isImage ? '图片' : '视频'
  this.method = this.isImage ? imageAPI : videoAPI
  
  
  // 业务使用必须用大写开头作为字段才可以认为是需要重写的参数
  // 在最后一步,动态拼接参数时需要使用到
  this.dynamicParams = [
  	// ...
    'Size',
    // ...
  ]
}

我们的 UI 组件在这里其实不需要关心他到底是图片还是视频还是音频还是谁谁谁,我们只需要处理特定的,对应的逻辑即可。比如日后又增加了好几种类型,我们完全可以不用动 UI 层面的任何逻辑,只需要维护中介者对象即可。让所有类型之间的耦合断开。

  1. 利用原型技术实现职责链模式,动态统一封装接口

例如我现在需要一个列表的接口,目前通过中介者模式已经将我需要的方法塞入 this.method 里面了,这个时候我们就根据这里的方法去调用对应的接口即可,但是我们并不知道当前是图片还是视频的接口,所以需要职责链模式来做处理

职责链模式:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间 5 的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。 《JavaScript 设计模式与实践》 -- 曾探

// 获取图片 / 视频列表
MediatorPattern.prototype.getList = function (params: any) {
  const newParams = this.rewriteParams(params)
  // 职责链模式,处理请求,如果当前链条无法处理,那么就往下继续处理
  // 这里没有考虑兜底策略,大家可以根据自己的需求实际需要来加
  return (
    this.method.getImageList?.(newParams) ??
    this.method.getVideoList?.(newParams)
  )
}

?.  和 ?? 都为 ES6+ 的语法,第一次见到的同学可以去查查看哦

所以我们只需要调用

const params = {
	pageNum: 1,
	pageSize: 10
}
const response = await MediatorPattern.getList(params)
// ... 其他逻辑

就可以获取到当前【类型】的列表了,所以类似于上传、下载、导入等等接口都可以这么写,所以 UI 组件层面在获取数据时,只需要关心你要获取的是什么接口即可。

  1. 动态拼接(重写)参数来实现后端的不同字段参数传递

因为后端的参数以及返回到前端的参数也无法统一,既然我们都将所有的类型、功能、接口都进行了封装,所以在参数层面也需要对应的封装。

例如后端在返回到前端的尺寸字段为 imageSize 以及 videoSize 所以前端也需要对应的拼接,这里就比较简单了。直接上拼接代码

// 参数动态封装,主要用来区分图片和视频的类似参数,例如
// { imageSize: '' }, { videoSize: '' } 实际使用时只需要 => { Size: '' },会自动添加 image or video 前缀
MediatorPattern.prototype.rewriteParams = function (params: any) {
  const newParams: any = {}
  for (const key in params) {
    if (this.dynamicParams.includes(key)) {
      newParams[`${this.prefix}${key}`] = params[key]
      continue
    }
    newParams[key] = params[key]
  }

  return newParams
}

我们根据变量命名来做的区分,这就需要我们的命名要保持良好的编程风格

总结

好了,到此我们的所有功能以及封装都已经完成了。

通过本次的需求开发,也想让大家感受一下,不要埋头苦干你目前的工作,每一次枯燥无味的需求里也有值得你去细细揣摩的思想!如果我只是埋头苦干这个需求,那么就是写完一个页面之后,直接复制组件,然后换一下文案以及 API 接口就可以了。对吧?总结一下这句话:Don’t repeat yourself,不要重复你自己。

好了,如果以上有哪里写的不对,大佬多担待,告诉我,及时更正!!

最后还是推荐一下曾探大佬的 《JavaScript 设计模式与开发实践》,推荐大家看看哦,十分的不错。

写文不易啊!!!上次写的摸鱼的文章,已经导致这个需求延期了 0.5d 了。这个文章也是我下班赶紧跑回家写的,需求上周就已经提测了。才有时间过来写这篇文章。如果这里有一个或多个知识点让你 get 到了。就点个赞吧!没 get 到的,如果认真看完了,就知道我写的也挺不容易的,也给点个赞呗~~