npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

jweb

v1.3.13

Published

A typeScript httpServer support annotation

Downloads

36

Readme

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)