面向复杂应用,Node.js中的IoC容器 -- Rockerjs/core
Rockerjs Core
基于 TypeScript 和注解的轻量级IoC容器,提供了依赖注入、面向切面编程及异常处理等功能。Rockerjs Core可在任意工程中引入,是一个框架无关的IoC容器。
@rockerjs/core模块不依赖于任何框架,并与现有框架、库、类等保持兼容。通过DI(Dependency Injection)实现代码解耦和依赖解耦,在构建复杂应用时保证可扩展性与灵活性;同时提供二维编程的能力,基于注解可在各个连接点(Advice)进行非核心业务的操作,减少代码冗余;最后,它提供一种基于注解配置的简易异常处理机制 -- Clamp机制,通过特定规则匹配异常处理程序实现处理。
一、快速上手
安装
npm install @rockerjs/core
@rockerjs/core最佳实践需要结合TypeScript的装饰器一起使用(也可使用接口),因此需要在项目根目录添加 tsconfig.json 文件,并配置编译选项 “experimentalDecorators”和“emitDecoratorMetadata”为 true
示例 1
import { Container, Inject } from '@rockerjs/core';
class User {
id: string = "testId";
name: string = "testName";
}
class UserService {
getUser(_id: string): User {
return new User();
}
}
@Inject
class ControlDefault {
@Inject
userService: UserService;
test() {
let user: User = this.userService.getUser("test");
console.log(user);
}
}
@Inject('controllor-with-args', new Date())
class ControlDefaultWithArgs {
name: string;
time: Date;
constructor(name: string, time: Date) {
this.name = name;
this.time = time;
}
@Inject
userService: UserService;
test() {
let user: User = this.userService.getUser("test");
console.log(user, this.name, this.time);
}
}
@Inject('controllor1', 'util', new Date())
class Control {
name: string;
time: Date;
constructor(name: string, time: Date) {
this.name = name;
this.time = time;
}
@Inject
userService: UserService;
test() {
let user: User = this.userService.getUser("test");
console.log(user, this.name, this.time);
}
}
// 通过getObject接口从容器中获取实例,参数为“单例的名称”(默认名称为类名首字母小写)
Container.getObject<ControlDefault>('controlDefault').test();
// 通过getObject接口从容器中获取实例,此例中并未提供实例名
Container.getObject<ControlDefaultWithArgs>('controlDefaultWithArgs').test();
// 通过getObject接口从容器中获取实例,此例中提供了3个参数,@rockerjs/core 认为第一个参数为实例名,剩下的参数则用于实例化
Container.getObject<Control>('controllor1').test();
示例 2 : RPC
import {Container, Inject} from '@rockerjs/core';
//PRC Demo实现
let RPC = {
config: function (cfg: { serviceUrl: string, interfaces: Function[] }) {
if (cfg.interfaces) {
cfg.interfaces.forEach((type: FunctionConstructor) => {
if (type.prototype) {
let newObj = {}, proto = type.prototype;
let nms = Object.getOwnPropertyNames(proto);
if (nms) {
nms.forEach((nm) => {
if (nm != 'constructor' && typeof(proto[nm]) === 'function') {
newObj[nm] = function () {
//{nm:方法名,arguments:参数表},改为调用远程请求过程
return arguments[0];//test return
}
}
})
}
Container.provides([type, () => {
return newObj;
}])
}
})
}
}
}
//--DEMO--------------------------------------------------------
//1. 接口声明(注意,此处只能使用Concrete class)
class Product {
getById(id: string): string {
return null;
}
}
//2. 应用RPC Framework
RPC.config({
serviceUrl: null,
interfaces: [Product]//提供接口描述,在RPC中构建factory
})
//3. Service class
@Inject
class Service {
@Inject
product: Product;
test() {
let id: string = 'tid';
let rst = this.product.getById(id);
console.log(rst);
}
}
//4.测试
Container.getObject<Service>('service').test();
二、依赖注入与容器
依赖注入 @Inject
提供了注解 @Inject
来实现依赖的注入,当我们有如下 GetDubboData
类时
class GetDubboData {
p0: number;
constructor(p0: number, p1: string) {
this.p0 = p0;
}
}
我们可以通过以下方式实例化这个类,同时传入指定的参数
直接传递构造函数的参数
class SomeControl {
@Inject(1, 'aaa')
private dubbo: GetDubboData
}
给出构造函数的工厂函数
class SomeControl {
@Inject(function () {
return [1, 'aaa']
})
private dubbo: GetDubboData
}
无构造函数或参数为空
class SomeControl {
@Inject
private dubbo: GetDubboData
}
操作类实例化容器
默认的实例化方法可以满足开发者的大部分需求,Rockerjs Core 提供了 provides 方法自定义实例化工厂,同时提供了获取类和类实例化函数映射表的方法。
注册、修改类的实例化方法
直接传入类或工厂函数
// 形式一:形如 Container.provides(class extends UserService{})
Container.provides(
class extends UserService {
getUser(id: string): User {
console.log(1);
return new User();
}
}
);
传入类及类的工厂函数
// 形式二:形如 Container.provides([UserService,FactoryFunction])
Container.provides([
UserService,
() => {
return new class extends UserService {
getUser(id: string): User {
console.log(2);
return new User();
}
}();
}
]);
获取实例化方法注册表
getGeneralHashmap()
返回一个构造函数-工厂方法映射表, 结构如下
const globalGeneralProviders: Map<FunctionConstructor, Function> = new Map<
FunctionConstructor,
Function
>();
手动实例化方法
Container.injectClazzManually
方法提供了直接实例化注册表中的类的功能,参数为构造函数以及想要传入的参数
class SomeControl {
transGet: GetTransData = Container.injectClazzManually(GetTransData, 1, 2);
async getProduct(_productId?: number) {
let json: any = await this.transGet.getDetail(_productId);
console.log(json);
}
}
完整例子
假设我们有一个获取异步数据的抽象类
abstract class GetTransData {
p0: number
constructor(p0: number, p1: string) {
console.log(p0 + p1)
this.p0 = p0
}
abstract async getDetail(_proId: number): Promise<string>;
}
可以通过 Container 的 provides
API 来指定对应类型的工厂函数
Container.provides([GetTransData, (_p0, _p1) => {
return new class extends GetTransData {
constructor(p0: number, p1: string) {
super(p0, p1);
}
async getDetail(_id: number): Promise<string> {
await ((ms) => new Promise(res => setTimeout(res, ms)))(100)
return `Hello ${this.p0}`
}
}(_p0, _p1);
}]);
最终通过 @Inject
方法注入在测试类里面实例化这个对象
@Inject
class SomeControl {
@Inject(666, 2)
transGet: GetTransData;
async getProduct(_productId?: number) {
let json: any = await this.transGet.getDetail(_productId);
console.log(json);
}
}
Container.getObject<SomeControl>('someControl').getProduct();
得到输出结果
668
Hello 666
三、面向切面编程 AOP
面向切面编程(AOP是Aspect Oriented Program的首字母缩写)是指在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想。Rockerjs Core 提供了 AOP 编程能力
简单的例子
假如我们想在下面的 foo
方法执行前后打点
class Test {
foo() {
console.log('foo')
}
}
new Test().foo()
我们可以声明一个日志类,通过@Aspect
注解声明其为一个切面类,通过@Pointcut
注解配置想要匹配的类、方法以及需要执行的钩子, 最后通过 @Before
和@After
等注解标识被修饰方法在处于对应生命周期时需要执行的方法
import { Aspect, Pointcut, Before, After } from "@rockerjs/core";
@Aspect
class Logger {
// 必须在静态方法上注册切点
@Pointcut({
clazz: Test, // 定位被修饰的类
rules: ".*foo.*", // 通过正则匹配到对应的方法,不填则匹配所有函数
advices: ["before:printStart", "after"] // 过滤将要执行的钩子 (可细致到函数名)
})
static main() {}
// 在执行被打点方法前执行的方法
@Before
printStart() {
console.log("log:start:", new Date());
}
// 可以指定多个方法
@Before
printStart2() {
console.log("log:start:", new Date());
}
// 在执行被打点方法后执行的方法
@After
printEnd() {
console.log("log:end:", new Date());
}
}
必须在切面类的静态方法上注册切点
Advices(可理解为生命周期,下文用生命周期代指advice)列表
Rockerjs Core 提供了Before
, After
,After_Throwing
, After_Returning
,Around
等生命周期
- Before:在被打点函数执行前执行
- After:在被打点函数执行后执行
- After_Throwing:在被打点函数抛出异常时执行
- After_Returning:在被打点函数返回结果后执行
- Around:在被打点函数执行前后执行,类似于 koa 中间件
@After_Returning
- 在 after 后执行
- 如果原生函数没有 return 任何东西则不执行
- 可以修改返回结果
@After_Returning
printReturn(ctx, result) {
// ctx 为函数执行上下文
// result 为函数执行的结果
result += 666
return result
}
@After_Throwing
@After_Throwing
printthrow(ctx, ex) {
// ctx 为函数执行上下文
// ex 为错误信息
console.log('Loggy catch: '+ ex.message);
console.log(ctx)
}
@Around
@Around
currentTime2(ctx, next) {
// ctx 为函数执行上下文
// next 为匹配到的函数
console.log('before',Date.now());
let ret = next();
console.log('after',Date.now(),ret);
return ret;
}
切面组合
我们可以为某个类同时注册多个切面类,再通过 composeAspects
方法将它们组合起来,默认按照声明的顺序来包裹被打点的函数,最后声明的类会包裹在最外面一层
@Aspect()
class Logger {
// ...
}
@Aspect()
class Logger2 {
@Pointcut({
clazz: Test,
advices: ["before", "after", "around", 'after_returning']
})
static main() {}
@Before
printStart() {
console.log("2:start");
}
@After
printafter() {
console.log("2:after");
}
@After_Returning
printReturn(ctx, result) {
console.log('2:after_returning', result)
return result + 2
}
@Around
printAround2(ctx, next) {
console.log("2:around:before");
let ret = next();
console.log("2:around:after", ret);
return ret;
}
}
@Aspect()
class Logger3 {
// ...
}
composeAspects({
clazz: Test,
// rules: '.*foo.*',
aspects: [Logger, Logger2, Logger3]
});
执行结果如下:
3:start
2:start
1:start
3:around:before
2:around:before
1:around:before
foo
1:around:after bar
2:around:after bar
3:around:after bar
1:after
2:after
3:after
1:after_returning bar
2:after_returning bar
3:after_returning bar
如果想自定义切面之间执行的顺序,可以在切面注解上传入切面的次序(数值小的在洋葱模型的外层):
@Aspect({
order: 2
})
class Logger { }
@Aspect({
order: 1
})
class Logger2 { }
@Aspect({
order: 3
})
class Logger3 { }
composeAspects({
clazz: Test,
aspects: [Logger, Logger2, Logger3]
});
执行顺序如下:
2:start
1:start
3:start
2:around:before
1:around:before
3:around:before
foo
3:around:after bar
1:around:after bar
2:around:after bar
3:end
1:end
2:end
四、异常处理 Exception
除了通过 Rockerjs Core AOP 中的 @After_Throwing
注解来实现错误捕获,我们还提供了更简便的实现错误捕获的方法,如下例,我们先声明一个错误捕获夹,然后在被包裹的函数上使用这个错误捕获夹,当函数执行过程中有异常发生时,我们能在捕获夹的 catch 方法中拿到错误信息以及函数执行的上下文。
import { Container, Inject, Catch, Clamp, ExceptionClamp } from "@rockerjs/core";
// 1. 声明一个捕获器,实现 catch 方法
@Clamp
class Clamper extends ExceptionClamp {
catch(ex, ctx) {
console.log("hahaha: ****", ex, ctx);
}
}
@Inject
class Test {
// 2. 使用捕获器
@Catch("Clamper")
test() {
throw new Error("12322");
}
}
Container.getObject<Test>('test').test();
与 @After_Throwing
同时使用时,@Catch
会先捕获到错误,再次将错误抛出, @After_Throwing
才捕获到错误
@Clamp
class Clamper extends ExceptionClamp {
catch(ex, ctx) {
console.log("hahaha: ****", ex, ctx);
throw ex // 将错误二次抛出
}
}
@Inject
class Test {
@Catch("Clamper")
test() {
throw new Error("12322");
}
}
@Aspect
class ExceptionClamp2 {
@Pointcut({
clazz: Test,
advices: ['after_throwing']
})
static main() {}
@After_Throwing
printThrow(ctx, ex) {
console.log('Loggy catch: '+ ex.message);
console.log(ctx)
}
}
Container.getObject<Test>('test').test();
Contribute
请参考 Contribute Guide 后提交 Pull Request。
面向复杂应用,Node.js中的IoC容器 -- Rockerjs/core的更多相关文章
- 在node.js中,使用基于ORM架构的Sequelize,操作mysql数据库之增删改查
Sequelize是一个基于promise的关系型数据库ORM框架,这个库完全采用JavaScript开发并且能够用在Node.JS环境中,易于使用,支持多SQL方言(dialect),.它当前支持M ...
- 如何在Node.js中合并两个复杂对象
通常情况下,在Node.js中我们可以通过underscore的extend或者lodash的merge来合并两个对象,但是对于像下面这种复杂的对象,要如何来应对呢? 例如我有以下两个object: ...
- Node.js中的Session,不要觉得简单哦。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,博客地址为http://www.cnblogs.com/jasonnode/ .学习网站上有对应 ...
- Node.js 中MongoDB的基本接口操作
Node.js 中MongoDB的基本接口操作 连接数据库 安装mongodb模块 导入mongodb模块 调用connect方法 文档的增删改查操作 插入文档 方法: db.collection(& ...
- 在node.js中使用COOKIE
node.js中如何向客户端发送COOKIE呢?有如下两个方案: 一.使用response.writeHead,代码示例: //设置过期时间为一分钟 var today = new Date(); v ...
- 初步揭秘node.js中的事件
当你学习node.js的时候,Events是一个非常重要的需要理解的事情.非常多的Node对象触发事件,你能在文档API中找到很多例子.但是关于如何写自己的事件和监听,你可能还不太清楚.如果你不了解, ...
- Node.js权威指南 (10) - Node.js中的错误处理与断言处理
10.1 使用domain模块处理错误 / 272 10.1.1 domain模块概述 / 272 10.1.2 创建并使用Domain对象 / 274 10.1.3 隐式绑定与显式绑定 / 276 ...
- Node.js中的URL
Node.js中的URL 什么是URL URL是Uniform Location Resource的缩写,翻译为"统一资源定位符",也就是描述资源位置的固定表示方法.被URL描述的 ...
- mysql语句在node.js中的写法
总结一下mysql语句在node.js中的各种写法,参考了npm网站mysql模块给的实例. 查询 select //1 db.query('select * from tuanshang_users ...
随机推荐
- 使用vue实现tab栏的点击切换样式
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8&quo ...
- [Day22]IO(File、递归)
1.File 1.1 IO概述 (1)当需要把内存中的数据存储到持久化设备上的这个动作称为输出(写)Output操作 (2)当把持久设备上的数据读取到内存中的这个动作称为输入(读)Input操作 1. ...
- bug和注意事项
bug: 1.新增角色,在选择权限树的时候,如果不选择根目录下的第一个节点,保存后,权限树会打不开. 2.文档页面有两个大字段,即ueditor编辑器的时候,保存后回显会有问题 不过一个页面有两个大文 ...
- VUE-001-在表格单元格(el-table-column)中添加超链接访问
在进行前端网页开发时,通常列表数据我们使用table展示.那么如何在 el-table-column 单元格中使用超链接呢? 如下即是解决方式的一种: 仅需要将如下代码: <el-table-c ...
- linux touch命令 创建文件
touch 创建文件,用法,touch test.txt,如果文件存在,则表示修改当前文件时间 [root@MongoDB ~]# touch /data/text.txt [root@MongoDB ...
- LINQ交集/并集/差集/去重
using System.Linq; List<string> ListA = new List<string>(); List<string> ListB = n ...
- 使用springmvc进行文件的上传和下载
文件的上传 SpringMVC支持文件上传组件,commons-fileupload,commons-fileupload依赖commons-io组件 配置步骤说明 第一步:导入包 commons-f ...
- 【git】强制覆盖本地代码(与git远程仓库保持一致)
git强制覆盖: git fetch --all git reset --hard origin/master git pull git强制覆盖本地命令(单条执行): git ...
- jQuery通过ajax请求php遍历json数组到table中的代码
html代码(test.html),js在html底部 具体代码如下所示: <!DOCTYPE html> <html lang="en"> <hea ...
- win10家庭版删除文件提示没有权限最简单的方式
1.cmd 2.右键-以管理员身份运行(重要) 3.输入:net user administrator /active:yes,开启超级管理员账号 4.win+r键打开运行对话框,输入 netplwi ...