iT邦幫忙

2022 iThome 鐵人賽

DAY 11
1
Modern Web

前端技能樹的十萬個為什麼系列 第 11

Day 11 - 為什麼要用 Yup

  • 分享至 

  • xImage
  •  

前言

由於昨天討論 React Hook Form 的過程中提到,若要做檢核(validation)動作,可以透過 Yup 這個 library 來處理,那可能就會有疑問了:

  • 為什麼要檢核?
  • 為什麼要用 Yup? 不能自己寫嗎?

於是今天算一個小章節,一起來討論 Yup 這項工具!

先想一下

  • Yup 是在什麼樣的時代誕生的?
  • Yup 怎麼解決問題?
  • Yup 的優缺點是什麼?
  • Yup 適合什麼情境?

Yup 是在什麼樣的時代誕生的?

即便我們手上有一些像 Redux FormReact Hook Form 之類的工具,可以有效管理表單欄位,並且在按下「Submit」同時,將所有欄位都蒐集到手上,準備送到後端。

但。。。中間是不是漏了什麼?

如果使用者輸入的帳號或密碼少於六位數?
如果使用者硬是在「金額」的欄位輸入中文?
甚至,如果駭客想要透過輸入欄位 inject 一些壞壞的程式碼?

是的,在「Submit」與「實際送出」之間,還需要加入一個「檢核」的流程

在還沒有像 Yup 這樣用來檢核的工具之前,檢核是相當土法煉鋼的,比如像 Redux Form 的 submit validation 就會出現類似這樣的 code:

// values 代表 Redux Form 幫你把所有欄位的數值集中到一個 object
const validate = values => {
  // errors 放入 key-value pair(錯誤的欄位 key,以及對應的錯誤訊息 value)
  const errors = {};
    
  if (!values.username) {
    errors.username = '必填欄位'
  } else if (values.username.length > 15) {
    errors.username = '至多 15 位字元'
  }
  if (!values.email) {
    errors.email = '必填欄位'
  } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
    errors.email = '未符合 email 格式'
  }
  if (!values.age) {
    errors.age = '必填欄位'
  } else if (isNaN(Number(values.age))) {
    errors.age = '必須為數字'
  } else if (Number(values.age) > 0) {
    errors.age = '數字需大於 0'
  }
  return errors
}

可以看到邏輯是複雜且難以閱讀的,不僅充斥著 if-else,也會出現許多類似的判斷

Yup 怎麼解決問題?

Yup is a schema builder for runtime value parsing and validation.

schema 可以說是對於「資料格式」與「數值」的一種描述架構,透過事先定義 schema,就像先訂好規則,然後要求 form 表單的資料要能夠匹配 schema,而 Yup 就是幫助我們整個這兩者的工具之一。

同樣是上面那個 usernameemailage 的範例,schema 的版本會像是這樣:

const userSchema = object({
  // String 格式,最多 15 個字元,必填
  username: string().max(15).required(),
  // String 格式,符合 email 格式,必填
  email: string().email().required(),
  // Number 格式,大於 0,整數,必填
  age: number().positive().integer().required(),
});

比起上面的範例,是否簡潔好讀了許多?甚至不需要註解也能懂!

欄位的資料類型,基本上該有的都有了:

yup.string()
yup.number()
yup.boolean()
yup.date()
yup.object()
yup.array()

而最方便的則是,一些很基本的檢核,比如是否為 null,數字大小區間,陣列至少包含一項等,通通都用淺顯易懂,且可以串接(chainable)的方式提供:

Schema.nullable()
Schema.required()
Schema.typeError()
Schema.oneOf()
number.min()
number.max()

當然,即便面對一些更複雜的檢核條件,Yup 也都提供相對應的 API 來處理:

  • 依賴檢核:需根據其他欄位的值動態判斷,如:.when() .test() .ref()
  • 非同步檢核:回傳 promise,透過非同步取得資料後再檢核,如:.validate()
  • 特定規則:透過客製化的 callback 來處理比較複雜的邏輯,如:.test()

Yup 的優缺點是什麼?

