1. IView 利用 render 函數(shù)實現(xiàn)菜單多級動態(tài)嵌套遞歸展示

1誉尖、前言

  • 最近公司做后臺管理系統(tǒng),前端缺乏人員腹暖,于是乎叫我這個 Java 程序員充當(dāng)前端人員汇在,還好本人閑暇的時候?qū)η岸诵屡d的技術(shù)也是頗感興趣,于是乎便嘗試了一把 Vue 全家桶脏答、IView 做 UI糕殉、利用 webpack 工程化前端項目(這事我早就想嘗試一把了),話題扯遠(yuǎn)了殖告,接下來進(jìn)入下面的主題吧阿蝶。

2、問題

  • IView 固然挺強大黄绩,可以使用的組件還是比較多的羡洁,單頁面文件寫著也是比較舒服的,但是廣大網(wǎng)友都知道爽丹,管理系統(tǒng)左側(cè)菜單通常是有層級結(jié)構(gòu)的筑煮,而且我們項目是不固定,什么意思呢粤蝎?就是可能有好幾層節(jié)點真仲,當(dāng)然一般也就兩三層了,層次多了菜單都找不到初澎。正如前面說到的秸应,這個菜單結(jié)構(gòu)是有層級的,配置文件就是一個嵌套的 json 結(jié)構(gòu)碑宴,類似于下面:
const settingMenus = [
    {
        name : 'foo1',
        children : [
            {name : 'foo1-1', attrs : {router : '404'}},
            {name : 'foo1-2', attrs : {router : '404'}}
        ]
    },
    { 
        name : 'foo2',
        children : [
            {name : 'foo2-1', attrs : {router : '404'}},
            {name : 'foo2-1', attrs : {router : '404'}}
            {
                name : 'foo2-2', 
                children : [
                    {name : 'foo2-2-1', attrs : {router : '404'}},
                    {name : 'foo2-3-1', attrs : {router : '404'}}
                ]
            }
        ]
    },
    ...
];    

好了软啼,上面的菜單需要渲染成多級菜單,怎么辦呢墓懂?查看了一下 IView 文檔焰宣,發(fā)現(xiàn)有嵌套菜單相關(guān)組件,但是遞歸的好像是木有捕仔,于是乎想了想:Vue 單頁面方式默認(rèn)是使用html模板的方式渲染的匕积,這個。榜跌。闪唆。似乎沒辦法根據(jù)這個結(jié)構(gòu)遞歸呀,找了下網(wǎng)上的解決方案钓葫,定義一個父組件和子組件悄蕾,子組件中子節(jié)點的渲染又調(diào)用自己,不得不說網(wǎng)友還是比較機(jī)智的,不過我還是需要想想有沒有什么更優(yōu)雅的方式呢帆调?

3奠骄、解決靈感

既然許多網(wǎng)友的方法我都不是很滿意,然而自己也一時間想不出辦法番刊!那就去 Vue 官方文檔看看含鳞,Vue 有沒有直接使用函數(shù)渲染頁面的方法,結(jié)果還真被我發(fā)現(xiàn)了芹务,這就是 render 方法蝉绷,文檔里面介紹如下:

Vue 推薦在絕大多數(shù)情況下使用 template 來創(chuàng)建你的 HTML。然而在一些場景中枣抱,你真的需要 JavaScript 的完全編程的能力熔吗,這時你可以用 render 函數(shù),它比 template 更接近編譯器佳晶。

上面這句話我在第一次看的時候還沒什么感觸桅狠,好吧,現(xiàn)在是時候發(fā)揮它的作用了宵晚,wait..., 讓我先看看這玩意兒咋用垂攘,經(jīng)過了 n 分鐘之后,終于大概了解這個玩意兒是干嘛的了淤刃,似乎是直接創(chuàng)建 VNode 渲染頁面晒他,更接近底層。逸贾。陨仅。不管了不管了,接下來就展示一下我琢磨出來的方法吧铝侵,什么都不說了灼伤,下面就上解決方案吧。

