樹形多級菜單數據源嵌套結構與扁平結構互轉


1.前言

在日常開發中,往往會有這樣的需求:根據后端返回的數據,動態渲染出一顆多級導航菜單樹,類似於計算機中資源管理器的樣子。如下圖所示:

要實現這樣的需求,其實不難,只是對后端返回的數據源有要求,如果后端返回的數據能夠很清楚的表現出節點與節點之間的層級關系,那么前端實現起來就易如反掌。

2.數據源格式

一般來說,要想動態的渲染出一個樹形菜單,如下所示的數據源格式對前端開發人員來說是十分友好的。

var data = [
	{
    name: "父節點1",
    children: [
      {
        name: "子節點11",
        children:[
          {
            name: "葉子節點111",
            children:[]
          },
          {
            name: "葉子節點112",
            children:[]
          },
          {
            name: "葉子節點113",
            children:[]
          },
          {
            name: "葉子節點114",
            children:[]
          }
        ]
      }
     //...
    ]
  },
];

后端返回這樣的數據源格式,節點之間的層級關系一目了然,前端人員拿到數據,只需進行遞歸遍歷,並判斷children.length是否等於0,等於0表明當前節點已為葉子節點,停止遍歷即可。在上一篇博文vue+element UI以組件遞歸方式實現多級導航菜單中,動態渲染多級導航菜單,也是推薦使用這種數據源格式的。

3.問題痛點

雖然前端人員想法是好的,但是在后端,這些數據通常是存儲在關系型數據庫中,后端開發將數據從數據庫中取出來返回給前端的數據往往這樣子的:

const data =[
  { id:1,   pid:0,  name:"父節點1"     },           
  { id:11,  pid:1,  name:"父節點11"    },
  { id:111, pid:11, name:"葉子節點111" },
  { id:112, pid:11, name:"葉子節點112" },
  { id:113, pid:11, name:"葉子節點113" },
  { id:114, pid:11, name:"葉子節點114" },
  { id:12,  pid:1,  name:"父節點12"    },
  { id:121, pid:12, name:"葉子節點121" },
  { id:122, pid:12, name:"葉子節點122" },
  { id:123, pid:12, name:"葉子節點123" },
  { id:124, pid:12, name:"葉子節點124" },
  { id:13,  pid:1,  name:"父節點13"    },
  { id:2,   pid:0,  name:"父節點2"     },
  { id:21,  pid:2,  name:"父節點21"    },
  { id:211, pid:21, name:"葉子節點211" },
  { id:212, pid:21, name:"葉子節點212" },
  { id:213, pid:21, name:"葉子節點213" },
  { id:214, pid:21, name:"葉子節點214" },
  { id:22,  pid:2,  name:"父節點22"    },
  { id:221, pid:22, name:"葉子節點221" },
  { id:222, pid:22, name:"葉子節點222" },
  { id:223, pid:22, name:"葉子節點223" },
  { id:224, pid:22, name:"葉子節點224" },
  { id:23,  pid:2,  name:"父節點23"    },
  { id:231, pid:23, name:"葉子節點231" },
  { id:232, pid:23, name:"葉子節點232" },
  { id:233, pid:23, name:"葉子節點233" },
  { id:234, pid:23, name:"葉子節點234" },
  { id:3,   pid:0,  name:"父節點3"     }
];

其中,層級關系是通過idpid提現的,id為節點的序號,pid為該節點的父節點序號,如果為頂級節點,則其pid為0。

其實,這樣的數據格式對前端來說,也不是不能用,就是沒有上面那種格式用起來方便,所以,有時候前端同學就得去跪舔后端人員:

“后端大哥,能不能給我返回像這樣子的數據呀?”

如果前端同學是個妹子還好,撒個嬌就完事了,可如果是個漢子,后端大哥往往會回應你:

“滾,給你返回數據就不錯了,還挑三揀四,想要啥樣子的自己造去。”

4.解決方案

為了防止被后端同學懟(其實以上對話是博主親身經歷,摔~~~),我們前端人員果斷自己動手,豐衣足食。

為了解決上述問題,博主自己寫了兩個方法,來實現兩種數據源格式互相轉化。我們姑且稱理想數據格式為“嵌套型格式”,后端返回的格式為“扁平型格式”,那么兩個互轉方法代碼如下:

