乐趣区

关于前端:Vue-3中依赖注入与组件定义相关的那点事儿

让咱们聊聊 Vue 3中依赖注入与组件定义相干的那点事儿。

次要内容

本次分享,咱们次要波及以下内容:

  • 📝 provide() & inject() – 依赖注入
  • 🛠 nextTick() – 下一个 DOM 更新周期后
  • 🎨 组件定义

    • defineComponent() – 组件定义类型推导辅助函数
    • defineAsyncComponent() – 异步组件
    • defineCustomElement() – 原生自定义元素类的结构器

provide() & inject()

provide()

提供一个值,能够被后辈组件注入。

function provide<T>(key: InjectionKey<T> | string, value: T): void

接管两个参数:

  • 要注入的 key,字符串或者 Symbol
export interface InjectionKey<T> extends Symbol {}
  • 对应注入的值

与注册生命周期钩子的 API 相似,provide() 必须在组件的 setup() 阶段同步调用。

inject()

注入一个由先人组件或整个利用 (通过 app.provide()) 提供的值。

// 没有默认值
function inject<T>(key: InjectionKey<T> | string): T | undefined

// 带有默认值
function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T

// 应用工厂函数
function inject<T>(
  key: InjectionKey<T> | string,
  defaultValue: () => T,
  treatDefaultAsFactory: true
): T
  • 第一个参数是注入的 keyVue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“笼罩”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject() 将返回 undefined,除非提供了一个默认值。
  • 第二个参数是可选的,即在没有匹配到 key 时应用的默认值。它也能够是一个工厂函数,用来返回某些创立起来比较复杂的值。如果默认值自身就是一个函数,那么你必须将 false 作为第三个参数传入,表明这个函数就是默认值,而不是一个工厂函数。

provide() & inject() – 官网示例

// provide
<script setup>
  import {(ref, provide)} from 'vue' import {fooSymbol} from
  './injectionSymbols' // 提供动态值 provide('foo', 'bar') // 提供响应式的值
  const count = ref(0) provide('count', count) // 提供时将 Symbol 作为 key
  provide(fooSymbol, count)
</script>
// inject
<script setup>
import {inject} from 'vue'
import {fooSymbol} from './injectionSymbols'

// 注入值的默认形式
const foo = inject('foo')

// 注入响应式的值
const count = inject('count')

// 通过 Symbol 类型的 key 注入
const foo2 = inject(fooSymbol)

// 注入一个值,若为空则应用提供的默认值
const bar = inject('foo', 'default value')

// 注入一个值,若为空则应用提供的工厂函数
const baz = inject('foo', () => new Map())

// 注入时为了表明提供的默认值是个函数,须要传入第三个参数
const fn = inject('function', () => {}, false)
</script>

provide() & inject() – ElementUI Plus 示例 Breadcrumb 组件

<script lang="ts" setup>
import {onMounted, provide, ref} from 'vue'
import {useNamespace} from '@element-plus/hooks'
import {breadcrumbKey} from './constants'
import {breadcrumbProps} from './breadcrumb'

defineOptions({name: 'ElBreadcrumb',})

const props = defineProps(breadcrumbProps)
const ns = useNamespace('breadcrumb')
const breadcrumb = ref<HTMLDivElement>()
// 提供值
provide(breadcrumbKey, props)

onMounted(() => {......})
</script>
<script lang="ts" setup>
import {getCurrentInstance, inject, ref, toRefs} from 'vue'
import ElIcon from '@element-plus/components/icon'
import {useNamespace} from '@element-plus/hooks'
import {breadcrumbKey} from './constants'
import {breadcrumbItemProps} from './breadcrumb-item'

import type {Router} from 'vue-router'

defineOptions({name: 'ElBreadcrumbItem',})

const props = defineProps(breadcrumbItemProps)

const instance = getCurrentInstance()!
// 注入值
const breadcrumbContext = inject(breadcrumbKey, undefined)!
const ns = useNamespace('breadcrumb')
 ......
</script>

provide() & inject() – VueUse 示例

createInjectionState 源码 / createInjectionState 应用

package/core/computedInject 源码

import {type InjectionKey, inject, provide} from 'vue-demi'

