資料介面卡工具的開發

goblin_pitcher發表於2022-05-13
專案地址檢視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配置可以通過決定是否對匹配資料樹進行裁剪實現。

相關文章