1.3.13 • Published 4 years ago

jweb v1.3.13

Weekly downloads
1
License
MIT
Repository
github
Last release
4 years ago

jweb

A typeScript httpServer support annotation

1. Usage

npm install
npm run build
npm run start

2. Javascript和TypeScript基础学习

JavaScript基础学习(ES6标准):http://es6.ruanyifeng.com/#README

TypeScript基础学习:https://www.tslang.cn/docs/home.html

ejs模版引擎学习:https://ejs.co | https://ejs.bootcss.com/

JWeb Controller方法注解和Entity中校验注解

3. 1. Controller方法注解

3.1. 1.1 注解的调用顺序

先看一个样例(下面的方法都是某一个Controller类中的方法),process方法会在请求到来时被调用:

  private preAround (ret) {
    console.log('preAround', ret)
  }

  private postAround (ret) {
    console.log('postAround', ret)
  }  

  private beforeCall (ret) {
    console.log('beforeCall' , ret)
    return ret
  }

  public afterCall (ret) {
    console.log('afterCall', ret)
    if (ret.err) {
      return {
        status: -1,
        err: ret.err
      }
    } else {
      return ret.data
    }
  }  
  @Get('/process/{uid0}')
  @Auth
  @ResponseBody('json')
  @Validation(UserEntity)
  @Transactional
  public async process (req: Request, res: Response, { uid0 }) {
    const user: UserEntity = req.entity
    console.log('inside call', user)
    let u = await this.userService.hello(user)
    let data = {
      a: 1,
      b: [2, 3, 4],
      uid: uid0,
      u: u
    }
    return data
  }

在样例函数中,定义了preAround、postAround、beforeCall和afterCall,使用了5个注解,每个注解都有一个preCall和postCall的属性,这两个属性的值是一个函数,如下:

Auth.preCall = function authPreCall(ret: any, param: string, req: Request, res: Response) {
  if (param === 'ignore') {
    return {
      err: "ignore",
      data: null,
      from: Auth.name
    }
  }
  return ret
}

Auth.postCall = function authPostCall(ret: any) {
  return ret
}

注解的调用基于上述两处代码,调用的原则是:

  • 首先调用beforeCall
  • 然后按照注解使用从上向下的顺序调用preCall
  • 然后调用Controller类中的preAround
  • 然后调用被注解的方法,在上例中调用process方法
  • 然后调用Controller类中的postAround
  • 最后按照注解使用从下到上的顺序调用postCall

在上面的示例中,调用链是(Get方法只会在初始化的时候调用,用于注册路由,不会参与到此处的调用链中):

beforeCall => Auth.preCall => ResponseBody.preCall => ... => preAround=> process => postAround => Transanctionl.postCall => ... => Auth.postCall => afterCall

当然,没有为注解定义postCall或者preCall,那么跳过相关调用。

3.2. 1.2 注解中的错误处理

​ 每一个注解的统一返回格式是:

{
    err,
    data
}

前一个注解调用的返回结果会传递给后一个注解调用,例如:

Auth.preCall = function authPreCall(ret: any, param: string, req: Request, res: Response) {
  if (param === 'ignore') {
    return {
      err: "ignore",
      data: null
    }
  }
  return ret
}

上述preCall函数中的参数ret,是上一个调用的返回值,Auth.preCall的返回值也会传给下一个调用。如果没有return,会使用之前在调用链上传递的ret传给下一个调用

3.2.1. 如何抛出错误?

如果注解调用中发生了错误,有两种错误抛出方式:

  • 如果需要继续执行调用链:在返回值中设置err属性,该属性可以是字符串,也可以是对象,只要不为null,就表明错误发生。这意味着你可以定义自己的错误结构,方便后面的错误捕获和处理。
  • 如果需要中断调用链:直接在preCall或者postCall中return null,当检测到调用链中某一个调用的返回值是null时,不会继续执行调用链。注意:这时框架不会响应请求,如果你希望中断调用链,那么你必须使用函数参数中的Request和Response自定义HTTP响应,如设置状态码,设置响应头,设置响应体。例如,在Auth中如果验证失败,你可能会返回401状态码

