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>### Serviceservice这里比较简单,核心思想就是实现抽象的接口,只有这样,才不会在controller中形成对实现类的强依赖。<br />ApiService实现了IService:```javascriptimport 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中定义的的值的实例// 建立对应关系// 好到时候注入用injectcontainer.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'; // 流式provideconst 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';// 构造containerconst 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
