深入 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 的全过程,以及这些经过的过程中,碰到的核心概念!

相关推荐
代码搬运媛1 小时前
Jest 测试框架详解与实现指南
前端
counterxing1 小时前
Agent 跑起来之后,难的是复用、观测和评测
node.js·agent·ai编程
counterxing2 小时前
我把 Codex 里的 Skills 做成了一个 MCP,还支持分享
前端·agent·ai编程
wangqiaowq2 小时前
windows下nginx的安装
linux·服务器·前端
之歆2 小时前
DAY_12JavaScript DOM 完全指南(二):实战与性能篇
开发语言·前端·javascript·ecmascript
发现一只大呆瓜2 小时前
Vite凭什么这么快?3分钟带你彻底搞懂 Vite 热更新的幕后黑手
前端·面试·vite
Maimai108083 小时前
React如何用 @microsoft/fetch-event-source 落地 SSE:比原生 EventSource 更灵活的实时推送方案
前端·javascript·react.js·microsoft·前端框架·reactjs·webassembly
kyriewen4 小时前
产品经理把PRD写成“天书”,我用AI半小时重写了一遍,他当场愣住
前端·ai编程·cursor
humcomm5 小时前
元框架的工作原理详解
前端·前端框架
canonical_entropy5 小时前
Attractor Before Harness: AI 大规模开发的方法论
前端·aigc·ai编程