Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

掌握甩锅技术: Typescript 运行时数据校验 #13

Open
SunshowerC opened this issue Apr 17, 2019 · 11 comments
Open

掌握甩锅技术: Typescript 运行时数据校验 #13

SunshowerC opened this issue Apr 17, 2019 · 11 comments

Comments

@SunshowerC
Copy link
Owner

SunshowerC commented Apr 17, 2019

背景

大家出来写 Bug 代码的,难免会出 Bug。

文章背景就发生在一个 Bug 身上,

有一天,测试慌张中带着点兴奋冲过来:
测试:"xxx系统前端线上出 Bug 了,点进xx页面一片空白啊"。
我:"纳尼?我写的Bug怎么会出现代码呢?"。
image

虽然大脑一片空白,但是锅还是要背的。
进入页面一看,哦豁,完蛋,cannot read the property 'xx' of undefined。确实是前端常见的报错呀。

背锅王,我当定了?未必。

我眉头一皱,发现事情并不是那么简单,经过一番猛如虎的操作之后,最终定位到问题是:后端接口响应的 JSON 数据中,一个嵌套比较深的字段没有返回,即前端只读到了 undefined

咱按章程办事,后端提供的接口文档指定了数据结构,那你没有返回正确数据结构,这就是你后端的锅,虽然严谨点前端也能捕获到错误进行处理,但归根到底,是你后端数据接口处理有问题,这锅,我不背。

甩锅又是一门扯皮的事情,杀敌一千自伤八百,锅已经扣下来了,想甩出去就难咯,。

唉,要是在接口出错的时候,能立刻知道接口数据出问题,先发制人,马上把锅甩出去那就好咯。

这就是本文即将要讲述的 "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

@taweiyuxlzj
Copy link

6666666666666

@shenghou
Copy link

json schema + swagger貌似还不错

@yeongjet
Copy link

class-validator会不会是一个更好的解决方案? class-validator,json schema,io-ts这三者有什么优缺点

@SunshowerC
Copy link
Owner Author

class-validator会不会是一个更好的解决方案? class-validator,json schema,io-ts这三者有什么优缺点

class-validator 要多写很多 class-validator 装饰器。
如果想要直接写 typescript 就能校验,只好将 typescript 声明 转换成 json-schema 来实现运行时校验数据。

@yeongjet
Copy link

yeongjet commented Nov 6, 2019

如果要限制一个数的范围,该怎么实现呢,像joi可以这样: Joi.number().min(1).max(10)

@Hydraz320
Copy link

image
作者你好,请问你是在哪里找到这种参数形式的?自动生成的 schema 都是 definition 的,用你这种写法会容易很多,但文档里没看到,源码也么得

@yenkos
Copy link

yenkos commented Jan 5, 2021

这文笔爱了爱了,什么时候出书,必买一本

@xujiahao
Copy link

及时雨呀~

@dodio
Copy link

dodio commented Jun 4, 2021

如果要限制一个数的范围,该怎么实现呢,像joi可以这样: Joi.number().min(1).max(10)

zod.js

@dqm07
Copy link

dqm07 commented Oct 11, 2022

从内容到形式,我觉得可以做朋友了

@cfj1996
Copy link

cfj1996 commented Jan 30, 2023

直接用grpc

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants