本来标题准备起 vue 组件设计,不过觉得不太好太过于广泛了,下面就以一个实际的例子来谈谈项目中用到的设计思想。
这个是一个很传统的级联选择器,在项目中使用大概有三个方面使用
- 第一个起到搜索作用,比如一个列表的筛选需要选定一定的条件
- 在创建或者编辑的时候使用,例如新建一个列表项
- 单独在某一个场景使用,比如只使用省这一级别
基于以上两点,直接硬怼肯定不行了,要保证这个组件的足够低耦合同时也要方便自己使用,比如使用的时候肯定期待,只需要放置组件,一些组件的变动交给内部处理。
为了方便讲解所有数据会固定,但是实际中可能数据是通过接口获取的
初步设计想法
向上抽离一个组件起到监听的作用,当根组件或者前置组件发生变化的时候,通知到watch
提醒watch
通知相关依赖组件,这里就是市
。
为了代码的精简,这里只展示两级
回想一下组件的通信,对于这种父子组件可以使用emit
的方式
不过在这里不行,因为上面举例子说了可能用作新建中。
也就是说可能是被这样包裹的
<a-form>
<watch>
<a-form-item>
<省><省 />
</a-form-item>
<a-form-item>
<市><市 />
</a-form-item>
</watch>
</a-form>
所以使用父子组件通信不太可行。 不过还是有两种方式的
- 使用
$parent
、$children
指定已创建的实例之父实例,在两者之间建立父子关系。子实例可以用 this.$parent
访问父实例,子实例被推入父实例的 $children
数组中。
- provide / inject
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。
组件实现
第一种方式的实现就是通过递归来实现,这个实现过程可以通过匹配组件相对应的name
实现,今天重点用provide / inject
的方式来实现。
代码本身不是特别多,直接就贴了
child.vue
<template>
<span class="cs-child">
<select v-bind="$attrs" :value="value" @change="change">
<option v-for="(item, k) of province" :key="k" :value="item"
>{{ item }}</option
>
</select>
</span>
</template>
<script>
export default {
props: ["value"],
model: {
prop: "value",
event: "change",
},
name: "cs-child",
inject: {
theme: {
default: () => undefined,
},
},
data() {
const province = [
"河北省",
"山西省",
"辽宁省",
"吉林省",
"黑龙江省",
"江苏省",
"浙江省",
"安徽省",
"福建省",
"江西省",
"山东省",
"河南省",
"湖北省",
"湖南省",
"广东省",
"海南省",
"四川省",
"贵州省",
"云南省",
"陕西省",
"甘肃省",
"青海省",
"台湾省",
];
return {
province,
};
},
methods: {
change(e) {
const value = e.target.value;
this.$emit("change", value);
this.trigger(value);
},
trigger(value = this.value) {
if (!this.theme) {
return;
}
this.theme.obj = {
...this.theme.obj,
child: value,
};
this.theme.$emit("change-item", value);
},
},
// 注意生命周期时机,在依赖组件我们是在created绑定的时间,需要隔离这个事件来触发事件
mounted() {
this.trigger();
},
};
</script>
relyonChild.vue
<template>
<span class="cs-relyonChild">
<select v-bind="$attrs" v-model="value">
<option v-for="(item, k) of list" :key="k" :value="item.value"
>{{ item.name }}</option
>
</select>
</span>
</template>
<script>
export default {
name: "cs-relyonChild",
inject: ["theme"],
data() {
return {
list: [{ name: 1, value: 1 }],
value: undefined,
};
},
created() {
this.theme.$on("change-item", this.change);
},
watch: {
value: {
handler(value) {
this.theme.obj = {
...this.theme.obj,
relyonChild: value,
};
},
immediate: true,
},
},
methods: {
change(value) {
this.list = new Array(100).fill().map((_item, key) => {
return {
name: `${value || ""}-${key}`,
value: key,
};
});
this.value = 0;
},
},
};
</script>
watch.vue
<template>
<span class="cs-watch">
<slot></slot>
</span>
</template>
<script>
export default {
name: "cs-watch",
props: ["value"],
model: {
prop: "value",
event: "change",
},
data() {
return {
obj: {
child: undefined,
relyonChild: undefined,
},
};
},
watch: {
obj: {
handler(value) {
this.$emit("change", value);
},
immediate: true,
},
},
provide() {
return {
theme: this,
};
},
};
</script>
OK,整体的代码就放到上面了,需要特别注意两点
- inject
inject: {
theme: {
default: () => undefined
}
},
这里之所以使用default
是为了让child.vue
可以单独使用,不然控制台会报一个错误。
$on
、$emit
绑定时机
relyonChild.vue
组件依赖child.vue
组件,在一个完整的生命周期,我们能拿到 this 的情况最早也是在created
中,所以relyonChild.vue
在created
中绑定了需要监听的事件,而child.vue
初始化的时候也需要传递一次事件,而这个事件只能晚于created
,否则会导致relyonChild.vue
监听不到。
使用方法
- index.js
export { default as child } from "./child.vue";
export { default as watch } from "./watch.vue";
export { default as relyonChild } from "./relyonChild.vue";
- App.vue
<template>
<div id="app">
<watch v-model="obj">
<label>
<span>省级区域:</span>
<child v-model="item"></child>
</label>
<label>
<span>子项区域:</span>
<relyonChild></relyonChild>
</label>
<button @click="click">查询</button>
</watch>
</div>
</template>
<script>
import { watch, child, relyonChild } from "./components/demo";
export default {
name: "App",
components: {
watch,
child,
relyonChild,
},
data() {
return {
item: "河南省",
obj: undefined,
};
},
};
</script>
最后
贴一个预览地址,有什么疑问可以多看几遍,子组件通过绑定$on
和$emit
来实现监听与触发