你們出來寫 Bug 代碼的,不免會出 Bug。git
文章背景就發生在一個 Bug 身上,github
有一天,測試慌張中帶着點興奮衝過來: 測試:"xxx系統前端線上出 Bug 了,點進xx頁面一片空白啊"。 我:"納尼?我寫的Bug怎麼會出現代碼呢?"。 typescript
雖然大腦一片空白,可是鍋仍是要背的。 進入頁面一看,哦豁,完蛋,cannot read the property 'xx' of undefined
。確實是前端常見的報錯呀。json
背鍋王,我當定了?後端
NO!api
我眉頭一皺,發現事情並非那麼簡單,通過一番猛如虎的操做以後,最終定位到問題是:後端接口響應的 JSON 數據中,一個嵌套比較深的字段沒有返回,即前端只讀到了 undefined
。bash
咱按章程辦事,後端提供的接口文檔指定了數據結構,那你沒有返回正確數據結構,這就是你後端的鍋,雖然嚴謹點前端也能捕獲到錯誤進行處理,但歸根到底,是你後端數據接口處理有問題,這鍋,我不背。數據結構
甩鍋又是一門扯皮的事情,殺敵一千自傷八百,鍋已經扣下來了,想甩出去就難咯,。ide
唉,要是在接口出錯的時候,能馬上知道接口數據出問題,先發制人,立刻把鍋甩出去那就好咯。
這就是本文即將要講述的 "Typescript 運行時數據校驗"。
衆所周知,Typescript
是 JavaScript
超集,能夠給咱們的項目代碼提供靜態類型檢查,避免由於各類緣由而未及時發現的代碼錯誤,在編譯時就能發現隱藏的代碼隱患,從而提升代碼質量。
可是,TypeScript
項目的一個常見問題是: 如何驗證來自外部源的數據並將驗證的數據與TypeScript類型聯繫起來。 即,如何避免後端 API 返回的數據與 Typescript
類型定義不一致致使的運行時錯誤。
Typescript
能用於運行時校驗數據類型,那麼有沒有一種方法,能讓咱們在 運行時 也進行 Typescript
數據類型校驗呢?
業界開源了一個運行時校驗的工具庫: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 數據格式的模式。
例如 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。
咱們能夠直接使用它的命令行工具,這裏就不仔細展開說明了,感興趣的能夠看下官方文檔:
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
結果
利用 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,就能夠實時知道到底錯在哪了,而且及時甩鍋給後端啦。
前面提到,咱們須要執行 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
文件husky
+ lint-staged
每次提交代碼自動執行 步驟1,保持git 倉庫的代碼 typescript
聲明 和 json-schema
時刻保持一致。那麼,當 Bug 出現的時候,你甚至能夠在測試都還沒發現這個 Bug以前,就已經把鍋甩了出去。
只要你跑得足夠快,Bug 就會追不上你。