如何骚着用 Vue3 的 provider/inject (干货)

我们做一个父子组件数据传递,一般会使用 props,当我们需要多层级向下子组件传递使用 `provider`, `inject` 。而接下来本文提供一种比较 hack 的方式去使用 provider/inject。

如何骚着用 Vue3 的 provider/inject (干货)

0x00

一般而言,我们做一个父子组件数据传递,使用 props,当我们需要多层级向下子组件传递使用 provider, inject 。摘官方文档:

父组件可以作为其所有子组件的依赖项提供程序,而不管组件层次结构有多深。这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这个数据。

https://v3.cn.vuejs.org/guide/component-provide-inject.html

0x01

我们可以得知,利用 provider, inject 可以在 Vue3 中可无视多子层级直接传递需要的数据。
而一般的用法可以如下:

import { createApp, defineComponent, provider, inject, reactive, readonly, toRefs } from 'vue';
// Provider 包装组件
const MyConfigProvider = defineComponent({
    name: 'MyConfigProvider',
    props: ['prefixCls', 'title', 'i18n'],
    setup (props, { slots }: SetupContext) {
    	const { prefixCls, title, i18n } = toRefs(props);
        const context = reactive({
          prefixCls,
          title,
          i18n
        });
        provide('myConfig', readonly(context));
        return () => slots.default?.();
    }
});

// 测试用子组件
const ChildComp = defineComponent({
    name: 'ChildComp',
    setup () {
        const myConfig = inject('myConfig', {});

        return () => (
            <>
                <p>{myConfig.prefixCls}</p>
                <p>{myConfig.title}</p>
            </>
        )
    }
});

// App.vue
const App = defineComponent({
    name: 'App',
    setup () {
        const state = reactive({
          prefixCls: 'myui',
          title: 'MyApp',
          i18n: (key: string) => key
        });
        return () => (
            <div id="#app">
                <MyConfigProvider {...state}>
                    <ChildComp />
                </MyConfigProvider>
            </div>
        )
    }
});

const app = createApp(App);
app.mount('#app');

从上面的用法,可以看出,如果需要默认参数,或者注入 KEY 等,我们需要每次频繁的写 inject(Key, defauleValue);,并且还需要创建一个专门用于向下传递的 provider 包装组件来进行参数传递。

0x02

为了减少这方面的代码,可以抽离出一个专门用来创建 上下文属性传递的 provider, inject 套娃封装。
如下:

// context.ts

// 注入类型
export type ContextType<T> = any;
// 返回类型
export type CreateContext<T> = {
  UnwrapRef<T> | T,
  DefineComponent<{}, () => VNode | VNode[] | undefined, any>,
}
// 创建 Provider 和响应式数据绑定方法 (provider)
export const createContext = <T>(
  context: ContextType<T>,
  contextInjectKey: InjectionKey<ContextType<T>> = Symbol(),
): CreateContext<T> => {
  const state = reactive<ContextType<T>>({
    ...toRaw(context),
  });

  const ContextProvider = defineComponent({
    name: 'ContextProvider',
    inheritAttrs: false,
    setup(props, { slots }: SetupContext) {
      // readonly 是为了防止被子组件修改状态
      provide(contextInjectKey, readonly(state));
      return () => slots.default?.();
    },
  });

  return [
    state,
    ContextProvider,
  ];
};

// 使用 Provider 传递的属性 方法(inject)
export const useContext = <T>(
  contextInjectKey: InjectionKey<ContextType<T>> = Symbol(),
  defaultValue?: ContextType<T>,
): T => {
  return readonly(inject(contextInjectKey, defaultValue || ({} as T)));
};

上面这样的"套娃封装",既完成了一个简单的对 provider, inject 封装,现在可以利用 createContext() 来创建一个 Provider 和返回相应的响应数据。

<template>
  <my-context-provider>
      <your-custom-component />
      <your-custom-component2 />
      <your-custom-component3 />
  </my-context-provider>
</template>

<script lang="ts">
// 忽略 import
interface MyContextProps {
  param1: string;
  param2: boolean;
  someData?: string[];
}
// 现在可以在需要进行传递属性的组件外进行一层 `MyContextProvider` 的包装,如:
const [ state, MyContextProvider ] = createContext<MyContextProps>({
  param1: 'abc',
  param2: false,
  someData: ['a', 'b', 'c', 'd']
});

export default defineComponent({
  components: {
  	MyContextProvider,
  },
  setup() {
    state.param1 = 'aaa';
    return {};
  }
});
</script>

现在这样,能做到 state 改变,对该 provider 下面的所有使用了 useContext 组件的内部注入属性均发生改变。
但目前状况,是同样需要填写 inject key 来进行注入的,所有又衍生的再次套娃封装

import { InjectionKey } from 'vue';
import { createContext, useContext } from './hooks/context';

export interface RouteContextProps {
  breadcrumb?: any;
  menuData?: any[];
  isMobile?: boolean;
  prefixCls?: string;
  collapsed?: boolean;
  hasSideMenu?: boolean;
  hasHeader?: boolean;
  sideWidth?: number;
  hasFooterToolbar?: boolean;
  hasFooter?: boolean;
  setHasFooterToolbar?: (bool: boolean) => void;
}
// 定义注入 key
const routeContextInjectKey: InjectionKey<RouteContextProps> = Symbol();
// 创建注入组件和响应式数据
export const createRouteContext = (context: RouteContextProps) =>
  createContext<RouteContextProps>(context, routeContextInjectKey);
// 子组件使用响应式数据
export const useRouteContext = () => useContext<RouteContextProps>(routeContextInjectKey);

上述详细源码和封装代码,可以从下面链接获取和查看

备注:以上的方法命名参考 React Hooks
本文同时发布在 掘金@Sendya

本文原创(@Sendya)转载请注明出处和原作者,谢谢。