通用類型的 Vue 組件#
- 在 Setup 中定義組件
- 通用 props
- 使用 SFC 作為基礎組件
- 從 SFC 組件中提取 Prop 類型
- 通用插槽
- 通用組件事件
BaseGenericComponent.vue
<template>
<pre>
<slot :current-value="value" :old-value="oldValue">
{{ value }}
</slot>
</pre>
</template>
<script setup lang="ts">
import { watch, defineProps, ref, PropType } from 'vue';
const props = defineProps({
value: {
type: null as PropType<unknown>,
required: true,
},
});
const oldValue = ref<unknown>();
watch(
() => props.value,
(_, oldVal) => {
oldValue.value = oldVal;
}
);
</script>
types.ts
export type ExtractComponentProps<TComponent> =
TComponent extends new () => {
$props: infer P;
}
? P
: never;
useGenericComponent.ts
import { defineComponent, h, VNode } from 'vue';
import BaseGenericComponent from './BaseGenericComponent.vue';
import { ExtractComponentProps } from './types';
// 我們也省略了 `onChanged` 事件,以便可以覆蓋它,與 `value` prop 相同的情況
// 這是因為在 Vue 3 中事件和 props 的處理方式相同,只是事件有 `on` 前綴
// 所以如果你想將事件轉換為 prop,你可以遵循這個約定(`changed` => `onChanged`)
type NonGenericProps = Omit<ExtractComponentProps<typeof BaseGenericComponent>, 'value' | 'onChanged'>
interface GenericProps<TValue> extends NonGenericProps {
value: TValue;
}
interface GenericSlotProps<TValue> {
currentValue: TValue;
oldValue: TValue;
}
export function useGenericComponent<TValue = unknown>() {
// 記得從第二個參數中抓取插槽對象
const wrapper = defineComponent((props: GenericProps<TValue>, { slots }) => {
// 在 `setup` 中返回函數意味著這是渲染函數
return () => {
// 我們傳遞插槽和事件處理程序
return h(BaseGenericComponent, props, slots);
};
});
// 將包裝器轉換為自身,以便不丟失現有的組件類型信息
return wrapper as typeof wrapper & {
// 我們通過添加一個 `$slots` 對象,並將插槽函數定義為屬性,來擴展包裝器類型
new (): {
// 與 `$slots` 相同的技巧,我們覆蓋該組件的 emit 信息
$emit: {
(e: 'changed', value: TValue): void;
};
$slots: {
// 每個函數對應於一個插槽,其參數是可用的插槽 props
// 這是默認插槽定義,提供 `GenericSlotProps` 屬性作為插槽 props。
// 它應返回一個 `VNode` 的數組
default: (arg: GenericSlotProps<TValue>) => VNode[];
};
};
};
}
如何從 SFC 中提取 prop 類型(您可能需要開啟深色模式才能在這裡看到圖片)
這是一個如何運作的示例:
<script lang="ts" setup>
import { useGenericComponent } from './genericComponent';
const StringComponent = useGenericComponent<string>();
interface User {
id: number;
name: string;
}
const ObjectComponent = useGenericComponent<User>();
function onChange(value: User) {
console.log(value);
}
</script>
<template>
<!-- 🛑 由於類型錯誤,這在 Volar 中現在應該會報錯 -->
<StringComponent
:value="str"
v-slot="{ currentValue, oldValue }"
@changed="onChange"
>
<div>當前: {{ currentValue }}</div>
<div>舊: {{ oldValue }}</div>
</StringComponent>
<ObjectComponent
:value="userObj"
v-slot="{ currentValue, oldValue }"
@changed="onChange"
>
<div>當前: {{ currentValue }}</div>
<div>舊: {{ oldValue }}</div>
</ObjectComponent>
</template>
實用的通用組件#
這個 SelectInput
組件應該:
- 接受一個選項列表。
- 發出所選值並支持
v-model
API(接受modeValue
prop 並發出update:modelValue
事件)。
暴露一個item
插槽以自定義浮動菜單中每個選項的 UI。
暴露一個selected
插槽以自定義所選選項的 UI。
https://stackblitz.com/edit/vitejs-vite-wemb2u?embed=1
使用組合 API 減少組件噪音#
- 我們可以在
setup
函數中動態構建組件並在模板中使用它。 - 我們可以創建作為通用組件的組件包裝器
這有幾種好處: - 預定義一些 Props
- 提供更清晰的 API,具有明確的行為
- 隱藏一些複雜性
features/modal.ts
import { ref, defineComponent, h } from 'vue';
import ModalDialog from '@/components/ModalDialog.vue';
export function useModalDialog<TData = unknown>(
onConfirmProp: (data: TData) => void
) {
// 上下文數據
const data = ref<TData>();
function onClose() {
data.value = undefined;
}
function show(value: TData) {
data.value = value;
}
function hide() {
data.value = undefined;
}
const DialogComponent = defineComponent({
inheritAttrs: false,
setup(_, { slots, emit }) {
function onConfirmed() {
if (data.value !== undefined) {
onConfirmProp(data);
}
}
return () =>
h(
ModalDialog,
{
onClose,
onConfirmed,
visible: data.value !== undefined,
},
{
default: () => slots.default?.({ data: data.value }),
}
);
},
});
return {
DialogComponent:
DialogComponent as typeof DialogComponent & {
// 我們通過添加一個 `$slots` 對象,並將插槽函數定義為屬性,來擴展包裝器類型
new (): {
$emit: {
(e: 'confirmed', data: TData): void;
};
$slots: {
default: (arg: { data: TData }) => VNode[];
};
};
},
show,
hide
};
}
示例用法#
<script setup lang="ts">
import { ref } from 'vue';
import { useModalDialog } from '@/features/modal';
interface Item {
id: number;
name: string;
}
const items: Item[] = ref([
//...
]);
const {
show: onDeleteClick,
DialogComponent: DeleteItemDialog,
hide,
} = useModalDialog<Item>((item) => {
const idx = items.value.findIndex((i) => i.id === item.id);
items.value.splice(idx, 1);
console.log('已刪除', item);
hide();
});
</script>
<template>
<ul>
<li
v-for="item in items"
:key="item.id"
@click="onDeleteClick(item)"
>
{{ item.name }}
</li>
</ul>
<DeleteItemDialog
v-slot="{ data: itemToDelete }"
>
您確定要刪除 {{ itemToDelete.name }} 嗎?
</DeleteItemDialog>
</template>
“當我點擊一個項目時,為它打開一個對話框,當操作被確認時讓我知道,以便我可以刪除該項目”。
您可以創建某種 “共享” 模態對話框,讓許多組件可以訪問並使用。也許使用 provide/inject
API:
// 在父頁面或組件中
const { DialogComponent, ...modalApi } = useModalDialog();
// 將模態 API 提供給子組件
provide('modal', modalApi);
// 在子組件的其他地方:
const modal = inject('modal');
modal.show(data);
參考: