iview 在今年 7 月 28 號發佈了 3.0.0 版本,大版本升級每每意味着功能、接口的大變動。 雖然官網已經有長長的更新日誌,但看起來仍是有些抽象了, 因此我決定作個新舊版本的比較,盤點新版本到底爲咱們帶來了什麼新特性。javascript
本篇是系列文章的第三篇,重點並不在介紹 MenuItem 的功能特性,而在於對其代碼的討論; 對其設計的思考。 班門弄斧,見諒。html
循例是該先聊聊新特性的。Menu 有四個關聯的組件,分別爲:Menu、MenuItem、SubMenu、MenuGroup, 這些組件的新舊版本之間並無太大差別,向後兼容的很好,理論上能夠平滑升級。 新版本只有 MenuItem 增長了一個特性:支持連接模式,能夠經過向組件傳入 to
屬性啓用,效果與 連接模式的 Button 徹底同樣,這裏就不贅述了。vue
MenuItem 是一個很是很是簡單的組件,一開始以爲並無太多好寫的,細細看了代碼...我的感受問題很多,仍是有必要單獨寫一篇文章聊聊的。java
首先,依然是代碼重複的問題,在 Button
篇中 咱們已經見識了一些無心義的重複,在 MenuItem
組件中也是不遑多讓啊:node
<template>
<a v-if="to" :href="linkUrl" :target="target" :class="classes" @click.exact="handleClickItem($event, false)" @click.ctrl="handleClickItem($event, true)" @click.meta="handleClickItem($event, true)" :style="itemStyle"><slot></slot></a>
<li v-else :class="classes" @click.stop="handleClickItem" :style="itemStyle"><slot></slot></li>
</template>
複製代碼
這段模板有兩處重複,一是標籤,二是事件綁定。git
模板中,經過判斷 to
屬性,肯定須要渲染的標籤類型,用於兼容新增的連接模式,這種寫法很符合直覺,但有另外一種更優雅的方案:is
特性,一樣的功能,用 is
實現:github
/* 模擬MenuItem組件 */
Vue.component("MenuItem", {
name: "MenuItem",
// 簡化過的模板,乾淨無重複
template: `<component :is="tagName" v-bind="tagProps"><slot></slot></component>`,
props: {
to: { type: String, required: false }
},
computed: {
isLink() {
const { to } = this;
return !!to;
},
// 使用計算器屬性,按需計算標籤名
// 這種方式能夠承載更復雜的計算邏輯
tagName() {
const { isLink } = this;
return isLink ? "a" : "li";
},
// 經過計算器+v-bind 語法,實現按標籤類型傳遞不一樣屬性
// 這裏把原本放在模板的運算,轉嫁到計算器上
tagProps() {
const { isLink, to } = this;
const baseProps = { class: "menu-item", style: { display: "block" } };
if (isLink) {
return Object.assign(baseProps, {
href: to,
target: "_blank"
});
}
return baseProps;
}
}
});
複製代碼
示例中使用了 computed 屬性、v-bind、is 三種特性,把本應在模板作的計算轉移到計算屬性;經過 v-bind
綁定複雜對象;經過 is
渲染不一樣的標籤類型...達成與 iView 相同的功能,運行效果歡迎到 在線 demo 體驗。這種寫法,有兩個好處,一是減小模板上的重複;二是減小模板上的計算,轉而在計算屬性上實現,配合緩存效果,有必定的性能提高。設計模式
另一個問題,在 iView 的 MenuItem 中,a
標籤會重複綁定三次相關的 click 回調,分別配以 exact
、ctrl
、meta
,這種寫法在 Button
組件也出現過,用以模擬 a
標籤的不一樣點擊效果,以前在 Button
篇已作過深刻討論,這裏再也不贅述。api
如今,咱們看看 Menu 與 MenuItem 的模板代碼:緩存
<!-- Menu 組件模板部分源碼 -->
<template>
<ul :class="classes" :style="styles"><slot></slot></ul>
</template>
<!-- MenuItem 組件模板部分源碼 -->
<template>
<a v-if="to"><slot></slot></a>
<li v-else :class="classes"><slot></slot></li>
</template>
複製代碼
若是沒有 to
屬性,MenuItem 渲染爲 li
,這沒問題,但若是是連接模式,渲染結果就會是:
<ul>
<a></a>
<a></a>
<a></a>
...
</ul>
複製代碼
ul
中直接包含了 a
!遙想我最初學習 html 的時候,就已經被一再警告 ul
就應該老老實實包着 li
,確實也偶爾會看到其餘一些框架漠視這條規則,沒成想在 iView 這裏也能遇到。 h5 包容性是很強,這段代碼徹底能夠 work,沒毛病,但沒毛病不表明足夠好,咱們本能夠作的更好,爲何不選擇作的更好呢? 解法很簡單,依然是 is
特性,只需多一層包裹,核心代碼以下:
<template>
<li class="menu-item">
<component :is="tagName">
<slot></slot>
</component>
</li>
</template>
<script> export default { computed: { tagName() { const { isLink } = this; return isLink ? "a" : "span"; } } }; </script>
複製代碼
這個問題有點複雜,要講述清楚並不容易,還望讀者朋友們能給多些耐心。
我注意到在 MenuItem 組件中有這樣 一行代碼:this.$on('on-update-active-name', (name) => {...}
,MenuItem 會在回調中給自身設置各類值。 事件綁定的代碼用的多了,但這種組件本身偵聽本身的方式卻很少見,更奇怪的是 MenuItem 並無 $emit
過 on-update-active-name
事件。 出於好奇,我仔細翻查源碼,發現真正發出 on-update-active-name
事件的是父級 Menu 組件!
通常狀況下,Menu 與 MenuItem 是以父子關係成對出現的組件,好比:
<template>
<Menu active-name="1">
<MenuItem name="1">內容管理</MenuItem>
<MenuItem name="2">用戶管理</MenuItem>
</Menu>
</template>
複製代碼
上例中,改變 Menu 的active-name
值後,Menu 會執行 this.broadcast('MenuItem', 'on-update-active-name', this.currentActiveName);
,即調用broadcast
函數,向下廣播 on-update-active-name
事件,注意咱們的關鍵字:向下廣播! 1.x 版本的 Vue 確實提供過兩種傳播事件的方法:$dispatch
、$broadcast
,其中 $broadcast
用於父組件向子組件 傳播 事件,但到 2.x 時放棄了這種設計,官網 提供的說法是這樣的:
由於基於組件樹結構的事件流方式實在是讓人難以理解,而且在組件結構擴展的過程當中會變得愈來愈脆弱。 這種事件方式確實不太好,咱們也不但願在之後讓開發者們太痛苦。而且
$dispatch
和$broadcast
也沒有解決兄弟組件間的通訊問題。
我確信這是一個合理的設計優化 —— $broadcast
這種組件通信方式會增長父子組件間的耦合性,不管是業務層面的開發,仍是框架層面的開發,都應該摒棄這種設計模式。 但 iView 卻大方復辟,不惜自行實現了 一套 $broadcast
邏輯,爲何?
我認爲一種可信的說法是:這是不得已的妥協。 Menu 組件提供了 active-name
屬性,用於指明當前處於激活態的菜單項,但真正使用 active-name
屬性的則是 MenuItem 組件。那麼 Menu 從用戶拿到 active-name
後,如何傳遞到 MenuItem 組件呢?iView 選擇了經過向下廣播事件的方式,將值傳遞給 Menu 組件下的 MenuItem,合理有效,只是 broadcast
的復辟,讓我以爲很是不舒服。
問題梳理清楚了,那麼如何優化?
最簡單的方式,是遵循 Vue 官網的建議,使用 Vuex 管理狀態。這在平常業務開發中是至關有效的,但做爲一個框架卻萬萬使不得 —— 你總不能強行綁着另外一個框架,要求用戶必須同時使用吧?
做爲一個變通,也能夠設計一個全局狀態變量,但這必然又會引起更多問題。
另外一種方法是經過 JSX 方式,在渲染 MenuItem 前以 props 方式,將 active-name
給傳過去:
Vue.component("MenuItem", {
render() {
const {
$slot: { default: children },
activeName
} = this;
return (
<ul> {children.map(node => { node.props = { activeName }; return node; })} </ul>
);
}
});
複製代碼
若是咱們只有 Menu、MenuItem,那上面的方式已經足夠實現功能,也算是比較優雅,但若是把 SubMenu、MenuGroup 組件加入考慮範圍,那麼問題就會變得更復雜 —— active-name
須要從 Menu 跨過中間的 SubMenu、MenuGroup 傳遞到 MenuItem。這種跨組件的信息傳遞,在 JSX 環境下,我只想到兩種解決方案:在 Menu 遞歸查找 MenuItem 組件;在 SubMenu、MenuGroup 中重複定義 props 的賦值邏輯。
最近新冒出來一個 UI 庫 —— ant-design
,它的 Menu
正是基於 JSX 方式實現的,原諒我才疏學淺,看起來實現費勁吃力。
Vue 2.2.0 版本後提供了 provide/inject 特性,官網是這麼介紹的:
這對選項須要一塊兒使用,以容許一個祖先組件向其全部子孫後代注入一個依賴,不論組件層次有多深, 並在起上下游關係成立的時間裏始終生效。若是你熟悉 React,這與 React 的上下文特性很類似。
這真是一個大殺器 —— 祖先組件能夠聲明須要向全部後代傳遞的值;然後代組件,不管多深層次的後代,均可以按需訂閱感興趣的內容。我用這個特性作了個簡單的 demo,核心代碼:
Vue.component("MenuItem", {
template: `<li :class="classes" class="menu-item"><slot></slot></li>`,
// 在此聲明「注入」activeName值
inject: ["activeName"],
props: {
name: { type: String, required: true }
},
computed: {
classes() {
const { activeName, name } = this;
return activeName === name ? "active" : "";
}
}
});
Vue.component("Menu", {
template: `<ul class="menu"><slot></slot></ul>`,
// 向全部後代組件傳遞此項
provide() {
return {
activeName: this.activeName
};
},
props: {
activeName: { type: String, required: true }
}
});
複製代碼
修改後的 Menu、MenuItem 依然能夠保持父子關係,互相之間卻不強耦合 —— 任何經過 provide
提供 activeName
屬性的組件,均可以做爲 MenuItem 的祖先。嵌套 Menu 也能夠變得更簡單些,我寫了另一個 demo,歡迎查閱,時間關係,再也不贅述。