️:轮子不是圆的吗?为什么是方的。
:因为圆能用。
要说依赖注入(DI,Dependency Injection),就不得不提控制反转(IoC,Inversion of Control),它们几乎已经是绑定概念了,那么两者有什么关系和区别呢。
很多文章会把两者混为一谈,但以我的理解,IoC是一种概念性的设计思想,除开Spring、Angular,就如React、Vue这样的框架,我们按照框架的规则写代码,框架拿到我们的代码进行一系列转换然后执行,这也算控制反转。相反的如jQuery、Lodash这样的工具库,我们调用库的方法实现目的,就不是控制反转。 回到DI,DI则是IoC思想的一种具体模式,主要用于降低Class之间的耦合。在Spring、Nest.js、Angular等框架中依赖注入是基本概念。
本文将基于TS实现一个基础的依赖注入过程,主要体现依赖注入的基本原理。
先看如下代码。
ts
复制代码
import { AuthService } from "./AuthService"; class AuthController { constructor() { this.authService = new AuthService(); } login() { this.authService.xxx() } }
我们在controller引入service类,并且直接实例化,类之间是强依赖的。
而在Java Spring、Nest.js类似框架里,将大概是下面这样子。
ts
复制代码
/** AuthService.ts **/ @Service() // 这个类要交给IoC容器 class AuthService { xxx() { // xxx } }
ts
复制代码
/** AuthController.ts **/ import { AuthService } from "./AuthService"; class AuthController { @AutoWired() // 容器帮忙创建和注入AuthService authService: AuthService; login() { this.authService.xxx() } }
这里没有再手动的new对象,而是给属性打上了一个注解,容器就会实例化对应的类再赋值给这个属性。这样做的好处有:
这里简单说一下,装饰器实质上就是高阶函数的语法糖,可以作用在类、方法、访问符(getter、setter)和方法参数上,可以获取并修改被作用元素的元信息。具体的用法大全与其我把文档搬过来,不如直接去看官方文档咯~
这里主要用到的是类装饰器和属性装饰器。
Component是类装饰器,打在要交给容器管理的类上。
AutoWired是属性装饰器,打在需要容器帮忙注入实例的属性上。(暂且只实现属性注入)
我们在装饰器中,可以拿到被作用元素的一些信息,比如类装饰器中可以拿到类的构造函数、构造函数名字(即类名),这些是默认就有的,也可以给其define一些新的信息,这些信息就称为元信息。我们可以通过定义和读取元信息,得到各个部件的依赖关系。
假设我们要实现一个获取用户信息的功能,采用Web最常用的Controller -- Service -- Repository架构。
我们维护两个列表,一个注册方法,一个get方法。然后就可以先把容器new出来了。
ts
复制代码
class Container { /** * components维护组件列表 * instances维护实例列表 */ components = new Map
这个非常简单,target就是UserRepo的构造函数,拿到构造函数target和别名alias,注册进容器然后将构造函数原样返回即可。
ts
复制代码
function Component(alias?: string) { return function (target: any) { iocContainer.regist(target, alias || target.name); return target; }; }
ts
复制代码
@Component() class UserRepo { getUserById(id: number) { return { user: "jj.zhang", id }; } }
然后容器的components会有UserRepo的构造函数。
ts
复制代码
function AutoWired(alias?: string) { return function (target: any, propertyName: string) { let name = alias; if (!name) { const classConstructor = Reflect.getMetadata( "design:type", target, propertyName ); console.log(99, classConstructor, target, propertyName); name = classConstructor.name; if (name === "Object") { // 没有写类型,则尝试将属性名转大写查找实例 name = camelcase(propertyName, { pascalCase: true }); } } const instance = iocContainer.get(name || ""); target[propertyName] = instance; return instance; }; }
ts
复制代码
@Component() class UserService { @AutoWired() userRepo!: UserRepo; getUserById(id: number) { return this.userRepo.getUserById(id); } }
这里用到Reflect.getMetadata("design:type", target, propertyName)
target
是UserService的原型对象
propertyName
是属性的名字,即"userRepo"
design:type
是内置的元数据key
整句表示获取target["userRepo"]
的类型即被装饰属性的构造函数,构造函数的name属性就是我们要的属性类型字符串,也就是UserRepo,然后找容器要一个UserRepo实例,赋值给target["userRepo"]
属性。
如果属性没有声明类型,比如这样:
ts
复制代码
@AutoWired() userRepo;
则name属性拿到的是"Object"
,此时我们就将属性名propertyName
转换为pascalCase作为alias去容器取实例。
最后容器的instances就会多出一个UserRepo的实例了。
Java Spring的接口注入是我觉得很精髓的地方,巧妙运用了多态的特性。
但是在TypeScript,装饰器中拿不到接口相关的信息,所以就无法实现接口注入了。
ts
复制代码
function Component(target: any) { // 这里拿不到任何关于TestInterface的信息 } interface TestInterface { test(): string; } @Component class A implements TestInterface { test() { return 'hello tuya'; } }
但是可以使用抽象类实现类似的效果:
容器新增一个abstracrs列表,维护每个抽象类的所有实现子类。
新增一个@Impl装饰器,收集每一对抽象类 & 子类,注册到容器,并写入元信息表明这是一个抽象类。
修改@AutoWired注入逻辑,根据第2步写入的元信息判断,如果属性类型是抽象类,就在该抽象类的所有实现类中,查找和属性名匹配的类实例,进行注入。
最后容器就是这个结构,顺藤摸瓜就可以找到实现类了。
但有意思的事,不管是Nest.js还是Midway的Injection库,都没有提供类似的方案,或许这是个伪需求?
毕竟即使在Java项目中,多数interface从生到死也只有一个Implements
ts
复制代码
abstract class IUserService { } @Impl(IUserService) @Component() class UserService extends IUserService { } @Impl(IUserService) @Component() class UserServiceV2 extends IUserService { } @Component() class UserController { // // 注入UserService // @AutoWired() // userService!: IUserService; // // 注入UserServiceV2 // @AutoWired("UserServiceV2") // userService!: IUserService; // 注入UserService,因为名称转pascal后就是UserService @AutoWired() userService; }
以上用最基础的手法,实现了依赖注入的基本原理。还有非常多的特性没有实现,比如构造函数注入、异步初始化等。
实际Component可能在出现在任意文件任意位置,而上面的例子中,如果将AutoWired移到Component前面,就找不到依赖了。
原因有二:
实际成熟框架的装饰器只负责写入元信息,不会实例化,由ApplicationContext启动时进行全局包扫描,获取所有的元信息,构建依赖图,确保所有Component都注册到容器中。时间所限这里不再继续展开,有兴趣的大佬可以移步源码Midway包扫描,injection源码。
来涂鸦工作: job.tuya.com/
作者:涂鸦大前端
链接:https://juejin.cn/post/6930898274639593480
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。