灏天阁

JS Proxy的应用

· Yin灏

熟悉vue3的人都知道,它的数据响应式是用[JS Proxy](ES6 入门教程)实现的,proxy 可以拦截诸如 get、set、apply 等很多操作,让我们可以通过“钩子”的方式,在这些操作时,做一些我们自己的动作。正好这段时间我们有一个项目应用到了它。

全局码表的使用

这个项目中定义了一些系统共用的常量,我们称为状态码,如下:

const CODE = {
  goods: {
    category: [
      { code: 3088, label: "面膜" },
      { code: 3089, label: "牙膏" },
    ],
  },
  order: {
    status: [
      { code: 10000, label: "待支付" },
      { code: 10100, label: "待采购" },
      { code: 10200, label: "待出库" },
    ],
  },
};

刚开始时,它只有几个状态,随系统就直接下发了。可后来这些状态码越来越多,并且出现了“动态”状态码,也就是不同用户打开系统时,实时计算他的状态码,由异步接口下发。

组件的抽离

因为这个状态表在系统的各处都在使用,我们想到的第一个方案是抽离了出一个公共模块,应用层 import 使用就行了。

import ajax from './util/ajax';

let codePromise;
export default async function getDynamicCode{
  if(codePromise) return codePromise;
  codePromise = new Promise(async (resolve)=>{
    const codeMap = await ajax.get('/getDynamicCode');
    resolve(codeMap);
  });
  return codePromise;
}

这样只要在业务逻辑里引用它就可以使用了,全局共享一份,不需要重复申请。

import getDynamicCode from "./getDynamicCode";

async function showOrderStatus(statusCode) {
  const CODE = await getDynamicCode();
  const ret = CODE.order.status.filter((item) => item.code == statusCode);
  return ret.length > 0 ? ret[0].label : "-";
}

问题的产生

然后,随着业务的发展,大家对状态表的使用频率越来越高,问题也越来越多了。归纳了一下,有以下几个问题:

  1. 因为是动态码表,不同的人看到的码表不一样,有些 code 只对特殊角色开放,其它角色拿不到,使用的时候,因为码表没有值,就会报错。比如有些人是不发订单状态表的,操作它就会报错
CODE.order.status.filter((item) => item.code == statusCode);

为了解决它,就得每次使用前判断一下

let statusDesc = "";
if (CODE.order && CODE.order.status) {
  const ret = CODE.order.status.filter((item) => item.code == statusCode);
  statusDesc = ret.length > 0 ? ret[0].label : "-";
}
return statusDesc;

因为要到处写这个兼容逻辑就很麻烦。 2. 因为要配合组件使用,经常要把码表格式化成一定结构,传给组件,最常用的是选择列表和字典结构。

// options给下接选择组件使用
const options = CODE.order.status.map(({ code: value, label }) => ({
  value,
  label,
}));
// dict给表格组件使用
const dict = Object.fromEntries(
  CODE.order.status.map(({ code, label }) => [code, label])
);

Proxy 的应用

为了解决以上两个问题,我们升级了 getDynamicCode 组件,使用到了 Proxy 技术。大致流程如下:

  1. 用 Proxy 包装 CODE,它在“读”的操作加钩子,每次读取前,判断一下要读取的字段是不是存在;
  2. 如果“存在”目标属性,把对应的属性值使用 Proxy 封装后返回;“不存在”目标属性,把空数组用 Proxy 封装后返回
  3. 如果属性值是“options”或者”dict”,直接把当前对象按照 Options 和 dict 要求的结构格式化后返回。

最终代码如下:

import ajax from './util/ajax';
const proxySetting = {
  get(target, propKey) {
    const isUndefined = typeof target[propKey] === "undefined";
    if (!isUndefined) {
      return new Proxy(target[propKey], proxySetting);
    }
    const isArray = Array.isArray(target);

    if (propKey === "options") {
      if (isArray)
        return target.map(({ code: value, desc: label }) => ({ value, label }));
      else return new Proxy([], proxySetting);
    }
    if (propKey === "dict") {
      if (isArray)
        return Object.fromEntries(target.map(({ code, desc }) => [code, desc]));
      else return new Proxy({}, proxySetting);
    }
    return new Proxy([], proxySetting);
  },
};

let codePromise;
export default async function getDynamicCode{
  if(codePromise) return codePromise;
  codePromise = new Promise(async (resolve)=>{
    const codeMap = await ajax.get('/getDynamicCode');
    const codeProxy = new Proxy(codeMap, proxySetting);
    resolve(codeProxy);
  });
  return codePromise;
}

这样就算读取了一个不存的属性,也不会报错,而是直接返回空数组。

const testCode = Code.category.bottle.glass;

虽然码表中没有它,但可以自由访问,不会触发 js 报错; 同时,可以直接访问 options 和 dict 属性,不用但心它们是否存在。

const columnsConfig = [
  { title: "订单状态", dataIndex: "status", valueEnum: CODE.order.status.dict },
];

- Book Lists -