umajs剖析

189 阅读9分钟

一、ts装饰器简介

  • ts官方

随着TypeScript和ES6里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。 装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式

  • 两种方式
// 构造器函数类型
type ConstructorType = {
   new (...args: any[]): any
}

type ClassDecoratorType = <T extends ConstructorType>(tarClass:T) => T | void

// 1. 普通方式 
function normalDecorator<T extends ConstructorType>(tarClass:T): T|void {
      return class extends tarClass {}
}

// 2. 工厂模式
function factoryDecorator(param:any):ClassDecoratorType {
   return (tarClass) => {
     tarClass.selfAttr = 1
     tarClass.prototype.selfAttr = 1
   }
}
  • 从类型声明来看

参考lib.es5.d.ts

// 属性描述对象类型
interface TypedPropertyDescriptor<T> {
    enumerable?: boolean;
    configurable?: boolean;
    writable?: boolean;
    value?: T;
    get?: () => T;
    set?: (value: T) => void;
}

/**
* 类装饰器
* params:(目标类)
* return: typeof 目标类 , 一般是子类
*/
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

/**
* 属性装饰器
* params: 非静态:(目标类原型,属性名) 
*         静态 (目标类,属性名)
* return: void
*/
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;

/**
* 方法装饰器
* params: 非静态:(目标类原型,方法名,描述对象) 
*         静态 (目标类,方法名,描述对象)
* return: void | 新的描述对象
* 备注: 如果有返回新的描述对象targetinfo,会在最后执行
* Object.defineProperty(target,propertyKey,targetinfo)
* 对方法重新定义
*/
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;

/**
* 方法参数装饰器
* params: 非静态:(目标类原型,方法名,参数索引) 
*         静态 (目标类,方法名,参数索引)
*         构造函数 (目标类,参数名,参数索引)
* return: void
*/
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

/**
* 访问器装饰器 
* 同方法装饰器,只不过get和set不能同时设置
*/

  • 编译结果

装饰器被编译成,其实是对类所对应函数进行重写,通过IIFE模式,执行装饰器函数


var Order = /** @class */ (function () {
    function Order(str) {
        this.a1 = new classB();
    }
    __decorate([
        // 装饰器工厂函数
        createPropertyDecorator('normal'),
        // 自带的元数据key [design:type]
        __metadata("design:type", classB)
    ], Order.prototype, "a1", void 0);
    return Order;
}());

==tips : 打开vscode,看看编译后的源码==

  • 执行顺序:

    1. 同一处多个装饰器:从下到上,从右到左
    2. 不同类型装饰器
      1. normal PropertyDecorator
      2. normal ParameterDecorator
      3. normal MethodDecorator
      4. static PropertyDecorator
      5. static ParameterDecorator
      6. static MethodDecorator
      7. constructor ParameterDecorator
      8. class classDecorator 3. tips: 至于get,set装饰器,如果在方法前面,那就排在该方法的参数装饰器和方法装饰器前,如果在后面,那就排在方法装饰器后面;static修饰时同理
  • 一个小发现

实践测试: 关于属性装饰器, 不接收第三个参数(属性描述对象),但是可以返回新的描述对象,对属性重新定义

根据编译后的源码解释一下:

1.装饰的属性没有声明赋值的情况下:

/*
*   (1) 函数中没有this.name,也就是类函数中没有name属性  
*   (2) 在原型上定义了同名属性
*   (3) 最终结果就是,类中定义的属性(没赋值的情况下),编译*         结果相当于没定义,直接访问原型上的属性了
*/ 
// 编译前
function Test(proto, key): any {
  return {
    get() {
      console.log(this instanceof A) // true
      return 'get name';
    },
  };
}

export default class A {
  @Test
  name: string ; // 没赋值
  age: any = 12;
}

// 编译后的js
var __decorate = (decorators,target,key,desc)=>{
  // 执行Test装饰器,返回属性描述对象
  desc = decorators[i](target,key)。 
  // 原型上定义属性
  Object.defineProperty(target, key, desc) 
 
}
function Test(proto, key) {
    return {
        get: function () {
            console.log(this instanceof A);
            return 'get name';
        },
    };
}
var A = /** @class */ (function () {
    function A() {
        this.age = 12; // 
    }
    __decorate([
        Test,
        __metadata("design:type", String)
    ], A.prototype, "name", void 0);
    return A;
}());
  1. 类属性有赋值的情况
