关于前端:数据适配器工具的开发

3次阅读

共计 3742 个字符,预计需要花费 10 分钟才能阅读完成。

我的项目地址查看 https://github.com/goblin-pitcher/data-adapter

背景

开发过程中往往须要升高对后端数据结构的依赖性,防止接口数据结构扭转引起的前端代码大面积批改。因而须要一个适配器工具,并对转换规则的对立治理,当接口数据结构扭转时,只需动静保护转换规则即可。

需要剖析

首先剖析 IO,须要实现的办法如下

/**
* @params {Object} data 须要转换的数据
* @params rules 数据结构待定,转换规则
* @params {Object} options 转换配置
* @returns {Object} adaptedData 返回数据
*/
function adapter(data, rules, options) {}

因为返回的数据量可能很大,为防止不必要的开销,最好在原地批改数据,因而返回的 adaptedData 满足adaptedData === data。若确定数据量不大,且须要返回新的数据时,最好在应用时传入cloneDeep(data)

旧版本

最后版是我的项目长期需要的产物,转换规则 rules 是一个 key 为匹配门路,value 为转换后的门路或值,具体代码可参考地址,示例如下:

/** 
* key 为匹配门路, 如:*  'e|a' 代表匹配 key 为 e 或 a,*  'b.c' 代表匹配门路['b', 'c']
*  '/^c/' 代表正则匹配 key 值,匹配规定为 /^c/
*  写法上是反对多种类型混用,如 'e|a.b./^c/',会匹配 obj[e|a][b][/^c/]
* value 可示意转换后的门路或值,规定如下:*   当 value 为字符串待变转换后的门路
*   当 value 为办法时代表转换后的值,参数别离为:*      data: 匹配门路的值
*      path: 匹配门路
*      obj: 原对象
*/
const rules= {
  "e|a": "b.a", // 将 obj.e 或 obj.a 的值放在 obj.b.a 下
  "b.c": "b.d", // obj.b.c 的值放在 obj.b.d 下
  "/^c/": "b.f", // 将 obj 下以 c 结尾的 key 放到 obj.b.f 下
  "b.ff": "b.g.f",
  e: (data, path, obj) => obj.a + obj.ca, // obj.e = obj.a + obj.ca
  "b.c": (data) => data ** 2, // obj.b.c = obj.b.c ** 2
};
const obj = {a:5,b:{g:{f:"xxx"},a:5,d:7,f:9},ca:8,cd:9}
adapter(obj, rules)

旧版本因为只是长期计划,无疑有很多问题。
规定定义上:比方 b.c 代表门路 ['b', 'c'],这会和作为 key 值的b.c 产生歧义,另外用字符串生成正则,写法上须要许多额定的本义符。

拓展性上:旧版本只反对一个配置,即 retain—— 是否保留转换前的项。这部分代码写的比拟仓促,耦合性太强,增加新的配置须要批改很多个中央,不不便拓展。

新版本

首先在 rules 的定义上,为了防止旧版本的诸多问题,新版本采纳了 Map 作为规定。key 为匹配门路,value 为转换的规定。

应用形式

具体可参考测试示例

npm i git+https://github.com/goblin-pitcher/data-adapter.git -S
--------
import adapter from 'data-adapter';
const obj = {
  a:5,
  b:{
    g:{f:"xxx"},
    a:5,
  }
}
const rules = new Map([['a', 'transKey-a'],
  [['b', /a|g/, 'f'], (path, value)=>`transKey-${path[path.length - 1]}`]
])

// 转换后数据格式如下:// {
//   'transKey-a':5,
//   b:{
//     g:{
//       'transKey-f':"xxx"
//     },
//     a:5,
//   }
// }
adapter(obj, rules)

adapter 办法格局如下:

interface IOptions {
  retain?: boolean;
  transValue?: boolean;
  matchFullRules?: boolean;
  relativePath?: boolean,
  priority?: ('string' | 'regExp' | 'function')[];}
