iview升级指南——MenuItem篇

iview升级指南——MenuItem篇
iview 在今年 7 ⽉ 28 号发布了 3.0.0 版本,⼤版本升级往往意味着功能、接⼝的⼤变更。 虽然官⽹已经有长长的,但看起来还是有些抽象了, 所以我决定做个新旧版本的⽐较,盘点新版本到底为我们带来了什么新特性。
本篇是系列⽂章的第三篇,重点并不在介绍 的功能特性,⽽在于对其代码的讨论; 对其设计的思考。 班门弄斧,见谅。
唯⼀新增的特性 —— ⽀持链接模式
吉林省第二实验中学
循例是该先聊聊新特性的。Menu 有四个关联的组件,分别为:Menu、MenuItem、SubMenu、MenuGroup, 这些组件的新旧版本之间并没有太⼤差异,向后兼容的很好,理论上可以平滑升级。 新版本只有 MenuItem 增加了⼀个特性:⽀持链接模式,可以通过向组件传⼊ to 属性启⽤,效果与 完全⼀样,这⾥就不赘述了。
问题
MenuItem 是⼀个⾮常⾮常简单的组件,⼀开始觉得并没有太多好写的,细细看了代码...个⼈感觉问题不少,还是有必要单独写⼀篇⽂章聊聊的。
问题⼀:代码重复
⾸先,依然是代码重复的问题,在 我们已经见识了⼀些⽆意义的重复,在 MenuItem 组件中也是不遑多让啊:
<template>
<a
maxplus2下载v-if="to"
:href="linkUrl"
:target="target"
:class="classes"
@act="handleClickItem($event, false)"
@l="handleClickItem($event, true)"
@a="handleClickItem($event, true)"
:><slot></slot></a>
<li v-else :class="classes" @click.stop="handleClickItem" :><slot></slot></li>
</template>
复制代码
这段模板有两处重复,⼀是标签,⼆是事件绑定。
1. 重复的标签定义
模板中,通过判断 to 属性,确定需要渲染的标签类型,⽤于兼容新增的链接模式,这种写法很符合直觉,但有另⼀种更优雅的⽅案:特性,同样的功能,⽤ is 实现:
/* 模拟MenuItem组件 */
Vueponent("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;
}
}
});
复制代码
⽰例中使⽤了 、、 三种特性,把本应在模板做的计算转移到计算属性;通过 v-bind 绑定复杂对象;通过 is 渲染不同的标签类型...达成与iView 相同的功能,运⾏效果欢迎到 体验。这种写法,有两个好处,⼀是减少模板上的重复;⼆是减少模板上的计算,转⽽在计算属性上实现,配合缓存效果,有⼀
定的性能提升。
2. 重复的事件绑定
另外⼀个问题,在 iView 的 MenuItem 中,a 标签会重复绑定三次相关的 click 回调,分别配以 exact、ctrl、meta,这种写法在 Button 组件也出现过,⽤以模拟 a 标签的不同点击效果,之前在 已做过深⼊讨论,这⾥不再赘述。
问题⼆:不符合 html 标准
现在,我们看看 Menu 与 MenuItem 的模板代码:
<!-- Menu 组件模板部分源码 -->
<template>
<ul :class="classes" :><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 与 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 却⼤⽅复辟,不惜⾃⾏实现了 ,为什么?
我认为⼀种可信的说法是:这是不得已的妥协。 Menu 组件提供了 active-name 属性,⽤于指明当前处于激活态的菜单项,但真正使⽤active-name 属性的则是 MenuItem 组件。那么 Menu 从⽤户拿到 active-name 后,如何传递到 MenuItem 组件呢?iView 选择了通过向下⼴播事件的⽅式,将值传递给 Menu 组件下的 MenuItem,合理有效,只是 broadcast 的复辟,让我觉得⾮常不舒服。
问题梳理清楚了,那么如何优化?
1. 通过 Vuex 管理状态
最简单的⽅式,是遵循 ,使⽤ Vuex 管理状态。这在⽇常业务开发中是相当有效的,但作为⼀个框架却万万使不得 —— 你总不能强⾏绑着另⼀个框架,要求⽤户必须同时使⽤吧?
作为⼀个变通,也可以设计⼀个全局状态变量,但这必然⼜会引发更多问题。
2. 通过 JSX 实现
另⼀种⽅法是通过 JSX ⽅式,在渲染 MenuItem 前以 props ⽅式,将 active-name 给传过去:
Vueponent("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 库 —— ,它的 正是基于 JSX ⽅式实现的,原谅我才疏学浅,看起来实现费劲吃⼒。
3. 通过实现
Vue 2.2.0 版本后提供了 特性,官⽹是这么介绍的:
这对选项需要⼀起使⽤,以允许⼀个祖先组件向其所有⼦孙后代注⼊⼀个依赖,不论组件层次有多深, 并在起上下游关系成⽴的时间⾥始终⽣效。如果你熟悉 React,这与 React 的上下⽂特性很相似。
这真是⼀个⼤杀器 —— 祖先组件可以声明需要向所有后代传递的值;⽽后代组件,⽆论多深层次的后代,都可以按需订阅感兴趣的内容。我⽤这个特性做了个简单的 ,核⼼代码:
Vueponent("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" : "";
}
}
});
Vueponent("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 也可以变得更简单些,我写了另外⼀个 ,欢迎查阅,时间关系,不再赘述。

本文发布于:2024-09-22 19:31:03,感谢您对本站的认可!

本文链接:https://www.17tex.com/xueshu/396761.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:组件   属性   事件   标签   代码   模板   问题
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2024 Comsenz Inc.Powered by © 易纺专利技术学习网 豫ICP备2022007602号 豫公网安备41160202000603 站长QQ:729038198 关于我们 投诉建议