Nodejs version here: https://github.com/jctaoo/nolace
- 完成 CLI
- 完善 Http 与 WebSocket 的装饰器
- 完善函数式开发体验
- 自动扫描 Endpoint 与 Controller
- 完善 Vuepress 文档
- 编写单元测试
- 面向接口: 轻易地扩展任何组件、降低应用程序的耦合度、让维护与调试变得简单...
- 为通信工作而不是HTTP: 可以是Http,RPC,甚至可以是任何自定义的通信,而不像其他框架一样以Http为中心
- 基于Deno和Typescript: 基于Deno运行时与强大的Typescript语言,我们计划在未来将Enlace带到更多平台上
simple.ts
@MainApplication
class DemoApplication extends Application {
@AddAdaptor(HttpAdaptor)
onAddHttpAdaptor(router: Router) {
router.useEndpointOn('/', () => 'HelloWorld');
}
}
run
git clone https://github.com/2pown/enlace
cd enlace
deno run -c ./tsconfig.json --allow-net --allow-read ./demo/simple.ts
- EnlaceEnvrionment: Enlace app 的运行环境,无需用户的配置和参与
- Application: 承载了 Server 对象和依赖注入的 Injector 对象,是 enlace app 的核心
- Server: 对请求进行处理的, 分配给某个 Adaptor 或者 Server 上的 Router 对象
- Router: 完成对 Endpoint 与 Middleware 的调度
- Adaptor: 用于支持具体通信协议的适配器,Enlace 内置了 Http 与 WebSocket 的适配器
- Middleware: 对请求进行过滤与加工,与 Endpoint 之前运行
- Endpoint: 外部请求进入 Enlace app 后的处理点
- Client: 用于唯一地标记每一次请求,由 Enlace 和 Adaptor 维护。
- EndpointInput: 统一的 Endpoint 输入。
EnlaceEnvrionment 包含一个 Application,并根据 Application 里的配置来运行 Enlace app,以及自动地调用 Applicaiton 里定义的回调函数。(详见 Application)
Application 可以简单地任务是您对自己 app 的描述,其中里面包含了应用准备就绪后要做的事情,应用如何配置等的信息,而不承担开始监听端口,处理请求等的工作。
只需要定义继承自 Application 类,然后加上 @MainApplication 的注解,在运行该类所在的 ts 文件后后 EnlaceEnvrionment 就会自动调用。
当 enlace 完成准备工作可以正常接受请求的时候调用,示例代码:
@MainApplication
class DemoApplication extends Application {
onStartUp() {
console.log("startup!!")
}
...
}
在 onStartUp
之前调用,用于配置路由、中间件、依赖注入等,示例代码(省略了 DemoApplication 类的定义):
configure(injector: Injector, server: EnlaceServer) {
// 注入依赖
injector.register(SomeClass);
// 添加 HttpAdaptor 以支持对 Http 连接的处理
server.addAdaptorWithConfigure(new HttpAdaptor(), { host: 'localhost', port: 20203 })
// 对来自所有 adaptor 的所有路径的请求注册中间件
server.router.useMiddlewareOn("*", (input, next) => {
console.log("middleware");
next();
})
}
在 configure 回调里配置 adaptor 十分繁琐,而且添加不同的 adaptor 的操作极为通用,因此您可以方便地使用 @AddAdaptor 注解来完成该任务。
其中第一个参数为要添加的 adaptor 的构造器,即使该构造器有依赖(enlace 内置的依赖注入管理器会为你完成一切依赖的处理);第二个参数为 adaptor 的配置(可选的,host 与 port)。
该注解标注的方法会在需要的 adaptor 添加成功后调用,因此您可以在该方法里进行路由的配置。
示例代码(省略了 DemoApplication 类的定义):
@AddAdaptor(HttpAdaptor)
onAddHttpAdaptor(router: Router) {
router.useEndpointOn('/', () => 'HelloWorld');
}
Server 记录了所有已经注册的 Adaptor 以及它们的配置信息。
当 Enlace app 启动时,Server 的 start 函数会被 EnlaceEnvrionment 自动调用,然后 Server 会调用每个注册了的 adaptor 上的 attachOnServer 函数以开始各个 adaptor 上的自定义的对端口监听等等行为。当收到来自 adaptor 的请求,Server 会将该请求分配到自己的 Rouer 或者对应 adaptor 的 Router 对象上。
Server 会优先将请求与自己的 Router 对象相匹配,如果有匹配成功的,将不会调用在 adaptor 的 router 上注册的 Middleware 和 Endpoint
Router 记录了所有已经注册的 Endpoint 与 Middleware 以及它们各自的配置信息。Server 持有一个 Router 对象,每个 Adaptor 持有一个 Router 对象。
在 server 上的 router 注册的 Endpoint 没有明确划分使用哪个 Adaptor,因此您可以通过设置 EndpointConfig 中的 selectAdaptor 来动态确定需要该 Endpoint 接收来自哪个 adaptor 的数据。
当接收到来自 Server 的请求后,Router 会找出该请求符合要求的 Middleware 和 Endpoint,其中 Middleware 可以有多个,但 Endpoint 只有一个,然后对 Middleware 进行调用,接下来是 Endpoint。
Router 对象不需要用户手动创建,您只需要在你想要的 adaptor 上的 router 对象上注册您的 Middlewear 或者是 Endpoint。
Adaptor 并不需要用户编写,而是由具体协议的实现者与 Enlace 的作者编写。目前 Enlace 里内置了 Http 和 WebSocket 的 Adaptor。
每个 Adaptor 都需要实现 Adaptor 抽象类,该抽象类定义如下:
export abstract class Adaptor {
// 每个 adaptor 需要手动维护每个连接与连接是的输入的关系,当某个连接不再有效时需要删除
clientToInput: Map<Client, GenericEndpointInput> = new Map();
// 定义该通信协议的初始化工作,比如对于 Http 来说就是监听端口
attachOnServer(server: EnlaceServer, configure: AdaptorConfigure): void;
// 定义如何将信息发送给客户端,可以在此处合理利用 clientToInput
abstract sendToClient(client: Client, content: unknown): void;
// 该字段的值由 Server 提供,只需要在收到请求后调用该函数,Server 就可以收到
didReceiveContent: (input: GenericEndpointInput, client: Client) => void = () => { };
}
您需要将您想在您的 app 中使用的通信协议告诉 Enlace,方法就是在在 Application 类中将具体的 Adaptor 实例注册到 Server 上(参见 Application):
configure(injector: Injector, server: EnlaceServer) {
// 添加 HttpAdaptor 以支持对 Http 连接的处理
server.addAdaptorWithConfigure(new HttpAdaptor(), { host: 'localhost', port: 20203 })
}
// 或者
@AddAdaptor(HttpAdaptor, { host: 'localhost', port: 20203 })
onAddHttpAdaptor(router: Router) {
// ......
}
对于 Middleware,没有什么特别的,有亮点需要注意的就是:
- 理论上 Middleware 执行的顺序应该是注册 Middleware 的顺序,但 Server 并不刻意维护 Middleware 的顺序,因此在编写 Middleware 时不应该假设自己的运行顺序(详情参见定义和扩展中间件指南)
- Middleware 于 Endpoint 之前运行
Middleware 的原型如下(被定义成只能用作函数形式是因为我们希望每个 Mddleware 足够简单,毕竟处理请求的主角是 Endpoint):
type MiddleWare = (input: GenericEndpointInput, next: Function) => void | Promise<void>;
于 Endpoint 的使用基本相同,示例如下:
// 类定义,使用构造器
adaptor.router.useMiddlewareOn("/", MiddleWare);
Endpoint 是一个具体处理外来请求的对象,原则上每个 Endpoint 定义
- 想要处理的请求是来自哪个 adaptor
- 请求 path 遵循的规则
然后 Server 会根据 adaptor 传入的请求数据对 Endpoint 进行调用。
值得注意的是,每个请求到最后只会有一个 Endpoint 处理。
具体通信协议的 adaptor、 endpoint 的接口以及 EndpointInput 中的内容均由具体通信协议的实现者提供,详情参见定义和扩展通信协议指南
由于 Enlace 的目标是处理通信而不是简单地处理 Http,因此合理地对处理请求的 Endpoint 对象进行抽象就显得至关重要,Enlace 需要赋予 Endpoint 处理各种通信的能力。
以两个比较常见的网络协议为例子:
- Http: 这是一个
半双工
的网络协议,只需要处理外来请求,而不需要考虑主动地将信息传送到客户端,为此,Enlace 提供了NormalEndpoint
,NormalEndpoint
的定义如下:
abstract class HttpEndpoint extends NormalEndpoint {
abstract receive(input: HttpEndpointInput): any | Promise<any>;
}
- WebSocket: 与 Http 不同,这是一个
全双工
的网络协议,需要处理主动像客户端推送信息的情况,为此,Enlace 提供了KeepAliveEndpint
,KeepAliveEndpoint
的定义如下:
export abstract class KeepAliveEndpoint extends ClassEndpoint {
clients: Client[];
abstract receive(input: GenericEndpointInput): void;
broadcast(message: unknown, clients: Client[]): void
sendMessageToClient(message: unknown, client: Client): void
}
您可以使用 broadcast 和 sendMessageToClient 方法来对指定客户端主动推送信息。
其中,clients 代表当前连接在改端点上的连接,由实现具体网络协议的 adaptor 来维护,您只需要直接使用就好。
Endpoint 的定义是多样的,您可以使用类的方式,也可以使用定义函数的方式。
下面是分别使用两种方式定义 Endpoint 的等效程序。
- 类:
class SimpleEndpoint extends HttpEndpoint {
receive(input: HttpEndpointInput): string {
const name = input.query("name");
return `Hello ${name}`;
}
}
- 函数:
注意: 目前为止, 函数 Endpoint 并不能支持 KeepAliveEndpint 里的 sendMessageToClient 等方法。(我们已经将该目标添加到待办!)
function(input: HttpEndpointInput): string {
const name = input.query("name");
return `functional endpoint name: ${name}`;
};
总有些 Endpoint 之间会有些共同点,比如一组 Endpoint 用于支撑用户系统,一组 Endpoint 用于支撑支付系统。
因此,Enlace 提供了定义一组 Endpoint 的方式 --- Controller
。
使用 Controller
需要使用 Typescript 的注解,示例如下:
// TureFunction 是永远返回 True 的函数,此处说明该 Controller 接受来自所有 adaptor 的请求
// expectedPath 配置了该 Controller 所期望的请求路径,只有遵循 expectedPath 的请求才会进入该 Controller
@ControllerMapping({ expectedPath: "/", selectAdaptor: TrueFunction })
class HelloController {
// 此处的 expectedPath 是基于 Controller 的 expectedPath 之上的,简单来说,这是相对路径而不是绝对路径
// 对于 selectAdaptor, 只有当请求通过了 Controller 的 selectAdaptor,才会尝试通过 Controller 里标注了 Endpoint 的方法上的 selectAdaptor
@Endpint({ expectedPath: "/hello", selectAdaptor: TrueFunction })
hello(): string {
return "Hello World"
}
}
单个 Endpoint 与 Controller 的使用完全相同,示例如下:
// 类定义,使用构造器
adaptor.router.useEndpointOn("/", SimpleEndpoint);
// 类定义,使用实例
adaptor.router.useEndpointOn("/", new SimpleEndpoint());
// 函数式定义
adaptor.router.useEndpointOn("/", FunctionEndpoint);
// 使用 Controller
adaptor.router.useEndpointOn("/", new HelloController())
comming soon...
MIT © 2pown