Desmond

Desmond

An introvert who loves web programming, graphic design and guitar
github
bilibili
twitter

Vue Composition API 的技巧

通用類型的 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 類型(您可能需要開啟深色模式才能在這裡看到圖片)
image

這是一個如何運作的示例:

<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);

參考:

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。