顺藤摸瓜🍉:用单元测试读懂 vue3 中的 provide/inject

简介: 顺藤摸瓜🍉:用单元测试读懂 vue3 中的 provide/inject

React Context API 提供了一种 Provider 模式,用以在组件树中的多个任意位置的组件之间共享属性,从而避免必须在多层嵌套的结构中层层传递 props。其围绕 Context 的概念,分别提供了 Provider 和 Comsumer 两种对象。

虽然 API 不同,且更倾向用于插件,但 Vue 中同样提供了 Provider 模式。比如 Vue 2.x 文档中对此的描述是:

这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。 ...... provide 和 inject 主要在开发高阶插件/组件库时使用。

Vue 3.x 的组合式 API 中也提供了两个类似的独立函数,Composition API RFC 中写道:

许多 Vue 的插件都向 this 注入 property ...... 当使用组合式 API 时,我们不再使用 this,取而代之的是,插件将在内部利用 provide 和 inject 并暴露一个组合函数。

延续系列的主题,本文将继续尝试立足于相关模块的单元测试解读和适度源码分析,主要考察 Vue 3.x Composition API 中的 provide() 和 inject() 两个方法;希望能在结合阅读文档的基础上,更好地理解相关模块。

我们将要观察三个代码仓库,分别是:

  • vue - Vue 2.x 项目
  • @vue/composition-api - 结合 Vue 2.x “提前尝鲜” Composition API 的过渡性项目
  • vue-next - Vue 3.x 项目,本文分析的是其 3.0.0-beta.15 版本

🔀 Vue 2.x + @vue/composition-api

1.1 函数签名


// composition-api/src/apis/inject.ts 
export function provide<T>(
    key: InjectionKey<T> | string, 
    value: T
): void
export function inject<T>(
    key: InjectionKey<T> | string
): T | undefined
export function inject<T>(
    key: InjectionKey<T> | string, 
    defaultValue: T
): T
interface InjectionKey<T> extends Symbol {}

1.2 测试用例

考察 composition-api/test/apis/inject.spec.js 文件:

test 1: 'Hooks provide/inject - should work'

  • 在根组件的 setup() 中,调用两次 provide(),并分别指定 Ref 和 Boolean 类型的 values
  • 根组件加载后,在消费者子组件中,能正确得到以上 values

test 2: 'should return a default value when inject not found'

  • 在组件的 setup() 中,调用 inject(不存在的key, defaultValue)
  • 组件加载后,上述 inject() 返回值为传入的 defaultValue

test 3: 'should work for ref value'

  • const Msg = Symbol() 作为 key
  • 在根组件的 setup() 中,provide() 中传入 Ref 类型的 value1
  • 在子组件的 setup() 的 return 中,返回 msg: inject(Msg)
  • 根组件加载后,立即以 app.$children[0].msg = 'bar' 的形式赋新值
  • 在 nextTick 中,应渲染出新传入的值 'bar'

test 4: 'should work for reactive value'

  • const State = Symbol() 作为 key
  • 在根组件的 setup() 中,调用 provide( State, reactive({ msg: 'foo' }) )
  • 在子组件的 setup() 的 return 中,返回 state: inject(State)
  • 根组件加载后,立即以 app.$children[0].state.msg = 'bar' 的形式赋新值
  • 在 nextTick 中,应渲染出新传入的值 'bar'

test 5: 'should work when combined with 2.x provide option'

  • 在根组件中,分别在 setup() 中调用 provide() 以及在 provide Options API 中指定属性
  • 在子组件的 setup() 中,能正确 inject() 到以上两种赋值

1.3 调用关系

简单分析源码,主要函数的调用关系为:

1.4 部分归纳

  • 核心部分仍是 Vue 2.x 中已经实现的 vm._provided 内部对象
  • 和原有的 Options API 中的 provide/inject 属性达到统一处理的效果
  • inject() 只能在 setup() 或 functional component 中使用
  • 在用例 test 3、test 4 中,顺带可以看出,直接从 vue 实例上访问 Ref 值是不用 .value 的;其基本实现如下:


// src/setup.ts 
function asVmProperty(
  vm: ComponentInstance,
  propName: string,
  propValue: Ref<unknown>
) {
  const props = vm.$options.props
  if (!(propName in vm) && !(props && hasOwn(props, propName))) {
    proxy(vm, propName, {
      get: () => propValue.value,
      set: (val: unknown) => {
        propValue.value = val
      },
    })
  }
}
  • 文档中提及的 “App 级别的 provide” 未在 Vue 2.x 和 @vue/composition-api 中找到实现

🔀 Vue 3.x 中的实现

Vue 3.x beta 中 provide/inject 的签名和之前 @vue/composition-api 中一致,在此不再赘述。

2.1 测试用例

考察文件 packages/runtime-core/__tests__/apiInject.spec.ts:

test 1: 'string keys'

  • 该例测试字符串 key,但一个看点其实是 setup() 和 functional component 混用情况


const Provider = {
  setup() {
    provide('foo', 1)
    return () => h(Middle)
  }
}
const Middle = {
  render: () => h(Consumer)
}
const Consumer = {
  setup() {
    const foo = inject('foo')
    return () => foo
  }
}

test 4: 'nested providers'

  • 在组件多层嵌套关系且有多个同名 key 的 provide() 下,消费者 inject() 到最近一层的值

test 6: 'reactivity with readonly refs'

  • provide() 的 value 为一个用 readonly() 包裹的 Ref 值
  • 在消费者组件中,对用 reject() 得到的上述 Ref 值进行操作,不会生效
  • test 8 中对readonly() 包裹的 Reactive 对象属性操作同样无效

test 10: 'should not warn when default value is undefined'

  • const foo = inject('foo', undefined) 且 'foo' 未在 provide() 中注册过的时侯,不应报错

2.2 调用关系

2.3 部分归纳

  • Vue 3.x 中的 provide/inject 是围绕 vue 实例上的 provides 属性进行的
  • test 4 中的嵌套关系其实就是在 provide() 时“合并”父组件的 provides 属性:


// packages/runtime-core/src/apiInject.ts
export function provide<T>(key: InjectionKey<T> | string, value: T) {
  if (!currentInstance) {
    if (__DEV__) {
      warn(`provide() can only be used inside setup().`)
    }
  } else {
    let provides = currentInstance.provides
    // 如果自身没有 provides,就直接用父组件的
    // 反之,以父组件的 provides 为原型创建自己的
    // 这样在 `inject` 中就可以简单地搜索到原型链上所有的了
    const parentProvides =
      currentInstance.parent && currentInstance.parent.provides
    if (parentProvides === provides) {
      provides = currentInstance.provides = Object.create(parentProvides)
    }
    provides[key as string] = value
  }
}

而这个 provides 根源上的初始值定义在:


// packages/runtime-core/src/apiCreateApp.ts 
export function createAppContext(): AppContext {
  return {
    ...
    provides: Object.create(null)
  }
}
  • test 10 中的情况对应于源码中第一个 else if,直接返回明确传入的 undefined:


if (key in provides) {
      return provides[key as string]
    } else if (arguments.length > 1) {
      return defaultValue
    } else if (__DEV__) {
      warn(`injection "${String(key)}" not found.`)
}
  • 文档中提到了 “全局 API 更改提案中 App 级别的 provide”:

store 也可以通过全局 API 更改提案中 App 级别的 provide 来提供,但是消费它的组件中的 useStore 风格的 API 还是相同的。

An app instance can also provide dependencies that can be injected by any component inside the app. This is similar to using the provide option in a 2.x root instance.

也给出了一个示例:


// in the entry
app.provide({
  [ThemeSymbol]: theme
})
// in a child component
export default {
  inject: {
    theme: {
      from: ThemeSymbol
    }
  },
  template: `<div :style="{ color: theme.textColor }" />`
}

vue-next 中实现了这部分逻辑:


