公共组件设计要点总结

前段时间写了一个列表框架公共组件(虽然是项目内的公共组件,但很多原则和真正的公共组件一样),可以快速灵活搭建各种列表页面,且具有高扩展性,已经完美经历了多个需求迭代。

开发背景:有一个内容管理系统(项目采用vue框架,UI框架使用view-design),内容种类不断的在扩展,最开始的文章,后来的游戏,接下来的教程……

每一种内容都有:待处理列表,已提交列表,已发布列表,回收站列表,草稿箱。

每一种列表的特点都不太相同,却基本是一个模式:

  1. 筛选排序条件各不相同,有的列表没有筛选排序条件,改变条件立即同步刷新列表(使用时,可以灵活定制各自的筛选排序)
  2. 有些列表需要记住并回显上次的选择的条件,甚至记住并标记出上次查看的列表项
  3. 有的列表需要对列表项进行多选,在此基础上进行批量操作(如批量删除,批量上下架),或者批量上传配音等,有的列表什么都不需要(可以扩展其他批量操作)
  4. 有的列表需要周期性的更新数据【点开看看,相信你会有所领悟】,有的不需要 由于数据的差异,已有列表不能复用。

于是便开发出一个列表框架 ListFrame: 使用它,只需要提供接口和列表item样式,就可以快速定制各种列表;改变搜索及时刷新,分页控制,多选控制,周期更新,条件记忆等逻辑,完全不用关心了,ListFrame 可灵活配置开启。

 

这也算是我花了很多心思的得意之作了[憨笑],很多设计思路一直想分享出来,总结了一下,抽取一个公共组件,至少应当仔细考虑一下4点:

1、公共组件不应该集成定制性强的功能,可以集成通用或较固定的功能,但应默认关闭那些不常用的功能

只有极少数列表用到的功能就不要封装了,封装他们 增加了公共组件的代码量,但又没有很高的利用率。

几乎所有的列表都可以用到,但不同种类的列表 表现不一样(如内容类别搜索,文章类型的类别,游戏类型的类别 完全不同),就是定制性很强的功能了。
定制性很强的功能封装起来需要包含各种情况,逻辑较复杂,另外也使列表的逻辑分散,不利于维护。试想一下,下次再扩展一个类型,岂不是还要修改这个ListFrame !
(开闭原则:公共组件不能保持稳定的,就是失败的)

这有点小儿科了吧!
实际上识别出哪些功能是通用的(应该封装的),哪些是常用的(应该默认开启的),不能光靠拍脑袋,使用公共组件的过程,不断修正总结 很重要。
维护公共组件,你一定要有一颗开放的心态。组件好不好,由使用的情况说了算,敢于承认失败,才能不断改进。

2、参数的配置要简单且灵活

如果你想让你的组件功能强大,通过配置就能快速扩展,那么一定要避免将配置设计的过于复杂。
配置复杂,无异于将工作量转移到了写配置上,没有任何好处,反而让代码更难理解(额外的 要先理解你的公共组件)。

将配置设计的简单关键就是:用好默认值 类型重载,且采用大家一看就明白的名称。

构建列表框架,你必须要考虑分页:
有些列表(如展示全部内容的列表)是没有分页
分页有pageNo和pageSize属性,且初始值是可以设定的,ajax请求时携带的参数名最好也是可以配置的
于是可能你的组件应该这样使用:

  <list-frame :showPager="{
show:true, pageNo:{ name:'pageNo', initial:1 }, pageSize:{ name:'pageSize', initial:10 }}"
></list-frame>

后台在设计接口时,通常参数名就是 pageNo 和 pageSize;前端使用场景中基本上初始的pageNo是1,pageSize为10。采用默认值,所有的配置都可以省略:

<list-frame :showPager="{show:true}"></list-frame>
<!-- 没有传入的字段,就使用默认值,而非空(做好空值适配很重要) -->

为了更简单,再加上类型重载。完整设计代码如下:

class PagerConfig{
    show?:boolean,
    pageNo?:{
        name?:string,
        initial?:string
    }
    pageSize?:{
        name?:string,
        initial?:string
    }
}

