Nest入门前需知的基础概念
前言
不知从什么开始Koa
、Express
甚至Egg.js
都已经不再是前端开发者首选的Node.js
服务端框架了,转而框架Top榜变成了Nest
、Midway
之类的服务端框架了。
古早时期,刚学Koa
、Express
的时候,就觉得原来写服务端这么简单,但是自己一旦进行开发,写出来的代码是乱糟糟的,毫无架构设计可言,并且Koa
或者Express
投入实际的生产中还是比较单薄的,并且开发过程中可能还需要一堆中间件和插件组合开发,所以这类框架算不上企业级的框架。
后面开始学习和使用Egg.js
,这或许也是大家使用的第一个企业级的Node.js服务端。其以koa为底层进行封装,把常用的中间件和三方工具集成起来,基本做到开箱即用,并且Egg.js的各种约定设计,也让我们项目的可维护性更高。但是我们这种写习惯函数(没办法,JS就是万物皆函数)
的前端开发者来说,需要开始写Class
,开始面向对象编程,确实还是有一定成本的。
现在随着TypeScript
的普及,Egg.js
也在慢慢落幕,新型Node.js
服务端框架更加强大,例如Nest
、Midway
都是构建于TypeScript
之上,并且这些框架中大量的使用依赖注入和模块化设计使得组件之间的耦合度降低,提升项目的可维护性。听着很酷,但是酷的代价就是它们带来了一堆可能前端很少接触或者没接触到的设计模式和编程概念,例如装饰器、反射、控制反转和依赖注入等等。这让学习成本直线上升。
估计大家第一时间看Nest
的文档,头是炸的,说实话这些技术和概念其实和前端还是有些许割裂的,真想说一句学不动了,如果写过SpringBoot
的同学会发现,这些框架除了JS
那点语法之外,跟用Java
的SpringBoot
写服务端几乎没有区别了。所以如果是一个后端的开发者,几乎可以无缝接手 “前端开发者” 用Nest
或Midway
写的后台项目。
而本文主要从通过讲解装饰器、反射、控制反转、依赖注入的概念,来理解Nest
这类新兴框架为什么要这么设计?
装饰器
装饰器这个东西对前端来说好像忽远忽近的,近是因为我记得他一直在ECMAScript
和Typescript
的提案中,并且我们可能react-redux
等各种三方库中使用过。说远呢,是因为在其他的语言(Java
、Python
)早就存在了,通常运用于类和类的成员变量或函数中,虽然ES6
推出了class
(底层实现其实还是函数,只是语法糖而已),但在函数是第一公民的JavaScript
中,我们前端开发人员对面向对象的设计模式总是不那么“感冒”。
相比于纯前端的Web
开发的不同,服务端使用面向对象的设计模式可以让我们对代码做更好的抽象,并且通过装饰器这种强大的语法特性,能够提高代码的可读性、可维护性和可扩展性,使开发者能够更加灵活地处理复杂的业务逻辑和功能需求,所以装饰器这个设计模式还是很重要的。
接下来我们介绍一下 TypeScript
装饰器的基础用法。本文的案例也是围绕5.0
以下的TypeScript
展开。建议开启tsconfig.json
的如下配置:
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
}
}
装饰器的使用
类装饰器
类装饰器是直接作用在类上的装饰器,它在执行时的入参只有一个,那就是这个类本身(可以理解是构造函数,而不是原型对象),可参考下图:
案例一: 为类添加一个实例方法
@setMethod()
class Person {
private name = "jack";
constructor(name: string) {
this.name = name;
}
}
function setMethod(): ClassDecorator {
return (target: any) => {
console.log("target", target); // Class Person
target.prototype.output = () => {
console.log("this is decorators method");
};
};
}
const person = new Person("caos");
person.output(); // this is decorators method
解析: 上面是实现了一个setMethod
的装饰器,用于装饰Person
这个类,其实我们能发现所谓的装饰器实际上就是一个函数,我们setMethod
这个装饰器方法中target
代表的就是class Person
这个类,可以看见Person
上是没有output
方法的,通过setMethod
我们最终的实现就是往Person
的原型上添加 (JS的Class实际上就是函数+原型的语法糖)output
方法,最后Person
的实例上能直接调用output方法。
案例二:方法覆盖
@overload
class Person {
public name = "jack";
constructor(name: string) {
console.log("Person constructor", name);
this.name = name;
}
getName() {
console.log("Person setName", this.name);
}
}
function overload(target: any) {
return class extends target {
setName(value: string) {
this.name = value;
}
getName() {
console.log("decorators name is", this.name);
}
};
}
const person = new Person("caos"); // 输出:Person constructor caos
person.setName("jacks");
person.getName(); // 输出:decorators name is jacks
解析: class extends target
是 TypeScript 中的一种语法,用于创建一个类并继承自另一个类或者构造函数。overload
通过继承被装饰的Person的类,通过装饰器中的新方法setName
中修改Person
实例name
的值,并覆盖原Person
的getName
方法。
函数装饰器
方法装饰器的入参包括类的原型、方法名以及方法的属性描述符(PropertyDescriptor
),而通过属性描述符你可以控制这个方法的内部实现 (即 value)、可变性 **(即 writable)**等信息。
案例三:实现中间件装饰器计算方法执行时间
class Data {
@middleware
public static output(data) {
console.log("data is ", data);
}
}
function middleware(target: any, property: string, descriptor: PropertyDescriptor) {
const metaFunction = target[property];
descriptor.value = (...args) => {
const now = Date.now();
console.log("before");
metaFunction.apply(target, args);
console.log("after", Date.now() - now);
};
}
Data.output("jackcaos");
// 输出
// before
// data is jackcaos
// after
解析: static function
是作为静态函数,直接挂载在Data
的类上的,无需实例化,可以直接调用。通过target[property]
获取到被装饰的函数,之后通过descriptor
直接重写原函数的value
。
属性装饰器
属性装饰器入参只有类的原型与属性名称,我们可以直接通过直接在类的原型上赋值来修改属性。比如在开发中可能需要向实例中注入一些配置,下面通过实现Inject
装饰器,完成属性值的注入。
案例四:给类注入属性值
const config = {
a: {
b: 1,
},
};
class Person {
@inject("a") public config;
getData() {
console.log(this.config);
}
}
function inject(key: string) {
// 这是入参
return (target: any, property: string) => {
const value = config[key];
Reflect.set(target, property, value);
};
}
new Person().getData(); // 输出:{ b: 1 }
解析: 获取config
值后,直接注入到Person
的原型上,实例化之后通过getData
可以访问到注入的值。
参数装饰器
参数装饰器它的入参包括类的原型、参数所在的方法名与参数在函数参数中的索引值(即第几个参数)。
案例五:打印参数信息
class Person {
print(index: number, @param() name: string) {
console.log("执行print方法", index, name);
}
}
function param() {
return (target: any, property: string, propertyIndex: number) => {
console.log("参数装饰器被执行:", target.constructor.name, property, propertyIndex);
};
}
new Person().print(1, "jackcaos"); // 输出: { name: 'jackcaos', age: 18 }
其实有时候我们单纯使用装饰器是不够的,我们知道装饰器实际上是在被装饰的方法和类之前执行的。但是有时候希望在程序运行时动态地检查、获取和修改对象的属性,这时候单纯靠装饰器,而无法访问参数的元数据类型的,这就得依托反射的技术了。
反射
什么是反射?
反射(Reflection)
是一种编程技术,它允许程序在运行时动态地检查、获取和修改对象的属性和行为。反射使得程序能够在运行时获取类型信息、调用方法、访问属性等,而不需要在编译时就确定这些信息。
JavaScript
不像Java
那样有专门的反射相关的API
,但是也可以通过一些内置的方法来实现反射功能:
- 利用
Object
相关的API去获取对象的属性,或者修改对象的属性 - 利用
typeof
获取变量的类型
在TypeScript
中可以通过Reflect-metadata
中的 Reflect API
提供了一种与装饰器关联的元数据进行交互的方法。
Reflect API
包括用于访问和修改给定装饰器的元数据的方法,除了常规的Reflect API
外,Reflect-metadata
还提供以下方法:
- 通过design:type 可以获取属性的类型
- 通过design:paramtypes 可以获取函数的参数的类型
- 通过design:returntype 可以获取函数的返回值类型
什么是元数据?
元数据是描述数据(代码)的数据,它提供了关于数据的额外信息,帮助程序理解和处理数据。
接下来我们来看一个使用Reflect-metadata
的案例:
案例六:获取装饰的属性和函数的类型
import "reflect-metadata";
class Person {
@getPropertyType private name: string;
@getFunctionType
setName(name: string): boolean {
this.name = name;
return true;
}
}
function getPropertyType(target: any, property: string) {
// 获取被装饰的属性的类型
const type = Reflect.getMetadata("design:type", target, property);
Reflect.defineMetadata("attribute.type", type, target, property);
}
function getFunctionType(target: any, property: string, descriptor: PropertyDescriptor) {
// 获取被装饰的方法的参数类型
const paramsType = Reflect.getMetadata("design:paramtypes", target, property);
// 获取被装饰的方法的返回值类型
const returnType = Reflect.getMetadata("design:returntype", target, property);
Reflect.defineMetadata("function.returnType", returnType, target, property);
}
const person = new Person();
// 对象运行时动态获取Reflect定义的值
console.log(Reflect.getMetadata("attribute.type", person, "name"));
// 输出 [Function: String]
console.log(Reflect.getMetadata("function.returnType", person, "setName"));
// 输出 [Function: Boolean]
解析: 我们通过getMetadata
获取到被装饰的属性的类型以及函数参数的类型,然后通过defineMetadata
定义所需要的元数据,最终我们可以在函数运行的时候装饰器中定义的数据。
接下来我们尝试着实现之前说的所谓的validate
装饰器,去校验我们所调用函数的参数情况,实际上我们利用Reflect.getMetadata
便可以获取所有的参数类型,从而完成参数的校验工作。
案例七:获取装饰的属性和函数的类型
import "reflect-metadata";
function validate(target: any, property: string, descriptor: PropertyDescriptor) {
descriptor.value = function(...args) {
// 这里返回的参数是装箱类型 例如 [Function: Number], [Function: Object], [Function: Array]
const metaTypes = Reflect.getMetadata("design:paramtypes", target, property);
// 所以需要转换为JS的类型进行比较
const types = transformTypes(metaTypes);
for (let i = 0; i < args.length; i++) {
const paramType = Object.prototype.toString.call(args[i]);
if (paramType === types[i]) {
continue;
} else {
console.error(
`validate fail: Invalid parameter type at index ${i}, got type ${paramType.slice(8, -1)}`,
);
}
}
};
return descriptor;
}
function transformTypes(types) {
return types.map((item) => Object.prototype.toString.call(item()));
}
class Person {
@validate
inputMsg(index: number, name: string, data: number[]) {
console.log("inputMsg", index, name, data);
}
}
const person = new Person();
person.inputMsg(1, 2, [3]);
// 输出:validate fail: Invalid parameter type at index 1, got type Number
依赖注入&控制反转
我们知道在 Nest
或者 Midway.js
框架里几乎到处都是依赖注入(DI),并且文档中也到处都提及了控制反转的概念,那这两个东西到底是什么呢?其实前面所了解的装饰器也好,利用Reflect
实现反射也好,最终都是为了本节做铺垫的。
现在我们假设一个场景,我们需要在类UserController
中,调用UserService
中的getData
方法,模拟我们controller
处理请求,然后service
获取数据的场景。
class UserController {
handleRequest() {
const userService = new UserService();
const data = userService.getData();
console.log("data", data);
}
}
class UserService {
getData() {
return {
name: "jackcaos",
age: 18,
};
}
}
new UserController().handleRequest(); // 输出: { name: 'jackcaos', age: 18 }
我们常规的写法都是这样的,先初始化对应的类,之后再调用相关的方法。当对象比较少的时候,这么写是没什么问题,但是一个大型的系统肯定不止几个类,可能会出现几十上百个类,对象之间的依赖关系也越来越复杂,经常会出现对象之间的多重依赖性关系,这样针对一个类进行改动,实际上就是牵一发而动全身的,类似于下图的情况。
我们可以看到一个个的对象类似于齿轮一样彼此耦合的,而为了降低代码之间的耦合度,工程师们提出了IoC
理论,来实现对象之间的解耦。
所以我们看到出现了一个IoC
容器串联起来了各个对象,而不会出现彼此耦合的情况。
控制反转
我们看到没有IoC
容器的时候,我们需要使用某个类的方法的话,是需要去主动去初始化然后再去调用的,这种叫做控制正转,因为这个初始化的控制权是在具体的类手中,比如上述例子中在UserController
初始化UserService
。
而当UserService
的初始化的工作交给了IoC
容器,然后IoC
容器通过向UserController
注入UserService实例,所以此时不是UserController
不再是自己主动去初始化UserService
类了,也无需关心初始化实例的细节,仅需被动接受一个UserService
实例,这就是控制反转。
优点
简单来说控制反转(IoC)
通过将组件之间的依赖关系交由容器管理,实现了组件之间的解耦合、灵活性、可测试性、可扩展性和集中管理,从而提高了代码的质量和可维护性,降低了系统的复杂度和耦合度。
依赖注入
根据上面我们顺水推舟的也推出了依赖注入(DI)
的概念,上面UserController
依赖UserService
实例,而loC容器将实例注入到需要他们的对象中,实际上这就是依赖注入,接下来我们把上面的代码修改为依赖注入的形式。
import "reflect-metadata";
class DependencyInjection {
private static locContainer: Map<string, any> = new Map();
static set(key: string, target: any): void {
this.locContainer.set(key, target);
}
static get<T>(target: any): T {
const isInjectable = Reflect.getMetadata("injectable", target);
if (!isInjectable) {
throw new Error("Target is not injectable");
}
const dependencies = Reflect.getMetadata("design:paramtypes", target) || [];
// 初始化依赖
const instances = dependencies.map((dep) => {
const obj = this.locContainer.get(dep.name);
return new obj();
});
return new target(...instances);
}
}
function Injectable() {
return function(target: any) {
Reflect.defineMetadata("injectable", true, target);
};
}
function Provide() {
return function(target: any) {
DependencyInjection.set(target.name, target);
};
}
@Provide()
class UserService {
getData() {
return {
name: "jackcaos",
age: 18,
};
}
}
@Injectable()
class UserController {
constructor(private userService: UserService) {}
handleRequest() {
const data = this.userService.getData();
console.log("data:", data); // 输出: { name: 'jackcaos', age: 18 }
}
}
const userController = DependencyInjection.get(UserController) as UserController;
userController.handleRequest();
解析: 我们实现了两个装饰器Provide
和Injectable
,Provide
是将需要注入的对象放入locContainer
中,然后Injectable
这个装饰器是负责标记需要注入的对象,最后通过DependencyInjection
的get
方法
完成UserService
的实例化, 最终我们就完成了userService
属性的装配工作,而无需显式的实例化对象。
结语
无论是Nest
也好,还是Midway
也好,虽然极大的提升的项目的可维护性,降低了模块之间的耦合,但是也导致它们所需要的学习成本也是不低的,简单学习了下Nest
的文档后,发现这些Node.js
的服务端框架也在无限的向其他语言的服务端框架靠拢。
未来前端开发也许会渐渐的向全栈工程师靠拢,前后端可能也不会再有那么清晰的界限了,也许以后前端会更卷吧,也或许正是因为JavaScript
语言之前的不完备性,而随着现在前端技术的不断发展,后人再不断的去填坑吧。
转载自:https://juejin.cn/post/7363545737873981479