4咪鲜、實現(xiàn)代碼

創(chuàng)建 CMenuTree 組件狐赡,為什么是這個名稱呢?因為 Menu疟丙、SubMenu颖侄、MenuItem 都是 IView 全局注冊的組件,不能重啊享郊,前面一個 C 標(biāo)識一下是自定義組件吧览祖。代碼如下。

export default {
    props : {
        data : {
            type : Array,
            default : []
        },
        activeName : {
            type : String,
            required : true
        },
        theme : {
            type : String,
            default : 'light'
        },
        openNames : {
            type : Array,
            required : true
        }
    },

    // 因為菜單渲染需要根據(jù)菜單節(jié)點進(jìn)行遞歸渲染, 使用 render 函數(shù)代替 html 模板渲染.
    render(createElement) {
        const create = (menuNode, createElement) => {
            let name = parent.name;

            // 根節(jié)點為數(shù)組, 直接創(chuàng)建 Menu 菜單包裹
            if(Array.isArray(menuNode)) {
                return createElement(
                    'Menu', 
                    {
                        props : {
                            activeName : this.activeName,
                            theme : this.theme,
                            width : 'auto',
                            openNames : this.openNames
                        },
                        on : {
                            'on-select' : this.select,
                            'on-open-change' : this.openChange
                        },
                        ref : 'menu'
                    },
                    [
                        createElement('template', {
                                slot : 'title'
                            },
                            menuNode.name
                        ),
                        ...menuNode.map(function(item) {
                            return create(item, createElement)
                        })
                    ]
                );
            }

            // 有子節(jié)點, 創(chuàng)建 SubMenu 節(jié)點
            if(Array.isArray(menuNode.children) && menuNode.children.length > 0) {
                return createElement(
                    'Submenu',
                    {
                        props : {
                            name : menuNode.id, // 設(shè)置 name
                        }
                    },
                    [
                        createElement('template', {
                                slot : 'title' //指定 title 插槽內(nèi)容
                            },
                            [   
                                // 創(chuàng)建圖標(biāo)
                                createElement('Icon', {
                                    props : {
                                        type : 'ios-navigate',
                                    }
                                }),
                                // 設(shè)置菜單名稱
                                menuNode.name
                            ]
                        ),
                        ...menuNode.children.map(function(item) {
                            return create(item, createElement)
                        })
                    ]
                )
            }

            // 創(chuàng)建 MenuItem 節(jié)點
            return createElement(
                'MenuItem',
                {
                    props : {
                        name : menuNode.id,
                    }
                },
                [
                    createElement('template', {
                            slot : 'default'
                        },
                        menuNode.name
                    ),
                ]
            );
        }

        return create(this.data, createElement);
    },

    watch : {
        openNames(newVal, oldVal) {
            this.$nextTick(function() {
                this.$refs.menu.updateOpened();
                this.$refs.menu.updateActiveName();
            });
        }
    },

    methods : {
        select : function(name) {
            this.$emit('on-select', name);
        },
        openChange(name) {
            this.$emit('on-open-change', name);
        }
    }
}

大家可能注意到了炊琉,我提供的菜單結(jié)構(gòu)中是沒有 id 屬性展蒂,上面的渲染過程中咋會多了個 id 呢?這個 id 是在渲染樹之前自動添加的,實現(xiàn)代碼如下:

const eachStandardData(nodes, callback, pId, index) {
        if ((typeof callback) === 'function') {
            let newPId = val => {
                if(!val) return '1';
                let arr = val.split('-');
                arr.push((index + 1) + '');
                return arr.join('-');
            }

            if (Array.isArray(nodes) && nodes.length > 0) {
                let id = newPId(pId);

                nodes.forEach((node, index) => {
                    this.eachStandardData(node, callback, id, index);
                });

                return;
            }

            if ((typeof nodes) === 'object') {
                let id = newPId(pId);
                let flag = callback(id, nodes);

                if (Array.isArray(nodes.children) && nodes.children.length > 0 && flag) {
                    nodes.children.forEach((child, index) => {
                        this.eachStandardData(child, callback, id, index);
                    });
                }
            }
        }
    }