// packages/runtime-core/src/apiCreateApp.ts
interface App<HostElement = any> {
  ...
  provide<T>(key: InjectionKey<T> | string, value: T): this
}
...
const app: App = {
    ...
    
    provide(key, value) {
        if (__DEV__ && key in context.provides) {
            warn(
                `App already provides property with key "${String(key)}". ` +
                `It will be overwritten with the new value.`
            )
        }
        // TypeScript doesn't allow symbols as index type
        context.provides[key as string] = value
        
        return app
    }
    ...
}

参考文档


相关文章
|
8天前
|
JavaScript 测试技术
Vue 3 单元测试实例
Vue 3 单元测试实例
19 4
|
8天前
|
JavaScript 测试技术 API
常见的 Vue 3 单元测试问题及解决方案
常见的 Vue 3 单元测试问题及解决方案
23 2
|
8天前
|
缓存 JavaScript 测试技术
Vue 3 单元测试最佳实践
Vue 3 单元测试最佳实践
13 1
|
2月前
|
前端开发
Vue3基础(十si)___引入Element-plus___Vant___全局引入___按需引入___测试打包大小
本文介绍了如何在Vue3项目中引入Element-plus和Vant UI库,包括全局引入和按需引入的方法,并通过配置vite.config.js实现按需引入,最后对比了不同引入方式对项目打包大小的影响。
57 0
Vue3基础(十si)___引入Element-plus___Vant___全局引入___按需引入___测试打包大小
|
3月前
|
JavaScript 测试技术 API
顺藤摸瓜🍉:用单元测试读懂 vue3 中的 defineComponent
顺藤摸瓜🍉:用单元测试读懂 vue3 中的 defineComponent
|
3月前
|
JavaScript 前端开发 测试技术
顺藤摸瓜🍉:用单元测试读懂 vue3 watch 函数
顺藤摸瓜🍉:用单元测试读懂 vue3 watch 函数
|
3月前
|
Java 测试技术 开发者
在软件开发中,测试至关重要,尤以单元测试和集成测试为然
在软件开发中,测试至关重要,尤以单元测试和集成测试为然。单元测试聚焦于Java中的类或方法等最小单元,确保其独立功能正确无误,及早发现问题。集成测试则着眼于模块间的交互,验证整体协作效能。为实现高效测试,需编写可测性强的代码,并选用JUnit等合适框架。同时,合理规划测试场景与利用Spring等工具也必不可少。遵循最佳实践,可提升测试质量,保障Java应用稳健前行。
49 1
|
15天前
|
测试技术 开发者 UED
探索软件测试的深度:从单元测试到自动化测试
【10月更文挑战第30天】在软件开发的世界中,测试是确保产品质量和用户满意度的关键步骤。本文将深入探讨软件测试的不同层次,从基本的单元测试到复杂的自动化测试,揭示它们如何共同构建一个坚实的质量保证体系。我们将通过实际代码示例,展示如何在开发过程中实施有效的测试策略,以确保软件的稳定性和可靠性。无论你是新手还是经验丰富的开发者,这篇文章都将为你提供宝贵的见解和实用技巧。
|
3月前
|
JSON Dubbo 测试技术
单元测试问题之增加JCode5插件生成的测试代码的可信度如何解决
单元测试问题之增加JCode5插件生成的测试代码的可信度如何解决
57 2
单元测试问题之增加JCode5插件生成的测试代码的可信度如何解决
|
2月前
|
IDE 测试技术 持续交付
Python自动化测试与单元测试框架:提升代码质量与效率
【9月更文挑战第3天】随着软件行业的迅速发展,代码质量和开发效率变得至关重要。本文探讨了Python在自动化及单元测试中的应用,介绍了Selenium、Appium、pytest等自动化测试框架,以及Python标准库中的unittest单元测试框架。通过详细阐述各框架的特点与使用方法,本文旨在帮助开发者掌握编写高效测试用例的技巧,提升代码质量与开发效率。同时,文章还提出了制定测试计划、持续集成与测试等实践建议,助力项目成功。
85 5