背景
目前有很多 web 应用是基于 express 框架写的,这样的 web 应用按照传统的部署方式可能部署在云主机上,用户可能不想购买云主机,也不想在运维上投入太多成本,函数计算是一个不错的选择。函数计算的入口方法如何适配 express 是一个相当复杂的问题,我们需要适配 http 触发器和 API 网关这两种类型,因为,这两种类型的函数方法签名是不一样的。比如 API 网关方式触发函数,需要把 event 映射到 express 的 request 对象上,而 express 的 response 对象需要映射到 callback 的数据参数上。
现在,我们提供了一个 npm 包,基于该 npm 包,可以将函数计算的请求转发给 express 应用,几行代码可以实现。
如果你需要快速开始,可以参考另一篇文章:开发函数计算的正确姿势——移植 Express。
使用说明
安装相关 npm 包
npm install @webserverless/fc-express express
http 触发器类型函数
const proxy = require('@webserverless/fc-express')
const express = require('express');
const app = express();
app.all('*', (req, res) => {
res.send('hello world!');
});
const server = new proxy.Server(app);
module.exports.handler = function(req, res, context) {
server.httpProxy(req, res, context);
};
API 网关类型函数
const proxy = require('@webserverless/fc-express')
const express = require('express');
const app = express();
app.all('*', (req, res) => {
res.send('hello world!');
});
const server = new proxy.Server(app);
module.exports.handler = function(event, context, callback) {
server.proxy(event, context, callback);
};
http 触发器类型自定义 body
http 触发器触发函数,会通过流的方式传输 body 信息,我们可以通过 npm 包 raw-body 来获取,获取流中 body 信息需要特别注意一点:在 node8 版本以下(包括 nodejs8),获取 body 信息的代码逻辑一定要在其他 await 或者 promise.then 等方法的前面,在某些特殊场景下,可能需要让 server.httpProxy 方法需要在一个 await 代码后面执行,再这种情况下,我们就需要自己手动获取 body,然后通过一种特殊的方式传递给代理服务。本质原因与 nodejs 的 Eevent Loop 机制有关。代码如下
const proxy = require('@webserverless/fc-express')
const express = require('express');
const getRawBody = require('raw-body');
const app = express();
app.all('*', (req, res) => {
res.send('hello world!');
});
const server = new proxy.Server(app);
const init = async () => {
.....
}
module.exports.handler = async (req, res, context) => {
req.body = await getRawBody(req); // 本行代码一定要放到其他 await 代码之前
await init();
server.httpProxy(req, res, context);
};
获取请求头
我们在浏览器端设置好请求头,@webserverless/fc-express 会将我们的请求头透传给 express 应用的 request 对象,通过 express 的 request 对象直接获取我们设置的请求头
设置响应头
我们只需要按照 express 方式设置好 response 的响应头,@webserverless/fc-express 会把该响应头透传出来,在浏览器可以获取透传出来的响应头。
Server 说明
@webserverless/fc-express 包导出了一个 Server 类,Server 负责构建代理服务,转发请求到 express 应用。
构造函数定义:
Server(
requestListener: (request: http.IncomingMessage, response: http.ServerResponse) => void,
serverListenCallback?: () => void,
binaryTypes?: string[]
)
构造函数参数说明:
参数 | 类型 | 必填 | 说明 |
---|---|---|---|
requestListener | (request: http.IncomingMessage, response: http.ServerResponse) => void | 是 | 被代理的 express 应用 |
serverListenCallback | () => void | 否 | http 代理服务开始监听的回调函数 |
binaryTypes | string[] | 否 | API 网关触发方式才有效,当 express 应用的响应头 content-type 符合 binaryTypes 中定义的任意规则,则返回给 API 网关的 isBase64Encoded 属性为 true |
成员方法:
方法 | 参数 | 返回值 | 说明 |
---|---|---|---|
proxy | (event, context, callback) | void | 当你的函数通过 API 网关触发,就需要使用 proxy 方法将函数计算的处理代理给 express 应用,参数对应着 API 网关类型的入口函数的参数 |
httpProxy | (request, response, context) | void | 当你的函数通过 http 触发器触发,就需要使用 httpProxy 方法将函数计算的处理代理给 express 应用,参数对应着 http 触发器类型的入口函数的参数 |
成员属性:
属性 | 类型 | 说明 |
---|---|---|
rawServer | http.Server | 负责将请求转发 express 应用的底层代理服务对象 |
API 网关中的 isBase64Encoded 参数
有两个地方会有 isBase64Encoded 参数:
- 函数 event 参数中包含的 isBase64Encoded 参数
- 函数返回值中包含的 isBase64Encoded 参数
当函数的 event.isBase64Encoded 是 true 时,我们会按照 base64 编码来解析 event.body,并透传给 express 应用,否则就按照默认的编码方式来解析,默认是 utf8。
当 express 应用响应的 content-type 符合 Server 构造函数参数 binaryTypes 中定义的任意规则时,则函数的返回值的 isBase64Encoded 为 true,从而告诉 API 网关如何解析函数返回值的 body 参数。
业务代码中获取函数 context 和 event 方法
我们提供了一个 express 中间件,用来获取函数的 event 和 context 对象,其中 event 对象,只有在 API 网关触发函数的时候才会有,且 event 是 JSON.parse 后的对象。代码如下:
const proxy = require('@webserverless/fc-express')
const express = require('express');
const app = express();
app.use(proxy.eventContext())
app.all(/.*/, (req, res) => {
console.log(req.eventContext.event); // http 触发器方式,没有 event 对象
console.log(req.eventContext.context);
res.send('hello world!');
});
const server = new proxy.Server(app);
module.exports.handler = function(event, context, callback) {
server.proxy(event, context, callback);
};
eventContext 中间件之所以能解析到 event 和 context 两个参数,是因为我们会将这两个参数序列化后,通过请求头透传给了 express 应用的 reques 对象。
eventContext 中间件提供了一个配置参数 options,options 参数是选填的,其中包含了两个属性 reqPropKey 和 deleteHeaders:
参数 | 类型 | 默认值 | 必填 | 说明 |
---|---|---|---|---|
reqPropKey | string | 'eventContext' | 否 | 控制从请求头解析出 event 和 context 对象放到 request 对象的属性名称,默认是 eventContext,则获取方式为 request.eventContext.event |
deleteHeaders | boolean | true | 否 | 控制从请求头解析出 event 和 context 后,是否需要删除与 event 和 context 相关的请求头,默认会删除 |
需要考虑的问题
- 无状态的。所以移植后的 express 也需要是无状态的,像 express session 就没法简单的用起来了,可以考虑使用 jwt 或者将状态持久化到相关存储中
- 冷启动。第一次访问有冷启动时间,一段时间没有请求,函数计算会释放掉实例,下次再有请求过来,也会有冷启动时间,可以通过预热来解决,另外,打包压缩代码,也可以减少冷启动时间
- 部分浏览器请求对象属性没有从函数计算中透传出来,比如:protocol、hostname 等,所以在 express 应用中无法获取
- 函数计算的最大超时时间 600 秒,API 网关最大超时时间是 30 秒,如果你使用了 API 网关,请确保你的请求能在 30 秒内处理完,如果你使用了 http 触发器,请确保你的请求能在 600 秒内处理完
- 无法使用本地库(像 Addons)
小结
使用 @webserverless/fc-express 包,我们可以几行代码让 express 接入函数计算,@webserverless/fc-express 会帮我们做很多适配的事情,让我们尽可能接近原生的方式使用 express 框架,适配的逻辑对用户是透明的。另外,我们还提供了一个 fun 模板,帮助我们更快地搭建一个基于函数计算的 express 项目,预置了编译、打包、调试和发布等开箱即用的功能,可以参考另一篇文章:开发函数计算的正确姿势——移植 Express。