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中就可能存在这样的代码:

  1. const router = new Router();
  2. const apiController = new ApiController();
  3. function initRouter(app) {
  4. // 页面路由
  5. router.get('/', apiController.actionIndex);
  6. app.use(router.routes())
  7. .use(router.allowedMethods());
  8. }

这样没有问题,但是不够好,怎么不好呢?

  • 不符合“开闭原则” —- 面向修改关闭,面向扩展开放。(比如我现在需要在添加一个新的路由,/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) : Promise{ const data = await this.apiService.getInfo(); ctx.body = data; } }

export default APIController;

  1. 对于这段代码:<br />我们通过两个装饰器@route('/api')、@route('/list')声明了具体actionList方法是匹配这个路由的'/api/list'。<br />依赖注入的的具体时机其实是一个Aspect,切面----就是constructor执行的时候。这里的代码相当于从container中拿过来了一个apiService实例,并在这个过程中对controller的依赖赋值,即注入。
  2. <a name="Service"></a>
  3. ### Service
  4. service这里比较简单,核心思想就是实现抽象的接口,只有这样,才不会在controller中形成对实现类的强依赖。<br />ApiService实现了IService
  5. ```javascript
  6. import IService from '../interface/IService';
  7. export default class ApiService implements IService{
  8. getInfo() {
  9. const fakeData = {
  10. success: true,
  11. data: [{name: 'yxnne', age: 31}]
  12. }
  13. return Promise.resolve(fakeData);
  14. }
  15. }

IService中简单的定义了下通用的方法, IData是数据结构定义:

  1. import IData from './IData';
  2. export default interface IService {
  3. getInfo(): Promise<IData>;
  4. }

入口文件

我们的入口文件要做的事情,自然是要构造容器,然后启动koa的app对象。

  1. import * as Koa from 'koa';
  2. import { createContainer, Lifetime } from 'awilix';
  3. import { loadControllers, scopePerRequest } from 'awilix-koa';
  4. const app = new Koa();
  5. // 构建容器
  6. const container = createContainer();
  7. container.loadModules([`${__dirname}/services/*.ts`], {
  8. formatName: 'camelCase',
  9. resolverOptions: {
  10. lifetime: Lifetime.SCOPED, // 访问权限,在当前的类内
  11. },
  12. });
  13. app.use(scopePerRequest(container));
  14. // 加载路由
  15. app.use(loadControllers(`${__dirname}/routers/*.ts`));
  16. app.listen(3000, () => {
  17. console.log('AOP IOC support Server Loaded~')
  18. })

我们通过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接口:

  1. export interface Student {
  2. learn(): string;
  3. }
  4. export interface Teacher {
  5. teaching(): string;
  6. }
  7. export interface Classroom {
  8. study(): string;
  9. }

然后定义几个实现类,@injectable()这个装饰器就来自inversify,用来标记这些类的实例是要被放入容器的:

  1. import { Student, Teacher, Classroom } from './interface';
  2. import { inject, injectable } from 'inversify';
  3. import "reflect-metadata";
  4. import TYPES from './types';
  5. @injectable()
  6. export class LittleStudent implements Student {
  7. learn(): string {
  8. return 'LittleStudent';
  9. }
  10. }
  11. @injectable()
  12. export class OldTeacher implements Teacher {
  13. teaching() {
  14. return 'OldTeacher'
  15. }
  16. }
  17. @injectable()
  18. export class ClassMath implements Classroom {
  19. private student: Student;
  20. private teacher: Teacher;
  21. constructor(@inject(TYPES.Student) student: Student, @inject(TYPES.Teacher) teacher: Teacher) {
  22. this.student = student;
  23. this.teacher = teacher;
  24. }
  25. study() {
  26. return `${this.teacher.teaching()} touch ${this.student.learn()} in Math class`
  27. }
  28. }

上面的代码中ClassMath这个类是要依赖Student和Teacher的,当然,这个依赖关系是通过IOC注入的,在ClassMath的constructor中会有这样的东西:@inject(TYPES.Student) student: Student,这个Type.Student其实就是唯一标示,我们会在构造容器的时候,将某个类的实例绑定成这个标示:

  1. export default {
  2. Student: Symbol.for('Student'),
  3. Teacher: Symbol.for('Teacher'),
  4. Classroom: Symbol.for('Classroom')
  5. }

下面我们就来到了刚说的构造container以及将将container中的元素绑定为Type中的标记的过程:

  1. import { Container } from 'inversify';
  2. import { LittleStudent, OldTeacher, ClassMatch } from './entitys';
  3. import { Student, Teacher, Classroom } from './interface';
  4. import TYPES from './types';
  5. const container = new Container();
  6. // container.bind
  7. // 根据那些@injectable的class生成的实例
  8. // 作为Types中定义的的值的实例
  9. // 建立对应关系
  10. // 好到时候注入用inject
  11. container.bind<Student>(TYPES.Student).to(LittleStudent);
  12. container.bind<Teacher>(TYPES.Teacher).to(OldTeacher);
  13. container.bind<Classroom>(TYPES.Classroom).to(ClassMatch);
  14. export { container };

最后我们在主入口中测试刚才做的一系列事情,测试classroom.study(),如预想一般:

  1. import { container } from './inversify.config';
  2. import TYPES from './types';
  3. import { Classroom } from './interface';
  4. // 从 container中得到实例
  5. const classroom = container.get<Classroom>(TYPES.Classroom);
  6. // 测试
  7. console.log('outputs ::> ', classroom.study());

WARNNING:import “reflect-metadata”;在使用inversify的时候是不可或缺的;

inversify-koa

我们看看,koa结合inversify的简单demo, 这里我们只看下Controller、Service构建Container以及主流程,IService / IData部分和上面是统一的:

Controller

在下面的代码中可以看到,controller中用inversify和awilix比较的话就是装饰器函数不一样了,但是整体思路还是一样的:

  1. import { interfaces, controller, httpGet, TYPE, queryParam } from 'inversify-koa-utils';
  2. import { inject } from 'inversify'
  3. import IService from '../interface/IService';
  4. import { provideThorowable } from '../ioc';
  5. import TAGS from '../constants';
  6. import { IRouterContext } from 'koa-router'
  7. @controller('/')
  8. @provideThorowable(TYPE.Controller, 'IndexController')
  9. export default class IndexController implements interfaces.Controller {
  10. private indexService: IService;
  11. constructor(@inject(TAGS.IndexService) indexService: IService) {
  12. this.indexService = indexService;
  13. }
  14. @httpGet('/')
  15. private async index(ctx: IRouterContext, next: () => Promise<unknown>): Promise<any> {
  16. const data = await this.indexService.getInfo();
  17. console.log('data: ', data);
  18. ctx.body = data.data;
  19. }
  20. // 带参数的
  21. @httpGet('list')
  22. private async list(@queryParam('name') name: number): Promise<any> {
  23. return [{a: name}];
  24. }
  25. }
  • 路由标记的装饰器是@controller(‘tag’) @httpGet(‘subTag’)等;
  • 构造器中注入用的是:@inject(TAGS.IndexService) indexService: IService;
  • 这里可看下list函数中,展示了带参数的请求的写法,标记参数用@queryParam(‘param’);
  • 这里还需要注意这个:@provideThorowable(TYPE.Controller, ‘IndexController’), 这是什么东西呢?
    这个是我们在另一个文件中定义的装饰器:
  1. // 这里用来注入容器
  2. import { fluentProvide } from 'inversify-binding-decorators'; // 流式provide
  3. const provideThorowable = (indentify, name) => {
  4. return fluentProvide(indentify).whenTargetNamed(name).done();
  5. }
  6. export {
  7. provideThorowable,
  8. }

上面这段代码其实返回了一个装饰器,这段代码的语意其实比较明确:fluentProvide(流式 提供)whenTargetNamed(当目标命名为)。
所以 @provideThorowable(TYPE.Controller, ‘IndexController’)就是说,在容器中构造一个以IndexController为名的类型是TYPE.Controller的对象。

Service

  1. import IService from '../interface/IService';
  2. import { provide } from 'inversify-binding-decorators';
  3. import TAGS from '../constants';
  4. @provide(TAGS.IndexService)
  5. export default class ApiService implements IService{
  6. getInfo() {
  7. const fakeData = {
  8. success: true,
  9. data: [{name: 'yxnne', age: 31}]
  10. }
  11. return Promise.resolve(fakeData);
  12. }
  13. }

这里我们看到仅仅使用@provide就好了,但是为什么我们在上面controller的时候,要用fluentProvide呢?
原因是,基本的@provide装饰器并不允许声明上下文约束、作用域和其他高级绑定功能。我们这里是设置了提供时机。具体可以参见文档:
http://febeacon.com/inversifyjs_docs_cn/routes/ecosystem/utilities/inversify-binding-decorators.html

入口文件

入口文件自然是:1. 构造container; 2. 构造koa实例; 3. 监听端口等一套工作:

  1. // 启动文件
  2. import 'reflect-metadata'; // 全局执行此文件代码
  3. import './ioc/loaders'; // 全局执行此文件代码
  4. import { InversifyKoaServer } from 'inversify-koa-utils';
  5. import { Container } from 'inversify';
  6. import { buildProviderModule } from 'inversify-binding-decorators';
  7. // 构造container
  8. const container = new Container();
  9. // 载入到container中
  10. container.load(buildProviderModule());
  11. // 构造koa实例
  12. const server = new InversifyKoaServer(container);
  13. const app = server.build();
  14. // 监听端口
  15. app.listen(3000, () => {
  16. console.log('inversify-ioc-koa is running...')
  17. })

关于reflect-metadata

简单来说就是叫我们的JS的反射,能像Java那样强大:
这有几篇参考:
https://www.npmjs.com/package/reflect-metadata
https://github.com/rbuckton/reflect-metadata
JavaScript Reflect Metadata 详解
详解学习Reflect Metadata