记录我的NestJS探究历程(一)

前言

我是今年公司需要做一个BFF Server开始使用NestJS进行开发的(虽然我早在18年就已经知道Nest的这个框架了,但是一直都没有使用它用来进行商业项目的开发),当初选择NestJS作为开发框架,主要出于2点考虑,一是基于TS,二是基于面向对象的编程方式(也横向对比过MidwayJs,但是考虑NestJS更加稳定就放弃了MidwayJs),过程中陆陆续续的爬了很多的坑,通过解决这些问题的过程也积累了很多经验,对于Nest也变的更加的喜爱。

后来我就想,可以将这些经验与大家分享,于是开始准备撰写这一系列的文章。

本文将会从源码的角度来探索NestJS的一些运行原理。此系列文章不适合初入Nodejs的同学(更不适合初入行前端的开发同学,需要先多看点儿基础知识夯实自己的技术体系才能理解到本系列文章的知识点),并且要求你对Java、C#等后端语言有了解,对软件设计中的耦合、设计模式,架构设计等高阶内容有一定认识和积累。

如果你读完这个系列的文章,或许能拓展您的知识面,好了,废话少说,就直接开始进入正题吧。

装饰器

装饰器,我个人对于它是又爱又恨,为什么爱呢,因为它从语法层面为AOP编程引入了简单的书写方式。这个语法在Java里面叫做Annotation,一般大家俗称它是注解,在C#里面叫做Attribute(我17年毕业之后再也没有用过它了,所以俗称我也不知道了)。

为什么又恨呢?对于JS(TS)来说,这个语法一直就没有稳定,在Vue3刚开始设计的时候 ,最初采用的是像Angular的那种思路,但是后来放弃了,尤大的回答是如果把一个框架设计建立在一个不确定的语法上,这个是一个相当大的风险,果然,尤大的眼光是超前的,装饰器的语法在2022年经历了一次大调整。

我是OOP的坚定支持者,所以我是非常喜欢装饰器这样的语法的,但是直到现在我仍然没有看过最新的装饰器的语法,除非它某一天完全稳定了,我才会重新学习装饰器的语法。

目前我看TS的官网介绍的装饰器的语法仍然是老语法,Nest目前使用的仍然是老的语法,我整个系列都将以TS的老语法进行阐述。

关于装饰器的原理,有兴趣的读者可以查看我早期的文章,从babel的编译结果来学习装饰器 - 掘金 (juejin.cn)

以下是从VSCode能看到的语法定义:

老语法定义

新语法定义

如果你对装饰器的新语法感兴趣的话,可以直接查看阮一峰老师的网络博客:装饰器 - ECMAScript 6入门 (ruanyifeng.com)

装饰器在实际开发中的用途

很多同学其实已经知道装饰器的语法,但是却不知道在实际开发中怎么使用,以下我将结合着实际的例子给大家讲解一些实用案例。

埋点

在曾探所著的《JavaScript设计模式》中关于装饰模式,他给的代码是这样的:

js 复制代码
/**
 * 增加前置执行的函数
 */
Function.prototype.log = function (fn) {
  const _this = this;
  return function wrapper() {
    fn.apply(this, arguments);
    return _this.apply(this, arguments);
  };
};

function onLoad() {
  console.log("我想处理一些业务逻辑");
}

// 不需要担心覆盖其它开发者增加的onload事件
window.onload =
  typeof window.onload === "function"
    ? window.onload.log(onLoad)
    : onLoad;

使用装饰器就可以这样写:

js 复制代码
// 定义埋点方法
function log(target, prop, descriptor) {
    const oldVal = descriptor.value;
    descriptor.value = function() {
        // 在事件处理器执行之前执行
        console.log('record something')
        return oldVal.apply(this, arguments);
    }
    return descriptor;
}

// 业务代码
class Activity {

    @log
    handleClick() {
        console.log('handle user click');
    }
}

同理,上述的思路也是可以进行防抖或节流的操作,此处不再赘述,有问题的读者可以私信我。

强制绑定this

这个操作不是必须的,因为可以使用箭头函数也能达到效果

ts 复制代码
function Bind(
  target: any,
  prop: string,
  descriptor: PropertyDescriptor,
): PropertyDescriptor {
  const originalMethod = descriptor.value;
  const adjustedDescriptor: PropertyDescriptor = {
    configurable: true,
    get() {
      const bindFn = originalMethod.bind(this);
      return bindFn;
    },
  };
  return adjustedDescriptor;
}

class AppService {
    @Bind
    getHelloWorld() {
    }
}

这个写法其实也就跟React早期的class组件需要这样写是同样的意思:

ts 复制代码
import React from 'react';

class Button extends React.Component{
    constructor(props){
        super(props);
        this.handleClick = this.handleClick.bind(this)
    }
    
    handleClick() {
        console.log()
    }
}

追加元数据

这个也是Nest的框架暴露那么多装饰器给我们的目的,使用了这些预制的装饰器才能够知道类与类之间的关系。

ts 复制代码
export const RequestMapping = (
  metadata: RequestMappingMetadata = defaultMetadata,
): MethodDecorator => {
  const pathMetadata = metadata[PATH_METADATA];
  const path = pathMetadata && pathMetadata.length ? pathMetadata : '/';
  const requestMethod = metadata[METHOD_METADATA] || RequestMethod.GET;
  return (
    target: object,
    key: string | symbol,
    descriptor: TypedPropertyDescriptor<any>,
  ) => {
    // 定义控制器上的某个方法将来需要处理的某个路由路径
    Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
    // 定义某个控制器上的某个方法奖励需要处理的http的请求方式。
    Reflect.defineMetadata(METHOD_METADATA, requestMethod, descriptor.value);
    return descriptor;
  };
};

