iT邦幫忙

2024 iThome 鐵人賽

DAY 17
1
Modern Web

Vue 和 TypeScript 的最佳實踐:成為前端工程師的進階利器系列 第 17

Day 17: Vee-Validate 和 Zod 結合處理複雜的表單場景 - 進階特性深度探索

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20240923/20117461KjOTEn2sVb.jpg

簡介

在現代 Web 應用程序中,處理複雜的表單場景需要強大的驗證工具和類型系統。本文將深入探討如何結合 Vee-Validate 和 Zod 的進階特性,以及 TypeScript 的高級類型系統,來創建一個強大、靈活且類型安全的表單處理系統。我們將特別關注 Zod 的高級用法和 TypeScript 的進階特性,展示如何將它們無縫整合到 Vue 3 和 Vee-Validate 中。

實作步驟

步驟 1: Zod 的進階用法

首先,讓我們深入探討 Zod 的一些進階特性:

(檔案 : src/schemas/advanceUser.ts)

// src/schemas/advanced-user.ts
import * as zod from 'zod'

// 使用 nativeEnum 定義角色
enum UserRole {
  Admin = 'ADMIN',
  User = 'USER',
  Guest = 'GUEST'
}

// 基本用戶信息 schema
const baseUserSchema = zod.object({
  username: zod.string().min(3).max(20),
  email: zod.string().email(),
})

// 使用 extend 擴展基本 schema
const extendedUserSchema = baseUserSchema.extend({
  age: zod.number().min(18).max(100),
})

// 使用 merge 合併 schema
const mergedUserSchema = baseUserSchema.merge(zod.object({
  birthDate: zod.string().transform((str) => new Date(str)),
}))

// 使用 pick 和 omit
const loginSchema = baseUserSchema.pick({ email: true }).extend({
  password: zod.string().min(8),
})

const publicUserSchema = extendedUserSchema.omit({ email: true })

// 使用 nativeEnum 和 enum 的差異
const roleSchema = zod.object({
  nativeEnumRole: zod.nativeEnum(UserRole),
  stringEnumRole: zod.enum(['ADMIN', 'USER', 'GUEST']),
})

// 使用 transform 和 coerce
const advancedUserSchema = extendedUserSchema.extend({
  role: zod.nativeEnum(UserRole),
  registrationDate: zod.string().transform((str) => new Date(str)),
  age: zod.coerce.number().min(18),  // 自動轉換輸入為數字
})


export {
  UserRole,
  baseUserSchema,
  extendedUserSchema,
  mergedUserSchema,
  loginSchema,
  publicUserSchema,
  roleSchema,
  advancedUserSchema,
}

補充: 關於 enum 冷知識

現在大致上在 typescript 使用 enum 本身不會有什麼大問題,不過在比較舊的 typescript 版本中,使用 enum 會有些許問題,比如說

enum CustomEnum {
  User = 'user',
  Manager = 'manager',
  Admin = 'admin'
}
// 舊版 typescript 在使用 Object.keys()

Object.keys(CustomEnum); // 舊版會連同 index 數量都會成為 keys 新版不會。

也因為這樣以前在處理 enum 並且要嚴謹的解決 typescript 問題時,會用另一種寫法(我們拿月份作為 enum 做顯示)

export const monthStatus = [
  "January",
  "February",
  "March",
  "April",
  "May",
  "June",
  "July",
  "August",
  "September",
  "October",
  "November",
  "December"
] as const;

export type MonthStatus = (typeof monthStatus)[number];

這時 MonthStatus 會成為 union type 所以可以完美解決相關問題,並且也可以使用 monthStatus 去解決 enum 抓值得問題。當然我沒記錯在 5.4 之後使用 enum 基本上沒有這些問題了,可以使用原生 enum 安心服用。

步驟 2: 創建高級表單處理 Composable

現在,讓我們創建一個更加強大的表單處理 composable,它充分利用了 Zod 和 TypeScript Generic的特性,以下為簡單例子:

import { computed, ref } from 'vue'
import { useForm, useField } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as zod from 'zod'
import { ZodTypeAny } from 'zod'

export function useAdvancedZodForm<T extends ZodTypeAny>(schema: T) {
  type FormType = zod.infer<T>

  const { handleSubmit, errors, resetForm } = useForm<FormType>({
    validationSchema: toTypedSchema(schema)
  })

  const formData = ref<Partial<FormType>>({})

  const onSubmit = (callback: (values: FormType) => void | Promise<void>) => {
    return handleSubmit((values) => {
      return callback(schema.parse(values))
    })
  }

  const setFormValues = (values: Partial<FormType>) => {
    Object.entries(values).forEach(([key, initialValue]) => {
      const { value: fieldValue } = useField(key, undefined, { initialValue });
      formData.value[key as keyof FormType] = fieldValue;
    })
  }

  const isValid = computed(() => Object.keys(errors.value).length === 0)

  return {
    onSubmit,
    errors,
    resetForm,
    setFormValues,
    formData,
    isValid
  }
}

