在 Vue.js 3 中使用 Composition API 的 provide/inject

笔者准备入职一家新公司,用的 Vue3 + TS 的技术栈,想着提前去熟悉下 Vue3 ,结果在查阅官方文档的时候遇到了一些疑惑,再深入研究了下,于是写下这篇文章用来记录,希望对在看的同学有所帮助,基于Vue.js 3 中使用 Composition API 的 provide/inject。

通常情况下,当我们要把数据从父组件传递给子组件时,会用到 props。Vue.js 让这件事变得简单易行。但有时,当我们需要把数据从父级组件传递给深层嵌套的子组件时,可能就会感到头疼了。

要是使用 props,不管组件在组件树结构里嵌套得多深,我们都得在组件树里一级一级地传递数据。这就叫做 "prop drilling",会让我们的应用看起来比实际情况更复杂。要是应用的状态很简单,使用 Vuex 就有点杀鸡用牛刀了。

幸运的是,Vue 有 provide / inject API,而且随着 Vue 3 引入 Composition API,它变得更好用了。

使用 provideinject 这一对功能,父组件就能向子组件发送数据,不管组件层次结构有多深。父组件有个 provide 函数来提供数据,子组件有个 inject 函数来获取这些数据。

在上面的图中,我们有三级子组件。我们想要传递的数据在父组件里,而数据的目标位置深深嵌套在组件树的第三层。我们可以用 props 来实现,但会牺牲代码的简洁性和可读性。下面来看看如何在不牺牲这两点的情况下完成这个操作。

首先,我们要用 create - vue 搭建一个新的 Vue 应用:

bash 复制代码
npm create vue@latest

使用 provide API

provide API 是一个函数,用于定义要传递给子组件的数据或对象。

要使用 provide 函数,我们先在 script 块里从 vue 显式导入它。然后,在组件的 setup() 函数里调用 provide,来定义要传递给子组件的数据或对象。

不过在这之前,你得知道 provide 函数接受两个参数:

  • 注入键:这是用来在后代组件中获取值的标识符,通常是一个字符串或符号。

  • 值:这是与注入键相关联的数据,后代组件可以使用。

html 复制代码
<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'

export default {
  setup() {
    const userLocation = inject('location', 'The Universe')
    const userGeolocation = inject('geolocation')

    return {
      userLocation,
      userGeolocation
    }
  }
}
</script>

在上面的代码中导入 provide 函数后,我们在 setup 函数里调用它。接下来,给第一个 provide 函数传递参数,注入键是 'location',值是 'North Pole'

对于第二个 provide 函数,传递一个包含 latitudelongitude 值的对象,键设置为 'geolocation'

使用 inject API

相比之下,inject API 用于获取由祖先组件(比如上一个例子中的提供组件)用 provide 函数提供的数据。

provide 函数一样,我们也得从 vue 导入 inject 函数,这样就能在组件的任何地方调用和使用这个函数了。

inject 函数也有两个参数:

  • 键:用来查找祖先组件提供的值的键,必须和 provide 函数中使用的注入键匹配。

  • 默认值(可选):如果找不到提供的键的值,就返回这个备用值。

看一下下面的代码:

html 复制代码
<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'

export default {
  setup() {
    const userLocation = inject('location', 'The Universe')
    const userGeolocation = inject('geolocation')

    return {
      userLocation,
      userGeolocation
    }
  }
}
</script>

首先,我们把 inject 函数导入到 MyMarker 组件中。然后,在 setup 函数里,把第一个 provide 函数中属性名为 'location' 的值赋给 userLocation 变量,还提供了一个可选的默认备用值 'The Universe'

接着,把第二个 provide 函数中属性名为 'geolocation' 的值赋给 userGeoLocation 变量。返回 userLocationuserGeoLocation 变量后,就能在 MyMarker 组件的任何地方使用它们的值了。

provide / inject 具备响应性

可惜的是,provide / inject 一开始并不是响应性的。不过幸运的是,有办法让它具有响应性,可以使用 Vue API 提供的 refreactive 函数。

我们得先从 vue 导入它们,然后调用 refreactive 函数,把要传递给子组件的值作为参数,再把函数存储在一个变量里。接着调用 provide 函数,传递注入键和它的值。

现在,如果任何属性发生变化,MyMarker 组件也会自动更新!

我们可以这样更新代码:

html 复制代码
<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue'

export default {
  components: {
    MyMarker
  },
  setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    provide('location', location)
    provide('geolocation', geolocation)
  }
}
</script>

导入 refreactive 函数后,调用 ref 函数并传入参数(值为 'North Pole'),然后把 ref 函数赋给 location 变量。

对于 reactive 函数,调用它并传入一个对象作为参数,再把 reactive 函数赋给 geolocation 变量。完成这些后,调用 provide 函数,传递要传递的数据的属性名和值。

在第一个 provide 函数中,把注入键设置为 'location',值设置为 location,也就是我们赋给 ref 函数的值。

在第二个 provide 函数中,把属性名设置为 'geolocation',值设置为 geolocation,也就是我们赋给 reactive 函数的值。

高级用例