// 当 options 为布尔类型时,代表配置{retain: options}
type RulesAndOptions = [rules: Rules, options: boolean | IOptions];
interface Adapter {(obj: Record<string, unknown>, ...args: RulesAndOptions): Record<string, unknown>;
  (obj: Record<string, unknown>, ...args: RulesAndOptions[]): Record<string, unknown>;
}
// adapter 也能够接管多个转换规则,即 adapter(data, [rules1, rules2, ....])

匹配规定

定义匹配规定 rules 为 Map 构造。假如 rules 值如下:

const testFunc = (path, value, matchPath, matchRule) => path[path.length - 1]==='f' && value>5;
const rules = new Map([[['b', /a|g/, testFunc], (path, value, matchPath, matchRule)=>`transKey-${path[path.length - 1]}`]
])

若以 rules 去转换 data,其中一条 rule 的 key 是 ['b', /a|g/, testFunc],代表先匹配data.b,而后寻找data.b.adata.b.g,并别离寻找 data.b.adata.b.g下满足 testFunc 的项,若该项存在,则将其 key 转换为transKey-${key}

配置阐明

interface IOptions {
  // 是否保留转换前的数据,默认为 false
  retain?: boolean;
  // rule.value 是否作为转换项的值,默认为 false
  // 假如某条规定为 new Map([['a', 'b']])://   1. 若该项为 true,代表 data.a = 'b'
  //   2. 该项为 false,代表 data.b = data.a
  transValue?: boolean;
  // 是否匹配全门路,默认为 true。// 比方某条规定为 new Map([[['a', 'b'], 'xxx']]), 假如 data.a.b 不存在://   1. 当 matchFullRules 为 true,则该条规定不失效
  //   2. 当 matchFullRules 为 false,则会退而求其次寻找 data.a,若 data.a 存在,则会转换 data.a
  matchFullRules?: boolean;
  // 转换后的门路是否绝对于转换前的门路,默认为 false.
  // 比方某条规定为 new Map([[['a', 'b'], 'xxx']])://   1. 当 relativePath 为 true,代表将 data.a.b 的值放到 data.a.xxx 下
  //   2. 当 relativePath 为 false, 代表将 data.a.b 的值放到 data.xxx 下
  relativePath?: boolean,
  // 匹配优先级,默认为['string', 'regExp', 'function']
  // 比方某条规定为 new Map([[['a', ['b', /^b/, testFunc]], 'xxx']])
  // 其中 ['b', /^b/, testFunc] 代表以多种规定去匹配 data.a 下的所有项,priority 代表匹配的优先级
  priority?: ('string' | 'regExp' | 'function')[];}

实现思路

旧版本因为工夫比拟紧,过后程度也比拟差,实现挺乱的,拓展性也差。重构后以更正当的数据结构实现该性能。

假如规定数据如下:

const testFunc = (path, value) => path[path.length-1].endsWith('b')
const rules = new Map([[[/a|e/, ['b', /^b/, testFunc], 'xxx'], 'transValue']]);
const data = {
    a: {
        b: {xxx: 7},
        ab: {abc: 4},
    },
    b: 5,
    e: {acb: {xxx: 6}}
}

能够发现当规定项中存在数组 (['b', /^b/, testFunc]) 时,匹配规定存在多种门路。而每种匹配门路,都有可能匹配多个门路的数据。因而定义两个 树结构

  • 规定树
  • 匹配数据树

转换流程如下图所示:

rules 生成的数据结构命名为 规定树

规定树和数据生成的数据结构命名为 匹配数据树

通过对规定树和匹配数据树的操作能够很不便的实现各种配置,如匹配优先级 options.priority, 能够通过批改规定树中各节点 children 的程序实现;options.matchFullRules 配置能够通过决定是否对匹配数据树进行裁剪实现。

正文完
 0