深入 Nestjs 底层概念(1):依赖注入和面向切面编程 AOP

前言

本文保证绝对大白话!简单易懂!

最近跟一个好友讨论,他说想学习 node.js 后端框架 nest.js,但对于 nest.js 的下面用法一头雾水,为什么要这么用呢?什么 Contoller, Provider,怎么跟一般的 javascript 代码的用法完全不一样呢?如下的 @Controller,@Injectable 你理解是什么意思吗?为什么要用这样的方式组织代码呢?

Javascript 复制代码
import { Controller, Get } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';


@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

基于此,我写了这篇文章,帮助大家从 0 开始,层层递进到所有这些让你陌生的 nest.js 的概念!

从编程范式说起

编程范式在之前面试经常被问到了,什么 FP(函数式编程),RP(响应式编程),FRP(函数式响应式编程),还有我们熟知的大兄弟 面相对象编程,现在 nest.js 又来个 AOP(面相切面编程),属实让人蒙圈了。。。。太多概念了。

别急,我们一个一个来!

函数式编程 - FP

简单来说就是要求你使用纯函数,什么是纯函数,就是输入一定的时候输出一定,就是传入相同的参数得到相同的结果。就这么简单,例如

javascript 复制代码
function add(a, b) {
    sum = a + b;
    return sum;
}

常见有几种情况会影响纯函数的纯净,最常见就是:

  • 修改传参的值,或者引用,或者全局状态,改 DOM,例如
javascript 复制代码
function add(a, b, c) { 
     // 假设 c 传入一个对象,c 的 xx 属性被修改
     c.xx = a + b; // 修改传参的值
     window.name = a; // 修改全局变量
    sum = a + b;
    return sum;
}
  • 做一些 i/o 操作,例如 console.log, 写文件。好了接下里说说 RP(响应式编程)

响应式编程 - RP

也能很好理解,我们举个例子:

就像"Excel 表格":

你改了 A1,B1 = A1 + 10 会自动更新,这就是典型的 RP。

在 vue 和 react 都非常常见,跟踪一个值变化,然后引发另一些值变化。

它强调:

  • 数据是"流"(stream)
  • 数据变化会自动传播到依赖它的逻辑(自动更新)
  • 你不需要手动触发

函数式响应式编程 - FRP

FRP 是"函数式编程 + 响应式编程"的结合。比较典型的就是 Rxjs 的风格:

javascript 复制代码
// 监听 DOM 的 input 事件
const input$ = fromEvent(searchInput, 'input'); // 输入是一个"流"

const results$ = input$.pipe(
  map(event => event.target.value),  // 函数式转换
);

results$.subscribe(renderResults);

简单理解就是当触发 DOMinput 事件触发的时候,也就是一个值改变的时候,触发 results$ 值的改变,这就是 RP(响应式编程),然后在 input 事件触发值改变的过程中,还执行了 map 方法,这是一个纯函数,所以也算 FP 函数式编程。

OOP - 面向对象编程

顾名思义,面相对象就是以对象为对象为单位,例如

javascript 复制代码
class Dog {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  bark() {
    console.log(`${this.name} is barking!`);
  }
}

使用到的数据,最好都封装为对象,如上面,使用的时候需要 new Dog() 返回我们新创造出来的实例,也就是 class 仅仅是个模板。对象是由模板创造出来的。

然后就是面向对象的特点,也就是最熟悉的三件套,封装,继承,多态。靠这三个,面向对象编程的思维模式认为可以模拟现实世界。

封装:很简单,例如 Dog 这个类,把跟 Dog 相关的属性和方法都放在一个类里就是封装,例如有 name 属性,狗的名字,你如果想加其它参数,继续拓展就行了,例如性别,品种。当然封装还有一些细节,例如封装私有属性什么的,这些细枝末节不用太在意。我们主要是简单介绍 OOP 的思想。

继承:可以将别的类的东西继承过来,例如, 很多动物都可以继承Animal 这个类:

javascript 复制代码
class Animal {
  constructor(public name: string) {}

  makeSound() {
    console.log("Some sound...");
  }
}

class Cat extends Animal {
  makeSound() {
    console.log(`${this.name} says meow~`);
  }
}

class Dog extends Animal {
  makeSound() {
    console.log(`${this.name} says woof!`);
  }
}

const cat = new Cat("Mimi");
const dog = new Dog("Coco");

cat.makeSound(); // Mimi says meow~
dog.makeSound(); // Coco says woof!

主要是为我们抽象代码用的,例如公共部分放到一个类中,子类单独实现。思想是挺好的,就是实现起来有时候会比较麻烦,因为刚开始设计的时候,不太容易一开始就能预知未来需要抽象哪些。

多态:简单来说就是一个接口,不同实现,Javascript 中没有多态,更多在 typescript 中出现,例如用重载实现多态。如下:

javascript 复制代码
class Calculator {
  add(a: number, b: number): number;
  add(a: string, b: string): string;
  add(a: any, b: any): any {
    return a + b;
  }
}

const calc = new Calculator();

console.log(calc.add(5, 10));       // 15
console.log(calc.add("foo", "bar")) // foobar

面相切面编程 - AOP

为我们提供了一种将代码注入现有函数或对象的方法,而无需修改目标逻辑。这句话大家看看就行了,很官方,我们简单理解就是:

把横跨多个功能的"通用逻辑"抽出来,统一管理,而不是在每个函数里重复写。

常见实现的方式,就是在函数前后,注册一个钩子,这样达到不修改当前函数逻辑的目的。例如:

javascript 复制代码
function logBefore(fn) {
  return function (...args) {
    console.log("Calling:", fn.name, "args:", args);
    return fn.apply(this, args);
  }
}

以上方法是一个 切面 ,也就是可以包装到另一个函数上,在这个函数调用之前打印日志,是一个独立的功能。例如:

ini 复制代码
const getUser = logBefore(function getUser(id) {
  // 在数据库搜索数据返回
  return db.query(`SELECT * FROM users WHERE id = ${id}`);
});

这样其它函数都可以共享这个 logBefore 的逻辑。

当然实际场景,例如鉴权,在调用一个方法前,看看有没有权限,就是一个很好的使用 AOP 的场景。

AOP 跟 nest.js 关系

nest.js 后面我们会讲一个很关键的概念,pipeline,我们这里先简单介绍一下,后面详细说。pipeline 就是当客户端收到请求到返回的过程。

首先 nest.js 需要注册一个 Controller 来处理请求,如下

javascript 复制代码
import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

一般情况下,get 方法访问路径 /cats 的时候,返回 'This action returns all cats'。

但实际上到 Controller 处理数据之前,还有许多步骤。

例如在 nest.js 中有 Middleware(中间件) 的概念,也有 Guard(守卫的概念) 等等概念,我们这里不细说,后面文章会有,这里只需要记住:请求来了之后,先到 Middleware 然后到 Guard。。。兜兜转转好几回才能到 Controller

之前我们提到,AOP 中的面向切面,一个 切面 ,也就是可以包装到另一个函数上,在这个函数调用之前打印日志,是一个独立的功能。

nest.js 中的 Middleware 可以打印日志,是不是在不直接影响 Controller 逻辑的前提下,实现了一个可以复用的独立的功能,因为所有路由接收的请求都会经过 Middleware

从这个意义上说 MiddlewareGuard 这些概念本质就是 "切面"。

如果你对node.js ,组件库,前端动画感兴趣,欢迎进群交流。这是我组件库教程官网。感谢star,再次感谢:

依赖注入和控制反转

这两个概念看起来挺唬人的,比较高大上。我们简单解释一下:

我们从一个简单例子说起:

假设有一个类叫老王,专指那些住在美女隔壁的靓仔们。他们喜欢助人为乐,送大帽子。能力上擅长逃跑。

typescript 复制代码
class Hobby {
    constructor(gender){
        return [gender, '助人为乐']
    }
}
class Skill {
    constructor(){
        return ['送帽子', '跑路']
    }
}
class Person {
    hobby: Hobby
    skill: Skill
    constructor(){
        this.hobby = new Hobby('女');
        this.skill = new Skill();
    }
}
console.log(new Person()); // { hobby: ['女', '助人为乐'], skill: ['送帽子', '跑路'] }

好了,这个Person类,我们看看有啥缺点:

  • 每次创建Person类的实例,都要传入Hobby类和Skill类,也就是对这两个类都产生了依赖,假如有一天我们想创建不同的老王,比如有的老王喜欢小鲜肉,有的老王喜欢老腊肉,这个Person类写死了,没法定制

有同学马上就会说,这个简单啊,Hobby和Skill当做参数传入不就行了,是的,这个方法确实能解决问题,如下:

typescript 复制代码
class Hobby {
    constructor(gender){
        return [gender, '助人为乐']
    }
}
class Skill {
    constructor(){
        return ['送帽子', '跑路']
    }
}
class Person {
    hobby: Hobby
    skill: Skill
    constructor(hobby, skill){
        this.hobby = hobby;
        this.skill = skill;
    }
}

// { hobby: ['男', '助人为乐'], skill: ['送帽子', '跑路'] }
console.log(new Person(new Hobby('男'), new Skill()); 
  • 但有没有更好的办法,也就是这个类我们不用自己去new,像下面这样,系统帮我们自动导入呢?
typescript 复制代码
class Person {
    constructor(hobby: Hobby, skill: Skill){
  
    }
    hello(){
        这里直接就可以用this.hobby了
    }
}

也就是说,在你new Person的时候,hobby和skill参数,自动帮你实例化导入,而且你还需要new Person,能不能不new Person 都是自动调用并把参数导入?

  • 这就引申出第一个概念就控制反转,以前我们都要自己主动去new,从而创建实例,现在是把创建实例的任务交给了一个容器(后面会实现,你就明白了,相当于一个掌控者,或者说造物主,专门来创建实例的,并管理依赖关系),所以控制权是不是就反转了,你主动的创建实例和控制依赖,反转为容器创建实例和控制依赖。
  • 对于控制反转,最常用件的方式叫依赖注入,意思是容器动态的将依赖关系注入到组件中。
  • 控制反转可以通过依赖注入实现,所以本质上是一回事。

到这里,其实大家也没觉得依赖注入有啥毛用吧!下面的讲解一言以蔽之就是,虽然我们js可以把函数或者类当参数传入另一个类里,这确实解决了之前讲的写死代码的问题,也就是说我们不用依赖注入也能解决代码耦合的问题,所以看起来我们并不是那么需要依赖注入。

小结

这里第一篇 nest.js 内容完毕,接下来会详细解释上文提到的 pipline,也就是一个请求经过 nest.js 的全过程,以及这些经过的过程中,碰到的核心概念!

相关推荐
let_code19 分钟前
CopilotKit-丝滑连接agent和应用-理论篇
前端·agent·ai编程
Apifox44 分钟前
Apifox 11 月更新|AI 生成测试用例能力持续升级、JSON Body 自动补全、支持为响应组件添加描述和 Header
前端·后端·测试
木易士心44 分钟前
深入剖析:按下 F5 后,浏览器前端究竟发生了什么?
前端·javascript
在掘金801101 小时前
vue3中使用medium-zoom
前端·vue.js
Q_Q5110082851 小时前
python+django/flask的结合人脸识别和实名认证的校园论坛系统
spring boot·python·django·flask·node.js·php
Q_Q5110082851 小时前
python+django/flask的选课系统与课程评价整合系统
spring boot·python·django·flask·node.js·php
xump1 小时前
如何在DevTools选中调试一个实时交互才能显示的元素样式
前端·javascript·css
折翅嘀皇虫1 小时前
fastdds.type_propagation 详解
java·服务器·前端
Front_Yue1 小时前
深入探究跨域请求及其解决方案
前端·javascript