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