步驟 4: 創建通用表單組件

使用我們的高級 composable 和 Zod schema 創建一個通用表單組件:

<!-- src/components/AdvancedForm.vue -->
<template>
  <form @submit="onSubmit">
    <template v-for="(field, key) in fields" :key="key">
      <div>
        <label :for="key">{{ field.label }}</label>
        <component
          :is="field.component"
          :id="key"
          v-model="formData[key]"
          v-bind="field.props"
        />
        <span v-if="errors[key]">{{ errors[key] }}</span>
      </div>
    </template>
    <button type="submit" :disabled="!isValid">Submit</button>
  </form>
</template>

<script lang="ts" setup>
import { PropType } from 'vue'
import { ZodTypeAny } from 'zod'
import { useAdvancedZodForm } from '../composables/useAdvancedZodForm'

interface FieldConfig {
  component: string
  label: string
  props?: Record<string, unknown>
}

const props = defineProps({
  schema: {
    type: Object as PropType<ZodTypeAny>,
    required: true
  },
  fields: {
    type: Object as PropType<Record<string, FieldConfig>>,
    required: true
  },
  initialValues: {
    type: Object as PropType<Record<string, unkown>>,
    default: () => ({})
  }
})

const emit = defineEmits(['submit'])

const { onSubmit, errors, resetForm, setFormValues, formData, isValid } = useAdvancedZodForm(props.schema)

setFormValues(props.initialValues)

const handleSubmit = onSubmit((data) => {
  emit('submit', data)
})
</script>

步驟 5: 使用高級表單組件

最後,讓我們使用我們的高級表單組件,展示如何利用 Zod 和 TypeScript 的特性:

<!-- src/views/AdvancedUserForm.vue -->
<template>
  <AdvancedForm
    :schema="advancedUserSchema"
    :fields="formFields"
    :initial-values="initialValues"
    @submit="handleSubmit"
  />
</template>

<script lang="ts" setup>
import { advancedUserSchema, UserRole } from '../schemas/advanced-user'
import AdvancedForm from '../components/AdvancedForm.vue'

const formFields = {
  username: { component: 'input', label: 'Username', props: { type: 'text' } },
  email: { component: 'input', label: 'Email', props: { type: 'email' } },
  age: { component: 'input', label: 'Age', props: { type: 'number' } },
  role: { 
    component: 'select', 
    label: 'Role', 
    props: { 
      options: Object.values(UserRole) 
    } 
  },
  registrationDate: { component: 'input', label: 'Registration Date', props: { type: 'date' } },
}

const initialValues = {
  username: 'johndoe',
  email: 'john@example.com',
  age: '30',  // 注意這裡是字符串,Zod 的 coerce 會自動轉換為數字
  role: UserRole.User,
  registrationDate: new Date().toISOString().split('T')[0]
}

const handleSubmit = (data: z.infer<typeof advancedUserSchema>) => {
  console.log('Form submitted with:', data)
  // 這裡的 data 已經被 Zod 解析和轉換,所以 age 是數字,registrationDate 是 Date 對象
}
</script>

https://ithelp.ithome.com.tw/upload/images/20240923/20117461chkTzs7X70.jpg

結論

在這個深入的教程中,我們探討了如何結合 Vee-Validate 和 Zod 的進階特性,以及 TypeScript 的高級類型系統,來處理複雜的表單場景。我們特別關注了以下幾點:

  1. Zod 的進階用法,包括 extendsmergepick/omitnativeEnumenum 的差異,以及 transformcoerce 的使用。
  2. TypeScript 的高級類型特性,如泛型、條件類型、映射類型等,以及它們在 Zod schema 定義中的應用。
  3. 創建了一個強大的 useAdvancedZodForm composable,它充分利用了 Zod 和 TypeScript 的特性。
  4. 開發了一個通用的高級表單組件,展示了如何將所有這些概念整合到實際的 Vue 組件中。

通過這種方法,我們創建了一個非常靈活、類型安全且高度可維護的表單處理系統。這個系統不僅能夠處理複雜的表單驗證邏輯,還提供了出色的開發體驗和運行時性能。

在實際應用中,這種方法可以大大提高表單處理的效率和可靠性。它允許開發者快速創建複雜的表單,同時保持代碼的清晰度和類型安全性。通過充分利用 Zod 和 TypeScript 的特性,我們可以在開發階段捕獲更多潛在的錯誤,減少運行時的問題。

最後,這種方法的可擴展性也值得注意。隨著應用的增長,我們可以輕鑑添加新的 schema、擴展現有的 schema,或者創建更專門化的表單組件,而不會失去類型安全性或增加太多的複雜性。這使得它特別適合於大型或不斷發展的專案。


上一篇
Day 16: 如何使用 Pinia 儲存並管理 API 請求的異步數據
下一篇
Day 18: 使用 Vue Router 實現多級嵌套路由與導航守衛
系列文
Vue 和 TypeScript 的最佳實踐:成為前端工程師的進階利器30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言