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,欢迎查阅,时间关系,再也不赘述。

相关文章
相关标签/搜索