科学甩锅技术: 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
相关文章
相关标签/搜索