// menuTreeMetaData 就是需要渲染的菜單數(shù)據(jù)
let menuTreeMetaData =...;
let menuMap = new Map(); // 保存 id -> menuNode锰悼,點擊按鈕方便查詢菜單節(jié)點信息柳骄。

eachStandardData(menuTreeMetaData, (id, node) => {
        node.id = id;
        menuMap.set(id, node);
        return true;
 });

執(zhí)行 eachStandardData 之后菜單元數(shù)據(jù)就會變成下面的樣子

const settingMenus = [
    {
        id : '1-1',
        name : 'foo1',
        children : [
            {id : '1-1-1', name : 'foo1-1', attrs : {router : '404'}},
            {id : '1-1-1', name : 'foo1-2', attrs : {router : '404'}}
        ]
    },
    { 
        id : '1-2',
        name : 'foo2',
        children : [
            {id: '1-2-1', name : 'foo2-1', attrs : {router : '404'}},
            {id: '1-2-2', name : 'foo2-1', attrs : {router : '404'}},
            {
                id : '1-2-3',
                name : 'foo2-2', 
                children : [
                    {id : '1-2-3-1', name : 'foo2-2-1', attrs : {router : '404'}},
                    {id : '1-2-3-2', name : 'foo2-3-1', attrs : {router : '404'}}
                ]
            }
        ]
    },
    ...
];

當(dāng)然,為每個節(jié)點按照層級生成這種有規(guī)律的 id 不止是為了好看松捉,主要還是為了下面的無關(guān)菜單自動收縮邏輯做準(zhǔn)備夹界,另外,大家可能注意到了隘世,MenuTree.vue 組件(其實直接定義成一個 JS 文件也沒問題,看個人習(xí)慣了)鸠踪,需要傳入一些屬性丙者,如下:

  • data: 菜單節(jié)點數(shù)據(jù),必須為一個數(shù)組营密。
  • activeName: 與 IView 中 Menu 組件的 activeName 一樣械媒,指示語法糖而已(語法糖都不算,算是又包裹了一層)评汰。
  • theme: 菜單主題纷捞,概念同上。
  • openNames: 展開的菜單被去,數(shù)組主儡,概念同上。

我們在使用 MenuTree 組件的時候惨缆,將我們上面準(zhǔn)備好的:menuTreeMetaData (data屬性), 'light'(theme)糜值,'1-1-1'(activeName) ,['1-1'](openNames) 傳入 MenuTree 組件即可坯墨。

  • 聰明的小伙伴會注意到寂汇,上面我不是說 id 是為無關(guān)菜單自動收縮邏輯做準(zhǔn)備嗎?怎么現(xiàn)在似乎沒看到呢5啡尽骄瓣?其實大家會發(fā)現(xiàn)當(dāng)我們知道了 activeName 就等于知道了父節(jié)點有哪些,那 openNames 應(yīng)該是根據(jù) activeName 計算的值耍攘,在 Vue 中將 openNames 定義為父組件就可以了榕栏,相關(guān)代碼如下:
openNames() {
       let arr = this.selected.split('-');
       let openNames = [];

        for(var i = 2; i < arr.length; i++) {
            openNames.push(
                 this.menuMap.get(
                       arr.slice(0, i)
                            .join('-')
                  ).id
             );
         }
        return openNames;
 }
// 當(dāng) MenuTree 觸發(fā) on-select 事件的時候直接將選擇的節(jié)點 id 賦值給 acitveName 即可,
// 這樣子組件無關(guān)菜單就會相應(yīng)收縮了少漆。其實 openNames 放在 MenuTree 內(nèi)部即可臼膏,
// 不用外部傳遞更方便,我這個寫法有點脫褲子放屁的感覺了示损。

