ホームページ > 記事 > ウェブフロントエンド > Vue3 での依存関係注入とコンポーネント定義について話しましょう
今回は主に、Vue3 の依存関係挿入とコンポーネント定義に関連するいくつかの API と、共通ライブラリ ElementUI Plus および Vueuse での使用方法を共有し、使用シナリオを理解するために例を使用します。
Vue 3
での依存関係注入とコンポーネント定義に関連することについて話しましょう。
子孫コンポーネントによって注入できる値を提供します。
function provide<T>(key: InjectionKey<T> | string, value: T): void
2 つのパラメータを受け取ります: 注入される
key
、文字列または Symbol
;export interface InjectionKey<T> extends Symbol {}
API に似ており、
provide() は ## 内にある必要があります。 # コンポーネント setup()
フェーズが同期的に呼び出されます。 [関連する推奨事項: vuejs ビデオ チュートリアル
、Web フロントエンド開発 ]
経由)。 <pre class="brush:js;toolbar:false;">// 没有默认值
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</pre>
です。 Vue
は親コンポーネント チェーンを走査し、key
と一致することによって提供された値を決定します。親コンポーネント チェーン上の複数のコンポーネントが同じ key
の値を提供する場合、より近いコンポーネントは、チェーンのさらに上位のコンポーネントによって提供される値を「上書き」します。 key
と一致する値がない場合、デフォルト値が指定されていない限り、inject()
は unknown
を返します。
がない場合に使用されるデフォルト値です。作成がより複雑な値を返すファクトリ関数にすることもできます。デフォルト値自体が関数である場合は、3 番目のパラメーターとして false
を渡し、この関数がファクトリー関数ではなくデフォルト値であることを示す必要があります。
// 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>
<script 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 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>
function nextTick(callback?: () => void): Promise<void>
注:
Vue で応答状態を変更すると、最後の DOM
更新は同期的に有効になりませんが、Vue# # によって行われます。 # それらをキューにキャッシュし、次の
"ティック" まで一緒に実行します。これは、状態変化が何回発生したかに関係なく、各コンポーネントが確実に 1 回の更新のみを実行するようにするためです。
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 の例
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 の例
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, }) }) } } } ) }
当你需要在修改了某些数据后立即对 DOM
进行操作时,可以使用 nextTick
来确保 DOM
已经更新完毕。例如,在使用 $ref
获取元素时,需要确保元素已经被渲染才能够正确获取。
在一些复杂页面中,有些组件可能会因为条件渲染或动态数据而频繁地变化。使用 nextTick
可以避免频繁地进行 DOM
操作,从而提高应用程序的性能。
当需要在模板中访问某些计算属性或者监听器中的值时,也可以使用 nextTick
来确保这些值已经更新完毕。这样可以避免在视图中访问到旧值。
总之,nextTick
是一个非常有用的 API,可以确保在正确的时机对 DOM
进行操作,避免出现一些不必要的问题,并且可以提高应用程序的性能。
在定义 Vue
组件时提供类型推导的辅助函数。
function defineComponent( component: ComponentOptions | ComponentOptions['setup'] ): ComponentConstructor
第一个参数是一个组件选项对象。返回值将是该选项对象本身,因为该函数实际上在运行时没有任何操作,仅用于提供类型推导。
注意返回值的类型有一点特别:它会是一个构造函数类型,它的实例类型是根据选项推断出的组件实例类型。这是为了能让该返回值在 TSX
中用作标签时提供类型推导支持。
const Foo = defineComponent(/* ... */) // 提取出一个组件的实例类型 (与其选项中的 this 的类型等价) type FooInstance = InstanceType<typeof Foo>
参考:Vue3 - defineComponent 解决了什么?
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()
是一个函数调用,所以它可能被某些构建工具认为会产生副作用,如 webpack
。即使一个组件从未被使用,也有可能不被 tree-shake
。
为了告诉 webpack
这个函数调用可以被安全地 tree-shake
,我们可以在函数调用之前添加一个 /_#**PURE**_/
形式的注释:
export default /*#__PURE__*/ defineComponent(/* ... */)
请注意,如果你的项目中使用的是 Vite
,就不需要这么做,因为 Rollup
(Vite
底层使用的生产环境打包工具) 可以智能地确定 defineComponent()
实际上并没有副作用,所以无需手动注释。
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()) } }, })
定义一个异步组件,它在运行时是懒加载的。参数可以是一个异步加载函数,或是对加载行为进行更具体定制的一个选项对象。
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 }
<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
搭配使用。类似 Vite
和 Webpack
这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue
单文件组件。
<script setup> 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> <VPCarbonAds :carbon-ads="carbonAds" /> </div> </template>
defineAsyncComponent()
使用场景:
当你需要异步加载某些组件时,可以使用 defineAsyncComponent
来进行组件懒加载,这样可以提高应用程序的性能。
在一些复杂页面中,有些组件可能只有在用户执行特定操作或进入特定页面时才会被使用到。使用 defineAsyncComponent
可以降低初始页面加载时的资源开销。
当你需要动态地加载某些组件时,也可以使用 defineAsyncComponent
。例如,在路由中根据不同的路径加载不同的组件。
除 Vue3
之外,许多基于 Vue 3
的库和框架也开始使用 defineAsyncComponent
来实现组件的异步加载。例如:
Vite
的官方文档工具,使用 defineAsyncComponent
来实现文档页面的异步加载。defineAsyncComponent
。Vue.js
的 UI 框架,从版本 2.0 开始支持 defineAsyncComponent
。Vue 3
的 UI 库,使用 defineAsyncComponent
来实现组件的异步加载。总之,随着 Vue 3 的普及,越来越多的库和框架都开始使用 defineAsyncComponent
来提高应用程序的性能。
这个方法和 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)
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 页面使用。例如:
Web Components
的移动端 UI 框架,从版本 6 开始支持使用 defineCustomElement
将 Ionic
组件打包成自定义元素。Web Components
库,提供类似 Vue
的模板语法,并支持使用 defineCustomElement
将 LitElement
组件打包成自定义元素。Ionic Team
开发的 Web Components
工具链,可以将任何框架的组件转换为自定义元素,并支持使用 defineCustomElement
直接将 Vue
组件打包成自定义元素。总之,随着 Web Components
的不断流行和发展,越来越多的库和框架都开始使用 defineCustomElement
来实现跨框架、跨平台的组件共享。
本次我们围绕着 Vue3
中的依赖注入与组件定义相关的几个 API,学习其基本使用方法,并且结合着目前流行的库和框架分析了使用场景,以此来加深我们对它们的认识。
内容收录于github 仓库
以上がVue3 での依存関係注入とコンポーネント定義について話しましょうの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。