顺藤摸瓜🍉:用单元测试读懂 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
    }
    ...
}

参考文档


相关文章
|
3月前
|
前端开发
Vue3基础(十si)___引入Element-plus___Vant___全局引入___按需引入___测试打包大小
本文介绍了如何在Vue3项目中引入Element-plus和Vant UI库,包括全局引入和按需引入的方法,并通过配置vite.config.js实现按需引入,最后对比了不同引入方式对项目打包大小的影响。
74 0
Vue3基础(十si)___引入Element-plus___Vant___全局引入___按需引入___测试打包大小
|
4月前
|
JavaScript 测试技术 API
顺藤摸瓜🍉:用单元测试读懂 vue3 中的 defineComponent
顺藤摸瓜🍉:用单元测试读懂 vue3 中的 defineComponent
|
4月前
|
JavaScript 前端开发 测试技术
顺藤摸瓜🍉:用单元测试读懂 vue3 watch 函数
顺藤摸瓜🍉:用单元测试读懂 vue3 watch 函数
|
7月前
|
移动开发 JavaScript 前端开发
学习vue3使用在线官方开发环境play.vuejs.org进行测试
学习vue3使用在线官方开发环境play.vuejs.org进行测试
120 1
|
JSON 资源调度 JavaScript
使用 ViteJs 将 Jest 测试集成到现有的 Vue 3 项目中
使用 ViteJs 将 Jest 测试集成到现有的 Vue 3 项目中
160 0
|
JavaScript 前端开发 测试技术
从0搭建Vue3组件库(十二):引入现代前端测试框架 Vitest
从0搭建Vue3组件库(十二):引入现代前端测试框架 Vitest
509 0
|
JavaScript 测试技术
Vue3+Vite2环境变量配置,分别配置本地,测试,正式
Vue3+Vite2环境变量配置,分别配置本地,测试,正式
379 0
|
JavaScript 前端开发 测试技术
【三十天精通Vue 3】第十四天 Vue 3 的单元测试详解
【三十天精通Vue 3】第十四天 Vue 3 的单元测试详解
167 0
|
JavaScript 测试技术 编译器
从0搭建Vue3组件库:引入单元测试框架Vitest
从0搭建Vue3组件库:引入单元测试框架Vitest
922 0
|
2天前
|
监控 JavaScript 测试技术
postman接口测试工具详解
Postman是一个功能强大且易于使用的API测试工具。通过详细的介绍和实际示例,本文展示了Postman在API测试中的各种应用。无论是简单的请求发送,还是复杂的自动化测试和持续集成,Postman都提供了丰富的功能来满足用户的需求。希望本文能帮助您更好地理解和使用Postman,提高API测试的效率和质量。
27 11