const createMappingDecorator =
  (method: RequestMethod) =>
  (path?: string | string[]): MethodDecorator => {
    return RequestMapping({
      [PATH_METADATA]: path,
      [METHOD_METADATA]: method,
    });
  };

export const Post = createMappingDecorator(RequestMethod.POST);

装饰器其实就是装饰模式的语法糖而已,总之一句简单的话就可以概括装饰器的使用场景,增强或削弱类的方法的行为的时候即可以使用装饰器

最后额外插一嘴,既然Nest提供的预制装饰器就是在为类绑定与数据,那么,如果想应用几个装饰器,那其实就有点儿像管道或者中间件的那种思路一样,全部拿过来挨着执行一遍就统统的应用上元数据了嘛。

所以它预制的applyDecorators装饰器的实现是非常简单的:

ts 复制代码
export function applyDecorators(
  ...decorators: Array<ClassDecorator | MethodDecorator | PropertyDecorator>
) {
  return <TFunction extends Function, Y>(
    target: TFunction | object,
    propertyKey?: string | symbol,
    descriptor?: TypedPropertyDescriptor<Y>,
  ) => {
    for (const decorator of decorators) {
      if (target instanceof Function && !descriptor) {
        (decorator as ClassDecorator)(target);
        continue;
      }
      (decorator as MethodDecorator | PropertyDecorator)(
        target,
        propertyKey,
        descriptor,
      );
    }
  };
}

TS中的元编程

首先我们先抛出一个疑问,为什么Nest框架在依赖注入的能够知道注入我们需要的类的实例?我们想象一下,如果用JS来实现的话,是不是总得有个Key-Value这样的结构映射类和实例的关系,既然这个基本点是无法脱离的,那么就可以肯定一个点,TS一定存在某些关键的信息实现了这个Key-Value的结构,盲猜一下,可能是使用类型作为Key。

事实确实如此的,接下来就向大家阐述TS的这个黑魔法。

在这之前,还得先提一嘴TS元编程的神器,这个库叫做reflect-metadata

这个库在TS的官方文档的Demo上给的示例就提到了它,它主要是扩展了ES6Reflect的能力,使得我们能够给类或者类的方法定义元数据,我在前2年研究过它的源码,其内部是使用ES6提供的Map结构实现的。(这也是为什么ES6要引入Map结构的原因,因为Object的Key只能用String或者Symbol,假设你用其它类型作Key,它直接给你toPrimitive转成了String,比如对象类型就是"[object Object]",这也是某些面试题的考点,得不到我们预期的结构

要想查看TS的编译元数据,需要有装饰器,并且还要开启:

ts 复制代码
{
    "compilerOptions": {
        "experimentalDecorators": true
    }
}

不过这个一般TS的项目都是开启的,可能我们也没有留意过它,接下来看一下源码和编译结果对比。

以下是一个Demo:

ts 复制代码
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  private appService: AppService;

  constructor(appService: AppService) {
    this.appService = appService;
  }

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

编译结果:

js 复制代码
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, 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);
    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;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppController = void 0;
const common_1 = require("@nestjs/common");
const app_service_1 = require("./app.service");
let AppController = class AppController {
    constructor(appService) {
        this.appService = appService;
    }
    getHello() {
        return this.appService.getHello();
    }
};
__decorate([
    (0, common_1.Get)(),
    // 关键,TS注入了编译元信息
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", String)
], AppController.prototype, "getHello", null);
AppController = __decorate([
    (0, common_1.Controller)(),
    // 关键,TS注入了编译元信息
    __metadata("design:paramtypes", [app_service_1.AppService])
], AppController);
exports.AppController = AppController;
//# sourceMappingURL=app.controller.js.map

这些元数据将会是将来Nest框架在运行的时候解析依赖关系的核心线索,现在大家可以先有个印象即可。

另外,这儿有一篇我曾经参考过的文章,也是难得的可以供大家参考的优质好文:TypeScript 中的元数据以及 reflect-metadata 实现原理分析 - 知乎 (zhihu.com)

考虑到篇幅和大家都阅读难度,本文暂时先阐述这么多内容,第二篇将和大家讲述一些设计模式和软件设计方面的知识点,敬请期待。

相关推荐
CaptainDrake7 分钟前
包管理工具
npm·node.js
谢尔登14 分钟前
Webpack 和 Vite 的区别
前端·webpack·node.js
谢尔登15 分钟前
【Webpack】Tree Shaking
前端·webpack·node.js
纳尼亚awsl1 小时前
无限滚动组件封装(vue+vant)
前端·javascript·vue.js
八了个戒1 小时前
【TypeScript入坑】TypeScript 的复杂类型「Interface 接口、class类、Enum枚举、Generics泛型、类型断言」
开发语言·前端·javascript·面试·typescript
蓝莓味柯基1 小时前
React——点击事件函数调用问题
前端·javascript·react.js
一嘴一个橘子1 小时前
js 将二进制文件流,下载为excel文件
javascript
Sam90291 小时前
【Webpack--013】SourceMap源码映射设置
前端·webpack·node.js
Jinuss2 小时前
npm的作用域介绍
npm·node.js
小兔崽子去哪了2 小时前
Element plus 图片手动上传与回显
前端·javascript·vue.js