AOP概念
AOP是Aspect Oriented Programming,面向切面编程。
从对程序的抽象理解上来看,AOP和OOP不同,OOP把系统看作多个对象的交互,AOP把系统分解为不同的关注点,或者称之为切面(Aspect)。但是两者并不冲突,更多是一种互补的关系。
这个所谓‘切面’,个人理解就是时机。就是在设计阶段,从流程的角度把大的流程抽象成通用的子流程,而这每一个子流程就可以被称作切面。
举个例子:
当进行插入数据这个业务逻辑的时候,需要进行登陆检查,日志写入等通用流程。而独立的把这些流程Aspect实现的过程其实就是面向切面编程。
IOC概念
高层模块不应该依赖于低层模块,二者都应该依赖于抽象 抽象不应该依赖于细节,细节应该依赖于抽象 这句话解释起来,‘抽象’就是容器(Container),所谓‘细节’就是本来相互依赖的几个类。 具体实践起来就是,构造一个容器,在容器中new了各种对象,当一个类A的实例a需要类B的实例的时候,我们在容器中new A的时候再给他B的实例b。整个注入过程不是程序员做的,把控制权交给了容器… 这过程其实就是DI(Dependency Inversion)依赖倒置。 上面描述的过程中,我们提到,”new A的时候再给他B的实例b”, 这个过程其实是属于AOP(面向切面)思想的范畴,我们可以理解实例化的过程就是一个切面(统一的时机)。
在BFF中实现DI其实就是按照这种思想,下面从应用的角度看下如何在Koa项目中借助一些库实现DI。
使用awilix-koa做ioc
https://www.npmjs.com/package/awilix
https://www.npmjs.com/package/awilix-koa
Controller
在使用koa写controller的时候,我们是这样定义一个路由的,比如./router.js中就可能存在这样的代码:
const router = new Router();
const apiController = new ApiController();
function initRouter(app) {
// 页面路由
router.get('/', apiController.actionIndex);
app.use(router.routes())
.use(router.allowedMethods());
}
这样没有问题,但是不够好,怎么不好呢?
- 不符合“开闭原则” —- 面向修改关闭,面向扩展开放。(比如我现在需要在添加一个新的路由,/list,你不得不再去修改曾经写好的文件)。
针对这个问题我们在awilix中的解法是借助装饰器,标出路由路径,这样避免了再去修改比如./router.js的问题,符合开发-关闭原则。 - 另外,从controller内部角度来说,还有一个点是controller会依赖service(这个是通用的MVC框架都约定成俗的东西,controller接受请求并分发给service,由service的方法具体做业务)。
这样,在controller内部,自然会对service存在强依赖关系,这样的强依赖关系耦合度比较高。
针对这个问题我们的解法也比较通用,第一点抽象Service,让controller内部都依赖这个抽象的service,第二点是通过依赖注入,用声明的方式将具体的service在运行时注入到这个controller中。
具体代码参见下面: ```javascript import { route, GET } from ‘awilix-koa’; import * as Router from ‘koa-router’ import IService from ‘../interface/IService’;
@route(‘/api’) class APIController {
private apiService: IService;
// 面向切面IOC constructor({apiService}){ this.apiService = apiService }
@route(‘/list’)
@GET()
async actionList(ctx: Router.IRouterContext, next: () => Promise
export default APIController;
对于这段代码:<br />我们通过两个装饰器@route('/api')、@route('/list')声明了具体actionList方法是匹配这个路由的'/api/list'。<br />依赖注入的的具体时机其实是一个Aspect,切面----就是constructor执行的时候。这里的代码相当于从container中拿过来了一个apiService实例,并在这个过程中对controller的依赖赋值,即注入。
<a name="Service"></a>
### Service
service这里比较简单,核心思想就是实现抽象的接口,只有这样,才不会在controller中形成对实现类的强依赖。<br />ApiService实现了IService:
```javascript
import IService from '../interface/IService';
export default class ApiService implements IService{
getInfo() {
const fakeData = {
success: true,
data: [{name: 'yxnne', age: 31}]
}
return Promise.resolve(fakeData);
}
}
IService中简单的定义了下通用的方法, IData是数据结构定义:
import IData from './IData';
export default interface IService {
getInfo(): Promise<IData>;
}
入口文件
我们的入口文件要做的事情,自然是要构造容器,然后启动koa的app对象。
import * as Koa from 'koa';
import { createContainer, Lifetime } from 'awilix';
import { loadControllers, scopePerRequest } from 'awilix-koa';
const app = new Koa();
// 构建容器
const container = createContainer();
container.loadModules([`${__dirname}/services/*.ts`], {
formatName: 'camelCase',
resolverOptions: {
lifetime: Lifetime.SCOPED, // 访问权限,在当前的类内
},
});
app.use(scopePerRequest(container));
// 加载路由
app.use(loadControllers(`${__dirname}/routers/*.ts`));
app.listen(3000, () => {
console.log('AOP IOC support Server Loaded~')
})
我们通过createContainer这个方法创造了容器, 然后将services放入容器中:container.loadModules。接着借助loadControllers也把cotrollers放到容器里面。
而容器这一套东西又是通过中间件的形式生效: app.use(scopePerRequest(container))
最后,启动监听端口,基本完成~
使用inversify
inversify可是一个狠角色:A powerful and lightweight inversion of control container for JavaScript & Node.js apps powered by TypeScript.简单理解就是支持TS的轻量级IOC容器。
使用它的公司很多:微软、Facebook、亚马逊等,国内有百度都在使用它。
https://github.com/inversify/InversifyJS
https://www.npmjs.com/package/inversify
https://www.npmjs.com/package/inversify-koa
https://www.npmjs.com/package/inversify-binding-decorators
https://www.npmjs.com/package/inversify-koa-utils
知乎:带你学习inversify.js系列 - inversify基础知识学习
大笑文档:inversify.js
OOP DEMO by inversity
下面通过一个简单的Demo,了解下inversity.js的用法,这部分和BFF无关。
先定义Student、Teacher、Classroom接口:
export interface Student {
learn(): string;
}
export interface Teacher {
teaching(): string;
}
export interface Classroom {
study(): string;
}
然后定义几个实现类,@injectable()这个装饰器就来自inversify,用来标记这些类的实例是要被放入容器的:
import { Student, Teacher, Classroom } from './interface';
import { inject, injectable } from 'inversify';
import "reflect-metadata";
import TYPES from './types';
@injectable()
export class LittleStudent implements Student {
learn(): string {
return 'LittleStudent';
}
}
@injectable()
export class OldTeacher implements Teacher {
teaching() {
return 'OldTeacher'
}
}
@injectable()
export class ClassMath implements Classroom {
private student: Student;
private teacher: Teacher;
constructor(@inject(TYPES.Student) student: Student, @inject(TYPES.Teacher) teacher: Teacher) {
this.student = student;
this.teacher = teacher;
}
study() {
return `${this.teacher.teaching()} touch ${this.student.learn()} in Math class`
}
}
上面的代码中ClassMath这个类是要依赖Student和Teacher的,当然,这个依赖关系是通过IOC注入的,在ClassMath的constructor中会有这样的东西:@inject(TYPES.Student) student: Student,这个Type.Student其实就是唯一标示,我们会在构造容器的时候,将某个类的实例绑定成这个标示:
export default {
Student: Symbol.for('Student'),
Teacher: Symbol.for('Teacher'),
Classroom: Symbol.for('Classroom')
}
下面我们就来到了刚说的构造container以及将将container中的元素绑定为Type中的标记的过程:
import { Container } from 'inversify';
import { LittleStudent, OldTeacher, ClassMatch } from './entitys';
import { Student, Teacher, Classroom } from './interface';
import TYPES from './types';
const container = new Container();
// container.bind
// 根据那些@injectable的class生成的实例
// 作为Types中定义的的值的实例
// 建立对应关系
// 好到时候注入用inject
container.bind<Student>(TYPES.Student).to(LittleStudent);
container.bind<Teacher>(TYPES.Teacher).to(OldTeacher);
container.bind<Classroom>(TYPES.Classroom).to(ClassMatch);
export { container };
最后我们在主入口中测试刚才做的一系列事情,测试classroom.study(),如预想一般:
import { container } from './inversify.config';
import TYPES from './types';
import { Classroom } from './interface';
// 从 container中得到实例
const classroom = container.get<Classroom>(TYPES.Classroom);
// 测试
console.log('outputs ::> ', classroom.study());
WARNNING:import “reflect-metadata”;在使用inversify的时候是不可或缺的;
inversify-koa
我们看看,koa结合inversify的简单demo, 这里我们只看下Controller、Service构建Container以及主流程,IService / IData部分和上面是统一的:
Controller
在下面的代码中可以看到,controller中用inversify和awilix比较的话就是装饰器函数不一样了,但是整体思路还是一样的:
import { interfaces, controller, httpGet, TYPE, queryParam } from 'inversify-koa-utils';
import { inject } from 'inversify'
import IService from '../interface/IService';
import { provideThorowable } from '../ioc';
import TAGS from '../constants';
import { IRouterContext } from 'koa-router'
@controller('/')
@provideThorowable(TYPE.Controller, 'IndexController')
export default class IndexController implements interfaces.Controller {
private indexService: IService;
constructor(@inject(TAGS.IndexService) indexService: IService) {
this.indexService = indexService;
}
@httpGet('/')
private async index(ctx: IRouterContext, next: () => Promise<unknown>): Promise<any> {
const data = await this.indexService.getInfo();
console.log('data: ', data);
ctx.body = data.data;
}
// 带参数的
@httpGet('list')
private async list(@queryParam('name') name: number): Promise<any> {
return [{a: name}];
}
}
- 路由标记的装饰器是@controller(‘tag’) @httpGet(‘subTag’)等;
- 构造器中注入用的是:@inject(TAGS.IndexService) indexService: IService;
- 这里可看下list函数中,展示了带参数的请求的写法,标记参数用@queryParam(‘param’);
- 这里还需要注意这个:@provideThorowable(TYPE.Controller, ‘IndexController’), 这是什么东西呢?
这个是我们在另一个文件中定义的装饰器:
// 这里用来注入容器
import { fluentProvide } from 'inversify-binding-decorators'; // 流式provide
const provideThorowable = (indentify, name) => {
return fluentProvide(indentify).whenTargetNamed(name).done();
}
export {
provideThorowable,
}
上面这段代码其实返回了一个装饰器,这段代码的语意其实比较明确:fluentProvide(流式 提供)whenTargetNamed(当目标命名为)。
所以 @provideThorowable(TYPE.Controller, ‘IndexController’)就是说,在容器中构造一个以IndexController为名的类型是TYPE.Controller的对象。
Service
import IService from '../interface/IService';
import { provide } from 'inversify-binding-decorators';
import TAGS from '../constants';
@provide(TAGS.IndexService)
export default class ApiService implements IService{
getInfo() {
const fakeData = {
success: true,
data: [{name: 'yxnne', age: 31}]
}
return Promise.resolve(fakeData);
}
}
这里我们看到仅仅使用@provide就好了,但是为什么我们在上面controller的时候,要用fluentProvide呢?
原因是,基本的@provide装饰器并不允许声明上下文约束、作用域和其他高级绑定功能。我们这里是设置了提供时机。具体可以参见文档:
http://febeacon.com/inversifyjs_docs_cn/routes/ecosystem/utilities/inversify-binding-decorators.html
入口文件
入口文件自然是:1. 构造container; 2. 构造koa实例; 3. 监听端口等一套工作:
// 启动文件
import 'reflect-metadata'; // 全局执行此文件代码
import './ioc/loaders'; // 全局执行此文件代码
import { InversifyKoaServer } from 'inversify-koa-utils';
import { Container } from 'inversify';
import { buildProviderModule } from 'inversify-binding-decorators';
// 构造container
const container = new Container();
// 载入到container中
container.load(buildProviderModule());
// 构造koa实例
const server = new InversifyKoaServer(container);
const app = server.build();
// 监听端口
app.listen(3000, () => {
console.log('inversify-ioc-koa is running...')
})
关于reflect-metadata
简单来说就是叫我们的JS的反射,能像Java那样强大:
这有几篇参考:
https://www.npmjs.com/package/reflect-metadata
https://github.com/rbuckton/reflect-metadata
JavaScript Reflect Metadata 详解
详解学习Reflect Metadata