優點

  • 每個檢核子介面都設計淺顯易懂,且可以串接,好閱讀
  • 基本檢核(如必填)一應俱全,不僅不用自己寫,也不用擔心團隊成員寫出奇怪的版本

缺點

沉默地出錯」是 yup 較令人詬病的一點,因為本身的錯誤處理已經被用來當作 schema validation 的錯誤訊息,反而導致 yup 內的程式碼出錯時,不會有任何反應,尤其容易發生在 .when() 或者 .test() 這種客製化邏輯的地方,建議都需要用 try-catch 包起來比較保險

Yup 適合什麼情境?

市面上支援 JavaScript 的 validation libraries 百百種,起碼光是 React Hook Form 提到的就有以下四種:joisuperstructyupzod

觀察到很有趣的是,為了吸引其他套件的使用者,有些 library 還會條列出其他套件的缺點(主觀地),讓其他使用者趕快來投奔,足見競爭之激烈XD

validation libraries 做為前端的第一層把關,能夠提早一步檢查要送往後端的資料,也往往是 UX 的一個重點項目。

因此這類 library 只要表單內的欄位數量多,就很適合用來提升可讀性,且在多人協作的專案,內建的檢核 API 也能確保每個人實作風格的一致

結語

心智圖放大版

檢核算是相對單純的需求,基本上就是「資料進來 -> 判斷 -> 給出錯誤訊息」,也因此才會百家爭鳴,今天我才知道有這麼多種 libraries,不過也正因為很單純,如何將 API 開得好讀、好用,可能才是這個戰場上最有競爭力的價值了!

參考資料

Yup
PJCHENder - Yup 筆記


上一篇
Day 10 - 為什麼要用 React Hook Form
下一篇
Day 12 - 為什麼要用 Day.js
系列文
前端技能樹的十萬個為什麼30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Chiahsuan
iT邦新手 4 級 ‧ 2022-09-27 09:00:40

你好~
關於缺點那個小段落,是否有文章或程式碼可以再更深入了解實際的狀況呢?
因為自己也有在使用 yup 這個工具,所以想確切了解這個工具的潛在問題,避免自己實際遇到卻不知道怎麼解決,謝謝你~

「沉默地出錯」是 yup 較令人詬病的一點,因為本身的錯誤處理已經被用來當作 schema validation 的錯誤訊息,反而導致 yup 內的程式碼出錯時,不會有任何反應,尤其容易發生在 .when() 或者 .test() 這種客製化邏輯的地方,建議都需要用 try-catch 包起來比較保險。

ycchiuuuu iT邦新手 4 級 ‧ 2022-09-27 14:52:33 檢舉

問得很好!的確舉個例會更好理解,比如像這樣

const validation = yup.object({
  field1: yup.string().required(),
  field2: yup.string().when('field1', (field1) => {
    if (field1.something.not.exist) {
      return yup.string().required();
    }
    return yup.string();
  }),
});

因為 field1.something.not.exist 這行取不存在的 key 會死掉,但因為是 runtime error,所以會等到「按下 submit 的那一刻」才死掉,而且畫面上不會有任何反應,乍看之下還會以為是不是 submit 的 function 出了什麼事。

要捕捉到這種錯誤就比較麻煩,需要用到 try-catch:

const validation = yup.object({
  field1: yup.string().required(),
  field2: yup.string().when('field1', (field1) => {
    try {
      if (field1.somethingNotExist) {
        return yup.string().required();
      }
      return yup.string();
    } catch (error) {
      console.log('error', error);
    }
  }),
});

但相對來說除了一些比較複雜的 case,其實是比較不會遇到啦XD“,只是下次如果發現,按下 submit 之後不明原因停留在原地,可以考慮看一下是不是檢核的問題囉!

Chiahsuan iT邦新手 4 級 ‧ 2022-09-27 18:28:08 檢舉

非常謝謝你提供程式碼解釋~原來還有這樣的寫法,我之前都只知道以下的寫法(文件範例)
以後遇到問題就知道朝哪個方向解決了~

let schema = object({
  isBig: boolean(),
  count: number().when('isBig', {
    is: true,
    then: (schema) => schema.min(5),
    otherwise: (schema) => schema.min(0),
  }),
});

我要留言

立即登入留言