ts装饰器
装饰器的最佳使用场景是面向切面编程,它可以在不修改代码自身的前提下,给已有代码增加额外的行为
面向切面编程(AOP) :运行时,动态将代码切入到类的指定方法,指定位置上的编程思想就是切面编程。比如在两个类里的每个方法做日志,按照面向对象的设计在一个独立的类里面定义,上面两个类分别调用,这样就产生了耦合。而切面编程则在解耦。 装饰器是一种特殊的声明,可附加在类、方法、访问器、属性、参数声明上。
装饰器使用 @log 的形式,称为注解型装饰器,其中 log 必须能够演算为在运行时调用的函数。
方法装饰器
方法装饰器有三个参数:
- target——当前对象
- property——方法名称
- descriptor——属性描述符
function log(target, property, descriptor) {
//target.property === descriptor.value
var oldValue = descriptor.value; //拿到老方法
//设置新方法
descriptor.value = function () {
console.log('打印日志', arguments);
return oldValue.apply(null, arguments);
}
return descriptor;
}
class Maths {
@log
add(a, b) {
console.log(a + b);
}
}
修改package.json配置
"scripts": {
"start": "ts-node-dev ./src/index.ts -P tsconfig.json --no-cache",
"build": "tsc -P tsconfig.json && node ./dist/index.js",
"tslint": "tslint --fix -p tsconfig.json"
},
npm start查看结果:
类装饰器
只有target 这一个参数,代表当前类
// 类装饰器
function anotationClass(target) {
console.log('===== Class Anotation =====')
console.log('target :', target)
}
// 方法装饰器
function anotationMethods (target, property, descriptor) {
// target
console.log('===== Method Anotation ' + property + "====")
console.log('target:', target)
console.log('property:', property)
console.log('descriptor:', descriptor)
}
@anotationClass
class Example {
@anotationMethods
instanceMember() { }
}
const example = new Example();
example.instanceMember()
查看结果:
并且方法装饰器先于类装饰器执行
装饰器原理
上面的方法装饰器 @anotationMethods 是语法糖
tsc index.ts
编译生成js文件
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
//2.给desc属性描述符赋值
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
//3.遍历取出注解方法
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
//5.给被注解的方法重新赋值,值为注解的方法
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var Example = /** @class */ (function () {
function Example() {
}
//4.执行注解方法
Example.prototype.instanceMember = function () { };
//1.执行__decorate方法
__decorate([
anotationMethods
], Example.prototype, "instanceMember");
return Example;
}());
var aaa = new Example();
aaa.instanceMember();
装饰器运行过程:
- 执行decorate方法
- Object.getOwnPropertyDescriptor(target.prototype,proterty) 给属性描述符赋值
- 取出注解方法
- 执行注解方法
- Object.defineProperty(target.prototype,proterty,descriptor) 给被注解的方法重新赋值,值为注解的方法
装饰器工厂
装饰器工厂就是一个简单的工厂函数,它返回一个装饰器函数,区别就在于调用装饰的时候传不传参数。如需要传参则升阶为装饰器工厂。
function color(value: string) { // 这是一个装饰器工厂
return function (target) { // 这是装饰器
// do something with "target" and "value"...
}
}
class Example {
@color('test')
method(){}
}
装饰器实现路由定义
- 创建路由 ./src/routes/user.ts
我们使用@get('/users'),@post('/users')实现装饰器路由
import * as Koa from 'koa'
const users = [{name: 'tom',age: 20}]
export default class User{
@get('/users')
public async list(ctx: Koa.Context){
ctx.body = {ok:1,data:users}
}
@post('/users')
public add(ctx: Koa.Context){
users.push(ctx.request.body);
ctx.body = {ok:1}
}
}
- 路由注册 ./utils/route-decors.ts @get('/users') 传入一个参数,所以我们先实现一个装饰器工厂函数
import * as koaRouter from 'koa-router'
type RouteOptions = {
/**
* 适用于某个请求比较特殊,需要单独制定前缀的情形
*/
prefix?: string;
}
const router = new koaRouter()
const get = (path: string,options?: RouteOptions) => (target,property,descriptor) => {
const url = options && options.prefix ? options.prefix + path : path
// router.get('/user',async ctx =>{})
router['get'](url,target[property])
}
const post = (path: string,options?: RouteOptions) => (target,property,descriptor) => {
const url = options && options.prefix ? options.prefix + path : path
// router.get('/user',async ctx =>{})
router['post'](url,target[property])
}
我们看到上面的get,post实际上市重复的代码,解决get post put delete方法公用逻辑 我们需要进一步对原有函数进行柯里化升阶
const method = (method) => (path: string,options?: RouteOptions) => (target,property,descriptor) => {
const url = options && options.prefix ? options.prefix + path : path
// router.get('/user',async ctx =>{})
router[method](url,target[property])
}
export const get = method('get')
export const post = method('post')
我们发现router变量 不符合函数式编程引用透明的特点,对后面移植不利,所以要再次进行柯里化升阶
const decorate = (router: koaRouter) => (method) => (path: string,options?: RouteOptions) => (target,property,descriptor) => {
const url = options && options.prefix ? options.prefix + path : path
// router.get('/user',async ctx =>{})
router[method](url,target[property])
}
const method = decorate(router)
export const get = method('get')
export const post = method('post')
自动扫描routes下文件进行自动注册
import * as glob from 'glob';
//参数是文件夹名称,返回类型是koaRouter
export const load = (folder: string): koaRouter => {
// 定义扩展名
const extname = '.{js,ts}'
// 同步扫描,文件夹下所有以某扩展名结尾的文件
glob.sync(require('path').join(folder,`./**/*${extname}`))
.forEach(item => require(item)) //引入所有类,执行装饰器,这时router挂满配置信息,返回router,调用时用的是这个挂有配置信息的router
return router
}
使用
/routes/user.ts
iport {get,post} from '../utils/route-decors'
index.ts
import * as Koa from 'koa'
import * as bodify from 'koa-body'
import { load } from './utils/decors';
import {resolve} from 'path'
app.use(bodify({
multipart: true,
// 使用非严格模式,解析delete请求的请求体
strict: false
})
)
const router = load(resolve(__dirname,'./routes'))
app.use(router.routes())
app.listen(3000,() => {
console.log('3000端口已启动')
})
装饰器实现类级别路由守卫
./routes/user.ts
@middlewares([async function guard(ctx: Koa.Context,next: () => Promise<any>){
console.log('guard',ctx.header);
if(ctx.header.token){
await next();
}else{
throw "请登录"
}
}])
export default class User{
}
我们写一个类装饰器
import {get,post,middlewares} from '../utils/route-decors'
export const middlewares = (middlewares) =>(target) => {
// 将中间件挂载在类原型上
target.prototype.middlewares = middlewares
}
修改decorate方法, 由于执行顺序是先执行方法装饰器在执行类装饰器,我们设置pocess.nextTick()改变执行顺序,让类装饰器先执行,下一秒在执行路由注册
const method = (router) => (method) => (path:string,options ?: RouteOptions) => (target,property) => {
// 晚一拍执行路由注册:因为需要等类装饰器执行完毕
process.nextTick(() => {
//添加中间件数组
const middlewares = [];
// 从类实例中取出中间件
if(target.middlewares){
middlewares.push(...target.middlewares)
}
//如果方法装饰器也传入中间件,执行该操作
if(options && options.middlewares){
middlewares.push(...options.middlewares)
}
middlewares.push(target[property])
const url = options && options.prefix ? options.prefix + path : path
router[method](url,...middlewares)
})
}
装饰器实现数据校验
./routes/user.ts
import {get,post,middlewares,query,body} from '../utils/route-decors'
export default class User{
@get('/users')
@query({
age: { type: 'int', required: false, max: 200, convertType: 'int' },
})
public async list(ctx: Koa.Context){
ctx.body = {ok:1,data:users}
}
}
./utils/router-decors.ts
//引入parameter库 数据校验
import * as Parameter from 'parameter'
const validator = (parameter) =>(params) => (rule) => (target,property,descriptor) => {
const oldValue = descriptor.value;
descriptor.value = function(){
// 拿到ctx
const ctx = arguments[0]
// 获取参数:1.ctx.request.body 2.ctx.request.query
const data = ctx.request[params]
// 规则和参数对应做校验
const error = parameter.validate(rule,data)
console.log(error)
if (error) throw new Error(JSON.stringify(error))
return oldValue.apply(null,arguments)
}
return descriptor
}
const parameter = new Parameter()
const validate = validator(parameter)
export const query = validate('query')
export const body = validate('body')