从 React 到 Vue:在 Vue3 中 创建 createContext 优雅地实现依赖注入
前言
vue的 provide/inject
也是一种实现组件之间依赖注入的方式,但是他也会存在一些痛点。
- 依赖注入层级关系不明确。提供的值可以被任何后代组件访问到。这种方式虽然提高了组件的复用性,但是也使得依赖注入的层级关系变得不明确,如果组件的依赖注入关系过于复杂,会导致代码的可读性和可维护性下降。
- 数据流向不够明显。 后台组件可以直接访问到提供的值,但是无法判断这些值是从哪里来的。这种方式可能会导致组件之间的耦合度增加,不利于组件的服用和维护。
- 不支持类型检查。
provide/inject
提供的方法和注入的方法类型割裂,只能用于泛型约束无法通过provide
时进行类型推导,导致类型容易错乱。
下面我实现了一个方法来规避这些问题。具体请看 createContext实现
createContext 实现
先来说一下 createContext
是用来做什么的?
createContext
能够让你创建一个 context
以便组件能够提供与读取局部层的数据。 直接说白一点就是 vue
中 provide/inject
的功能, 可以来完成提供与注入数据的工作
为了处理以上我们所描述的一些痛点,我基于 provide/inject
封装了一个类似于React的createContext
方法来优雅地实现依赖注入,具体步骤如下:
- 在vue项目中使用
provide/inject
来实现依赖注入。 - 创建一个
createContext
方法,该方法返回一个对象包含了Provider
Consumer
useContext
。 - 在
Provider
组件中,将提供的值存储在组件的provide
中。 - 在
Consumer
组件中,使用inject
来获取提供的值。 - 在
useContext
方法中,返回了ComputedRef
对应的 value 是inject
获取提供的值。 - 在使用时,可以通过
Provider
提供需要共享的数据,通过Consumer
来消费这些数据。
优势
- 更加灵活的数据流向。 createContext的Provider组件可以将数据传递给它的后代组件,而后代组件在不经过父组件的情况下直接访问到这些数据。这种方式可以避免props层层传递、emit事件层层发射 的问题,提高了组件的可复用性。
- 更加简介的使用方式。 通过使用
useContext
函数,组件可以直接访问到Provider
提供的值,而不需要使用Consumer
组件来包裹子组件,这种方式可以减少组件的嵌套层次,提高了代码的可读和可维护性。
下面是代码实现部分:
核心代码
// context.ts
import { defineComponent, provide, computed, inject, type PropType, type ComputedRef } from 'vue';
export function createContext<T>(defaultValue: T) {
const KEY = Symbol('CREATE_CONTEXT_KEY');
const Provider = defineComponent({
props: {
value: {
type: [Object, Number, String, Boolean, null, undefined, Function] as PropType<T>,
required: true,
},
},
setup(props, ctx) {
provide(
KEY,
computed(() => props.value || defaultValue),
);
return () => ctx.slots.default?.();
},
});
const useContext = () => inject<ComputedRef<T>>(KEY) || computed(() => defaultValue);
const Consumer = defineComponent({
setup(props, ctx) {
const value = useContext();
return () => ctx.slots.default?.(value.value);
},
});
return {
Provider,
Consumer,
useContext
};
}
使用参考
createContext(defaultValue)
在任意组件外调用 createContext
来创建一个上下文
import {createContext} from './context.ts'
const ThemeContext = createContext("light")
参数
defaultValue
: 当包裹的组件没有提提供值时,就会使用该值作为默认的上下文,倘若你不需要指定任何默认值时,可以不传,默认上下文为 undefined
, 该值用作于最后的备选数据,永远不会改变。
返回值
createContext
返回一个 context 对象
ThemeContext.Provider
: 他是一个DefineComponent
,接收一个value
参数,让你提供上下文的值给被他包裹的子组件。ThemeContext.Consumer
: 他是一个DefineComponent
, 他的默认插槽default
会提供一个value
供子组件使用。ThemeContext.useContext
: 他是一个方法,该方法返回是一个ComputedRef
对应的值时 最近一层Provider
的值。
ThemeContext.Provider
使用上下文 Provider 包裹组件,来为里面所有的组件指定一个 context
template 写法
<script setup lang="ts">
import { ref } from 'vue';
import { ThemeContext } from './ThemeContext';
import Page from './Page.vue';
const theme = ref('light');
</script>
<template>
<ThemeContext.Provider :value="theme">
<Page></Page>
</ThemeContext.Provider>
</template>
jsx 写法
<script lang="tsx">
import { ref, defineComponent } from 'vue';
import { ThemeContext } from './ThemeContext';
import Page from './Page.vue';
export default defineComponent({
setup() {
const theme = ref('light');
return () => (
<ThemeContext.Provider value={theme.value}>
<Page></Page>
</ThemeContext.Provider>
);
},
});
</script>
Props
value
: 该值为你想传递给所有处于这个 provider 内读取该 context 的组件,无论它们层级嵌套的有多深。都可以获取,该值你可以定义为任何类型。该 provider 内的组件可以通过调用 useContext()
来获取它上面最近的context provider 的值。相当于使用 vue3
中的 provide(key, value)
方法。
ThemeContext.Consumer
在useContext没有出来之前,这是一种很老的方法来读取上下文。
template 写法
<script setup lang="ts">
import { ThemeContext } from './ThemeContext';
</script>
<template>
<ThemeContext.Consumer #default="theme"> {{ theme }} </ThemeContext.Consumer>
</template>
<style lang="less"></style>
jsx 写法
<script lang="tsx">
import { defineComponent } from 'vue';
import { ThemeContext } from './ThemeContext';
export default defineComponent({
setup() {
return () => (
<ThemeContext.Consumer>
{{
default(theme) {
return <div>{theme}</div>;
},
}}
</ThemeContext.Consumer>
);
},
});
</script>
<style lang="less"></style>
ThemeContext.useContext
template 写法
<script setup lang="ts">
import { ThemeContext } from './ThemeContext';
const theme = ThemeContext.useContext();
</script>
<template>
{{ theme }}
</template>
jsx 写法
<script lang="tsx">
import { defineComponent } from 'vue';
import { ThemeContext } from './ThemeContext';
export default defineComponent({
setup() {
const theme = ThemeContext.useContext();
return () => <div>{theme.value}</div>;
},
});
</script>
<style lang="less"></style>
在以上示例中我基本演示了一下三种方法的使用场景, Provider
提供了一个 theme
,使用 Consumer
和 useContext
来消费这个值。 当后代组件使用这个值的时候,将会被 vue
进行响应依赖收集,那 theme
发生变化, 使用组件的 redner
就会重新渲染。
这种方式可以让我们根据 provide/inject
优雅地实现依赖注入,避免了组件之间的耦合,提高了代码的复用性。
总结
本文借鉴了 React 中 createContext api,并且在原有的基础上调整,使用 provide/inject
创作而成。
优雅地实现依赖注入可以提高代码的可读性和可维护性,避免了组件之间的耦合,提高了代码的复用性,在实际开发中,需要根据具体场景选择合适的依赖注入方式,避免出现问题。
最后感谢大家的阅读,希望本文对你在前端开发的学习和实践中有所帮助。继续保持好奇心,追求卓越,享受前端开发的旅程!
转载自:https://juejin.cn/post/7249624871722221623