上面的語法使用了 ES6 的一些特性渗磅,直接粘貼在瀏覽器中不能使用哦。

5、如何使用?

  • 小伙伴們看來上面的代碼可能有點懵始鱼,怎么使用呢仔掸?下面我簡單的組合使用一下,假設(shè)您已經(jīng)準(zhǔn)備好如下三個文件了:
  • menus.js(文件中已近做了 id 自動生成處理)
const menuTreeMetaData = {};
export menuTreeMetaData;
  • CMenuTree.js(其實是 CMenuTree.vue, 這里我換用 js 方式直接使用)
export default {...}
  • Index.vue(使用位置)
// template 部分
<CMenuTree :data="menuData" :activeName="selected" :openNames="openNames" @on-select="menuSelect"></CMenuTree>

// script 部分
import Menu from './menus'
import CMenuTree from './CMenuTree'

export default {
    components : {
        CMenuTree
    },
    data() {
            return {
                menuMap,
                menuData : menus.settingMenus,
                selected : '1-5-2',
            }
        },

        mounted() {
        },

        computed : {
            openNames() {
                let arr = this.selected.split('-');
                let openNames = [];

                for(var i = 2; i < arr.length; i++) {
                    openNames.push(
                        this.menuMap.get(
                            arr.slice(0, i)
                                .join('-')
                        ).id
                    );
                }

                return openNames;
            }
        },
        methods: {
            menuSelect(name) {
                this.selected = name;
            }
        }
}

ok, 打完收功夫医清,怎么感覺比廣大網(wǎng)友的復(fù)雜一些呢F鹉骸?其實這種事情個人覺得還是仁者見仁智者見智吧会烙!不過也算不同的解決方案了负懦,這里和大家分享一下,如有問題請評論指出柏腻。

6纸厉、總結(jié)

經(jīng)過這個例子實際體驗 render 函數(shù)的強大,不過建議日常使用就別用這個 render 函數(shù)了五嫂,畢竟這個寫著太繁瑣了颗品,正常情況直接用模板就可以了,簡單方便沃缘。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末躯枢,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子槐臀,更是在濱河造成了極大的恐慌锄蹂,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件峰档,死亡現(xiàn)場離奇詭異败匹,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)讥巡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進(jìn)店門掀亩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人欢顷,你說我怎么就攤上這事槽棍。” “怎么了抬驴?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵炼七,是天一觀的道長。 經(jīng)常有香客問我布持,道長豌拙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任题暖,我火速辦了婚禮按傅,結(jié)果婚禮上捉超,老公的妹妹穿的比我還像新娘。我一直安慰自己唯绍,他們只是感情好拼岳,可當(dāng)我...
    茶點故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著况芒,像睡著了一般惜纸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上绝骚,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天耐版,我揣著相機(jī)與錄音,去河邊找鬼压汪。 笑死椭更,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蛾魄。 我是一名探鬼主播,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼湿滓,長吁一口氣:“原來是場噩夢啊……” “哼滴须!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起叽奥,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤扔水,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后朝氓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體魔市,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年赵哲,在試婚紗的時候發(fā)現(xiàn)自己被綠了待德。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡枫夺,死狀恐怖将宪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情橡庞,我是刑警寧澤较坛,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站扒最,受9級特大地震影響丑勤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜吧趣,卻給世界環(huán)境...
    茶點故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一法竞、第九天 我趴在偏房一處隱蔽的房頂上張望耙厚。 院中可真熱鬧,春花似錦爪喘、人聲如沸颜曾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泛豪。三九已至,卻和暖如春侦鹏,著一層夾襖步出監(jiān)牢的瞬間诡曙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工略水, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留价卤,地道東北人。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓渊涝,卻偏偏與公主長得像慎璧,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子跨释,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,066評論 2 355