
在之前的文章《TS/JS 模块自加载与热重载》中,我简单扼要地讲述了 NodeJS 应用实现自动加载和热重载的模块处理方案,在这篇文章中,我将介绍基于前文原理实现并提供更加丰富特性的 Alar 框架。
Alar (Auto-Load And Remote) 框架的目的是提供一个约定式的模块编写方案,并在此基础上通过框架内置的引导来实现自动加载模块,并能够监听模块以便在文件发生改变的时候重载模块,然后可以在任何时候不修改代码而立即将所代理的模块转换为远程 RPC 服务运行在独立的进程或者主机中。(PS:这里的“模块”是指每一个独立的 NodeJS 应用程序文件。)
后面的代码实例将使用 TypeScript 语言,如果仅使用 JavaScript 开发,那么可以忽略所有关于类型声明的地方。
为什么用 Alar 框架来加载模块?
在 NodeJS 中,由于使用了 CommonJS 模块解决方案,require 和 import 会立即加载对应的模块,并在当前作用域创建一个引用。这会带来两个问题:
- 如果模块没有完成初始化,如递归引用(A 应用 B,B 引用 A),那么程序就会无法预期工作;
- 如果模块文件被修改,新代码无法立即运行,除非重启服务器进程。
而 Alar 框架,则选择另外的方向,基于 ES6 Proxy(和 TypeScript 命名空间),它创建了一个指向模块的弱引用,且仅在需要的时候才加载模块(A.K.A. 懒加载),它不会在当前模块中创建代理模块的引用,因此在程序文件发生变化的时候,它能够监听然后清除内存中的缓存,并促使程序重新加载模块。
如何使用 Alar 框架?
使用 Alar 框架很简单,只需要创建一个根模块代理(ModuleProxy)实例,并将其赋值到全局作用域中,这样其他程序文件就能够直接访问这个实例而不需要使用导入,并享受到声明合并(TypeScript)带来的好处。(PS:一个程序中可以存在多个代理实例,这样就可以实现代理不同的目录。)
// src/app.ts
import { ModuleProxy } from "alar";
// Expose and merge the app as a namespace under the global namespace.
declare global {
namespace app { }
}
// Create the instance.
export const App = global["app"] = new ModuleProxy("app", __dirname);
// Watch file changes and hot-reload modules.
App.watch();
Alar 框架的设计初衷是为 SFN 框架提供热重载能力的,因此在 SFN 框架中,有下面这样一些代理实例,它们分别为 SFN 框架代理不同目录下的模块,也可以作为参考案例:
global["app"] = {};
global["app"].services = new ModuleProxy("app.services", APP_PATH + "/services");
global["app"].controllers = new ModuleProxy("app.controllers", APP_PATH + "/controllers");
global["app"].models = new ModuleProxy("app.modules", APP_PATH + "/models");
global["app"].locales = new ModuleProxy("app.locales", APP_PATH + "/locales");
global["app"].utils = new ModuleProxy("app.utils", APP_PATH + "/utils");
// ...
而在其他的程序文件中,则只需要定义并导出一个默认类,并将其类型合并到命名空间 app 下,这样在其他文件中就可以直接通过命名空间来访问了。(PS:Alar 框架提供默认导出最高优先权,如果一个模块没有默认导出,那么整个模块的导出内容都会被代理,并当作一个原型模块。)
// src/bootstrap.ts
declare global {
namespace app {
const bootstrap: ModuleProxy<Bootstrap>
}
}
export default class Bootstrap {
init() {
// ...
}
}
// src/service/user.ts
// The namespace must correspond to the filename.
declare global {
namespace app {
namespace service {
const user: ModuleProxy<User>
}
}
}
export default class User {
constructor(private name: string) { }
getName() {
return this.name;
}
}
然后在其他文件中,这些被代理的模块能够直接通过命名空间访问。
// src/index.ts
import "./app";
// The instance() method will link to the singleton instance of the module.
app.bootstrap.instance().init();
// The create() method will create a new instance.
var user = app.service.user.create("Mr. Handsome");
console.log(user.getName()); // Mr. Handsome
远程服务能力
通过将本地服务转换为 RPC 远程服务,Alar 提供了快速实现分布式应用的能力。因为模块是通过 Alar 框架进行代理的,因此其内部知道模块指向的位置,对于本地服务,框架会将返回本地模块的实例,而对于注册为远程服务的模块,当调用模块的方法时,框架会自动将其转换为 RPC 远程调用,以此实现模块状态的无缝过渡。
例如,当我需要将 user 服务运行在远端时,我只需要做下面这些事:
// src/service/user.ts
declare global {
namespace app {
namespace service {
const user: ModuleProxy<User>
}
}
}
export default class User {
constructor(private name?: string) {}
// Any method that will potentially be called remotely should be async.
async getName() {
return this.name;
}
// Static method getInstance() is used to create the singleton instance.
static getInstance() {
return new this("Mr. Handsome");
}
}
需要注意的是,为了避免代码迁移,应当一开始就将服务类的方法定义为 async 修饰的异步方法。
然后编写一个程序入口文件来启动 RPC 服务器(此处使用 IPC),并注册服务。
// src/remote-service.ts
import { App } from "./app";
(async () => {
let service = await App.serve("/tmp/my-app/remote-service.sock");
service.register(app.service.user);
console.log("Service started!");
})();
然后在本地调用之前,先连接这个 RPC 服务器,并同时将服务注册到 RPC 客户端上,这样程序自己就能够实现重定向流量。
// index.ts
import { App } from "./app";
(async () => {
let service = await App.connect("/tmp/my-app/remote-service.sock");
service.register(app.service.user);
// Access the instance in local style but actually remote.
console.log(await app.service.user.instance().getName()); // Mr. Handsome
})();
关于远程服务的热重载
本地的程序能够监听本地文件的变化并重载模块和单例,但不会对远程主机上的改动有任何反应。这需要远程模块自己来做这些事情,例如上面的例子中,remote-service 也启动了文件监听,这样当其感知到改变并重载时,本地调用方法时总能够获取新的结果。
另外,RPC 客户端内部维护了短线重连机制,这也意味着,即使整个远程服务宕机了,或者手动重启了,那么在若干时间段之后,客户端会自动重新进行连接。从这个方面来说,Alar 支持的远程服务重载是双重含义的。
更多关于 Alar 框架的细节,如按主机名和端口号连接、总是连接本地服务、连接多个远程服务并自动定向等,请查看其 API 文档。
Alargithub.com如果对 SFN 框架感兴趣,以及想要了解更多关于 Alar 框架的应用,则访问 SFN 项目查看其细节。
hyurl/sfngithub.com
目前,Alar 框架应该还有一些可以优化的地方,例如 ping/pong 机制还在考虑中......