前言
本文保证绝对大白话!简单易懂!
最近跟一个好友讨论,他说想学习 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);
简单理解就是当触发 DOM 的 input 事件触发的时候,也就是一个值改变的时候,触发 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。
从这个意义上说 Middleware,Guard 这些概念本质就是 "切面"。
如果你对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 的全过程,以及这些经过的过程中,碰到的核心概念!