更好用、能擴充套件、支援多國語言提示的表單驗證類庫
fire
更好用、能擴充套件、支援多國語言提示的表單驗證類庫,使用GoLang
開發。
用法
我們先看一個最簡單的例子,這是fire
類庫的Hello World
。
import (
"fmt"
"github.com/joyant/fire"
)
func main() {
v := fire.New(fire.Rule{
"name":"required|lengthBetween:5,10",
})
data := fire.Data{"name":"Tom"}
qulified, err := v.Validate(data)
}
使用fire
是簡單的。
傳入 fire.Data
fire.Data
是fire
定義的資料型別,用於傳入資料用,我們可以直接使用它。
v := fire.New(fire.Rule{
"name":"required",
})
data := fire.Data{"name":"Tom"}
v.Validate(data)
fire.Data
本質上一個map[string]interface{}
型別,所以我們也可以傳入map[string]interface{}
型別。
v := fire.New(fire.Rule{
"name":"required",
})
data := map[string]interface{}{"name":"Tom"}
v.Validate(data)
傳入 struct
type User struct {
Name string
}
v := fire.New(fire.Rule{
"name":"required",
})
u := User{Name:"Tom"}
傳入指標也是可以的,但是指標必須指向一個結構體。
u := &User{Name:"Tom"}
v.Validate(u)
除了fire.Data
、map[string]interface{}
、結構體以及指向結構體的指標外,Validate
方法並不支援傳入其他型別的引數,如果傳入了錯誤型別的引數,返回的err
不為空,檢查New
函式返回的err
是否為空是有必要的。
結構體 (struct) 與 Rule 的對映關係
在上一部分的例子裡,我們看到結構體的屬性是Name
,而Rule
的key
是name
,嚴格來說,它們並不相等 (用==判斷),那麼fire
是如何將他們對應起來的呢?
fire
會先將struct
的屬性按照一定的規則轉為Rule
的key
,當這些規則都用完了還沒有找到對映關係,那fire
就會
認為這個規則並不存在,規則如下:
Name -> Name, name, name
FirstName -> FirstName, first_name, firstname
First_Name -> First_Name, first_name, first_name
當fire
拿到一個key
,它會依次尋找其原始、下劃線形式、全小寫在Rule
是否有對應的key
,如果都沒有找到,就會認為其對應的規則是空,也就是說這個屬性是不會被校驗的。
假設傳入Validate
函式的引數是fire.Data{"name":"Tom"}
,規則是Rule{"Name":"required","name":"int"}
,fire
會依次在Rule
中尋找Name
、name
、name
,先找到誰,就認定那個驗證規則,在這個例子裡,規則就是required
,而不會驗證name
是否是int
型別。
在GoLang
的世界裡,很多驗證類庫的方式都利用了tag
,這樣做的好處是,key
的定義非常清晰,沒有歧義;但也有弊端,它會使結構體中包含很多讓人眼花繚亂的tag
,舉一個在真實的專案裡見過的例子:
type User struct {
Name string `json:"name", validator:"name", form:"name", db:"name"`
}
在上面的結構體中,json
標籤是用來序列化和反序列化用的,validator
標籤是驗證表單用的,db
和form
也各有其用途,本來是一個"純粹"的結構體,在加入很多的tag
後,結構體變得非常臃腫;我們在fire
中偏愛"約定優於配置"的原則,這些約定都是比較符合一般做法和直覺的,不會令人覺得怪異,這樣就不大需要寫tag
了。
但是,我們的約定可能無法滿足所有的要求,因為我們不總是從一個的專案的零步做起,很可能接手的是一個老專案,而那裡大概率已經有一些約定俗稱的規則了,我們不能因為引入了一個表單驗證的類庫就打破原來的規則,所以,fire
也是支援tag
的,當約定不能滿足要求的時候,可以將tag
應用於特殊的需求,fire
給了tag
最高的優先順序,請看下面的例子:
type User struct {
Name string `fire:"nickname"`
}
這樣name
對應的key
就是nickname
,即使data
有另外一個名為name
的key
,fire
也不會去驗證它,因為我們
給了tag
最高的優先順序。
如果你不喜歡fire
這個預設的tag
(也可能是想複用名為json
的tag
),可以通過以下設定修改:
fire.Tag = "json" // 也可以是其他任意設定的tag
驗證規則
我們把內建的規則都列出來:
token | 用途 | 舉例 | 備註 |
---|---|---|---|
alphaNum | 英文字元或數字 | alphaNum | 英文字元或數字 |
alpha | 英文字元 | alpha | 英文字元 |
between | 數字大小必須在指定範圍內 | between:1, 10 between:1.1, 9.9 |
左右包含 |
bool | 布林值 | bool | 布林值的選項如下 1, t, T, true, TRUE, True 0, f, F, false, FALSE, False |
contains | 包含指定字元 | contains:abc contains:abc/i |
字串裡必須包含 abc abc 連在一起時才能通過驗證 abc/i 表示不區分大小寫 含有 ABC 的字串也能通過驗證 |
date | 日期格式 | date date:2006-01-02 |
只寫一個 date 時,以下資料均能通過 2020 2020-01-02 2020-01-02 15:15:15 date 後面可以指定日期格式 2006 2006-01 2006-01-02 2006-01-02 15:04:05 |
different | 和指定 key 對應的值不同 | different:username | 資料不能和 key 為 username 的資料相同 常用於判斷密碼不能和賬號相同 |
郵箱格式 | 郵箱格式 | ||
equals | 與指定 key 的值相同 | equals:confirm_password | 必須與指定 key 的值相同 常用於二次輸入密碼和 第一次輸入是否相同的驗證 |
in | 在指定字串之內 | in:1,2,3 | 必須在 [1,2,3] 之內 會自動 trim 逗號之間的空白 |
notIn | 不能在指定字串之內 | notIn:1,2,3 | 不能在 [1,2,3] 之內 會自動 trim 逗號之間的空白 |
integer int |
整型 | integer int |
必須是整型 |
ip | ipv4 或 ipv6 格式 | ip | 必須是 ipv4 或 ipv6 格式 |
ipv4 | ipv4 格式 | ipv4 | 必須是 ipv4 格式 |
Ipv6 | ipv6 格式 | Ipv6 | 必須是 ipv6 格式 |
lengthBetween | 字串長度在指定範圍之內 | lengthBetween:1,10 | 字串長度在 1~10 之間 左右包含,utf8 編碼 |
lengthMax | 字串長度最大範圍 | lengthMax:10 | 字串長度不能超過 10 utf8 編碼 |
lengthMin | 字串長度最小範圍 | lengthMin:10 | 字串長度不能小於 10 utf8 編碼 |
length | 字串長度必須是指定值 | length:5 | 字串長度必須是 5,utf8 編碼 |
max | 最大值 | max:100 | 值不能超過 100 內部實現是轉成 float64 再比較的 |
min | 最小值 | min:10 | 值不能小於 10 內部實現是轉成 float64 再比較的 |
numeric | 必須是數字(整數或小數) | numeric | 必須是數字(整數或小數) 100 和 99.99 都能通過驗證 |
regexp | 匹配指定正規表示式 | regexp:\\d+{1,10} | 匹配 1~10 個數字 |
required require |
必填項 | required require |
必填項 |
url | 網址格式 | url | 必須是網址格式 http://www.abc.com https://www.abc.com http://abc.com https://abc.com www.abc.com abc.com abc.com/path1/path2?key=1 都可以通過驗證 |
需要注意的是,規則之間要用|
分開,|
和:
都要用半形輸入,比如我們可以設定以下規則來驗證:
rule := Rule{
"username":"in:Tom,Jim,Jerry|lengthBetween:1,10|email|int|numeric|alpha",
}
內建的中英文提示
不能驗證通過時返回的提示資訊非常重要,它能明確告訴使用者哪個欄位不能同通過驗證,而不是籠統告訴使用者發生錯誤了。在fire
內建的這些規則中,都有中英文提示,比如我們要驗證一個key
為birthday
的值,它不能通過日期格式(date)
驗證時,返回的中文提示是date必須是日期格式
,英文提示是date must be date format
。在某些時候,我們可能不希望使用者看到中英文摻雜的文字提示,換句話說,我們想讓birthday
有指定的中文翻譯,這時,我們就可以呼叫fire
的RegisterI18nDataKey
函式:
fire.RegisterI18nDataKey(fire.DataKey("birthday"), map[fire.Lang]string{
fire.LangZH:"生日"
})
這個函式的作用是為birthday
註冊多語言的翻譯,這個時候我們就能看到全中文提示了生日必須是日期格式
,如果你的使用者本來就是英文世界的,那就不需要呼叫以上的函式了,只需要把預設提示語言設定成英文就行了,設定方法有兩種:
//第一種方法,是全域性設定
fire.DefaultLang = fire.LangEN
//第二種方法是以validator為單位設定,這個validator返回的驗證結果將會是英文提示,不會被全域性設定覆蓋
fire.New(fire.Rule{}, fire.LangEN)
fire
的預設語言設定是中文,即使正合你的需要,還是建議設定預設語言。我們內建了一些常量供開發者使用,這些常量是為開發擴充套件和設定訊息提供方便,並不是說fire
內建了這些語言的提示,內建的只有中文和英文。
LangZH Lang = "zh" // 漢語
LangEN Lang = "en" // 英語
LangDE Lang = "de" // 德語
LangFR Lang = "fr" // 法語
LangRU Lang = "ru" // 俄語
LangES Lang = "es" // 西班牙語
LangJA Lang = "ja" // 日語
LangAR Lang = "ar" // 阿拉伯語
LangKR Lang = "kr" // 韓語
LangPT Lang = "pt" // 葡萄牙語
多國語言支援
假設你的專案有不少中東使用者,所以需要一套阿拉伯文的驗證結果提示,但是內建的語言只有中文和英文,該如果解決這個問題呢?我們還拿生日來舉例子,直接看程式碼:
fire.RegisterMsgFormat("birthday", map[fir.Lang]fire.MsgFormat{
fire.LangAR:"${0} يجب أن يكون شكل عيد ميلاد ", //翻譯成中文就是:必須是生日格式
})
這是我們第一次接觸到資訊提示的佔位符,在以上的例子裡${0}
就代表birthday
,關於佔位符,我們會在擴充套件驗證部分更多提到。
擴充套件驗證規則
雖然fire
內建了常用的驗證規則,但一定會遇到不夠用的時候,在這種情況下,fire
允許使用者擴充套件驗證規則,假設我們現在有一個比較奇怪的規則:驗證姓名,如果是英文名字,那必須得是全名 (full name),其他名字(中文,阿拉伯文等)必須滿足指定的最小長度才可以通過驗證;這個規則顯然是常用的規則不能滿足要求的,那我們來擴充套件一個規則吧。
原理
希望所有的擴充套件開發者都能知道擴充套件的原理是什麼,這樣一定寫得更好。在fire
內部,每個驗證規則都對應了一個Token
例項,Token
是一個介面,有一系列規定的方法。
type Token interface {
Evaluate(value DataValue, data Data) (qualified bool, literalValue []string, err error)
TokenType() TokenType
I18nMsgFormat(Lang) MsgFormat
ParseLiteral(literal string) error
Clone() Token
}
fire
在驗證資料的時候,就是呼叫Token
的一連串方法來判斷的,所以我們只需要註冊一個新的Token
讓fire
知道就可以了。
程式碼
type specialNameToken struct {
literal string
length int
}
func (t *specialNameToken) Evaluate(value DataValue, data Data)
(qualified bool, literalValue []string, err error) {
if value == nil { //如果傳入的資料是空,那我們就不驗證了
qualified = true
return
}
v, ok := value.(string)
if !ok {
err = fmt.Errorf("%s's value must be string", t.TokenType())
return
}
isENName, includeSpace := isName(v)
if isENName {
return includeSpace, []string{t.literal}, nil
} else {
return utf8.RuneCountInString(v) >= t.length, []string{t.literal}, nil
}
}
func (t *specialNameToken) TokenType() TokenType {
return "specialName" //token獨一無二的名稱,不應該和其他規則重複
}
func (t *specialNameToken) I18nMsgFormat(lg Lang) MsgFormat {
if lg == LangZH {
return "${0}必須是指定格式" // ${0}是一個佔位符,將會被替換成資料的key
} else if lg == LangEN {
return "${0} must be special format"
} else if lg == LangAR {
... //如果你的專案需要支援多種語言,那麼你可以寫更多的分支來支援不同語言的提示,這裡就省略了
}
return ""
}
//ParseLiteral接收到的引數是specialName:後面的值,
//假設規則是specialName:20,它表示如果名字是英文,那麼長度不能超過20
//在這個例子裡,literal的值是"20"
func (t *specialNameToken) ParseLiteral(literal string) error {
if literal != "" {
length, err := strconv.Atoi(literal)
if err != nil {
return err
}
t.length = length
}
t.literal = literal
return nil
}
//clone用來保證深拷貝一個物件,修改拷貝時不會影響原來的物件, 請保證深拷貝
func (t *specialNameToken) Clone() Token {
c := *t
return &c
}
func isName(s string) (isENName bool, includeSpace bool) {
for _, v := range s {
if !((v >= 'a' && v <= 'z') || (v >= 'A' && v <= 'Z') || v == ' ') {
return false, false
}
if v == ' ' {
includeSpace = true
}
}
isENName = true
return
}
我們完成了一個擴充套件,並且規定了其名稱是specialName
,名稱是TokenType()
的返回值決定的;但這個token
是fire
所不認識的,我們把它註冊給fire
,然後就可以放心的使用了:
fire.RegisterToken(&specialNameToken{})
rule := fire.Rule{
"name":"specialName:20",
}
佔位符
按我們約定的,現在詳細的來看下佔位符,我們拿fire
內建的一個驗證規則來舉例子,比較有代表性的規則是lengthBetween
:
// fire.Rule{"name":"lengthBetween:1, 10"}
func (t *lengthBetweenToken) I18nMsgFormat(lg Lang) MsgFormat {
if lg == LangZH {
return "${0}長度必須在${1}和${2}之間"
} else if lg == LangEN {
return "${0}'s length must between ${1} and ${2}"
}
return ""
}
${0}
將會被name
填充,${1}
將會被1
填充,${2}
將會被10
填充。
我們為什麼要解釋佔位符呢?因為佔位符不但在開發擴充套件的時候有用,在設定多國語言提示的也是有用的。在"多國語言"部分,我們提到了如果我們的專案是中文和英文世界之外的使用者使用,我們如何給他們提示相應的方言呢?看程式碼:
fire.RegisterMsgFormat("birthday", map[fir.Lang]fire.MsgFormat{
fire.LangAR:"${0} يجب أن يكون شكل عيد ميلاد ",
})
這樣我們就註冊了一個date
規則對應的訊息提示,當錯誤發生時,就會返回阿拉伯文的提示,當然,前提是設定了預設的語言。
別名
請看以下程式碼:
fire.RegisterI18nDataKey(fire.DataKey("class_name"), map[fire.Lang]string{
fire.LangZH:"班級名稱"
})
v := fire.New(fire.Rule{
"name":"alias:class_name|required"
})
由於name
這個關鍵詞實在太廣泛了,它在user
表中表示使用者名稱,在class
表中表示班級名稱,在teacher
表中又表示教室名稱,但是在不同的介面中,它們都有一個相同的名稱:name
,我們可不希望每個介面都提示為名稱
,因為這個提示太不具體了,我們還是希望提示:"名稱不能為空","班級名稱不能為空","教室名稱不能為空"……,所以fire
內建了名為alias
的規則,在上面的例子裡,當name
不能通過驗證時,提示的是"班級名稱是必填項"。
最佳實踐
在fire
的設計裡,我們故意把解析Rule
和驗證資料分開了,當呼叫fire.New
方法返回一個介面時,已經把規則都解析好,"快取"起來,剩下的就是資料的驗證了,因為驗證本身不會修改物件的成員變數,所以同一個物件的Validate
方法可以在多個協程中同時呼叫,而不會因為併發發生錯誤。所以比較好的實踐是不要每次收到請求時都去呼叫fire.New
,而應該在專案初始的時候就把物件例項化好,在需要驗證資料的時候,呼叫指定物件的Validate
方法就可以了,這減少了很多解析規則的開銷,舉個簡單的例子,假設以handleAPI
為字首的函式執行在不同的協程中:
var v1 = fire.New(rule1)
var v2 = fire.New(rule2)
func handleAPI1(data fire.Data) {
v1.Validate(data)
}
func handleAPI2(data fire.Data) {
v2.Validate(data)
}
下面是錯誤的例子:
func handleAPI1(data fire.Data) {
var v1 = fire.New(rule1)
v1.Validate(data)
}
func handleAPI2(data fire.Data) {
var v2 = fire.New(rule2)
v2.Validate(data)
}
在每個請求到來時都建立fire.Validator
物件是沒有必要的,增加了不必要的開銷。
在fire
裡,那些Register
開頭的函式,實際的作用是把一些我們需要的資料以鍵值對的形式存放在map
裡,fire
使用的就是GoLang
內建的map
,沒有加鎖,不支援同時讀寫,所以註冊類的函式呼叫應該在呼叫fire.New
前進行,一旦有fire.Validator
例項被建立出來了,任何註冊類函式都不應該再被呼叫。
起源
表單驗證類庫在web
應用中是如此重要,它是業務邏輯的起始,我自己在開發專案時,總傾向於用一種儘量簡單的方法驗證資料的正確性,而把更多的精力用在真正業務邏輯的編寫上。受到thinkphp
框架以及很多其他驗證類庫的啟發,就想把簡單的方法帶到經手的專案中來,這就是fire
的來源。
在開發fire
的過程中,我也從解析器中借鑑了一些方法,把驗證規則變成可驗證的函式,其實就是把字串解析成有特定目的的資料結構。所以先確定下了幾個重要介面:Token
,parser
,Validator
,明確了他們各自的職責,Token
負責制定規則並驗證資料,parser
被Token
呼叫把字串轉為有意義的資料結構,就像解釋有指定語法的程式碼,Validator
負責把資料傳給相應的Token
,讓它去驗證,最後把結果返回給呼叫者。
得益於GoLang
的介面,先定義介面,寫出主要的業務邏輯和單元測試,剩下的工作就是完成一個個內建Token
的開發,開發完後註冊到儲存Token
的map
裡。
所以,fire
最重要的並不是那些內建的規則,而是基於介面的一系列呼叫,即使fire
中一個內建的規則都沒有,開發者還是可以通過自己開發Token
然後註冊到fire
來使用,這些Token
甚至可以覆蓋內建的Token
,制定自己的驗證規則。
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- 表單驗證使用擴充套件套件
- eayui 驗證擴充套件UI套件
- Laravel 驗證擴充套件包Laravel套件
- jquery easyui 擴充套件驗證jQueryUI套件
- Cilium 1.3:支援Envoy、Cassandra和Memcached的Go語言擴充套件Go套件
- 單隊玩法擴充套件多隊套件
- 擴充套件表套件
- iOS擴充-語言國際化iOS
- WPF多語言支援:簡單靈活的動態切換,讓你的程式支援多國語言
- 用C語言擴充套件Python的功能C語言套件Python
- 分類擴充套件套件
- 寫一個簡單易用可擴充套件vue表單驗證外掛(vue-validate-easy)套件Vue
- 寫一個Laravel中文驗證擴充套件包Laravel套件
- C++對C語言的擴充套件(1)--引用C++C語言套件
- 一種簡單好用的Vue表單驗證Vue
- DcatAdmin 擴充套件: 自定義表單(動態表單)套件
- 《自然》證實:計算機語言更類似人類語言計算機
- C++11語言擴充套件:常規特性C++套件
- XBRL(可擴充套件商業報告語言套件
- Go 語言編寫 CPython 擴充套件 goPyGoPython套件
- HttpContext擴充套件類HTTPContext套件
- 表空間自動擴充套件 AUTOALLOCATE 的擴充套件規律套件
- 【實驗】修改資料庫檔案為自動擴充套件以達到表空間自動擴充套件的目的資料庫套件
- Laravel Bss 專案中用到的第三方擴充套件一,驗證碼擴充套件包Laravel套件
- 分享一些好用的 Chrome 擴充套件Chrome套件
- Swift 小貼士:語言的擴充套件和自定義Swift套件
- 基於PCNTl擴充套件的PHP多程式管理庫套件PHP
- weex ios擴充套件類的作用iOS套件
- 擴充套件表空間套件
- PostgreSQL資料庫擴充套件語言程式設計之plpgsql-1SQL資料庫套件程式設計
- DLR 的擴充套件庫 Dynamitey套件MIT
- phpMyAdmin提示缺少mysqli擴充套件PHPMySql套件
- Vue-手動清空Form表單的驗證及驗證提示(紅字提示)VueORM
- Solon詳解(六)- Solon的校驗擴充套件框架使用與擴充套件套件框架
- 讓前端也能填充資料庫的 Reach Seeder 擴充套件前端資料庫套件
- ASP.NET Core擴充套件庫之Http通用擴充套件ASP.NET套件HTTP
- 解密JavaChassis3:易擴充套件的多種註冊中心支援解密JavaS3套件
- INFORMIX表的預設初始擴充套件、下一個擴充套件資料塊以及一個表允許的最大擴充套件數。ORM套件