除了简单的数据共享,provide / inject 还可以用于一些高级场景,解决复杂的组件交互问题。以下是一些高级用例:

依赖注入模式

这些是在 Vue 应用中使用 provide / inject 函数对管理依赖的技术,同时避免像组件紧密耦合这样的痛点。其中一种模式是服务的依赖注入。

服务的依赖注入是一种在多个组件之间共享服务或工具的技术,不会让它们与服务的实现紧密耦合。例如,假设我们有一个日志记录服务,想注入到多个组件中:

javascript 复制代码
// Logger服务
class Logger {
  log(message) {
    console.log(`[LOG]: ${message}`);
  }
}

使用 provide / inject,我们可以让组件与日志记录服务的实现解耦,同时保持组件的可重用性:

javascript 复制代码
// 提供日志记录服务
provide('logger', new Logger());

// 注入服务
const logger = inject('logger');
logger.log('This is a log message');

借助 Symbols 动态注入

在大型项目中,创建多个 provideinject 函数是不可避免的,这会增加由于意外覆盖和命名键冲突而导致键冲突的风险。不过,使用 Symbols 可以避免这个问题。

使用 Symbols,你可以创建唯一标识符,让键更清楚地表示提供值的预期用途,减少名称冲突的风险。

要使用 Symbols,首先在提供组件(父组件)中创建一个 Symbol 并提供一个值:

javascript 复制代码
import { Symbol } from 'vue'

const themes = Symbol('theme');

provide(themes, {
  color: 'blue',
  fontSize: '14px'
});

然后,在注入组件(子组件)中注入该值:

javascript 复制代码
const theme = inject(themes);
console.log(theme) // 输出 "theme"

从上面的例子可以看出,Symbols 需要声明额外的变量,这容易导致组件膨胀和维护问题。幸运的是,我们可以创建一个单独的实用 JavaScript 文件,包含并导出整个应用程序所需的所有键,然后在提供组件和注入组件中动态使用它们。

javascript 复制代码
// keys.js
export const userFirstName = Symbol();
export const userLastName = Symbol();
export const userFullName = Symbol();
export const userAge = Symbol();
export const userTitle = Symbol();
export const userDescription = Symbol();

然后这些键可以在提供组件和注入组件中动态导入和使用:

javascript 复制代码
// 提供组件
import { userFirstName } from './keys.js'

provide(userFirstName, 'Luca');

// 注入组件
import { userFirstName } from './keys.js';

const firstName = inject(userFirstName);
console.log(firstName); // 输出 'Luca'

类插件架构

鉴于 provide / inject 机制的松散耦合特性,我们可以用这对函数创建一个类似插件的系统,组件可以向父级或全局上下文注册自己。一个很好的例子是通知系统,组件可以注册到全局通知中心:

javascript 复制代码
// 提供组件
provide('notify', (message) => {
  notificationCenter.add(message);
});

然后,注入一个 notify 函数,它把消息发送到通知中心,就像插件的工作方式一样:

javascript 复制代码
// 注入组件
const notify = inject('notify');
notify('New message received');

利用 provide / inject 简化测试

使用 provideinject 可以大大简化 Vue 应用中的测试,特别是当你需要模拟某些依赖项时。注入模拟的依赖项、状态或服务可以让你隔离被测试的组件,控制其依赖项的行为。

例如,如果我们想单独测试前面的日志记录服务示例:

javascript 复制代码
// LoggerService.js
export class LoggerService {
  log(message) {
    console.log(message);
  }
}

// MyComponent.vue
<template>
  <div>{{ message }}</div>
</template>

<script>
export default {
  setup() {
    const logger = inject('logger');
    const message = 'Hello, World!';
    logger.log(message);
    return { message };
  }
};
</script>

我们可以注入 LoggerService 类的模拟版本,而不是真实的版本,如下所示:

javascript 复制代码
import { mount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';

test('logs the message on creation', () => {
  const mockLogger = {
    log: jest.fn(), // 模拟log函数
  };

  const wrapper = mount(MyComponent, {
    global: {
      provide: {
        logger: mockLogger, // 注入模拟的日志记录器
      },
    },
  });

  expect(mockLogger.log).toHaveBeenCalledWith('Hello, World!');
});

通过这种方法,你可以专注于测试组件的行为,而不用担心 LoggerService 类的实现细节。

控制测试环境

另一种简化测试的方法是创建一个受控且可预测的环境,在这个环境中,组件可以被隔离,专注于自己的行为,不受外部因素干扰。

一个很好的例子是确定用户是否登录,以及在依赖于身份验证服务的组件中:

javascript 复制代码
// AuthService
export class AuthService {
  isAuthenticated() {
    return true; // 实际实现
  }
}

// MyComponent
<template>
  <div v-if="isLoggedIn">Welcome back!</div>
  <div v-else>Please log in.</div>
</template>

<script>
export default {
  setup() {
    const auth = inject('auth');
    const isLoggedIn = auth.isAuthenticated();
    return { isLoggedIn };
  }
};
</script>

我们可以通过注入模拟的 AuthService 轻松控制身份验证状态:

javascript 复制代码
import { mount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';

test('shows login prompt when user is not authenticated', () => {
  const mockAuth = {
    isAuthenticated: jest.fn().mockReturnValue(false), // 模拟身份验证服务以模拟未认证状态
  };

  const wrapper = mount(MyComponent, {
    global: {
      provide: {
        auth: mockAuth, // 注入模拟的身份验证服务
      },
    },
  });

  expect(wrapper.text()).toContain('Please log in.');
});

test('welcomes the user when authenticated', () => {
  const mockAuth = {
    isAuthenticated: jest.fn().mockReturnValue(true), 
  };

  const wrapper = mount(MyComponent, {
    global: {
      provide: {
        auth: mockAuth, 
      },
    },
  });

  expect(wrapper.text()).toContain('Welcome back!');
});

这样一来,无需更改实际组件代码,就能轻松测试不同场景(这里是 "已认证" 和 "未认证")。

痛点及应对方法

和其他工具一样,provide / inject 函数对也并非十全十美。在使用它们时,可能会碰到一些痛点和潜在问题。

隐式依赖

provide / inject 机制可能会造成隐藏或难以追踪的依赖关系,这使得更难搞清楚哪个组件依赖哪些数据。这可能会引发在组件中不清楚某些数据是否可用或缺失的问题,让代码库更难维护,特别是在大型项目里。

解决办法:

在组件里记录 provide / inject 关系,并确保为提供的键使用清晰一致的命名规范,让注入的内容清晰易懂。

调试与工具支持有限

调试与 provide / inject 相关的问题可能颇具难度,因为数据流不像 props 那样一目了然。与 props 不同,注入的值在像 Vue 开发工具这样的开发工具中不太容易检查。这可能导致在尝试追踪特定组件中缺失的数据时,调试时间变长。

解决办法:

关于如何调试 provide / inject 相关问题并没有既定规则。具体做法取决于你使用函数对的频率。一种避免或减少调试需求的方式是临时记录注入的值,以保证它们符合预期。

组件紧密耦合

provide / inject 机制旨在解决组件耦合问题,即组件之间相互高度依赖,导致难以修改或重用。但讽刺的是,当组件严重依赖注入的值时,这对函数可能会引入一种新的耦合形式。

这可能使得在不同上下文中重用这些组件变得困难,因为在这些上下文中可能没有提供相应的值,最终降低了组件的可重用性,使重构变得棘手。

解决办法:

避免注入使子组件难以重用的高度特定数据,并确保注入抽象服务或接口,而非基本实现。

数据流不清晰

props 相比,provide / inject 的数据流不够清晰。当数据通过 props 传递时,在组件接口中很清楚它的预期和来源。而使用 provide / inject 时,这种关系是隐藏的,使得组件架构更难理解,数据来源更难追踪。

解决办法:

为注入的值使用描述性强且易于理解的键,让它们所代表的内容清晰明了。此外,与其注入单个值,不如考虑注入一个上下文对象,将相关依赖项组合在一起。

何时使用 provide / inject 函数对

使用 provide / inject 机制的主要决定因素是应用的简单性或复杂性。provideinject 函数对处于使用 props 处理小型应用和采用状态管理工具处理大型复杂应用之间的过渡地带。

以下是一些使用 provideinject 函数对的参考指标:

  • 如果应用状态相对简单,使用 Vuex 会显得过于复杂。
  • 如果应用有过多组件层级,且在数据传递到目标组件之前,中间组件不使用该数据。
  • 如果数据仅被少数组件使用。但如果数据将被更多组件使用,Vuex 可能是更好的解决方案。

结论

我们已经学会了如何在 Vue.js 3 中结合 Composition API 使用 provide / inject 函数对在深度嵌套的组件之间传递数据。我们还了解了如何使其具有响应性以及在哪些情况下应该使用它的不同用例。要获取更多关于 provide / inject 函数对的信息,请访问官方文档。

相关推荐
可涵不会debug5 分钟前
前端基础技术全解析:从HTML前端基础标签语言开始,逐步深入CSS样式修饰、JavaScript脚本控制、Ajax异步通信以及WebSocket持久通信
前端·javascript·css·ajax·html
莫离老师来啦11 分钟前
HTML&HTML5革命:构建现代网页的终极指南 - 0. 课程目录设计
前端·html·html5
hswizy1 小时前
flutter web 路由问题
前端·javascript·flutter
我爱学习_zwj4 小时前
深入浅出Node.js-1(node.js入门)
前端·webpack·node.js
IT 前端 张5 小时前
2025 最新前端高频率面试题--Vue篇
前端·javascript·vue.js
喵喵酱仔__5 小时前
vue3探索——使用ref与$parent实现父子组件间通信
前端·javascript·vue.js
_NIXIAKF5 小时前
vue中 输入框输入回车后触发搜索(搜索按钮触发页面刷新问题)
前端·javascript·vue.js
InnovatorX5 小时前
Vue 3 详解
前端·javascript·vue.js
布兰妮甜5 小时前
html + css 顶部滚动通知栏示例
前端·css·html
种麦南山下5 小时前
vue el table 不出滚动条样式显示 is_scrolling-none,如何修改?
前端·javascript·vue.js