如果路由函数中要抛出错误,需要throw BusinessException,BusinessException位于jbean包中。例如:

  @Get('/process/{uid0}')
  @Auth
  @ResponseBody('json')
  @Validation(UserEntity)
  @Transactional
  public async process (req: Request, res: Response, { uid0 }) {
    const user: UserEntity = req.entity
    let u = await this.userService.hello(user)

    throw new BusinessException('test Exception') // 在路由函数中抛出错误
    let data = {
      a: 1,
      b: [2, 3, 4],
      uid: uid0,
      u: u
    }
    return data
  }

3.2.2. 如何处理错误?

​ 如果是设置err的错误抛出方式,自定义的注解需要自行定义错误处理方式。通过参数中的上一个函数的返回值中的err属性,可以监听到前面的调用链中是否发生了错误。发生与未发生错误时如何处理完全由注解自行决定。如果调用链上的错误始终未被处理,则服务器会返回500错误

关于自定义注解的编写请参阅: 1.3 如何编写自定义注解?(#1.3 如何编写自定义注解?)

注意:jweb提供的内置注解,比如Validation,如果检测到错误,会直接return ret

如果中断调用链,必须自行处理响应。

3.3. 1.3 如何编写自定义函数注解?

3.3.1. 注解示例

先看一个自定义注解的例子:

import { AnnotationType, annotationHelper, BeanFactory } from 'jbean'
import { Request, Response } from 'jweb'
import { jsonEncode, xmlEncode } from '../../lib/utils'

export default function ResponseBody (component?: any, type?: any) {
  return annotationHelper(arguments, callback)
}

const callback = function (annoType: AnnotationType, ctor: object | Function) {
  if (annoType === AnnotationType.clz) {
    BeanFactory.addBeanMeta(AnnotationType.clz, ctor, null, ResponseBody, [arguments[2]])
  } else if (annoType === AnnotationType.method) {
    BeanFactory.addBeanMeta(AnnotationType.method, ctor, arguments[2], ResponseBody, [arguments[4]])
  }
}

ResponseBody.preCall = function rbdPreCall(ret: any, type: string, req: Request, res: Response) {
  switch (type) {
    case 'json':
      res.type('application/json')
      break
    case 'xml':
      res.type('application/xml')
      break
    default:
      break
  }
  console.log("response body precall", ret)
  return ret
}

ResponseBody.postCall = function rbdPostCall(ret: any, type: string, req: Request, res: Response) {
  console.log("jsonbody line 31", ret)
  switch (type) {
    case 'json':
      if (typeof ret === 'object') {
        ret.data = jsonEncode(ret.data)
      }
      break
    case 'xml':
      ret.data = xmlEncode(ret.data)
      break
    default:
      break
  }
  return ret
}

​ 上述代码定义了一个名为ResponseBody的注解,该注解的主要功能是,根据@ResponseBody('{type}')来决定返回的数据格式类型,type可选值是json或者xml。接下来我们讨论该注解的详细定义过程。

3.3.2. 示例的依赖说明

  • 首先,注解相关的处理都在jbean包中,这里我们从JBean中引入了AnnotationType、annotationHelper、BeanFactory。其中:

    • AnnotationType:用于定义注解的类型,jweb中支持的注解类型有三种,类、方法、域。分别对应AnnotationType.clz,AnnotationType.method,AnnotationType.field

    • annotationHelper:jbean提供的分析注解参数的函数,该函数接收两个参数,args和callback,args是参数;callback是回调函数,在annotationHelper执行完毕后调用。annotationHelper会根据args参数计算出注解类型,以及能够从typescript注解获取对应的构造函数/原型对象、键名、descriptor对象等,细节请参阅typescript注解。拿到这些数据后,annotationHelper会将其作为参数传入给callback。callback的详细参数列表请参阅callback参数列表

      annotationHelper统一处理TypeScript装饰器工厂和装饰器直接使用,比如Auth('ignore')和@Auth分别对应工厂调用和直接调用

    • BeanFactory:管理Bean、BeanMeta等信息的工厂类。如果我们希望我们定义的注解的preCall和postCall在请求到来时调用,需要将对应的BeanMeta注册到BeanFactory中

3.3.3. 注解的编写步骤

1. 导入依赖:
import { AnnotationType, annotationHelper, BeanFactory } from 'jbean'
import { Request, Response } from 'jweb'
import { jsonEncode, xmlEncode } from '../../lib/utils'

注意:jsonEncode,xmlEncode是自行编写格式处理工具,与jweb和jbean无关。

2. 定义注解:
export default function ResponseBody (component?: any, type?: any) {
  return annotationHelper(arguments, callback)
}

​ 其中ResponseBody是我们定义的注解名,当作为工厂调用时,即以@ResponseBody('参数1', '参数2')形式调用时,ResponseBody函数的参数就是工厂调用时传入的参数。否则其参数是typescript装饰器传入的参数。

​ 我们使用时,可直接传arguments,annotationHelper会帮助我们处理所有细节,并将统一的参数形式传到callback中。接下来我们详细解释callback的使用。

3. 定义callback方法:
const callback = function (annoType: AnnotationType, ctor: object | Function) {
  if (annoType === AnnotationType.clz) {
    BeanFactory.addBeanMeta(AnnotationType.clz, ctor, null, ResponseBody, [arguments[2]])
  } else if (annoType === AnnotationType.method) {
    BeanFactory.addBeanMeta(AnnotationType.method, ctor, arguments[2], ResponseBody, [arguments[4]])
  }
}
callback参数列表
const callback = function(annoType: AnnotationType, ctor: Function|object, field: string, descriptor: PropertyDescriptor, ...args)

参数说明:

  • annoType:注解的类型,取值为AnnotationType.clz,AnnotationType.method,AnnotationType.field,分别代表类,方法,域
  • ctor:构造函数或者原型对象
  • field:如果被注解的是方法或者域,则该值存在且为键名
  • descriptor:如果被注解的是方法,则该值存在且为该方法对应的描述符对象
  • ...args:注解工厂调用时传入的参数,例如@ResponseBody('json')中传入的‘json'

注意,如果你希望你的注解在被注解的函数调用时调用,请确保将其添加到BeanFactory的BeanMeta中,并定义响应的preCall、postCall函数。因为callback仅会在初始化时调用。

注册BeanMeta请参考下面的代码:

BeanFactory.addBeanMeta(AnnotationType.method, ctor, arguments[2], ResponseBody, [arguments[4]])
4. 定义preCall、postCall函数

​ preCall可以在路由函数被调用前调用,可以起到拦截作用,如果你的业务中需要拦截器功能的话,定义preCall就可以实现。postCall在路由函数被调用后调用,可以对数据进行加工。preCall、postCall的使用、参数请参考上述示例。

3.4. 1.4 使用注解实现完整业务功能的示例

一个完整的Controller示例代码如下:

import { Autowired } from 'jbean'
import { BaseController, Controller, Get, Post, Request, Response, Transactional, Validation } from 'jweb'
import UserService from '../lib/account/UserService'
import PayService from '../lib/account/PayService'
import Auth from '../annos/Auth'
import ResponseBody from '../annos/response_body'
import UserEntity from '../lib/account/entity/user'

@Controller('/user')
@Transactional
export default class User extends BaseController {

  @Autowired('userService0')
  private userService: UserService

  @Autowired
  private payService: PayService

  constructor () {
    super()
  }

  private beforeCall (ret) {
    return ret
  }

  public afterCall (ret) {
    console.log('afterCall', ret)
    if (ret.err) {
      return {
        status: -1,
        err: ret.err
      }
    } else {
      return ret.data
    }
  }

  @Get('/process/{uid0}')
  @Auth
  @ResponseBody('json')
  @Validation(UserEntity)
  @Transactional
  public async process (req: Request, res: Response, { uid0 }) {
    const user: UserEntity = req.entity
    console.log('inside call', user)
    let u = await this.userService.hello(user)
    let data = {
      a: 1,
      b: [2, 3, 4],
      uid: uid0,
      u: u
    }
    return data
  }
}

这里我们只关注和process方法有关的部分,Get('/process/{uid0}')注册了一个动态路由,其路由参数是uid0。然后会一次依据1.1 注解的调用顺序(#1.1 注解的调用顺序)调用注解和方法。注意,注解调用链的参数传递与process方法无关,process方法只关注自身的业务处理,所以这里没有返回{err,data,from}这种结构,也不会被传入ret,process方法直接返回data

在这个示例中,我们将错误的统一拦截放到了afterCall中:

public afterCall (ret) {
    console.log('afterCall', ret)
    if (ret.err) {
      return {
        status: -1,
        errmessage: ret.err
      }
    } else {
      return {
          status: 1
          data: ret.data
      }
    }
  }

我们获取前面传过来的ret,判断是否发生了错误,如果发生了错误,将业务定义的错误码以及错误信息返回。

如果成功,将成功码和获取到的数据返回。

4. 2. Entity校验注解

4.1. 2.1 使用示例

user.ts

import { Entity, Type } from 'jweb'
import {Required, Min, Max, Size} from 'jweb'

@Entity
export default class User {

  @Type('string')
  @Required("uid是必填的参数")
  public uid = undefined

  @Required
  @Size(20, 30, 'name的长度应该位于20-30之间')
  public name = undefined

  @Type('number', true)
  @Required("age is required")
  @Min(18)
  @Max(100)
  public age = undefined

}

代码中定义了一个User类,类上有@Entity注解,@Entity表明这是一个实体类,与某一张数据库表相对应,会自动根据类名生成表名,如上例中,对应user表。默认的表名规则是下划线分割驼峰式命名,例如:PersonLikeBeer会对应表person_like_beer。

可以通过给@Entity传参数可以自定义表名,例如:@Entity('user_log')表名当前的类对应user_log表。

注意到每一个字段都给了一个默认值undefined,这是因为如果不给默认值,typescript编译后不会生成对应的属性

可以通过给表中的字段添加验证注解,来进行参数的校验,在上例中:

  @Type('number', true)
  @Required("age is required")
  @Min(18)
  @Max(100)
  public age = undefined
  • @Type('number', true) 表明这个字段必须是数字,true表示运行进行转换以期望得到正确的类型。注意,一般情况下请将其设置成true,因为获取到的参数默认都是字符串,例如:可能表单提交的是21,但是获取的参数会是'21',这时候会导致验证不通过。但如果你不想其他类型转为字符串,可以不写或者传false
  • @Required("age is required")表明这个字段是必须的,括号里面的是验证不正确时的提示信息,如果不给定,会使用默认的提示信息
  • @Min(18)最小值18
  • @Max(100)最大值100

4.2. 2.2 注解的调用顺序

注解会按照使用顺序,从上到下调用,如果一个字段有多个验证规则,中间有一个规则不通过时,不会继续验证后面的规则,直接使用该注解的错误信息。

每一个注解验证后会返回一个值,这个值会传给下一个验证注解,最后的验证注解返回的值会赋值给对应的字段。

所有注解的最后一个参数都是验证出错时提供的消息,如果没有提供该参数,将会使用默认值。

4.3. 2.4 jweb内置注解和说明

4.3.1. Min

使用:@Min(minval:number,mes?: string)

规则:对应字段的值是否比@Min指定的值大

4.3.2. Max

使用:@Max(minval:number,mes?: string)

规则:对应字段的值是否比@Max指定的值小

4.3.3. Size

使用:@Size(minval:number,maxval?:number, mes?: string)

规则:对应字段的值是否在指定的区间中

4.3.4. Required

使用:@Requred(mes?: string)

规则:必须的字段

4.3.5. Type

使用:@Type(type?: string)

规则:字段的数据类型,可选值string、number、integer

4.4. 2.5 编写自己的验证器

4.4.1. 自定义验证器示例

import { AnnotationType, annotationHelper, BeanFactory } from 'jbean'

export default function Max(maxVal: number, mes?: string) {
  return annotationHelper([maxVal, mes], callback)
}
function validate(maxVal: number) {
  return (val):{valid: boolean, val: any} => {
    if (val <= maxVal) {
      return {valid: true, val: val}
    } else {
      return { valid: false, val: null}
    }
  }
}
function message(field: string, maxVal:number, mes?: string) {
  if (mes) {
    return () => mes
  } else {
    return () => `the value of ${field} must smaller than ${maxVal}`
  }
}
Max['validate'] = {}
const callback = function(annoType: AnnotationType, ctor: Function, field: string, maxVal:number, mes?: string) {
  // add descriptor info into BeanFactory, using it in Validation
  Max['validate'][field] = {
    validate: validate(maxVal),
    message: message(field, maxVal, mes)
  }
  BeanFactory.addBeanMeta(annoType, ctor, field, Max)
}

4.4.2. 示例关键代码说明

每一个验证器需要两个方法,一个是validate,用于验证是否满足规则,另一个是message,用于返回验证失败时的消息。请确保传给Max['validate'][field]的validate和message属性的值均是函数。validate和message返回的函数都会被传入一个参数——被验证的字段的值

我们将这两个方法与挂在到对应注解方法的validate属性上,在示例中是Max'validate',然后:

Max['validate'][field] = {
    validate: validate(maxVal),
    message: message(field, maxVal, mes)
  }

使用field是因为不同的域上可能有相同的注解,而验证规则可能是闭包相关的,所以这里为每一个域都绑定validate和message方法。

最后需要把注解注册到BeanFactory中,采用让其validate和message函数在验证时被调用。

4.4.3. 自定义验证器必须遵循的规则

  1. 确保验证器函数有如下属性,且validate和message必须是函数。

    Max['validate'][field] = {
        validate: validate(maxVal),
        message: message(field, maxVal, mes)
    }
  2. 在{ validate: validate(maxVal), message: message(field, maxVal, mes) }中,validate返回值的结构必须是下面的形式:

    {
        valid: boolean, // 表示验证是否成功,如果成功,其值为true,否则为false
        val: any // 验证之后应该返回的值,这个值会传给下一个验证器
    }

    val的值会传给下一个验证器,最后一个验证器的val值会赋值给实体,注意验证成功或者失败时返回的val值

    message返回值必须是一个字符串,用作验证失败时的提示信息。

  3. 确保验证器被注册到BeanFactory中,调用如下API注册

    BeanFactory.addBeanMeta(annoType, ctor, field, Max)
1.3.13

4 years ago

1.3.12

4 years ago

1.3.11

4 years ago

1.3.10

4 years ago

1.3.9

4 years ago

1.3.8

5 years ago

1.3.7

5 years ago

1.3.6

5 years ago

1.3.5

5 years ago

1.3.4

5 years ago

1.3.2

5 years ago

1.3.1

5 years ago

1.2.0

5 years ago

1.1.8

5 years ago

1.1.7

5 years ago

1.1.6

5 years ago

1.1.5

5 years ago

1.1.4

5 years ago

1.1.3

5 years ago

1.1.2

5 years ago

1.1.0

5 years ago

1.0.13

5 years ago

1.0.12

5 years ago

1.0.11

5 years ago

1.0.10

5 years ago

1.0.9

5 years ago

1.0.8

5 years ago

1.0.7

5 years ago

1.0.6

5 years ago

1.0.5

5 years ago

1.0.4

5 years ago

1.0.3

5 years ago

1.0.2

5 years ago

1.0.1

5 years ago

1.0.0

5 years ago

0.0.17

5 years ago

0.0.16

5 years ago

0.0.15

5 years ago

0.0.14

5 years ago

0.0.13

5 years ago

0.0.12

5 years ago

0.0.11

5 years ago

0.0.10

5 years ago

0.0.9

5 years ago

0.0.8

5 years ago

0.0.7

5 years ago

0.0.6

5 years ago

0.0.5

5 years ago

0.0.4

5 years ago

0.0.3

5 years ago

0.0.2

5 years ago

0.0.1

5 years ago