本文首發於 github 部落格
如文章對你有幫助,你的 star 是對我最大的支援
背景
大家出來寫 Bug 程式碼的,難免會出 Bug。
文章背景就發生在一個 Bug 身上,
有一天,測試慌張中帶著點興奮衝過來: 測試:"xxx系統前端線上出 Bug 了,點進xx頁面一片空白啊"。 我:"納尼?我寫的Bug怎麼會出現程式碼呢?"。
雖然大腦一片空白,但是鍋還是要背的。
進入頁面一看,哦豁,完蛋,cannot read the property 'xx' of undefined
。確實是前端常見的報錯呀。
背鍋王,我當定了?
NO!
我眉頭一皺,發現事情並不是那麼簡單,經過一番猛如虎的操作之後,最終定位到問題是:後端介面響應的 JSON 資料中,一個巢狀比較深的欄位沒有返回,即前端只讀到了 undefined
。
我們按章程辦事,後端提供的介面文件指定了資料結構,那你沒有返回正確資料結構,這就是你後端的鍋,雖然嚴謹點前端也能捕獲到錯誤進行處理,但歸根到底,是你後端資料介面處理有問題,這鍋,我不背。
甩鍋又是一門扯皮的事情,殺敵一千自傷八百,鍋已經扣下來了,想甩出去就難咯,。
唉,要是在介面出錯的時候,能立刻知道介面資料出問題,先發制人,馬上把鍋甩出去那就好咯。
這就是本文即將要講述的 "Typescript 執行時資料校驗"。
為什麼要執行時校驗資料?
眾所周知,Typescript
是 JavaScript
超集,可以給我們的專案程式碼提供靜態型別檢查,避免因為各種原因而未及時發現的程式碼錯誤,在編譯時就能發現隱藏的程式碼隱患,從而提高程式碼質量。
但是,TypeScript
專案的一個常見問題是: 如何驗證來自外部源的資料並將驗證的資料與TypeScript型別聯絡起來。 即,如何避免後端 API 返回的資料與 Typescript
型別定義不一致導致的執行時錯誤。
Typescript
能用於執行時校驗資料型別,那麼有沒有一種方法,能讓我們在 執行時 也進行 Typescript
資料型別校驗呢?
io-ts 解決方案?
業界開源了一個執行時校驗的工具庫:io-ts。
// io-ts 例子
import * as t from 'io-ts'
// ts 定義
interface Category {
name: string
categories: Array<Category>
}
// 對應上述ts定義的 io-ts 實現
const Category: t.Type<Category> = t.recursion('Category', () =>
t.type({
name: t.string,
categories: t.array(Category)
})
)
複製程式碼
但是,如上面的程式碼所示,這工具看起來就有點囉嗦有點難用,對程式碼的侵入性非常強,要全盤依據它的語法來重寫程式碼。這對於一個團隊來說,存在一定的遷移成本。
而我們更希望做到的理想方案是:
寫好介面的資料結構 typescript
定義,不需要做太多的額外變動,直接就能校驗後端介面響應的資料結構是否符合 typescript
介面定義
理想方案探索
首先,我們瞭解到,後端響應的資料介面一般為 JSON
,那麼,拋開 Typescript
,如果要校驗一個 JSON 的資料結構,我們可以怎麼做到呢?
答案是JSON schema
。
JSON schema
JSON schema 是一種描述 JSON 資料格式的模式。
例如 typescript 資料結構:
type TypeSex = 1 | 2 | 3
interface UserInfo {
name: string
age?: number
sex: TypeSex
}
複製程式碼
等價於以下的 json schema :
{
"$id": "api",
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"UserInfo": {
"properties": {
"age": {
"type": "number"
},
"name": {
"type": "string"
},
"sex": {
"enum": [
1,
2,
3
],
"type": "number"
}
},
"required": [
"name",
"sex"
],
"type": "object"
}
}
}
複製程式碼
根據已有 json-schema
校驗庫,即可校驗資料物件
someValidateFunc(jsonSchema, apiResData)
複製程式碼
這裡大家可能就又會困惑:這json-schema
寫起來也太費勁了?還不一樣要學習成本,那和 io-ts
有什麼區別。
但是,既然我們同時知道 typescript
和 json-schema
的語法定義規則,那麼就兩者必然能夠互相轉換。
也就是說,即便我們不懂 json-schema
的規範與語法,我們也能通過typescript
轉化生成 json-schema
。
那麼,在以上的前提下,我們的思路就是:既然 typescript
本身不支援執行時資料校驗,那麼我們可以將 typescript
先轉化成 json schema
, 然後用 json-schema
校驗資料結構
typescript -> json-schema
要將 typescript
宣告轉換成 json-schema
,這裡推薦使用 typescript-json-schema。
我們可以直接使用它的命令列工具,這裡就不仔細展開說明了,感興趣的可以看下官方文件:
Usage: typescript-json-schema <path-to-typescript-files-or-tsconfig> <type>
Options:
--refs Create shared ref definitions. [boolean] [default: true]
--aliasRefs Create shared ref definitions for the type aliases. [boolean] [default: false]
--topRef Create a top-level ref definition. [boolean] [default: false]
--titles Creates titles in the output schema. [boolean] [default: false]
--defaultProps Create default properties definitions. [boolean] [default: false]
--noExtraProps Disable additional properties in objects by default. [boolean] [default: false]
--propOrder Create property order definitions. [boolean] [default: false]
--required Create required array for non-optional properties. [boolean] [default: false]
--strictNullChecks Make values non-nullable by default. [boolean] [default: false]
--useTypeOfKeyword Use `typeOf` keyword (https://goo.gl/DC6sni) for functions. [boolean] [default: false]
--out, -o The output file, defaults to using stdout
--validationKeywords Provide additional validation keywords to include [array] [default: []]
--include Further limit tsconfig to include only matching files [array] [default: []]
--ignoreErrors Generate even if the program has errors. [boolean] [default: false]
--excludePrivate Exclude private members from the schema [boolean] [default: false]
--uniqueNames Use unique names for type symbols. [boolean] [default: false]
--rejectDateType Rejects Date fields in type definitions. [boolean] [default: false]
--id Set schema id. [string] [default: ""]
複製程式碼
github 上也有所有型別轉換的 測試用例,可以對比看看 typescript
和 轉換出的 json-schema
結果
json-schema 校驗庫
利用 typescript-json-schema
工具生成了 json-schema
檔案後,我們需要根據該檔案進行資料校驗。
json-schema
資料校驗的庫很多,ajv,jsonschema 之類的,這裡用 jsonschema
作為示例。
import { Validator } from 'jsonschema'
import schema from './json-schema.json'
const v = new Validator()
// 繫結schema,這裡的 `api` 對應 json-schema.json 的 `$id`
v.addSchema(schema, '/api')
const validateResponseData = (data: any) => {
// 校驗響應資料
const result = v.validate(data, {
// SomeInterface 為 ts 定義的介面
$ref: `api#/definitions/SomeInterface`
})
// 校驗失敗,資料不符合預期
if (!result.valid) {
console.log('data is ', data)
console.log('errors', result.errors.map((item) => item.toString()))
}
return data
}
複製程式碼
當我們校驗以下資料時:
// 宣告檔案
interface UserInfo {
name: string
sex: string
age: number
phone?: number
}
// 校驗結果
validateResponseData({
name: 'xxxx',
age: 'age應該是數字'
})
// 得出結果
// data is { name: 'xxxx', age: 'age應該是數字' }
// errors [ 'instance.age is not of a type(s) number',
// 'instance requires property "sex"' ]
複製程式碼
配合上前端上報系統,當線上系統介面返回了非預料的資料,導致出 bug,就可以實時知道到底錯在哪了,並且及時甩鍋給後端啦。
commit 時自動更新 json-schema
前面提到,我們需要執行 typescript-json-schema <path-to-typescript-files-or-tsconfig> <type>
命令來宣告 typescript 對應的 json-schema
檔案。
那麼,這裡就有個問題,介面數量有可能增加,介面資料也有可能變動,那也就代表著,我們每次變更介面資料結構,都要重新跑一下 typescript-json-schema
,時刻保持 json-schema
和 typescript一一對應。
這我們就可以用 husky 的 precommit
, 加上 lint-staged 來實現每次更新提交程式碼時,自動執行 typescript-json-schema
,無需時刻關注 typescript 介面定義的變更。
總結
綜上,我們實現了
typescript
宣告檔案 轉換生成json-schema
檔案- 程式碼介面層攔截校驗資料,如校驗失敗,通過前端上報系統(如:sentry)進行相關上報
- 通過
husky
+lint-staged
每次提交程式碼自動執行 步驟1,保持git 倉庫的程式碼typescript
宣告 和json-schema
時刻保持一致。
那麼,當 Bug 出現的時候,你甚至可以在測試都還沒發現這個 Bug之前,就已經把鍋甩了出去。
只要你跑得足夠快,Bug 就會追不上你。