/*
* (1) 类属性的赋值,会被编译为this.name = xx ,存在于类对
*      象上,访问时,覆盖原型
* (2) 类属性的赋值,执行要在装饰器执行的后面
*/
// 编译前
function Test(proto, key): any {
  return {
    value: 'decorator name',
    writable: true
  };
}

export default class A {
  @Test
  name: string = 'class name' ; // 没赋值
  age: any = 12;
}

new A().name // 'class name'
// 编译后
var A = /** @class */ (function () {
    function A() {
        //执行在装饰器函数的后面
        this.name = 'class name' 
        this.age = 12;  
    }
    __decorate([
        Test,
        __metadata("design:type", String)
    ], A.prototype, "name", void 0);
    return A;
}());

而官方:

属性描述符不会做为参数传入属性装饰器,这与TypeScript是如何初始化属性装饰器的有关。 因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。==返回值也会被忽略==。因此,属性描述符只能用来监视类中是否声明了某个名字的属性

  • 元数据meta-data
  // 装饰器编译后的源码里出现了
 var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};

  __metadata("design:type", ClassB),
  __metadata("design:paramtypes", [String]),
  __metadata("design:returntype", Function)
  1. 使用元数据,需要npm i reflect-metadata --save
  2. api:
    • Reflect.defineMetadata(k,v,target,prop?)
    • Reflect.getMetadata(k,v,target,prop?)
    • Reflect.getOwnMetadata(k,v,target,prop?)
    • Reflect.hasMetadata(k,target,prop?)
    • Reflect.hasOwnMetadata(k,target,prop?)
    • Reflect.getMetadataKeys(target,prop?)
    • Reflect.deleteMetadata(k,target,prop?)

理解:

const map = new Map()
map.set( [target?.prop] , {k:v} )
  1. 装饰器方式
function Service(){ 
  return (target,key)=>{
    const typeClass = Reflect.getMetadata('design:type')
    const instance = new typeClass() // new classA()
    return {
      get(){
         return instance
      }
    }
  }
}

@Reflect.metadata(k,v)
// 相当于Reflect.defineMetadata(k, v, Test)
class Test{
   @Service()
   @Reflect.metadata(k,v) 
   // 相当于Reflect.defineMetadata(k,v,Test.prototype,'a')
   a: classA
}
  1. 内置的元数据
    • design: paramtypes // 所有参数所属类,组成的数组
    • design: type // 类属性或方法所属类
    • design: returntype // 类方法返回值所属类

二、Umajs中的装饰器

一些过场话: 开发北斗时看到一些装饰器的应用,好奇底层实现~

2.1 两种设计模式

  • 依赖注入

链接:umajs.gitee.io/development…

  • 面向切片

链接:umajs.gitee.io/development…

2.2 装饰器的应用

2.2.1 Umajs_v2版本

  • 依赖注入

特点: 创建和使用分离

  1. @Resource @Inject

源码链接:github.com/Umajs/Umajs…

@Resource('name', 12)
class UserModel{
  constructor(public name:string, public age: number)
}

class TestController{
  @Inject(UserModel) //只能用于属性,赋值实例
  user: UserModel
}
  1. @Service

tip: 其实按照概念上的解释,其实并没有第三方收集所有依赖的容器,只是创建和使用分离

源码链接:github.com/Umajs/Umajs…

-- 1.校验target instanceof BaseController
-- 2.校验desc是否为undefined,属性装饰器没有第三个参数
     (typeHelper.isUndef 自定义守卫)
class UserService extends BaseService{}

class TestController extends BaseController{
  @Service(UserService)
  user:UserService
}

另一种方式

function Service(){
  return (target, key ) =>{
     // 元数据获取类
     cosnt Service  = Reflect.getMetadata('design: type',target, key)
     return {
       get(){return Reflect.construct(Service,[this.ctx])}
     }
  }
}

==先预留一个问题, this.ctx哪里来?? (后面讲)==


额外说下ts的类型守卫

/**
* ts自带的类型守卫
*   typeof ,in instanceof , == === != !==  obj.xx存在
* 会自动缩小类型范围
*/

/**
* 自定义类型守卫, 用于人为断言变量类型 var is type
*/
type A = {a:string}
type B ={a: number,b:number}
function isA(o:any): o is A {return !!o.a}

function test(str: number | A | B){
    if(typeof str === 'number'){
       str.toFixed() // 判断为number类型了
    }else if(isA(str)) {
      str.a.charAt(0)  
      // 虽然判断条件有问题
      // 只要进入这个代码块,就判断为A类型
    }else {
      str.b // 判断为B类型了Ï
    }
}


  • 面向切片
  1. @Aroud 环绕通知