/**
 * 创立能够注入到组件中的全局状态
 */
export function createInjectionState<Arguments extends Array<any>, Return>(composable: (...args: Arguments) => Return
): readonly [useProvidingState: (...args: Arguments) => Return,
  useInjectedState: () => Return | undefined] {const key: string | InjectionKey<Return> = Symbol('InjectionState')
  const useProvidingState = (...args: Arguments) => {const state = composable(...args)
    provide(key, state)
    return state
  }
  const useInjectedState = () => inject(key)
  return [useProvidingState, useInjectedState]
}

nextTick()

期待下一次 DOM 更新刷新的工具办法。

function nextTick(callback?: () => void): Promise<void>

阐明:当你在 Vue 中更改响应式状态时,最终的 DOM 更新并不是同步失效的,而是由 Vue 将它们缓存在一个队列中,直到下一个 “tick” 才一起执行。这样是为了确保每个组件无论产生多少状态扭转,都仅执行一次更新。

nextTick() 能够在状态扭转后立刻应用,以期待 DOM 更新实现。你能够传递一个 回调函数 作为参数,或者 await 返回的 Promise

nextTick() 官网示例

<script setup>
import {ref, nextTick} from 'vue'

const count = ref(0)

async function increment() {
  count.value++

  // DOM 还未更新
  console.log(document.getElementById('counter').textContent) // 0

  await nextTick()
  // DOM 此时曾经更新
  console.log(document.getElementById('counter').textContent) // 1
}
</script>

<template>
  <button id="counter" @click="increment">{{count}}</button>
</template>

nextTick() – ElementUI Plus 示例

ElCascaderPanel 源码

export default defineComponent({
  ......
  const syncMenuState = (newCheckedNodes: CascaderNode[],
    reserveExpandingState = true
  ) => {
    ......
    checkedNodes.value = newNodes
    nextTick(scrollToExpandingNode)
  }
  const scrollToExpandingNode = () => {if (!isClient) return
    menuList.value.forEach((menu) => {
      const menuElement = menu?.$el
      if (menuElement) {const container = menuElement.querySelector(`.${ns.namespace.value}-scrollbar__wrap`)
        const activeNode = menuElement.querySelector(`.${ns.b('node')}.${ns.is('active')}`) ||
          menuElement.querySelector(`.${ns.b('node')}.in-active-path`)
        scrollIntoView(container, activeNode)
      }
    })
  }
  ......
})

nextTick() – VueUse 示例

useInfiniteScroll 源码

export function useInfiniteScroll(
  element: MaybeComputedRef<HTMLElement | SVGElement | Window | Document | null | undefined>
  ......
) {const state = reactive(......)
  watch(() => state.arrivedState[direction],
    async (v) => {if (v) {const elem = resolveUnref(element) as Element
        ......
        if (options.preserveScrollPosition && elem) {nextTick(() => {
            elem.scrollTo({
              top: elem.scrollHeight - previous.height,
              left: elem.scrollWidth - previous.width,
            })
          })
        }
      }
    }
  )
}

应用场景:

  1. 当你须要在批改了某些数据后立刻对 DOM 进行操作时,能够应用 nextTick 来确保 DOM 曾经更新结束。例如,在应用 $ref 获取元素时,须要确保元素曾经被渲染才可能正确获取。
  2. 在一些简单页面中,有些组件可能会因为条件渲染或动态数据而频繁地变动。应用 nextTick 能够防止频繁地进行 DOM 操作,从而进步应用程序的性能。
  3. 当须要在模板中拜访某些计算属性或者监听器中的值时,也能够应用 nextTick 来确保这些值曾经更新结束。这样能够防止在视图中拜访到旧值。

总之,nextTick 是一个十分有用的 API,能够确保在正确的机会对 DOM 进行操作,避免出现一些不必要的问题,并且能够进步应用程序的性能。

defineComponent()

在定义 Vue 组件时提供类型推导的辅助函数。

function defineComponent(component: ComponentOptions | ComponentOptions['setup']
): ComponentConstructor

第一个参数是一个组件选项对象。返回值将是该选项对象自身,因为该函数实际上在运行时没有任何操作,仅用于提供类型推导。

