科學甩鍋技術: Typescript 運行時數據校驗

本文首發於 github 博客
如文章對你有幫助,你的 star 是對我最大的支持前端

背景

你們出來寫 Bug 代碼的,不免會出 Bug。git

文章背景就發生在一個 Bug 身上,github

有一天,測試慌張中帶着點興奮衝過來: 測試:"xxx系統前端線上出 Bug 了,點進xx頁面一片空白啊"。 我:"納尼?我寫的Bug怎麼會出現代碼呢?"。 typescript

image

雖然大腦一片空白,可是鍋仍是要背的。 進入頁面一看,哦豁,完蛋,cannot read the property 'xx' of undefined。確實是前端常見的報錯呀。json

背鍋王,我當定了?後端

NO!api

我眉頭一皺,發現事情並非那麼簡單,通過一番猛如虎的操做以後,最終定位到問題是:後端接口響應的 JSON 數據中,一個嵌套比較深的字段沒有返回,即前端只讀到了 undefinedbash

咱按章程辦事,後端提供的接口文檔指定了數據結構,那你沒有返回正確數據結構,這就是你後端的鍋,雖然嚴謹點前端也能捕獲到錯誤進行處理,但歸根到底,是你後端數據接口處理有問題,這鍋,我不背。數據結構

甩鍋又是一門扯皮的事情,殺敵一千自傷八百,鍋已經扣下來了,想甩出去就難咯,。ide

唉,要是在接口出錯的時候,能馬上知道接口數據出問題,先發制人,立刻把鍋甩出去那就好咯。

這就是本文即將要講述的 "Typescript 運行時數據校驗"。

爲何要運行時校驗數據?

衆所周知,TypescriptJavaScript 超集,能夠給咱們的項目代碼提供靜態類型檢查,避免由於各類緣由而未及時發現的代碼錯誤,在編譯時就能發現隱藏的代碼隱患,從而提升代碼質量。

可是,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 有什麼區別。

可是,既然咱們同時知道 typescriptjson-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 數據校驗的庫不少,ajvjsonschema 之類的,這裏用 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"' ]
複製代碼

徹底例子請看 github

配合上前端上報系統,當線上系統接口返回了非預料的數據,致使出 bug,就能夠實時知道到底錯在哪了,而且及時甩鍋給後端啦。

commit 時自動更新 json-schema

前面提到,咱們須要執行 typescript-json-schema <path-to-typescript-files-or-tsconfig> <type> 命令來聲明 typescript 對應的 json-schema 文件。

那麼,這裏就有個問題,接口數量有可能增長,接口數據也有可能變更,那也就表明着,咱們每次變動接口數據結構,都要從新跑一下 typescript-json-schema ,時刻保持 json-schema 和 typescript一一對應。

這咱們就能夠用 huskyprecommit , 加上 lint-staged 來實現每次更新提交代碼時,自動執行 typescript-json-schema,無需時刻關注 typescript 接口定義的變動。

徹底例子請看 github

總結

綜上,咱們實現了

  1. typescript 聲明文件 轉換生成 json-schema 文件
  2. 代碼接口層攔截校驗數據,如校驗失敗,經過前端上報系統(如:sentry)進行相關上報
  3. 經過 husky + lint-staged 每次提交代碼自動執行 步驟1,保持git 倉庫的代碼 typescript 聲明 和 json-schema 時刻保持一致。

那麼,當 Bug 出現的時候,你甚至能夠在測試都還沒發現這個 Bug以前,就已經把鍋甩了出去。

只要你跑得足夠快,Bug 就會追不上你。

image
相關文章
相關標籤/搜索