源码链接:github.com/Umajs/Umajs…

// 两个类型
interface IJoinPoint {
  target: Object; // controller 实例
  args: Array<any>; // 方法参数
}

interface IProceedJoinPoint extends IJoinPoint {
  proceed: Function; // 原方法,Around 专属
}


async function AroundAspect( proceedPoint: IProceedJoinPoint) {
     const {target , args , proceed} = proceedPoint
    // 执行around before方法,即在被修饰方法执行前操作,当做一些前置校验的时候如果不满足可以直接return
    console.log('this is around');

    // 执行被修饰的方法
    const result = await proceed(); // 调用原方法
    
    // 执行around after方法,即在被修饰方法执行后操作
    console.log('this is around after');

    return result;
}


export default class Index extends BaseController {
    // 为index方法添加around修饰,method为对应要执行的around通知方法
    @Around(AroundAspect)
    index() {
      return Result.send('ok');
    }
}

// Around 源码

function Around(aroundAspect) {
   return funciton aroundDecorator(target, k, desc){
      if(!k){
         // 装饰的类
         Relfect.ownKeys(target.prototype).forEach(method=>{
             const methodDesc = Reflect.getOwnPropertyDescriptor(target.prototype, method)
             // 递归,对每个方法进行装饰处理,获取新的描述对象
             const newMethodDesc = aroundDecorator(target.prototype,method,methodDesc)
             // 重新定义方法
             Reflect.defineProperty(target.prototype, method, newMethodDesc);
         })
      }
      // 装饰的方法
      // 取到原方法的值method
      const { value: method, configurable, enumerable } = desc;
      return {
        ...desc,
        writable:true,
        value: (...args) => {
            // 执行原方法的proceed
           const proceed=(...args1)=> Reflect.apply(method,this,args1.lenghth? args1: args)
           return Reflect.apply(aroundAspect,this,[{
                target: this, // 当前实例对象
                args,
                proceed
             }])
        }
      }
   }
}

  1. @Middleware

==跳到底下,这里,先简单讲koa2中间件及原理==

源码链接:github.com/Umajs/Umajs…

@Middlerware(middleware) 作用,在被装饰的controller方法调用前,执行middleware中间件,而被装饰的方法的执行,被包装成一个新方法proceed,被传给中间件middleware(ctx,next)的next形参;

本质是路由中间件里,匹配出路由对应的controller和method,调用前,执行这个middlerware, ctx就是koa中间件的ctx或扩展

const middleware = async (ctx,next)=>{
    console.log("****** middleware before ******");
    // 校验登录
    await next();
    console.log("****** middleware after *******");
}

export default class Index extends BaseController {
    @Middleware(middleware)
    @Path('/')
    index() {
        return this.view('index.html', {
            frameName: this.testService.returnFrameName(),
        });
    }

// Middleware源码实现 
function Middleware(middleware){
  return Around(middlewareToAspect(middleware))
}

function middlewareToAspect(middleware){
  return (proceedPoint: IProceedJoinPoint) => {
     const {target , proceed ,args} = proceedPoint
     // 获取ctx, 自定义next = () => await proceed()
     middleware(target.ctx, async()=>{
        await proceed(...args)
     })
     
  }
}

// 另一种思路 Around(proceedPoint, middleware)
// 相当于闭包传参

2.2.2 路由相关装饰器,及路由底层实现

源码思路流程图:

![源码思路流程图]

image.png

==一个重要问题,所有的controller类何时加载的? ( 因为加载了@Path相关等装饰器才会执行,才会收集controller信息到info/controllersInfo)==

  • @Path

源码链接:github.com/Umajs/Umajs…

import {BaseController, Path} from 'Umajs/core'

@Path('/bam')
class BamController extends BaseController {
   @Path({value:'/getList', method: RequestMethod.POST})
   getList(@Query('q' q: string)){
       
   }
}
  • 参数装饰器
    1. @Param
    2. @Query
    3. @Body

源码链接:github.com/Umajs/Umajs…

1).@Query装饰器,

class TestController extends BaseController{
    async delete(
      @Query('q') q: string,
    ){
        // 相当于路由匹配时调用方法时,直接传入的delete(ctx.query.q)
        // 无需 const q = this.ctx.query.q去获取
    }
}