留神返回值的类型有一点特地:它会是一个构造函数类型,它的实例类型是依据选项推断出的组件实例类型。这是为了能让该返回值在 TSX 中用作标签时提供类型推导反对。

const Foo = defineComponent(/* ... */)
// 提取出一个组件的实例类型 (与其选项中的 this 的类型等价)
type FooInstance = InstanceType<typeof Foo>

参考:Vue3 – defineComponent 解决了什么?

defineComponent() – ElementUI Plus 示例

ConfigProvider 源码

import {defineComponent, renderSlot, watch} from 'vue'
import {provideGlobalConfig} from './hooks/use-global-config'
import {configProviderProps} from './config-provider-props'
......
const ConfigProvider = defineComponent({
  name: 'ElConfigProvider',
  props: configProviderProps,

  setup(props, { slots}) {......},
})
export type ConfigProviderInstance = InstanceType<typeof ConfigProvider>

export default ConfigProvider

defineComponent() – Treeshaking

因为 defineComponent() 是一个函数调用,所以它可能被某些构建工具认为会产生副作用,如 webpack。即便一个组件从未被应用,也有可能不被 tree-shake

为了通知 webpack 这个函数调用能够被平安地 tree-shake,咱们能够在函数调用之前增加一个 /_#**PURE**_/ 模式的正文:

export default /*#__PURE__*/ defineComponent(/* ... */)

请留神,如果你的我的项目中应用的是 Vite,就不须要这么做,因为 Rollup (Vite 底层应用的生产环境打包工具) 能够智能地确定 defineComponent() 实际上并没有副作用,所以无需手动正文。

defineComponent() – VueUse 示例

OnClickOutside 源码

import {defineComponent, h, ref} from 'vue-demi'
import {onClickOutside} from '@vueuse/core'
import type {RenderableComponent} from '../types'
import type {OnClickOutsideOptions} from '.'
export interface OnClickOutsideProps extends RenderableComponent {options?: OnClickOutsideOptions}
export const OnClickOutside = /* #__PURE__ */ defineComponent<OnClickOutsideProps>({
    name: 'OnClickOutside',
    props: ['as', 'options'] as unknown as undefined,
    emits: ['trigger'],
    setup(props, { slots, emit}) {
      ... ...

      return () => {if (slots.default)
          return h(props.as || 'div', { ref: target}, slots.default())
      }
    },
  })

defineAsyncComponent()

定义一个异步组件,它在运行时是懒加载的。参数能够是一个异步加载函数,或是对加载行为进行更具体定制的一个选项对象。

function defineAsyncComponent(source: AsyncComponentLoader | AsyncComponentOptions): Component
type AsyncComponentLoader = () => Promise<Component>
interface AsyncComponentOptions {
  loader: AsyncComponentLoader
  loadingComponent?: Component
  errorComponent?: Component
  delay?: number
  timeout?: number
  suspensible?: boolean
  onError?: (
    error: Error,
    retry: () => void,
    fail: () => void,
    attempts: number
  ) => any
}

defineAsyncComponent() – 官网示例

<script setup>
import {defineAsyncComponent} from 'vue'

const AsyncComp = defineAsyncComponent(() => {return new Promise((resolve, reject) => {resolve(/* 从服务器获取到的组件 */)
  })
})

const AdminPage = defineAsyncComponent(() =>
  import('./components/AdminPageComponent.vue')
)
</script>
<template>
  <AsyncComp />
  <AdminPage />
</template>

ES 模块动静导入也会返回一个 Promise,所以少数状况下咱们会将它和 defineAsyncComponent 搭配应用。相似 ViteWebpack 这样的构建工具也反对此语法 (并且会将它们作为打包时的代码宰割点),因而咱们也能够用它来导入 Vue 单文件组件。

defineAsyncComponent() – VitePress 示例

<script setup lang="ts">
import {defineAsyncComponent} from 'vue'
import type {DefaultTheme} from 'vitepress/theme'
defineProps<{carbonAds: DefaultTheme.CarbonAdsOptions}>()
const VPCarbonAds = __CARBON__
  ? defineAsyncComponent(() => import('./VPCarbonAds.vue'))
  : () => null
