Hooks 技巧 | 借用中间件的思维,实现了嵌套交互的无序化随意组合

1,445 阅读9分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

文章阅读小提示,本文字数:3000 左右,阅读完需:约 10 分钟

前言

快到年底了,所以最近一直在对之前定的目标进行查漏补缺。

有那么一两项已经放弃了,但是有几项还可以挣扎一下。

  • 比如做一次部门内部技术分享,分享文章已经完成1/3了,主题是「UI 层面减少重复开发」。
  • 比如以实际开发的角度梳理 Hooks 的内容,也我近期一直再做的事情。

这段时间的文章在对 Hooks 的知识点梳理着重于实用性,会结合实际开发情况,输出内容。

今天分享嵌套交互灵活组合的功能。我之前对于嵌套交互的实现,还局限于一系列相似的操作。这次花了些时间,实现了可无序化随意组合。

关于内部分享和开发心得的个人总结,我放在了文末,不在前面的篇幅占用文章主题的空间。

嵌套交互

操作描述

网站使用者从触发某个操作到打开展示界面过程中,可能根据当前操作的数据状态、数据权限等多个因素的影响,最终实际展示的界面内容会有不同。

比如说常见的数据删除,在内部的后台管理系统中,一些数据管理是可以有删除操作的。以城市信息为例,当前城市下没有生成订单,允许删除,但是如果已经生成了用户订单,是不允许删除的。

还有更复杂的情况,比如再增加一条和订单无关的条件,绑定了正在销售期间的商品内不可以删除。

是否可操作状态表

果然借助表格可能让逻辑功能更加直观

城市状态数据是否可以删除
有在售商品
已有订单
没有再售商品且没有订单

操作流程图

  • 在售商品和订单的判断是不区分先后的,因为这两个条件只要有一个条件成了 ,城市数据就不允许删除的;
  • 有在售商品或有订单数据的情况下,操作删除,会给出不可操作的提示,当然一般提示内容会不同。(因为是内部使用系统,某些提示会具体一些方便业务人员更加清楚可以进行哪些操作)
  • 没有再售商品且没有订单的情况下,城市数据才允许被删除,这个时候会进行删除的http接口请求。

归纳提炼

上面这类操作,在工作中其实算比较常见的,大多数情况是单一判断条件,当满足条件的时候做删除,不满足条件的时候给出提示。

这次的功能在之前的基础上增加了一层判断。于是我开始思考,将操作看成一个单链表,未来功能的走向可能如下:

判断条件可以不断累加,不同条件的位置也可以任意调换。

将文字转换成操作流程图如下:

整个操作流程是清晰且明朗的,可能复杂点在于每个判断条件,有些判断条件的状态值容易拿到,有些则需要通过http请求获取数据。

数据结构的内容有点忘了,哪里如果写的不太对,欢迎评论指正。

中间件

这次功能设计的时候,我想到了之前归纳过批量处理操作,当时的功能设计将多个相似操作做了统一处理,多个批量操作拥有相似的条件判断——是否已选择了数据项和数据的状态判断,也是借助了中间件的开发思路。

两种实现方案各有特色,等下详细介绍,先来做个技术延展。

技术延展

我在学习中间件的时候,总会技术文章里面看到下面这张图帮助更好的理解中间件。

首先是洋葱模型, 因为这种类型的功能,执行过程很像是剥洋葱,一层层进行,所以因此得名。

洋葱模型的定义是

当请求进入之后,通过中间件逐层调用next,进而进入下一个中间件,中间会通过context对象向下传递,直到到达最后一个中间件。

可行性实验

我先写了个Demo做实验,发现单纯的中间件无法满足我的需求,因为它会一直向下传递,直到最后一个中间件。但是我需要再特定情况下做终止操作,所以我开始思考如果在中间件的基础之上,做功能升级。

中间件+操作终止

既然洋葱模型不能完全实现我的需求,那么就在此基础上,进行代码的升级。

在中间件上加上操作终止的功能,组合起来就是我想要的功能了。

公共方法

  • 方法的入参是一组操作的函数数组;
  • 循环数组,每次中间件函数就是当前排在函数数组中的第一个,数组返回第一个的方法比较熟悉——shift();
  • 当flag的值是next时,进入下一步。这里我在实验的时候注意到因为有些取值方法是异步的,所以判断条件需要加上一条:「flag需要存在值」;
  • 当flag的值是suspend时,中止方法数组继续循环。此时当前方法可进行一些操作比如提示之类;
/**
   * 嵌套交互方法组合 flag的值:suspend-中止,next-进入下一步
   * @param {Array} funcList 列表数据
   */
  nestedInteractionCompose(funcList) {
    let next;
    while (funcList.length > 0) {
      let middle = funcList.shift();
      let flag = middle();
      // flag需要存在值 因为可能有些方法是异步的
      if (flag && flag === 'next') {
        next = middle;
      } else {
        // 中止方法列表继续循环
        break;
      }
    }
    return next;
  }

删除按钮

每天表格数据都会有删除按钮

<Button type='link' onClick={() => operateDelete(record)}>
  删除
