javascript 資料型態/結構驗證庫 : Skeletons

timtnlee發表於2019-02-19

Skeletons 是一個簡單直覺的純JS資料驗證庫

前言

當初專案需要將資料以JSON格式儲存在本地端,萬一資料結構出了問題或是不符合預其,後面程式都會出問題,因此想寫一個簡單直覺的純JS資料驗證方法,並開源到npm上。

希望對大家有幫助。喜歡可以給個星:) 有任何討論都歡迎。

原始碼

Javascript 型態

先來介紹一下Javascript有趣的資料型態,如有錯誤請幫忙提出修正~

JS 共有七種資料型態

其中包含六種 Primitive types :

  • Boolean
  • Null
  • Undefined
  • Number
  • String
  • Symbol

和 Object

特別的是 Function 廣義來說也是屬於物件。

我們可以用 typeof 來檢查型態 (回傳一個字串)

typeof 1 // `number`
typeof "" // `string`
typeof true // `boolean`
typeof undefined // `undefined`
typeof null // `object`
typeof Symbol // `symbol`
typeof function(){ } // `function`
typeof {} // `object`
複製程式碼

等等,你有沒有發現什麼端倪?我們一個一個看。

Number

一般我們會直接定義變數:

let a = 1
複製程式碼

也可以用 function 定義

let a = Number(1)
typeof a // `number`
複製程式碼

但如果加上 new ,則會建立一個Number物件

let a = new Number(1)
typeof a // `object`
複製程式碼

NaN 也是屬於Number

typeof NaN // `number`
複製程式碼

如果要判斷變數a是可計算的數字且不是NaN,以下是行不通的

if(typeof a===`number` && a!==NaN) //... a !== NaN 永遠是 true
複製程式碼

因為 NaN 很特別,它不會等於任何值

// 以下通通都是 false !
NaN == 0 
NaN == false
NaN == NaN
NaN === NaN
複製程式碼

你可能會想到用 boolean 來判斷 true/false

if(typeof a===`number` && !a) // ...
複製程式碼

但是別忘了還有個 0 :

if(typeof a===`number` && !a && a !== 0) // ...
複製程式碼

當然最簡單的是用 isNaN 這個方法區分

if(typeof a===`number` && !isNaN(a)) // ...
複製程式碼

Boolean, String

和 Number 很像,注意用new的話會一樣是建立object。

let a = true  // boolean
a = Boolean(true) //boolean
a = new Boolean(true) //object
複製程式碼

Undefined

也是一種premitive type

undefinednull 是沒有 funtion的 ,直接指定值就好

typeof undefined // `undefined`
複製程式碼

值得一提的是,雖然以下都是否定值 (false)

Boolean(0)
Boolean(false)
Boolean(``)
Boolean(undefined)
Boolean(null)
複製程式碼

但動態型別方面0, false, `` 是一夥的,
undefined, null則是另外一個團體

0 == false  // true
0 == ``     // true
false == `` // true

undefined == null // true

undefined == false // false
undefined == 0 //false
null == `` //false
複製程式碼

Null

null也是一種type,但是。。。

typeof null // `object`
複製程式碼

沒錯,typeof 列印出來的是 `object`

上網查了一下,有些人說是JS當初設計的錯誤。

我們要判斷一個變數是物件的話可以這樣:

if(typeof a === `object` && a!==null) // ...
複製程式碼

Symbol

最後一個 premitive type symbol

建立一個 symbol:

let a = Symbol()
typeof a // `symbol`
複製程式碼

注意不能用 new,會丟出錯誤

let a = new Symbol()
> Uncaught TypeError: Symbol is not a constructor
複製程式碼

Object

除了上面六種 primitive type,其他都歸類為物件

但特別的是 function,使用typeof檢查會回傳function字串:

typeof function(){} // `function`
複製程式碼

讓我們能很好的區別 function 和其他一般的物件

Skeletons

接下來要介紹這個庫了,有興趣的話可以先看看介紹

請先記好上面 Javascript 原生定義的資料型別,因為這個庫的分類有些不一樣

Skeletons 中可定義的型別除了原本的七種JS型別,額外分出 array 和 function

原因是這兩個都是很常用的,將他們從物件特別區分出來。

使用方法

定義一個規則,使用 validate 來驗證資料

const rule = new Skeletons(schema)
rule.validate(data)
複製程式碼

Schema

定義規則需要傳入一個schema,也就是你設想的資料結構以及形態

schema 可以有四種

1. premitive type function

共有四種可以用 (undefined和null是沒有function的,我們後面談如何定義)

  • Number
  • Boolean
  • String
  • Symbol

分別定義四種形態,使用上不用呼叫,直接傳入function

如下,定義一個型態為數字的schema

const schema = Number
複製程式碼

2. 使用 object literal

使用最值覺的 object literal 來定義一個物件 (注意,在Skeletons會排除array和function)的key

每個key都可指派另一個schema

如下定義了一個有 x, y 兩個鍵的物件,且兩個鍵的值都是數字型態

const schema = {
  x: Number,
  y: Number
}
複製程式碼

使用這種方式,讓你能夠輕易地定義結構較深的物件

const userSchema = {
  name: String,
  id: String,
  VIP: {
    code: Number,
    details: {
      type: String,
      level: Number,
      expired: Boolean
    }
  },
}
複製程式碼

3. array literal

使用array literal來定義有固定元素數量的array

const schema = [String, Number, Skeletons.Function()]
複製程式碼

4.呼叫Skeletons的靜態方法

  • Skeletons.Number()
  • Skeletons.String()
  • Skeletons.Boolean()
  • Skeletons.Null()
  • Skeletons.Symbol()
  • Skeletons.Any()
  • Skeletons.Array()
  • Skeletons.Object()
  • Skeletons.Function()
  • Skeletons.MapObject()

共有十種方法,分別代表是五種premitive type (不含undefined)、Object, 從物件中分出來的 Array, Function,以及特殊的Any(任何非undefined的型態) 和 MapObject

每種方法都接受一個options物件當做引數,

且都可定義三個基本的property

  • options.required

    type: Boolean

    default: true

    Skeletons對於任何 undefined 值都會認定為驗證失敗:

    new Skeletons({
      a: Number
    }).validate({})
    // data.a got undefined, validation filed
    複製程式碼

    如果要允許該層資料可以為 undefined,設 options.required 為 false

    new Skeletons({
      a: Skeletons.Number({
        required: false
      })
    })
    複製程式碼
  • options.default

    type: 任何

    default: undefined

    有時後資料的預設值(或者空值)的型態可能會和資料有值的時後不一樣,比方說有人可能會用null來替代空的物件。

    new Skeletons(Skeletons.Object({
      default: null
    }))
    複製程式碼
  • options.validator

    type: Function

    傳入一個function,回傳true/false來驗證資料

    validator(value, data)
    複製程式碼

    該函式可接收兩個引數:value代表該層資料的值,data代表整個資料

    以下這個例子,value等於120, data等於整個datasource

    const datasource = {
      a: 120,
      b: 240
    }
    
    new Skeletons({
      a: Skeletons.Number({
        validator: (val, data) => {
         // in this case, val = 120, data = datasource
         return val === data.b*2
        }
      }),
      b: Number
    })
    複製程式碼

更多詳細的介紹可以參考檔案

驗證

驗證可分為

  • 使用 console 列印出錯誤資訊
  • 直接丟擲錯誤

如何設定可參考檔案

每次驗證後,可由warnings屬性獲得錯誤資訊

const rule = new Skeletons(Number)
rule.validate(`1`)
rule.warnings // 一串array 包含所有錯誤資訊
複製程式碼

關於錯誤資訊可參考warnings

示例

接下來演示一些資料定義的範例

範例一 : 陣列

定義一個schema代表不是NaNnumber

// ex: 1
const calcNum = Skeletons.Number({
  allowNaN: false
})
複製程式碼

定義一個array,每個元素是含有x,y屬性,值為非NaN數字的物件

// ex: [{x: 1, y: 2}, {x: 3, y: 5}]
new Skeletons(
  Skeletons.Array({
    item: {
      x: calcNum,
      y: calcNum
    }
  })
)
複製程式碼

規定array一定要有元素

// ex: [{x: 1, y: 2}, {x: 3, y: 5}]
new Skeletons(
  Skeletons.Array({
    validator: ary=>ary.length>0,
    item: {
      x: calcNum,
      y: calcNum
    }
  })
)
複製程式碼

範例二 : 和其他資料比對

假設有一筆資料,當age大於 18,grownup等於true代表已成年,反之則為false

const ex = {
  age: 19,
  grownup: true
}
複製程式碼

我們可以用 validator來進行檢查

new Skeletons(
  {
    age: Number,
    grownup: Skeletons.Boolean({
      validator: (val, data) => val === data.age>=18
    })
  }
).validate(ex)
複製程式碼

範例三: 不限制物件的key

有時後物件作為一個類似map來除存對應的key,代表並沒有固定的屬性值和數量。這時可以使用 MapObject

例如 room以房間的id當做key來mapping

const room = {
  idkfd: {
    name : `have fun`,
    members: 4,
    id: `idkfd`
  },
  ckclo: {
    name : `My room`,
    members: 2,
    id: `ckclo`
  }, 
  ppqkd: {
    name : `User0001`s room`,
    members: 8,
    id: `ppqkd`
  } 
}
複製程式碼

可這樣定義

new Skeletons(
  Skeletons.MapObject({
    keyValidator: (k, data)=> k === data[k].id,
    item: {
      name: String,
      members: Number,
      id: String
    }
  })
)
複製程式碼

結語

就先介紹到這,這個庫比較像是開發階段、測試使用的,可確保資料的結構、型態符合自己的要求,避免後續程式出錯,

並希望用直覺簡單的方式就能定義複雜的結構。

希望對大家能有所幫助,謝謝。

相關文章