起因
最近有人给我的 jz-gantt
提了个bug,说页面中放多个 gantt 组件会出现异常。我复现了一下,还真是。原因呢,也很简单,之前的升级小记中也记录过,就是因为 全局变量 冲突,当时没有意识到这会成为一个问题。这个之前还没考虑到,也是因为刚开始用 vue3,以为它会自动管理全局变量,但是,并不是~
因为挂载多个组件,导致变量名重叠,最后只有最后一个挂载的组件中的变量会生效,这就导致了多个组件出现了异常情况。简单来说,之前那种全局变量的方法是真正的全局变量,它适合用在项目中,而并不适合用在组件中。
修改思路
起初想直接挂个 vuex 完事,但总觉得很重,不友好,其实就是用一下状态管理,要组件级别的,挂个 vuex 完全没有必要,于是从其他方面入手考虑。
provide / inject
provide
与 inject
是 vue 的一大特性,也是官方推荐的一种 多层传递数据 的方式。
那在 vue3 中,可不可以让数据初始化之后,统一进行注入呢?答案是显然的。
有了这种思路,就可以着手开始改造了。
搭建过程
这里我只需要两个方法,一个负责 provide
,另一个负责 inject
即可。
我们要知道,一定是在父组件中
provide
,子组件才可以通过inject
获取到对应的内容。不能在同一组件或同一层兄弟组件中使用这种方式。
那么我们需要在组件最外层套一个 Wrap
作为根组件,这样所有子组件就都可以获取到对应数据。
我们的组件结构可以如下:
Wrap.vue
├-- Child1.vue
└-- Child2.vue
├-- GrandChild1.vue
└-- GrandChild2.vue
首先在 Wrap.vue
中直接注入需要的数据,这里要做的仅仅是提供数据:
<script lang="ts">
import { defineComponent, ref, provide } from 'vue';
export default defineComponent({
setup() {
const count = ref(1);
provide('count', count); // 将 count 注入到组件中
return { count };
}
});
</script>
这样就把变量注入到系统中,后代组件可以通过 inject
取出使用:
// Child1.vue
<script lang="ts">
import { defineComponent, inject, computed, Ref } from 'vue';
export default defineComponent({
setup() {
// 这里就可以获取数据,但是需要注意需要判空,所有 inject 返回的都是给定类型以及 undefined 两种类型
// 除了判空,还可以给 inject 添加默认值
const count = inject<Ref<number>>('count', 0);
const myCount = computed(() => count.value * 2);
}
});
</script>
这样就可以进行取值操作了。看上去很简单,但这里有两个问题:
- 注入与取出的字符串应当被定义,而不是自行填写
- 取值时的类型需要手动填写
如果小项目,这倒无所谓,但是随着项目增大,这样的问题就会尤为明显。而且这样的方式虽然简单,但是并不利于我们维护。所以我们要对它进行改造。
升级过程
为了以后可以更好的维护,我们可以参考 vuex 的写法,单独存放数据,再由各个组件统一调用。
首先新建一个文件,比如我们创建一个 src/store/index.ts
,只需要填写两个方法:
import { ref, provide, inject, Ref } from 'vue';
// 统一管理变量名,可以使用字符串,也可以使用 Symbol
export const COUNT = Symbol('count');
export const initStore = () => {
const count = ref<number>();
provide(COUNT, count);
}
export const useStore = () => {
// 这样会返回 number | undefined 类型
count: inject<Ref<number>>(COUNT),
// 如果不想判空,直接写类型,可以使用下面方式:
count: inject(COUNT) as Ref<number>,
// 再或者通过给默认值,有时候这样的写法更加安全可靠
count: inject<Ref<number>>(COUNT, ref(0)),
}
然后,我们只需要在 Wrap.vue
中引用并初始化 initStore
即可:
// Wrap.vue
<script lang="ts">
import { defineComponent } from 'vue';
import { initStore } from '@/store';
export default defineComponent({
setup() {
initStore();
}
});
</script>
而在其后代组件中直接调用 useStore
即可:
// SubChild2.vue
<template>
<div>{{ myCount }}</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
import { useStore } from '@/store';
export default defineComponent({
setup() {
const store = useStore();
const myCount = computed(() => store.count.value * 2);
}
});
</script>
这样的写法与 vuex 基本无两,只不过这个更加轻量,非常适合组件内部使用,而且易于扩展,只需要两个方法即可。
如何测试它
很多教程到此就结束了。但我并不这么认为,测试仍然是重要的一部分,为了项目更加稳定,我们需要测试。但是针对上面的方法,我们如何测试呢?
起始很简单,只需要通过 @vue/test-utils
提供的 global
参数就可以快速构建:
import { shallowMount } from '@vue/test-utils';
import SubChild2 from '@/components/SubChild2.vue';
import { COUNT } from '@/store/index.ts';
const wrapper = shallowMount(SubChild2, {
global: {
provide: {
COUNT: ref(1)
}
}
});
describe('Check SubChild', () => {
it('SubChild html', done => {
expect(wrapper.html()).toBe('<div>1</div>'); // true
done();
})
})
至此,才算完结。
文章评论