provide和inject原来用的不多,只是见人引用axios的时候在main.js里使用provide来注入
app.provide('axios', axios)
这样,在所有的vue文件里都可以使用inject来获取这个注入的axios
const axios = inject("axios");
这种利用provide和inject做全局变量的注入是比较常见的用法,后来我也一直使用这种方式进行axios的引用。
直到我看到ElementUI Tabs的做法后,才发现provide和inject还可以用到多级组件的协同上。
我们知道Tabs是由多个TabPane组成的,Tabs要负责显示标题,提供统一的切换功能,TabPane提供具体单个内容的展示
上面这个就是由两个TabPane组成的Tabs
这个两级关系的复杂关系在于,在具体实现的时候,每个pane的标题等信息是在pane里指定的,但是具体的展示实现,却是在父级的tabs里面去提供一个切换按钮。类似这样的结构(APIcat/ui/src/plugins/tabs.vue):
<div class="tabs-wrapper">
<ul class="tabs-labels">
<li v-for="(label, i) in tabs" :key="label?.name || i" @click="selectTab(label.name)"
:class="{ active: state.selectedIndex == (label?.name || i) }">
{{
label?.label
}}</li>
</ul>
<div class="contents">
<slot></slot>
</div>
</div>
父级通过一个独立的ul控件实现按钮和选中状态的展示,并通过另外一个div来包装子级内容。
如果子级的TabPane无法动态向父级提供数据的话,能想到的解决方案大概有两个:
一般方案1:使用template渲染子级
父级改为这样:
<div class="tabs-wrapper">
<ul class="tabs-labels">
<li v-for="(label, i) in tabs" :key="label?.name || i" @click="selectTab(label.name)"
:class="{ active: state.selectedIndex == (label?.name || i) }">
{{
label?.label
}}</li>
</ul>
<div class="contents">
<slot v-for="pane,index in panes" :key="index" :name="'pane-'+index"></slot>
</div>
</div>
使用时,不适用tab-pane组件,改为在tabs使用template-1,2,3来提供具体内容
<tabs :panes="['统计简报','客户报告']">
<template #pane-0></template>
<template #pane-1></template>
</tabs>
这样的写法能解决问题,但是缺乏子层造成子层不能做更多自定义属性的设置,同时,总的观感也差了很多,理解起来也更为费劲,远比“在tabs再套一层tab-pane”来的复杂。
一般方案2:使用props向下层传递
在调用方中进行tab-pane和tabs的数据传递
<tabs :panes="panes">
<tab-pane label="统计简报" @add-pane="pane=>panes.push(pane)"></tab-pane>
<tab-pane label="客户报告" @add-pane="pane=>panes.push(pane)"></tab-pane>
</tabs>
tab-pane通过emit向调用方传递pane的具体信息,调用方再通过tabs的props把具体的pane传递给父级。
这样的话,信息还需要调用页码来处理,整体的模块设计的独立性就破坏了。
优化方案:使用provide和inject
使用provide和inject,为我们提供了一套优化方案:
第一步,在tabs定义一个状态参数,利用reactive将其设置为响应式参数,再通过provide提供
<script setup>
import { provide, reactive } from "vue";
const state = reactive({
selectedIndex: null,
count: 0,
labels: [], //可选,子层上传自己的名称
});
provide("tabsProvider", state)
</script>
状态里主要是两个属性:
- selectedIndex表示当前的选中面板的index
- count表示tab的总数
第二步,在tab-pane子级中,通过inject获取到这个状态参数,并在onBeforeMount回调中更新该值
import { inject, onBeforeMount, watch, ref } from "@vue/runtime-core";
const tabs = inject("tabsProvider");
const no = ref(-1);
onBeforeMount(() => {
//设置一个默认的选中项
if (tabs.selectedIndex == null) {
tabs.selectedIndex = tabs.count
}
//把目前的count设置为自己的序号
no.value = tabs.count;
// 向上层设置标签内容
tabs.labels[no.value]=props.label;
//如果预设了选中index并且和自己的序号相同,则显示自己的内容
if (tabs.selectedIndex == no.value) {
isActive.value = true;
}
tabs.count++;
});
//监听选中index的变化情况,根据情况设置显示或隐藏
watch(
() => tabs.selectedIndex,
() => {
isActive.value = no.value === tabs.selectedIndex;
if (isActive.value) {
emits("shown")
}
}
);
这种方案的巧妙之处在于他充分利用了dom加载顺序,在onBeforeMount回调中实际利用了tab-pane顺序加载的过程,实现了父级计数和子级序号获取,并依靠provide提供的响应式全局状态变量实现了对选中状态的子父级传递。
上面的示例基本完成了子级的按选中状态设置,和tab-pane的标题属性传递给父级的主要设置功能。
这样设计的组件就可以完美的达到两级自定义组件协同的效果:
<tabs>
<tab-pane label="统计简报"></tab-pane>
<tab-pane label="客户报告"></tab-pane>
</tabs>
官方文档关于provide和inject的介绍,主要介绍了在多级父子关系中的全局穿透能力:
依赖注入 | Vue.js (vuejs.org)https://cn.vuejs.org/guide/components/provide-inject.html这当然是provide的重要功能之一,但是缺了本文介绍的在组件中和响应式配合达到动态协同效果这样的奇妙用法介绍,因此今天补充一文介绍这一特性,如果有需要参考的,可以参考我们的开源项目APIcat的相关代码,在ui/src/plugins里面,基本是Element-UI的简化版本,主要为了节省代码大小,自己做了一个。
并祝大家新年快乐!