/**
 * 將一個普通的節點數組(帶有指向父節點的指針)轉換為嵌套的數據結構。
 * @param {*} data  一組數據
 * @param {*} option 包含以下字段的對象:
 *      parentProperty(String):可以找到父節點鏈接的屬性的名稱。默認值:'pid'。
 *      childrenProperty(String):將存儲子節點的屬性的名稱。默認值:'children'。
 *      idProperty(String):唯一的節點標識符。默認值:'id'。
 *      nameProperty(String):節點的名稱。默認值:'name'。
 */
function FlatToNested(data, option) {
  option = option || {};
  let idProperty = option.idProperty || 'id';
  let parentProperty = option.parentProperty || 'pid';
  let childrenProperty = option.childrenProperty || 'children';
  let res = [],
    tmpMap = [];
  for (let i = 0; i < data.length; i++) {
    tmpMap[data[i][idProperty]] = data[i];
    if (tmpMap[data[i][parentProperty]] && data[i][idProperty] != data[i][parentProperty]) {
      if (!tmpMap[data[i][parentProperty]][childrenProperty])
        tmpMap[data[i][parentProperty]][childrenProperty] = [];
      tmpMap[data[i][parentProperty]][childrenProperty].push(data[i]);
    } else {
      res.push(data[i]);
    }
  }
  return res;
}
/**
 * 嵌套型格式轉扁平型格式
 * @param {Array} data 
 */
function NestedToFlat(data,pid) { 
  var res = []
  for (var i = 0; i < data.length; i++) {
    res.push({
      id: data[i].id,
      name: data[i].name,
      pid: pid || 0
    })
    if (data[i].children) {
      res = res.concat(NestedToFlat(data[i].children, data[i].id));
    }
  }
  return res;
}

5.使用方法

5.1 ”扁平型格式“轉”嵌套型格式“

如果在實際開發中后端返回的數據跟如下實例數據中字段一致的情況下:

//示例數據
const data =[
  { id:1,   pid:0,  name:"父節點1"     },           
  { id:11,  pid:1,  name:"父節點11"    },
  { id:111, pid:11, name:"葉子節點111" },
  { id:112, pid:11, name:"葉子節點112" },
  { id:113, pid:11, name:"葉子節點113" },
  { id:114, pid:11, name:"葉子節點114" },
  { id:12,  pid:1,  name:"父節點12"    },
  { id:121, pid:12, name:"葉子節點121" },
  { id:122, pid:12, name:"葉子節點122" }
  //...
];

那么直接調用方法

FlatToNested(data)

即可將扁平化數據轉化成嵌套型數據。

如果返回的數據和示例數據的字段不一致,那么您也無需更改源代碼,方法提供了可配置選項,如下所示:

例如,您收到這樣的數據:

const data =[
    { _id:1,   parentID:0,  text:"父節點1"     },           
    { _id:11,  parentID:1,  text:"父節點11"    },
    { _id:111, parentID:11, text:"葉子節點111" },
    { _id:112, parentID:11, text:"葉子節點112" },
    { _id:113, parentID:11, text:"葉子節點113" },
    { _id:114, parentID:11, text:"葉子節點114" },
    { _id:12,  parentID:1,  text:"父節點12"    },
    { _id:121, parentID:12, text:"葉子節點121" },
    { _id:122, parentID:12, text:"葉子節點122" }
    //...
  ];

那么,您可以這樣調用函數:

FlatToNested(nodes,{
    idProperty:'_id',            //唯一的節點標識符。
    nameProperty:'text',         //節點的名稱。
    parentProperty:'parentID',  //可以找到父節點鏈接的屬性的名稱。
    childrenProperty:'son'      //將存儲子節點的屬性的名稱。
})

5.2 ”嵌套型格式“轉”扁平型格式“

假如有如下嵌套型格式的數據:

let data = [
  {
      id:1,
      name:'根節點',
      children:[
        {
          id:11,
          name: '父節點11',
          children: [
            {
              id:111,
              name:'父節點111',
            },
            {
              id:112,
              name:'父節點112',
            }
          ]
        },
        {
          id:12,
          name:'父節點12',
        }
      ]
  }
]

那么直接調用方法

NestedToFlat(data)

即可將嵌套型數據轉化成扁平型數據:

[
  { id: 1, name: '根節點', pid: 0 },
  { id: 11, name: '父節點11', pid: 1 },
  { id: 111, name: '父節點111', pid: 11 },
  { id: 112, name: '父節點112', pid: 11 },
  { id: 12, name: '父節點12', pid: 1 }
]

6.小結

有了這兩個方法,我們前端人員再也不用去跪舔后端,要啥有啥,美滋滋!

(完)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM