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ù)了五嫂,畢竟這個寫著太繁瑣了颗品,正常情況直接用模板就可以了,簡單方便沃缘。