
在 Vue 應用開發中,表單驗證是一項至關重要的功能,尤其是當表單數據變得複雜且需要高度自定義時。Zod 作為一個強大的 JavaScript 驗證庫,可以與 Vee-Validate 無縫結合,提供靈活且強大的運行時驗證能力。本文將介紹如何使用 Zod 的 refine、superRefine、safeParse 和 safeParseAsync,結合 Vee-Validate 和 @vee-validate/zod,實現 email、IP、台灣電話號碼等複雜驗證,並進行異步驗證操作。
Zod 提供了多種高階驗證方法,其中 refine 和 superRefine 可以幫助我們在 Schema 中實現自定義驗證邏輯。以下是定義表單驗證的 Zod schema,包含 email、IP、url、地址、台灣電話號碼,以及使用 fetch API 進行的異步驗證。
(檔案: src/schemas/userFormSchema.ts)
import * as zod from 'zod';
import { useDebounceFn } from '@vueuse/core'; // 後續會介紹講到 vueuse 的部分
export const userFormSchema = zod
  .object({
    email: zod.string().email('請輸入有效的電子郵件地址'),
    imageUrl: zod.string().url('請輸入有效的圖片 url'),
    ip: zod.string().ip('請輸入有效的 ip 位置'),
    taiwanPhone: zod.string().refine((value) => {
      const taiwanPhoneRegex = /^09\d{8}$/;
      return taiwanPhoneRegex.test(value);
    }, '請輸入有效的台灣電話號碼'),
    token: zod.string().refine(useDebounceFn(async (value) => {
      // 模擬 API 驗證
      const response = await fetch(`https://api.example.com/validate?token=${value}`);
      const data = await response.json();
      return data.isValid;
    }, '驗證失敗,請輸入有效 token'),
    password: zod.string(),
    confirmPassword: zod.string()
  }, 800))
  .superRefine(({ password, confirmPassword }, ctx) => {
    if (password === confirmPassword) {
      ctx.addIssue({
        code: zod.ZodIssueCode.custom,
        message: '確認密碼和密碼不符',
        path: ['password', 'confirmPassword'],
      });
    }
  });
  
export type UserFormSchema = zod.infer<typeof userFormSchema>;
這個 schema 使用了 refine 來驗證台灣電話號碼,以及一個需要異步驗證的 token。superRefine 用於更複雜的驗證邏輯,例如密碼和確認密碼必須一致。
接下來,我們將使用 useForm 和 useField 在 Vue 中進行表單驗證,並將 Zod schema 結合到 Vee-Validate。
(檔案: src/components/userFormComponent.vue)
<script lang="ts" setup>
  import { useUserForm } from '../composables/useUserForm';
  const {
    // state::
    errors,
    // field::
    email,
    imageUrl,
    ip,
    taiwanPhone,
    token,
    password,
    confirmPassword,
    // methods::
    submitForm
  } = useUserForm();
</script>
<template>
  <form @submit.prevent="submitForm">
    <div>
      <label for="email">電子郵件</label>
      <input id="email" v-model="email" />
      <span v-if="errors.email">{{ errors.email }}</span>
    </div>
    <div>
      <label for="imageUrl">圖片網址</label>
      <input id="imageUrl" v-model="imageUrl" />
      <span v-if="errors.imageUrl">{{ errors.imageUrl }}</span>
    </div>
    <div>
      <label for="ip">IP 地址</label>
      <input id="ip" v-model="ip" />
      <span v-if="errors.ip">{{ errors.ip }}</span>
    </div>
    <div>
      <label for="taiwanPhone">台灣電話號碼</label>
      <input id="taiwanPhone" v-model="taiwanPhone" />
      <span v-if="errors.taiwanPhone">{{ errors.taiwanPhone }}</span>
    </div>
    <div>
      <label for="token">驗證信 token</label>
      <input id="token" v-model="token" />
      <span v-if="errors.token">{{ errors.token }}</span>
    </div>
    <div>
      <label for="password">密碼</label>
      <input id="password" v-model="password" />
      <span v-if="errors.password">{{ errors.password }}</span>
    </div>
    <div>
      <label for="confirmPassword">確認密碼</label>
      <input id="confirmPassword" v-model="confirmPassword" />
      <span v-if="errors.confirmPassword">{{ errors.confirmPassword }}</span>
    </div>
    <button type="submit">提交</button>
  </form>
</template>
(檔案: src/composables/useUserForm.ts)
import { useForm, useField } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { userFormSchema, type UserFormSchema } from '../schemas/userFormSchema';
export const useUserForm = () => {
  const validationSchema = toTypedSchema(userFormSchema);
  
  const initialValues: UserFormSchema = {
    email: '',
    imageUrl: '',
    ip: '',
    taiwanPhone: '',
    token: '',
    password: '',
    confirmPassword: '',
  };
  // 設置 Vee-Validate 的 useForm 並綁定 Zod schema
  const { handleSubmit, errors } = useForm({
    validationSchema,
    initialValues
  });
  // 使用 useField 將每個字段的驗證邏輯和 UI 結合
  const email = useField<string>('email');
  const imageUrl = useField<string>('imageUrl');
  const ip = useField<string>('ip');
  const taiwanPhone = useField<string>('taiwanPhone');
  const token = useField<string>('asyncValue');
  const password = useField<string>('password');
  const confirmPassword = useField<string>('confirmPassword');
  const submitForm = handleSubmit((values) => {
    console.log('表單驗證成功:', values);
  });
  return {
    // state::
    errors,
    // field::
    email,
    imageUrl,
    ip,
    taiwanPhone,
    token,
    password,
    confirmPassword,
    // methods::
    submitForm
  };
};
export type UseUserForm = typeof useUserForm;
Zod 的 safeParse 和 safeParseAsync 讓我們能夠在提交表單前或其他情況下手動驗證數據,這樣可以靈活處理表單提交的驗證流程。
// 手動驗證數據的範例
import { formSchema } from '../schemas/userFormSchema';
// 同步驗證數據,因為 refine內有非同步動作,所以建議用 safeParseAsync 進行驗證
// 這裡僅是示範
const result = userFormSchema.safeParse({
  email: 'user@example.com',
  imageUrl: 'http://www.exampleImage.com/test.jpg',
  ip: '192.168.0.1',
  taiwanPhone: '0912345678',
  token: 'someValue',
  password: 'hello password',
  confirmPassword: 'hello password',
});
if (!result.success) {
  console.error('同步驗證失敗:', result.error.errors);
} else {
  console.log('同步驗證成功:', result.data);
}
// 異步驗證數據
const asyncResult = await userFormSchema.safeParseAsync({
  email: 'user@example.com',
  imageUrl: 'http://www.exampleImage.com/test.jpg',
  ip: '192.168.0.1',
  taiwanPhone: '0912345678',
  token: 'someValue',
  password: 'hello password',
  confirmPassword: 'hello password',
});
if (!asyncResult.success) {
  console.error('異步驗證失敗:', asyncResult.error.errors);
} else {
  console.log('異步驗證成功:', asyncResult.data);
}
結合 Zod 的強大驗證功能和 Vee-Validate 的靈活性,我們可以實現複雜且高效的動態表單驗證。通過 refine 和 superRefine,可以實現自定義驗證邏輯,而 safeParse 和 safeParseAsync 則提供了更多控制驗證流程的方式。這些功能使得我們在處理高階組件設計時能夠更加從容和靈活。
希望這篇文章能幫助你在 Vue 項目中更好地應用 Zod 和 Vee-Validate 進行動態表單驗證!