// 类型重载
 showPager:Boolean | PagerConfig

// 默认值的定义与使用:
const defaultPager = {
  show: true,
  pageNo: {
    name: 'pageNo',
    initial: 1
  },
  pageSize: {
    name: 'pageSize',
    initial: 10
  }
};

this.pagerConfig = Object.assign(
  {},
  defaultPager,
  typeof this.showPager === 'boolean' ? { show: this.showPager } : this.showPager
);
// this.pagerConfig 就是最终的配置了!

那么使用时,就可以这样写了

<list-frame :showPager="true"></list-frame>
或:
<list-frame show-pager ></list-frame>

现在可以看到,一切变的非常简单,同时又保持了足够的灵活性!(灵活性是指,任然可以灵活的配置参数名称 和 初始值)

补充于2020年9月8日15:38:40:

为什么要将pageNo等请求参数名称设置为可配置的?

很多新手在开始写公共组件时,喜欢将字段名设置为固定的:你要用我的组件,必须提供xxx这样的对象!这样设计也许能满足大多数的典型场景,但一旦遇到和你的前置要求有冲突的场景(比如刚好pageNo字段有了别的含义——这很常见,比如在试卷相关的项目中“试卷编号”很可能被命名成pageNo,而分页页码就只能用其他的了),公共组件就歇菜了。

让一切可配置,去掉“要使用我的组件,你必须……”这样的情况!

3、相关的配置要集中

 将相关的配置集中在一起,更利于用户使用,可读性更强。

ListFrame 封装了 批量删除 和 批量上下架 的功能。为什么要将这两个功能封装呢?因为:
1、有很多业务无关逻辑值得我们去封装,如disable校验,删除和下架时要弹出输入框,要求用户输入删除原因或下架原因;
2、通用性很强,几乎所有的列表都有删除功能,所有已发布列表都有上下架功能;
3、不同列表除了对应的上下架接口,删除接口不同外,好像没有什么其他不同的。

看看下方代码:

    <!-- 集中配置前: -->
    <list-frame
      :getPageFn="getPage"
      :itemsDelAble="true"
      :delFn="delFn"
      :shelfOperateAble="true"
      :offshelfFn="offshelfFn"
      :onshelfFn="onshelfFn"
    ></list-frame>

如果开启批量删除 除了设置 itemsDelAble 为 true,还要提供 delFn 删除方法;
如果开启批量上下架 除了设置 shelfOperateAble 为 true ,还需要提供 offshelfFn 和 onshelfFn 两个方法。

既然提开启功能和供方法必须同时存在,为什么不把他们绑定在一起呢?我们将 itemsDelAble 和 shelfOperateAble 有bool类型变成了 object 类型,于是很容易实现了集中配置的目标。

对比一下将配置集中前后的代码,是否后者更容易理解,更方便使用了呢?

    <!-- 集中配置后: -->
    <list-frame
      :getPageFn="getPage"
      :itemsDelAble="{delFn}"
      :shelfOperateAble="{offshelfFn,onshelfFn}"
    ></list-frame>

(相关配置集中起来,实际上是对你的组件参数,按提供的功能模块进行划分!)

4、避免出现职能交叉的配置

我发现,这一点错误,即便是大型UI框架也会犯。我现在开发的项目所采用的UI框架,关于Button就存在这个问题。下面就用个api截图来说明:(就不发 链接,也不指明是哪个框架了)

 

按钮的 type 大部分值都是表示主题颜色的,如default,primary,info等,但 text(文本按钮) 和 dashed(虚线框按钮)除外,我觉得这两个应该是属于shape(形状)的值。
这样的 type 和 shape 职能交叉了。现在要实现一个文本按钮,有不同颜色 —— 竟然做不到了!

我们在开发 ListFrame 也存在这样的错误,后面使用时发现后才改正的,这里就不说了,因为上面举的例子已经能很好的说明这个注意点了。
总之设置配置项时,保持高内聚,低耦合的原则是不变的!

posted @ 2020-07-20 16:41  enne5w4  阅读(1757)  评论(0编辑  收藏  举报