多年前写的 Koa Js 服务,本来跑在服务器上,要一点花销。后来决定薅各大云厂商的羊毛,就将它容器化了,跑在免费的 Okteto 提供的 K8S 环境里。但是最近 Okteto 提供的 url 访问不了了,虽然发了邮件请 Okteto 帮忙协助,但是本着狡兔三窟的原则,决定再部署一个实例到其他的服务上。

于是将目光瞄准到 Vercel,Vercel 提供免费的 Serverless Function,虽然类似 AWS Lambda,但又不太一样,所以记录一下。如果只是将 koa 服务部署到 AWS lambda,那么只需要使用一个 serverless-http 的框架转化一下就好。关于薅 AWS Lambda 羊毛的文章已经写过多篇,参见:

https://zhuanlan.zhihu.com/p/415577362

https://zhuanlan.zhihu.com/p/351889768

Koa Js

Koa Js 是由 Express Js 的原班人马设计的 Web 框架,更小巧但是不再捆绑任何中间件,这一点也是我在将它部署到 Vercel 时碰到坑的原因。尽管它比 Express 新,但 Express.js 仍然更加流行,也许这也是 Vercel 内置对 Express.js 的支持的原因吧。

Express Js 目前的星标数在 57.6 K,而 koa 的星标数大约是它的一半多一点:

image.png

image.png

看了一下 koa 的贡献者列表,我居然也名列其中呢:

image.png

Vercel

Vercel 是一个用来部署前端应用的云平台,但也可以用来构建轻量级的事件驱动 API,并部署到它们的全球边缘网络。

Vercel 的 Serverless Function 有些特别

传统 API 托管在运行着的服务器上。当应用需要扩展时,希望更低成本、更灵活、更安全、资源快速分配并启动等等,使用传统服务器很难做到。但是用 Serverless 就比较容易,因为仅仅是一些后端代码片段在无状态环境中运行着,它们由事件(比如 http 请求)触发并只在一次调用中存活。这可以全部自动化并且在毫秒级扩展。更好的是,不用再维护服务器了。开发者只需要关注业务逻辑——返回值的函数。

如果我们部署一个服务器程序到 Serverless Function,我们就为每个请求执行了一个完整的服务器实现,这是一个反模式,因为 Serverless 函数仍然是函数,只应服务于一个目的。将服务器程序部署在 Serverless Function 上,相当于强行将庞大的逻辑混杂在一个函数里。虽然这是一个反模式,但是羊毛在那里,不得不薅。对于有钱的企业级服务器程序,还是建议绕道。

Vercel 的 Serverless Function 和 AWS Lambda 很像,比如都是一个对外暴露 handler 函数的模块,但是特别之处在于其 handler 签名不一样。AWS Lambda 的 handler:

javascript export default const handler = (event, context) => Hello World

但是 Vercel 的 handler 接收一个 req 和 res 参数(https://github.com/Jeff-Tian/v/blob/master/api/test.js): javascript export default function handler(req, res) { const { name = World } = req.query; return res.send(Hello ${name}!); }

https://v.pa-pa.me/api/test?name=Jeff
image.png

这签名看起来很像是 Express.js 中间件,有意思的是,它真的支持完整的 Express.js 应用,只需要将入口文件放在 /api/index.js 里即可。

Vercel Serverless Function 对 Koa 的支持

经过实验,Vercel Serverless Function 是不支持 Koa.js 应用的。因为它的签名和 (ctx, next) => ctx.body = Hello 这种 Koa.js 风格就不相容。

koa-to-express

想将 Koa 应用搬到 Vercel Serverless Function,但是不希望改已有的 Koa 代码,最自然的方式莫过于增加一个 adapter,将 koa 风格的中间件函数适配成 express.js 的中间件函数。于是找到了 koa-to-express 这个库。

增加 /api/index.js 文件

该 index.js 引用 Koa 应用的入口文件,并将它的中间件做个转换,伪装成一个 Express.js 应用。这样,原来的 Koa 应用在服务器环境中仍然照常运行,同时又可以在 Vercel Serverless Function 环境里运行。如果再使用 serverless-http,再在 AWS Lambda 里部署一个适配,那么就是名副其实的“狡兔三窟”了。

image.png

修复 koa-to-express 的一个 BUG

其实没有那么顺利,由于原 Koa 应用使用了 koa-router 这个中间件,触发了 koa-to-express 的一个 BUG。于是只能 Fork 了 koa-to-express ,在自己的版本中做了修复。虽然给原作者提交了 PR,但在他合并并发新版之前,我得临时使用自己的版本,于是要对 package.json 做个修改,在安装 koa-to-express 时,从自己的仓库里下载代码:

https://github.com/Jeff-Tian/v/commit/cd11bf4f7100fd61ef9dda98397eb255b764a396#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519

package.json: diff

  • koa-to-express: ^3.1.4,

给它们的 PR 链接:https://github.com/xingxingted/koa-to-express/pull/10/files

完成 /api/index.js

最终的 /api/index.js 文件如下:
https://github.com/Jeff-Tian/v/blob/cd11bf4f7100fd61ef9dda98397eb255b764a396/api/index.js javascript const k2e = require(koa-to-express);

process.env.ROUTER_PREFIX = /api;

const app = require(../app);

const expressApp = require(express)();

app.middleware.map(m => { expressApp.use(k2e(m)); })

module.exports = expressApp;

重定向

Vercel Serverless Function 默认将 /api/xxx 路由到 /api/xxx.js,所以 /api/xy/z,就不会被 /api/index 处理,所以需要增加一个 vercel.json 文件,将所有 /api/ 下的请求,重定向到 index.js:

json { rewrites: [{ source: /api/(.*), destination: /api }] }

完成

效果和在服务器上运行 koa 一模一样:

image.png