2.2.3 Umajs_v1中的区别

  • 再讲切片其他通知
  1. @Aspect
  2. @Aspect.before
  3. @Aspect.around
  4. @Aspect.after
  5. @Aspect.afterThrowing
  6. @Aspect.afterReturning

源码链接:github.com/Umajs/Umajs…

  • 通过文件名注册实例
import BamService form '../service/bam.service'
class Controller extends BaseController {
   @Service('bam')
   bamService: BamService
}

function Service(service: stirng){
  //根据传入的文件名前缀(类名),去获取该文件所导出的serviceClass
  const serviceClass = ServiceLoader.getService(service)
}

具体流程和部分源码:

// core/uma.ts实例化
class Uma {
   async start(){  await this.prepare() }
   async prepare(){ await this.load() }
   private async load(){ await this.loadService() }
   loadService(){
      ServiceLoader.loadServiceDir(path.resolve(this.options.ROOT, 'service'));
   }
}

// ServiceLoader类
const ServiceMap: Map<string, Function> = new Map();
class ServiceLoader{
  static loadServiceDir(){
     // 递归遍历跟路径下的所有文件,对xx.service.ts文件,进行处理
     const filePath = Path.resolve('xx.service.ts')
     loadService(filePath)
  }
  static loadService(filePath){
      // 动态导入获取文件的类
      const clazz = Require.default(filePath) 
      // 获取类名,也就是前缀xx
      const className = filePath.split('.')[0] 
      // 存入map
      ServiceMap.set(className, clazz)
  }
  static getService(serviceName){
    return ServiceMap.get(serviceName)
  }
}

2.2.4 express和koa2中间件原理简述

  • express
const http = require('http')
const slice = Array.prototype.slice

class LikeExpress {
    constructor() {
        // 存放中间件的列表
        this.routes = {
            all: [],   // app.use(...)
            get: [],   // app.get(...)
            post: []   // app.post(...)
        }
    }
    register(path) {
        const info = {}
        if (typeof path === 'string') {
            info.path = path
            info.stack = slice.call(arguments, 1)
        } else {
            info.path = '/'
            info.stack = slice.call(arguments, 0)
        }
        return info
    }
    use() {
        const info = this.register.apply(this, arguments)
        this.routes.all.push(info)
    }
    get() {
        const info = this.register.apply(this, arguments)
        this.routes.get.push(info)
    }
    post() {
        const info = this.register.apply(this, arguments)
        this.routes.post.push(info)
    }
    match(method, url) {
        let stack = []
        if (url === '/favicon.ico') return stack
        let curRoutes = []
        // 这里的执行顺序和写的顺序不一致,但请忽略,不是此次的重点
        curRoutes = curRoutes.concat(this.routes.all)
        curRoutes = curRoutes.concat(this.routes[method])
        curRoutes.forEach(routeInfo => {
            if (url.indexOf(routeInfo.path) === 0) {
               stack =stack.concat(routeInfo.stack)//扁平化
            }
        })
        return stack
    }
    // 核心的 next 机制
    handle(req, res, stack) {
        const next = () => {
            const middleware = stack.shift()
            middleware && middleware(req, res, next)
        }
        next()
    }
    callback() {
        return (req, res) => {
            const url = req.url
            const method = req.method.toLowerCase()
            const stack = this.match(method, url)
            this.handle(req, res, stack)
        }
    }
    listen(...args) {
        const server = http.createServer(this.callback())
        server.listen(...args)
    }
}

// 使用
const app = new LikeExpress()
app.use('/api',(req,res,next)=>{},(req,res,next)=>{})
app.listen(8080,()=>{})
  • koa2
const http = require('http')
class LikeKoa2 {
    constructor() {
        this.middlewareList = []
    }
    use(...fns) {
        this.middlewareList= this.middlewareList.concat(fns);
        return this
    }
    createContext(req, res) {
        const ctx = { req, res }
        ctx.query = req.query
        return ctx
    }
    // 核心的 next 机制
    handle(ctx) {
        const stack = this.middlewareList
        const next = async () => {
            const middleware = stack.shift()
            try { 
               await middleware && middleware(ctx, next)
            }catch(e){}
        }
        next()
    }
    callback() {
        return (req, res) => {
            const ctx = this.createContext(req, res)
            return this.handle(ctx)
        }
    }
    listen(...args) {
        const server = http.createServer(this.callback())
        server.listen(...args)
    }
}
// 使用
const app = new LikeKoa2()
app.use(async (ctx,next)=>{})
app.listen(80)