</script>
<template>
  <div class="VPDocAsideCarbonAds">
    <VPCarbonAds :carbon-ads="carbonAds" />
  </div>
</template>

defineAsyncComponent()应用场景:

  1. 当你须要异步加载某些组件时,能够应用 defineAsyncComponent 来进行组件懒加载,这样能够进步应用程序的性能。
  2. 在一些简单页面中,有些组件可能只有在用户执行特定操作或进入特定页面时才会被应用到。应用 defineAsyncComponent 能够升高初始页面加载时的资源开销。
  3. 当你须要动静地加载某些组件时,也能够应用 defineAsyncComponent。例如,在路由中依据不同的门路加载不同的组件。

Vue3 之外,许多基于 Vue 3 的库和框架也开始应用 defineAsyncComponent 来实现组件的异步加载。例如:

  • VitePress: Vite 的官网文档工具,应用 defineAsyncComponent 来实现文档页面的异步加载。
  • Nuxt.js: 基于 Vue.js 的动态网站生成器,从版本 2.15 开始反对 defineAsyncComponent
  • Quasar Framework: 基于 Vue.js 的 UI 框架,从版本 2.0 开始反对 defineAsyncComponent
  • Element UI Plus: 基于 Vue 3 的 UI 库,应用 defineAsyncComponent 来实现组件的异步加载。

总之,随着 Vue 3 的遍及,越来越多的库和框架都开始应用 defineAsyncComponent 来进步应用程序的性能。

defineCustomElement()

这个办法和 defineComponent 承受的参数雷同,不同的是会返回一个 原生自定义元素类 的结构器。

function defineCustomElement(
  component:
    | (ComponentOptions & { styles?: string[] })
    | ComponentOptions['setup']
): {new (props?: object): HTMLElement
}

除了惯例的组件选项,defineCustomElement() 还反对一个特地的选项 styles,它应该是一个内联 CSS 字符串的数组,所提供的 CSS 会被注入到该元素的 shadow root 上。
返回值是一个能够通过 customElements.define() 注册的自定义元素结构器。

import {defineCustomElement} from 'vue'
const MyVueElement = defineCustomElement({/* 组件选项 */})
// 注册自定义元素
customElements.define('my-vue-element', MyVueElement)

应用 Vue 构建自定义元素

import {defineCustomElement} from 'vue'

const MyVueElement = defineCustomElement({
  // 这里是同平时一样的 Vue 组件选项
  props: {},
  emits: {},
  template: `...`,
  // defineCustomElement 特有的:注入进 shadow root 的 CSS
  styles: [`/* inlined css */`],
})
// 注册自定义元素
// 注册之后,所有此页面中的 `<my-vue-element>` 标签
// 都会被降级
customElements.define('my-vue-element', MyVueElement)
// 你也能够编程式地实例化元素://(必须在注册之后)document.body.appendChild(
  new MyVueElement({// 初始化 props(可选)})
)
// 组件应用
<my-vue-element></my-vue-element>

除了 Vue 3 之外,一些基于 Vue 3 的库和框架也开始应用 defineCustomElement 来将 Vue 组件打包成自定义元素供其余框架或纯 HTML 页面应用。例如:

  • Ionic Framework: 基于 Web Components 的挪动端 UI 框架,从版本 6 开始反对应用 defineCustomElementIonic 组件打包成自定义元素。
  • LitElement: Google 推出的 Web Components 库,提供相似 Vue 的模板语法,并反对应用 defineCustomElementLitElement 组件打包成自定义元素。
  • Stencil: 由 Ionic Team 开发的 Web Components 工具链,能够将任何框架的组件转换为自定义元素,并反对应用 defineCustomElement 间接将 Vue 组件打包成自定义元素。

总之,随着 Web Components 的一直风行和倒退,越来越多的库和框架都开始应用 defineCustomElement 来实现跨框架、跨平台的组件共享。

小结

本次咱们围绕着 Vue3 中的依赖注入与组件定义相干的几个 API,学习其根本应用办法,并且联合着目前风行的库和框架剖析了应用场景,以此来加深咱们对它们的意识。

内容收录于 github 仓库

退出移动版