iview 升級指南 —— MenuItem 篇

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

1. 重複的標籤訂義

模板中,經過判斷 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-bindis 三種特性,把本應在模板作的計算轉移到計算屬性;經過 v-bind 綁定複雜對象;經過 is 渲染不一樣的標籤類型...達成與 iView 相同的功能,運行效果歡迎到 在線 demo 體驗。這種寫法,有兩個好處,一是減小模板上的重複;二是減小模板上的計算,轉而在計算屬性上實現,配合緩存效果,有必定的性能提高。設計模式

2. 重複的事件綁定

另一個問題,在 iView 的 MenuItem 中,a 標籤會重複綁定三次相關的 click 回調,分別配以 exactctrlmeta,這種寫法在 Button 組件也出現過,用以模擬 a 標籤的不一樣點擊效果,以前在 Button已作過深刻討論,這裏再也不贅述。api

問題二:不符合 html 標準

如今,咱們看看 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 並無 $emiton-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 的復辟,讓我以爲很是不舒服。

問題梳理清楚了,那麼如何優化?

1. 經過 Vuex 管理狀態

最簡單的方式,是遵循 Vue 官網的建議,使用 Vuex 管理狀態。這在平常業務開發中是至關有效的,但做爲一個框架卻萬萬使不得 —— 你總不能強行綁着另外一個框架,要求用戶必須同時使用吧?

做爲一個變通,也能夠設計一個全局狀態變量,但這必然又會引起更多問題。

2. 經過 JSX 實現

另外一種方法是經過 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 方式實現的,原諒我才疏學淺,看起來實現費勁吃力。

3. 經過 provide/inject 實現

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,歡迎查閱,時間關係,再也不贅述。

相關文章
相關標籤/搜索