需求分析
Vue3简单实现一个MVP版本的Menu,需求分析:
props接收一个menuList,作为菜单选项列表进行渲染- 消费组件(父组件)可以通过
v-model获取到当前被选中的菜单选项的值,即menuList[index] - 消费组件可以通过监听
Menu组件的changeMenu事件来指定在菜单切换时的回调
结构搭建
MVP布局的话,单纯使用一个ul作为容器,menuList中的每一项用li渲染即可,但这里出于多种考虑,外面再包一层div.menu-container,这样的好处是:
- 增加导航栏的布局灵活性,易于拓展。例如我们一个横向的菜单导航,左边可能几个选项,右边也有几个选项,这时候可以在
div.container里面再添加一个ul,实现选项集合两个ul之间的左右布局 - 作为一个vue组件,包裹一个
div在开发者工具中看dom结构也更加清晰 - ...
并简单添加样式,让li标签一行显示,并增加一定的间距
<template>
<div class="menu-container">
<ul class="menu-ul">
<li>导航选项1</li>
<li>导航选项2</li>
<li>导航选项3</li>
<li>导航选项4</li>
</ul>
</div>
</template>
<style scoped lang="less">
.menu-container {
.menu-ul {
display: flex; // 布局属性: li标签一行排列
column-gap: 50px; // 样式属性: 控制flex项目之间的间隙
li {
list-style-type: none; // 样式属性: 去除li标签的默认小圆点样式
background-image: linear-gradient(90deg, red, green); // 样式属性: 方便观察
}
}
}
</style>
效果:
实现props接收渲染列表
<script>中添加一个props配置项,我们图方便直接用default给一个默认值,然后<template>中也v-for替换渲染数据
<template>
<div class="menu-container">
<ul class="menu-ul">
<li v-for="(menuItem, index) in menuList" :key="index">{{ menuItem }}</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
export default defineComponent({
name: "Menu",
props: {
menuList: {
type: Array as PropType<string[]>,
default() {
return [
"产品&市场",
"技术&研发",
"管理&职场",
"活动&生活",
"其他",
];
},
}
},
setup(props) {
return {};
},
});
</script>
效果:
但这里有个坑,如果在setup中通过props.menuList的形式访问props属性可能会报错,我不知道具体是不是因为打包工具的原因,用vue-cli创建的基于webpack的项目上面的代码会TS报错:Property 'menuList' does not exist on type 'Readonly<LooseRequired<Readonly<ExtractPropTypes<readonly string[] | Readonly<ComponentObjectPropsOptions<Record<string, unknown>>>>> & { ...; }>>',但是基于vite创建的项目就没事。报错时如果想要在js代码中访问props属性,暂时我只想到了(props as any).menuList。针对这个错误真的很无奈...
添加v-model支持
相当于给自定义组件添加v-model,首先Menu组件中需要添加一个props属性,这个属性即为消费组件(父组件)中v-model:冒号后面的值,即父组件
App.vue:
<template>
// 即我们希望通过activeMenuInfo变量来获取Menu组件中选中的标签值,v-model:activeMenuItem,这里的activeMenuItem对应Menu组件中的props.activeMenuItem
<Menu v-model:activeMenuItem="activeMenuInfo" :menuList="menuList"/>
</template>
<script setup lang="ts">
import Menu from "@/components/Menu.vue";
import {ref} from "vue";
const menuList = ["产品&市场", "技术&研发", "管理&职场", "活动&生活", "其他"];
const activeMenuInfo = ref(menuList[0]); // activeMenuInfo初始化值理论上应为传给Menu组件的渲染列表的第一项
</script>
Menu.vue:
...
props: {
...
activeMenuItem: { // 添加activeMenuItem
type: String
}
},
},
同时,给li标签绑定Menu导航切换时的回调函数changeMenu,并传入index,这样可以在changeMenu函数体中通过props.menuList[index]访问选中的新导航的值。
<Menu />组件中emits数组中添加一个"update:activeMenuItem"事件,只需要调用emit("update:activeMenuItem", value)即相当于把父组件中通过v-model:activeMenuItem="xxx"绑定的xxx变量修改为value,这里emit是从setup函数第二个对象参数中解构出来的:
<template>
<div class="menu-container">
<ul class="menu-ul">
<li
v-for="(menuItem, index) in menuList"
:key="index"
@click="changeMenu(index)"
>
{{ menuItem }}
</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
export default defineComponent({
name: "Menu",
props: {
menuList: {
type: Array as PropType<string[]>,
default() {
return [
"产品&市场",
"技术&研发",
"管理&职场",
"活动&生活",
"其他",
];
},
},
activeMenuItem: { // 这个props属性即为父组件中 v-model: 冒号后面的字段,同时也是emit事件 update: 冒号后面的字段
type: String
}
},
emits: ["update:activeMenuItem"],
setup(props, {emit}) {
const changeMenu = (index: number) => {
emit("update:activeMenuItem", props.menuList[index]); // 切换导航时修改父组件中v-model绑定的变量为props.menuList[index]
}
return {
changeMenu
};
},
});
</script>
这样已经完成了Menu组件对v-model的支持,可以在父组件中通过watch函数监听来测试v-model绑定的变量的变化,点击导航后newValue和oldValue都正常访问,证明功能实现是没有问题的。
当然一般都会给选中的导航添加一些样式,所以我们在Menu组件中维护一个被激活导航的标识变量activeIndex,v-for遍历生成li时给index === activeIndex的li添加样式。
实现父组件对导航切换事件的监听
这个需求很简单,只需要在Menu组件中emits数组中添加一个changeMenu事件,然后在Menu组件内部的导航切换回调中执行emit("changeMenu"),即可触发父组件中对changeMenu事件监听的回调函数。
<Menu />最终代码
Menu.vue:
<template>
<div class="menu-container">
<ul class="menu-ul">
<li
v-for="(menuItem, index) in menuList"
:key="index"
@click="changeMenu(index)"
:class="{
'active-li': index===activeIndex
}"
>
{{ menuItem }}
</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, ref} from "vue";
export default defineComponent({
name: "Menu",
props: {
menuList: {
type: Array as PropType<string[]>,
default() {
return [
"产品&市场",
"技术&研发",
"管理&职场",
"活动&生活",
"其他",
];
},
},
activeMenuItem: {
type: String
}
},
emits: ["update:activeMenuItem", "changeMenu"],
setup(props, {emit}) {
// 选中导航的标识变量
const activeIndex = ref(0);
// 切换导航的回调
const changeMenu = (index: number) => {
activeIndex.value = index;
emit("update:activeMenuItem", props.menuList[index]);
emit("changeMenu");
}
return {
changeMenu,
activeIndex
};
},
});
</script>
<style scoped lang="less">
.menu-container {
.menu-ul {
display: flex; // 布局属性: li标签一行排列
column-gap: 50px; // 样式属性: 控制选项间隙,在flex项目与项目之间添加间隙
li {
list-style-type: none; // 样式属性: 去除li标签的默认小圆点样式
background-image: linear-gradient(
90deg,
red,
green
); // 样式属性: 方便观察
&.active-li {
background-image: linear-gradient(
270deg,
red,
green
);
}
}
}
}
</style>
App.vue:
<template>
<Menu
v-model:activeMenuItem="activeMenuInfo"
:menuList="menuList"
@changeMenu="test"
/>
</template>
<script setup lang="ts">
import Menu from "@/components/Menu.vue";
import {ref, watch} from "vue";
const menuList = ["产品&市场", "技术&研发", "管理&职场", "活动&生活", "其他"];
const activeMenuInfo = ref(menuList[0]); // activeMenuInfo初始化值理论上应为传给Menu组件的渲染列表的第一项
watch(activeMenuInfo, (newValue, oldValue) => {
console.log(newValue, oldValue);
})
const test = () => {
console.log("父组件中切换导航的回调函数");
}
</script>
<style>
* {
margin: 0;
padding: 0;
}
</style>