likes
comments
collection
share

从 React 到 Vue:在 Vue3 中 创建 createContext 优雅地实现依赖注入

作者站长头像
站长
· 阅读数 14

前言

vue的 provide/inject 也是一种实现组件之间依赖注入的方式,但是他也会存在一些痛点。

  1. 依赖注入层级关系不明确。提供的值可以被任何后代组件访问到。这种方式虽然提高了组件的复用性,但是也使得依赖注入的层级关系变得不明确,如果组件的依赖注入关系过于复杂,会导致代码的可读性和可维护性下降。
  2. 数据流向不够明显。 后台组件可以直接访问到提供的值,但是无法判断这些值是从哪里来的。这种方式可能会导致组件之间的耦合度增加,不利于组件的服用和维护。
  3. 不支持类型检查provide/inject 提供的方法和注入的方法类型割裂,只能用于泛型约束无法通过provide 时进行类型推导,导致类型容易错乱。

下面我实现了一个方法来规避这些问题。具体请看 createContext实现

createContext 实现

先来说一下 createContext 是用来做什么的?

createContext 能够让你创建一个 context 以便组件能够提供与读取局部层的数据。 直接说白一点就是 vueprovide/inject 的功能, 可以来完成提供与注入数据的工作

为了处理以上我们所描述的一些痛点,我基于 provide/inject 封装了一个类似于React的createContext方法来优雅地实现依赖注入,具体步骤如下:

  1. 在vue项目中使用 provide/inject 来实现依赖注入。
  2. 创建一个 createContext 方法,该方法返回一个对象包含了 Provider Consumer useContext
  3. Provider 组件中,将提供的值存储在组件的 provide 中。
  4. Consumer 组件中,使用 inject 来获取提供的值。
  5. useContext 方法中,返回了ComputedRef 对应的 value 是 inject 获取提供的值。
  6. 在使用时,可以通过 Provider 提供需要共享的数据,通过 Consumer 来消费这些数据。

优势

  1. 更加灵活的数据流向。 createContext的Provider组件可以将数据传递给它的后代组件,而后代组件在不经过父组件的情况下直接访问到这些数据。这种方式可以避免props层层传递emit事件层层发射 的问题,提高了组件的可复用性。
  2. 更加简介的使用方式。 通过使用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 ,使用 ConsumeruseContext 来消费这个值。 当后代组件使用这个值的时候,将会被 vue 进行响应依赖收集,那 theme 发生变化, 使用组件的 redner 就会重新渲染。

这种方式可以让我们根据 provide/inject 优雅地实现依赖注入,避免了组件之间的耦合,提高了代码的复用性。

总结

本文借鉴了 React 中 createContext api,并且在原有的基础上调整,使用 provide/inject 创作而成。 优雅地实现依赖注入可以提高代码的可读性和可维护性,避免了组件之间的耦合,提高了代码的复用性,在实际开发中,需要根据具体场景选择合适的依赖注入方式,避免出现问题。

最后感谢大家的阅读,希望本文对你在前端开发的学习和实践中有所帮助。继续保持好奇心,追求卓越,享受前端开发的旅程!