php hapijs,使用typescript实现依赖注入框架

首先思考一个问题:我们为什么需要依赖注入(Dependency injection下面简称DI)?

之前用java的spring、php的laravel和angular时发现它们的模式非常相似,框架会把请求处理、线程管理、错误处理等都封装好,你只需要实现对应的横向和纵向切面,然后让框架来管理和调用你的代码,这就是设计模式中有名的控制反转(简称IOC)。

而DI是IOC的一种比较通用的实现方式,举个例子我们的web服务中有controller(接口层)和service(业务逻辑层),我们需要在controller中调用service的代码,但是service一般会有上下文(context)(比如使用了当前的请求对象、数据库连接、全局参数等)。如果我们每次在调用service时都要手动给它这么多参数实在太麻烦了,而且代码会很耦合。此时DI就能解决这个问题了,我们只需要声明需要的对象,框架就能自动创建好带有上下文对象。那么下面我们来看看怎么用ts实现一个简单的依赖注入框架。

下文写的时候我还没有使用过nodejs写过复杂的后端服务,所以造了个简单的轮子来梳理项目代码,使用的hapijs社区也不太活跃,所以本文仅适合作为参考和学习使用。要使用nodejs开发大型应用的话建议使用nest.js或者eggjs。

核心API

先看看实现的API长什么样

import * as Knex from 'knex';

import { autowired, impl, context } from '../injection';

class MyController {

@autowired userRepository: IUserRepository;

getUsers() {

return this.userRepository.getAllExistUsers();

}

}

// 这里用抽象类来表示接口(下面会通称为“接口”)

abstract class IUserRepository {

abstract getAllExistUsers(): PromiseLike;

}

@impl(IUserRepository)

class UserRepositoryImpl extends IUserRepository {

@context('knex') knex: Knex;

getAllExistUsers() {

return this.knex('users').select().where('deleted', false);

}

}

这里的API设计稍微参考了下spring,还有一些妥协设计(比如为什么要用abstract class而不用interface、为什么 @impl 需要传入对应接口),这些下面会解释。

API实现原理

这里虽然实现了3个decorator,但是这些decorator的作用其实和java里的annotation一样 —— 定义metadata,所以实现上很简单,基本上都是一句话就能讲清楚里面的逻辑:

@autowired (需要自动注入的变量):把当前的property key('userRepository')以及对应的type(IUserRepository)存到当前类的metadata中,方便后面注入的时候传入。

@impl (实现某个接口的类):将当前的接口和类保存到一个全局Map。

@context(需要注入当前应用上下文的变量):将当前key('knex')与需要注入的context key('knex')保存到当前类的metadata

下面是autowired的实现

export const metaKey = Symbol('autowiredKeys');

interface IAutowiredKey {

// 字段名

key: string;

// 对应类型,通过metadata返回的类型必定是Object与其子类

type: Function;

}

export default function autowired(target: any, propertyKey: string) {

const autowiredKeys = getAutowiredKeys(target);

// 得到当前装饰成员变量的类型

const type = Reflect.getMetadata('design:type', target, propertyKey);

autowiredKeys.push({ key: propertyKey, type });

// 将变量保存到当前类的metadata里

Reflect.defineMetadata(metaKey, autowiredKeys, target);

}

/**

* 拿到在当前类上定义的需要自动注入的key和type

*/

export function getAutowiredKeys(target: any): IAutowiredKey[] {

return Reflect.getMetadata(metaKey, target) || [];

}

Typescript metadata

typescript可以通过metadata拿到3种类型信息

对象上的成员变量类型

函数的参数类型

函数的返回类型

但是又有非常大的限制,可以看一下这一节文章,简单来说就是拿不到 interface 的类型,而 abstract class 可以,所以使用中需要用 abstract class 来代替 interface 。

另外关于 @impl 为什么要传入对应接口,主要是因为如果不传入接口的话,在注入@autowired变量时,我必须要遍历被@impl装饰的类来判断其是否是该变量类型的本身或者子类。

这里可能会出现一个问题,如果@autowired的变量类型是interface啥的话,由于上面提到的限制我只能拿到 Object 这个类型,由于所有类都是其子类,所以就会注入错误的类型了。

注入

关于@autowired字段的注入实现非常简单,实现以下几步就行了:

拿到对象需要注入的字段及其类型

根据类型判断并创建需要注入的对象

递归注入上一步生成的对象,并注入上下文

将生成的对象传给成员变量

const implMap: Map = new Map();

export default function impl(p1: T) {

return function (ctor: C) {

implMap.set(p1, ctor);

}

}

export function injectAutowired(target: any, context: { [key: string]: any }) {

const needAutowiredKeys = getAutowiredKeys(target);

needAutowiredKeys.forEach(({ key, type }: { key: string, type: any }) => {

const ctor = implMap.get(type);

let inst = null;

if (ctor && typeof ctor === 'function') {

inst = new ctor(context);

} else {

// type must be Object

inst = new type(context);

}

injectAutowired(inst, context);

injectContext(inst, context);

target[key] = inst;

});

}

路由设计

路由层参考laravel框架,因为我个人认为将路由放在一个地方同一管理比spring那种分散到Controller上定义要方便索引(api -> controller)。

提供的API如下

import { Route } from '../injection';

const route = new Route();

// 设置放置controllers的目录,默认是 ${work directory}/controllers

route.setControllersRoot('server/controllers');

// 指定Controller的method作为handler

route.post('/apples/{id}', 'SampleController@updateApple');

route.get('/users', 'SampleController@getUsers');

// 直接传入函数作为hanlder

route.match(['get', 'post'], '/healthz', () => 'ok');

// prefix

route.prefix('admin').group((r) => {

r.post('users/{id}/ban', 'AdminController@banUser');

})

export default route;

这里除了将 Controller 引入并绑定到对应的 path 上外,还要检测对应的方法是否存在,这样就能将错误放在程序启动时而不是运行时抛出了。

接口层的IO

目前设计的API如下

// controller内

class MyController {

getUsers(@param id: number, @query detail: boolean = false, @payload body: Object) {

return {

users: []

};

}

getUser(@query('name') userName: string, request: Hapi.Request, h: Hapi.ResponseToolkit) {

return {

users: []

};

}

}

// 直接传入路由的函数

route.get('welcome/{name}', (name: string) => {

return {

name,

message: `welcome ${name}`

}

});

这里有3个decorator,分别代表 路径参数(@param)、查询参数(@query)、和请求体(@payload),作用同样是设置metadata。另外有一些框架特定类型的参数(Hapi.Request和Hapi.ResponseToolkit),是为了支持更加特殊的需求。

对于直接传入路由的函数,我对其的定位是“不需要复杂输入的简单逻辑”,所以只会把路径参数的指根据顺序传进去。

注入数据时需要考虑参数类型,我这里定了几个规则:

如果类型是 string、number、boolean,那么需要将数据转为对应的基础类型

如果类型是一些特定类型,比如Hapi.Request,那么由对应框架的bind来判断注入

如果类型是 Object(可能是object、interface等),那么将数据原样返回

如果类型是 Function(class),分为以下的情况先new对应的类,如果注入的数据不是基础类型,并且对应的class的构建函数没有参数,那么将注入数据Object.assign给新建对象

如果对应的class的构建函数有参数,或注入的数据是基础类型,那么将注入数据传入class的构建函数

返回类型和异常处理都是目前是由Hapi.js自己处理的,还没研究过express这些库的处理方式,不过应当遵循下面的规则:

返回类型应当支持所有能JSON序列化的值和Promise。

抛出异常应当可以直接throw,并有一个统一处理方法

项目结构

因为对于依赖注入的API来说controllers、services和repositories都是一样的,所以项目结构其实可以由自己的项目情况决定,不过建议分为以下几个层面:

controllers: 负责接口IO处理,表单验证,流程控制

services: 负责业务模块逻辑

repositories: DAO层,负责与数据库打交道

models: 数据模型

routes.ts: 定义路由

app.ts: 项目的启动、配置

绑定Hapi.js

目前在项目里用到的service端实现是hapi.js,所以讲讲injection与hapi.js的bind需要实现的功能:

根据路由配置生成hapi的路由配置

在handler里注入所有的接口依赖、上下文依赖以及方法的参数依赖

import { injectAutowired, injectContext, callHanlderWithInjection } from '../injection';

// 生成Hapi route handler的函数

function createControllerHandler(Controller: T, methodName: string, context: { [key: string]: any }) {

return (request: Hapi.Request, h: Hapi.ResponseToolkit, err?: Error): Hapi.Lifecycle.ReturnValue => {

// 将请求对象绑定到当前上下文

const contextInLifecycle = Object.assign({ request }, context);

const c: any = new Controller(contextInLifecycle);

injectAutowired(c, contextInLifecycle);

injectContext(c, contextInLifecycle);

return c[methodName](request, h, err);

};

}

Todo路由层的权限控制

更加通用的参数验证

更加通用的错误处理

更加通用的Request与Resposne结构

DAO层使用ORM

实现Laravel里的Facades模式?

利用typescript的compiler解决上面的局限问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值