</Button>;

删除操作

删除操作一共包含三个方法,分别是:

orderCheck:是否已有订单的校验函数

onsaleCheck:是否有在售商品的校验函数

deleteConfirm:请求http提交删除操作的函数

将包含三个函数的数组作为参数传入nestedInteractionCompose方法中。值得一提的是,前面说过订单存在的校验和在售商品存在的校验是不区分先后的,所以函数数组的前两个元素是可以颠倒顺序的。

/**
 * 操作校验-已有订单
 * @param {Object} record 操作的列表数据
 * @param {Function} callback 回调函数
 */
const orderCheck = (record, callback) => {
  http1({}).then(res => {
    if (res) {
      return callback && callback('next');
    }
  });
};

/**
 * 操作校验-在售商品
 * @param {Object} record 操作的列表数据
 * @param {Function} callback 回调函数
 */
const onsaleCheck = (record, callback) => {
  http2({}).then(res => {
    if (res) {
      return callback && callback('next');
    }
  });
};

/**
 * 操作校验-提交删除
 * @param {Object} record 操作的列表数据
 * @param {Function} callback 回调函数
 */
const deleteConfirm = (record, callback) => {
  http3({}).then(res => {
    if (res) {
      // 刷新列表
    }
  });
};

/**
 * 删除操作
 * @param {Object} record 操作的列表数据
 */
const operateDelete = record => {
  /** @name 在售商品校验函数  */
  let onsaleFunc = () =>
    onsaleCheck(record, flag => {
      return () => {
        return flag;
      };
    });
  /** @name 已有订单校验函数  */
  let orderFunc = () =>
    orderCheck(record, flag => {
      return () => {
        return flag;
      };
    });
  nestedInteractionCompose([onsaleFunc, orderFunc, deleteConfirm]);
};

多个批量操作的实现

打开我的历史文章,满屏写着「人有七窍叶一一只通了六窍」。虽然文字写的有些呆板,表达能力上也不尽人意。不过现在写的也没有好到「字字珠玉」的程度。

不过偶尔今夕对比,还是可以唤醒一些美好记忆的。昨夜西风过东楼,冬将至,几杯热茶暖思绪。今观过往所写代码,思路清晰,前后通达,不算上乘,但有可取之处。与前面所讲实现之方法,悉中间件的实现处理上不同,但是整体思路差不多:

  • 入参都包含一个操作函数的数组,且数组元素可以随意交换,当然最后一个函数是明确的,结束函数一般都是固定的;
  • 但是这里的功能,在确定对下一个操作的实现跟前面嵌套交互的不太一样。这里会将下一个操作函数在数组中的索引值传入中间件函数中。

各有特点,不过我未来遇到相似的功能会优先选择前面嵌套交互的实现方案。

/**
 * 获取下一步函数的处理事件 直接执行下一步操作
 * @param {string} type 操作的按钮类型
 * @param {Array} chainCalls 所有的链式函数列表
 * @param {Function} funcType 当前步骤的方法名
 * @return {Function} 下一步方法执行
 */
const goToNext = (type, chainCalls, funcType) => {
  let funcIndex = 0;
  chainCalls.forEach((chainItem, chainIndex) => {
    if (chainItem === funcType) {
      funcIndex = chainIndex;
    }
  });
  return chainCalls[funcIndex + 1](type, chainCalls)();
};

对多个批量操作实现感兴趣的可以看我这篇早期的文章☞《【功能实现】多个批量操作的链式实现

总结

今日总结

功能讲完,来到今天的总结时刻

  • 如果遇到像单链表结构的操作,实现的时候,适当思考一下未来的功能走向,尽量做到灵活拓展和低成本维护两个方面,可以考虑采用中间件的实现思路。
  • 流程图可以帮助更好的梳理流程比较长的功能。
  • 不必拘泥于模型自身能够实现的功能,当无法完全满足开发需要的时候,可以想办法修改模型,补充可以满足自己需求的代码部分。
  • 功能实现的方式往往是具有多样性的,可以通往成功的路有很多条,尽可能选择对未来有帮助的那一条。所以我对比了本次的实现方案和之前批量操作的实现方案,选择了本次的实现方案。

分享

最近有两个不错的心得跟大家分享一下。

内部技术分享

我在我老大的提醒下,察觉到内部的技术分享和我再技术设计进行的分享还是有不同之处的。

  • 日常的技术分享着重对于技术观点的输出,可能这个观点是已经实验过的,也可能这个观点是实验中的。
  • 内部的技术分享,除了考虑技术对开发自身的提升,其实还要考虑它能否落实以及落实的意义(好处)在哪。

明白这一点,我今后在做内部分享的时候会具象化我分享的内容,比如引入某个技术,实际降低了多少代码量或者节省了多少开发速度。如果是影响代码书写或者规范之类的,可以列出添加前后的对比图。

开发心得

跟大家分享我的开发心得,我嫣说这是PDCA循环的理念。

我们一直在做尝试,不必担心一次尝试不能做到完美,接受不完美,但是会不断追求完美。