laf 的架构图
Laf 当前使用 Node.js 做为云函数运行时环境,从架构图可以看到执行用户云函数的请求会被路由到用户对应的app(Node.js 运行时)中,最后这个函数(代码)在该 app 中被执行。
在 laf 中有两种路由,一种是 apisix 路由负责路由静态资源托管,函数执行等请求,另一种是 laf server web 框架的路由,负责laf server的各种 api。
下面我们抛开请求的路由过程,以及 app 的其他资源如数据库资源(mongodb),静态资源托管(minio)等,直奔主题,如何手撸一个 laf 的 Node.js 运行时。
laf 的运行时与其他函数计算平台的区别
Laf 函数计算的运行时与其他函数计算平台的运行时的核心区别在于 laf 的运行时是常驻的。
常驻代表你的 app 就可以做很多高级的事情,比如支持 WebSocket 长连接,支持全局缓存,无冷启动等等。
其他很多函数计算平台的运行时采用的是冷启动方案,就是有请求到达后才会去准备运行时的资源,创建运行时的资源,当没有请求,应用不活动时,资源会被销毁,虽然这样可以节省内存资源,但无法支持很多高级的功能,导致开发习惯的割裂,还有众多开发者接受不了的响应时长。
手把手写一个 laf 运行时
首先我罗列一下写一个 laf 运行时需要完成哪些东西
- 包装一个 web 框架
- 解决依赖安装,和网络文件的 import
- 让功能更完善,包装更多的 http 请求信息,提供更多的函数运行上下文
- 写更多的 sdk,让你的运行时可以对接其他资源,例如数据库
- 优化 runtime,比如缓存数据,减少对数据的 io 等
- 提供对云编辑器的支持
现在我们开始动手了,我们写一个 mini 版的 laf 函数计算运行时,完整版本的可以去 laf 的仓库看看。
要运行这段代码首先得安装下 express,然后 node 执行下面的代码就行了,laf 的 Node.js 运行时用 vm 模块执行云函数,这样会有不错的安全性。
下面的代码用 database 模拟将函数存储在数据库中,通过自定义的 import 解决网络文件的导入。
在解决导入文件可以思考一下如何解决循环依赖,这部分代码就没展示了,仓库里有,提示一下,定义一个列表,每次导入就把导入的模块名 push 到列表中,这样下次导入的模块如果列表有那就是循环依赖了。
const express = require("express");
const bodyParser = require("body-parser");
const vm = require("vm");
const app = express();
const port = 3000;
const database = {
cloud_functionb: `
function funcB(query, body) {
return "Received Query: " + JSON.stringify(query) + " Body: " + JSON.stringify(body);
}
module.exports = funcB(context.query, context.body);
`,
cloud_functiona: `
const resultFromFuncB = require('cloud_functionb');
function funcA(query, body) {
return "From funcA with result: " + resultFromFuncB;
}
module.exports = funcA(context.query, context.body);
`,
};
function customRequire(moduleName, context) {
if (moduleName.startsWith("cloud_")) {
const script = new vm.Script(database[moduleName]);
const moduleExports = {};
const customContext = {
module: { exports: moduleExports },
exports: moduleExports,
console: console,
require: (moduleName) => customRequire(moduleName, context),
context,
};
script.runInNewContext(customContext);
return customContext.module.exports;
} else {
return require(moduleName);
}
}
app.use(bodyParser.json());
app.all("/*", (req, res) => {
const funcName = req.path.substring(1);
const context = {
query: req.query,
body: req.body,
};
const result = customRequire(funcName, context);
res.send(result);
});
app.listen(port, () => {
console.log(`App listening at http://localhost:${port}`);
});
这个 mini 版的运行时为了足够简单,省去了很多东西,比如没有缓存数据,数据库连接的优化,sdk 的开发,提供云编辑器支持等。
这些在 laf 的源码里都有,如何提供对云编辑器的支持,可以看看 language-server-protocol
Laf 也快支持 LSP 了,具体实现可以参考我们之前分享的:如何创建集成 LSP 的 Web 代码编辑器。