如何將傳統(tǒng) Web 框架部署到 Serverless
如何將傳統(tǒng) Web 框架部署到 Serverless
https://www.zoo.team/article/serverless-web
背景
因?yàn)?Serverless 的“無(wú)服務(wù)器架構(gòu)”應(yīng)用相比于傳統(tǒng)應(yīng)用有很多優(yōu)點(diǎn),比如:無(wú)需關(guān)心服務(wù)器、免運(yùn)維、彈性伸縮、按需付費(fèi)、開(kāi)發(fā)可以更加關(guān)注業(yè)務(wù)邏輯等等,所以現(xiàn)在 Serverless 應(yīng)用已經(jīng)逐漸廣泛起來(lái)。
但是目前原生的 Serverless 開(kāi)發(fā)框架還比較少,也沒(méi)有那么成熟,另外主流的 Web 框架還不支持直接 Serverless 部署,但好在是現(xiàn)在國(guó)內(nèi)各大云廠(chǎng)商比如阿里云、騰訊云已經(jīng)提供能力能夠?qū)⑽覀兊膫鹘y(tǒng)框架以簡(jiǎn)單、快速、科學(xué)的方式部署到 Serverless 上,下面讓我們一起研究看看它們是怎么做的吧。
我們以 Node.js 的 Express 應(yīng)用為例,看看如何通過(guò)阿里云函數(shù)計(jì)算,實(shí)現(xiàn)不用按照傳統(tǒng)部署方式購(gòu)買(mǎi)云主機(jī)去部署,不用自己運(yùn)維,快速部署到 Serverless 平臺(tái)上。
傳統(tǒng)應(yīng)用與函數(shù)計(jì)算的入口差異
傳統(tǒng)應(yīng)用的入口文件
首先看下傳統(tǒng) Express 應(yīng)用的入口文件:
const express = require('express')
const app = express()
const port = 3000
// 監(jiān)聽(tīng) / 路由,處理請(qǐng)求
app.get('/', (req, res) => {
res.send('Hello World!')
})
// 監(jiān)聽(tīng) 3000 端口,啟動(dòng) HTTP 服務(wù)
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
可以看到傳統(tǒng) Express 應(yīng)用是:
1.通過(guò) app.listen() 啟動(dòng)了 HTTP 服務(wù),其本質(zhì)上是調(diào)用的 Node.js http 模塊的 createServer() 方法創(chuàng)建了一個(gè) HTTP Server
2.監(jiān)聽(tīng)了 / 路由,由回調(diào)函數(shù) function(request, response) 處理請(qǐng)求
函數(shù)計(jì)算的入口函數(shù)
Serverless 應(yīng)用中, FaaS 是基于事件觸發(fā)的,觸發(fā)器是觸發(fā)函數(shù)執(zhí)行的方式, 其中 API 網(wǎng)關(guān)觸發(fā)器與 HTTP 觸發(fā)器與均可應(yīng)用于 Web應(yīng)用的創(chuàng)建。函數(shù)計(jì)算會(huì)從指定的入口函數(shù)開(kāi)始執(zhí)行,其中 API 網(wǎng)關(guān)觸發(fā)器對(duì)應(yīng)的入口函數(shù)叫事件函數(shù),HTTP 觸發(fā)器對(duì)應(yīng)的入口函數(shù)叫 HTTP 函數(shù),它們的入口函數(shù)形式不同。
API 網(wǎng)關(guān)觸發(fā)器的入口函數(shù)形式
API 網(wǎng)關(guān)觸發(fā)器的入口函數(shù)形式如下,函數(shù)入?yún)?event、context、callback,以 Node.js 為例,如下:
/*
* handler: 函數(shù)名 handler 需要與創(chuàng)建函數(shù)時(shí)的 handler 字段相對(duì)應(yīng)。例如創(chuàng)建函數(shù)時(shí)指定的 handler 為 index.handler,那么函數(shù)計(jì)算會(huì)去加載 index.js 文件中定義的 handler 函數(shù)
* event: 您調(diào)用函數(shù)時(shí)傳入的數(shù)據(jù),其類(lèi)型是 Buffer,是函數(shù)的輸入?yún)?shù)。您在函數(shù)中可以根據(jù)實(shí)際情況對(duì) event 進(jìn)行轉(zhuǎn)換。如果輸入數(shù)據(jù)是一個(gè) JSON 字符串 ,您可以把它轉(zhuǎn)換成一個(gè) Object。
* context: 包含一些函數(shù)的運(yùn)行信息,例如 request Id、 臨時(shí) AK 等。您在代碼中可以使用這些信息
* callback: 由系統(tǒng)定義的函數(shù),作為入口函數(shù)的入?yún)⒂糜诜祷卣{(diào)用函數(shù)的結(jié)果,標(biāo)識(shí)函數(shù)執(zhí)行結(jié)束。與 Node.js 中使用的 callback 一樣,它的第一個(gè)參數(shù)是 error,第二個(gè)參數(shù) data。
*/
module.exports.handler = (event, context, callback) => {
// 處理業(yè)務(wù)邏輯
callback(null, data);
};
HTTP 觸發(fā)器的入口函數(shù)形式
一個(gè)簡(jiǎn)單的 Node.js HTTP 函數(shù)示例如下所示:
module.exports.handler = function(request, response, context) {
response.send("hello world");
}
差異對(duì)比
對(duì)比可以看出,在傳統(tǒng)應(yīng)用中,是啟動(dòng)一個(gè)服務(wù)監(jiān)聽(tīng)端口號(hào)去處理 HTTP 請(qǐng)求,服務(wù)處理的是 HTTP 的請(qǐng)求和響應(yīng)參數(shù);而在 Serverless 應(yīng)用中, Faas 是基于事件觸發(fā)的,觸發(fā)器類(lèi)型不同,參數(shù)映射和處理不同:
若是 API 網(wǎng)關(guān)觸發(fā)器
當(dāng)有請(qǐng)求到達(dá)后端服務(wù)設(shè)置為函數(shù)計(jì)算的 API 網(wǎng)關(guān)時(shí),API 網(wǎng)關(guān)會(huì)觸發(fā)函數(shù)的執(zhí)行,觸發(fā)器會(huì)將事件信息生成 event 參數(shù),然后 FaaS 以 event 為參數(shù)執(zhí)行入口函數(shù),最后將執(zhí)行結(jié)果返回給 API 網(wǎng)關(guān)。所以傳統(tǒng)應(yīng)用和 Serverless 應(yīng)用在請(qǐng)求響應(yīng)方式和參數(shù)的數(shù)據(jù)結(jié)構(gòu)上都有很大差異,要想辦法讓函數(shù)計(jì)算的入口方法適配 express。
若是 HTTP 觸發(fā)器
相對(duì) API 網(wǎng)關(guān)觸發(fā)器參數(shù)處理會(huì)簡(jiǎn)單些。因?yàn)?HTTP 觸發(fā)器通過(guò)發(fā)送 HTTP 請(qǐng)求觸發(fā)函數(shù)執(zhí)行,會(huì)把真實(shí)的 HTTP 請(qǐng)求直接傳遞給 FaaS 平臺(tái),不需要編碼或解碼成 JSON 格式,不用增加轉(zhuǎn)換邏輯,性能也更優(yōu)。
適配層
下面我們通過(guò)解讀阿里云 FC 提供的將函數(shù)計(jì)算的請(qǐng)求轉(zhuǎn)發(fā)給 express 應(yīng)用的 npm 包 @webserverless/fc-express 源碼,看看函數(shù)計(jì)算的入口方法是如何適配 express 的,如何適配 API 網(wǎng)關(guān) 和 HTTP 觸發(fā)器這兩種類(lèi)型。
根據(jù)上述分析,Web 應(yīng)用若想 Serverless 化需要開(kāi)發(fā)一個(gè)適配層,將函數(shù)計(jì)算接收到的請(qǐng)求轉(zhuǎn)發(fā)給 express 應(yīng)用處理,最后再返回給函數(shù)計(jì)算。
API 網(wǎng)關(guān)觸發(fā)的適配層
實(shí)現(xiàn)原理
API 網(wǎng)關(guān)觸發(fā)的情況下,通過(guò)適配層將 FaaS 函數(shù)接收到的 API 網(wǎng)關(guān)事件參數(shù) event 先轉(zhuǎn)化為標(biāo)準(zhǔn)的 HTTP 請(qǐng)求,再去讓傳統(tǒng) Web 服務(wù)去處理請(qǐng)求和響應(yīng),最后再將 HTTP 響應(yīng)轉(zhuǎn)換為函數(shù)返回值。整體工作原理如下圖所示:
適配層核心就是:把 event 映射到 express 的 request 對(duì)象上, 再把 express 的 response 對(duì)象映射到 callback 的數(shù)據(jù)參數(shù)上。
API 網(wǎng)關(guān)調(diào)用函數(shù)計(jì)算的事件函數(shù)時(shí),會(huì)將 API 的相關(guān)數(shù)據(jù)轉(zhuǎn)換為 Map 形式傳給函數(shù)計(jì)算服務(wù)。函數(shù)計(jì)算服務(wù)處理后,按照下圖中 Output Format 的格式返回 statusCode、headers、body 等相關(guān)數(shù)據(jù)。API 網(wǎng)關(guān)再將函數(shù)計(jì)算返回的內(nèi)容映射到 statusCode、header、body等位置返回給客戶(hù)端。
(此圖來(lái)源于阿里云)
核心過(guò)程
通過(guò)分析 @webserverless/fc-express 源碼,我們可以抽取核心過(guò)程實(shí)現(xiàn)一個(gè)簡(jiǎn)易版的適配層。
1.創(chuàng)建一個(gè)自定義 HTTP Server,通過(guò)監(jiān)聽(tīng) Unix Domain Socket,啟動(dòng)服務(wù)
(友情鏈接:不清楚 Unix Domain Socket 的小伙伴可以先看下這篇文章: Unix domain socket 簡(jiǎn)介 (https://www.cnblogs.com/sparkdev/p/8359028.html))
第一步我們?nèi)粝氚押瘮?shù)計(jì)算接收的 event 參數(shù)映射到 Express.js 的 request 對(duì)象上,就需要?jiǎng)?chuàng)建并啟動(dòng)一個(gè)自定義的 HTTP 服務(wù)來(lái)代替 Express.js 的 app.listen,然后接下來(lái)就可以將函數(shù)的事件參數(shù) event 轉(zhuǎn)換為 Express.js 的 request 請(qǐng)求參數(shù)。
首先創(chuàng)建一個(gè) server.js 文件如下:
// server.js
const http = require('http');
const ApiGatewayProxy = require('./api-gateway-proxy');// api-gateway-proxy.js 文件下一步會(huì)說(shuō)明其內(nèi)容
/*
* requestListener:被代理的 express 應(yīng)用
* serverListenCallback:http 代理服務(wù)開(kāi)始監(jiān)聽(tīng)的回調(diào)函數(shù)
* binaryTypes: 當(dāng) express 應(yīng)用的響應(yīng)頭 content-type 符合 binaryTypes 中定義的任意規(guī)則,則返回給 API 網(wǎng)關(guān)的 isBase64Encoded 屬性為 true
*/
function Server(requestListener,serverListenCallback,binaryTypes) {
this.apiGatewayProxy = new ApiGatewayProxy(this); // ApiGatewayProxy 核心過(guò)程 2 會(huì)介紹
this.server = http.createServer(requestListener);// 1.1 創(chuàng)建一個(gè)自定義 HTTP Server
this.socketPathSuffix = getRandomString(); // 隨機(jī)生成一個(gè)字符串,作為 Unix Domain Socket 使用
this.binaryTypes = binaryTypes ? binaryTypes.slice() : [];// 當(dāng) express 應(yīng)用響應(yīng)的 content-type 符合 Server 構(gòu)造函數(shù)參數(shù) binaryTypes 中定義的任意規(guī)則時(shí),則函數(shù)的返回值的 isBase64Encoded 為 true,從而告訴 API 網(wǎng)關(guān)如何解析函數(shù)返回值的 body 參數(shù)
this.server.on("listening", () => {
this.isListening = true;
if (serverListenCallback) serverListenCallback();
});
this.server.on("close", () => {
this.isListening = false;
}).on("error", (error) => {
// 異常處理
});
}
// 暴露給函數(shù)計(jì)算入口函數(shù) handler 調(diào)用的方法
Server.prototype.proxy = function (event, context, callback) {
const e = JSON.parse(event);
this.apiGatewayProxy.handle({
event: e,
context,
callback
});
}
// 1.2 啟動(dòng)服務(wù)
Server.prototype.startServer = function () {
return this.server.listen(this.getSocketPath()); // 采用監(jiān)聽(tīng) Unix Domain Socket 方式啟動(dòng)服務(wù),減少函數(shù)執(zhí)行時(shí)間,節(jié)約成本
}
Server.prototype.getSocketPath = function () {
/* istanbul ignore if */
/* only running tests on Linux; Window support is for local dev only */
if (/^win/.test(process.platform)) {
const path = require('path');
return path.join('\\\\?\\pipe', process.cwd(), `server-${this.socketPathSuffix}`);
} else {
return `/tmp/server-${this.socketPathSuffix}.sock`;
}
}
function getRandomString() {
return Math.random().toString(36).substring(2, 15);
}
module.exports = Server;
在 server.js 中,我們定義了一個(gè)構(gòu)造函數(shù) Server 并導(dǎo)出。在 Server 中,我們創(chuàng)建了一個(gè)自定義的 HTTP 服務(wù),然后隨機(jī)生成了一個(gè) Unix Domain Socket,采用監(jiān)聽(tīng)該 Socket 方式啟動(dòng)服務(wù)來(lái)代替 Express.js 的 app.listen。
2.將函數(shù)計(jì)算參數(shù) event 轉(zhuǎn)換為 Express.js 的 HTTP request
下面開(kāi)始第 2 步,創(chuàng)建一個(gè) api-gateway-proxy.js 文件,將函數(shù)計(jì)算參數(shù) event 轉(zhuǎn)換為 Express.js 的 HTTP request。
//api-gateway-proxy.js
const http = require('http');
const isType = require('type-is');
function ApiGatewayProxy(server) {
this.server = server;
}
ApiGatewayProxy.prototype.handle = function ({
event,
context,
callback
}) {
this.server.startServer()
.on('listening', () => {
this.forwardRequestToNodeServer({
event,
context,
callback
});
});
}
ApiGatewayProxy.prototype.forwardRequestToNodeServer = function ({
event,
context,
callback
}) {
const resolver = data => callback(null, data);
try {
// 2.1將 API 網(wǎng)關(guān)事件轉(zhuǎn)換為 HTTP request
const requestOptions = this.mapContextToHttpRequest({
event,
context,
callback
});
// 2.2 通過(guò) http.request() 將 HTTP request 轉(zhuǎn)發(fā)給 Node.js Server 處理,發(fā)起 HTTP 請(qǐng)求
const req = http.request(requestOptions, response => this.forwardResponse(response, resolver));
req.on('error', error => {
//...
});
req.end();
} catch (error) {
// ...
}
}
ApiGatewayProxy.prototype.mapContextToHttpRequest = function ({
event,
context,
callback
}) {
const headers = Object.assign({}, event.headers);
return {
method: event.httpMethod,
path: event.path,
headers,
socketPath: this.server.getSocketPath()
// protocol: `${headers['X-Forwarded-Proto']}:`,
// host: headers.Host,
// hostname: headers.Host, // Alias for host
// port: headers['X-Forwarded-Port']
};
}
// 核心過(guò)程 3 會(huì)介紹
ApiGatewayProxy.prototype.forwardResponse = function (response, resolver) {
const buf = [];
response
.on('data', chunk => buf.push(chunk))
.on('end', () => {
const bodyBuffer = Buffer.concat(buf);
const statusCode = response.statusCode;
const headers = response.headers;
const contentType = headers['content-type'] ? headers['content-type'].split(';')[0] : '';
const isBase64Encoded = this.server.binaryTypes && this.server.binaryTypes.length > 0 && !!isType.is(contentType, this.server.binaryTypes);
const body = bodyBuffer.toString(isBase64Encoded ? 'base64' : 'utf8');
const successResponse = {
statusCode,
body,
headers,
isBase64Encoded
};
resolver(successResponse);
});
}
module.exports = ApiGatewayProxy;
在 api-gateway-proxy.js 中,我們定義了一個(gè)構(gòu)造函數(shù) ApiGatewayProxy 并導(dǎo)出。在這里我們會(huì)將 event 轉(zhuǎn)換為 HTTP request,然后向 Node.js Server 發(fā)起請(qǐng)求,由 Node.js Server 再進(jìn)行處理做出響應(yīng)。
3.將 HTTP response 轉(zhuǎn)換為 API 網(wǎng)關(guān)標(biāo)準(zhǔn)數(shù)據(jù)結(jié)構(gòu),作為 callback 的參數(shù)返回給 API 網(wǎng)關(guān)
接著繼續(xù)對(duì) api-gateway-proxy.js 文件中的http.request(requestOptions, response => this.forwardResponse(response, resolver))分析發(fā)出 HTTP 請(qǐng)求后的響應(yīng)處理部分。
//api-gateway-proxy.js
ApiGatewayProxy.prototype.forwardRequestToNodeServer = function ({
event,
context,
callback
}) {
const resolver = data => callback(null, data); // 封裝 callback 為 resolver
//...
// 請(qǐng)求、響應(yīng)
const req = http.request(requestOptions, response => this.forwardResponse(response, resolver));
//...
}
//3.Node.js Server 對(duì) HTTP 響應(yīng)進(jìn)行處理,將 HTTP response 轉(zhuǎn)換為 API 網(wǎng)關(guān)標(biāo)準(zhǔn)數(shù)據(jù)結(jié)構(gòu),作為函數(shù)計(jì)算返回值
ApiGatewayProxy.prototype.forwardResponse = function (response, resolver) {
const buf = [];
response
.on('data', chunk => buf.push(chunk))
.on('end', () => {
const bodyBuffer = Buffer.concat(buf);
const statusCode = response.statusCode;
const headers = response.headers;
const contentType = headers['content-type'] ? headers['content-type'].split(';')[0] : '';
const isBase64Encoded = this.server.binaryTypes && this.server.binaryTypes.length > 0 && !!isType.is(contentType, this.server.binaryTypes);
const body = bodyBuffer.toString(isBase64Encoded ? 'base64' : 'utf8');
// 函數(shù)返回值
const successResponse = {
statusCode,
body,
headers,
isBase64Encoded //當(dāng)函數(shù)的 event.isBase64Encoded 是 true 時(shí),會(huì)按照 base64 編碼來(lái)解析 event.body,并透?jìng)鹘o express 應(yīng)用,否則就按照默認(rèn)的編碼方式來(lái)解析,默認(rèn)是 utf8
};
// 將 API 網(wǎng)關(guān)標(biāo)準(zhǔn)數(shù)據(jù)結(jié)構(gòu)作為回調(diào) callback 參數(shù),執(zhí)行 callback,返回給 API 網(wǎng)關(guān)
resolver(successResponse);
});
}
接著第 2 步,Node.js Server 對(duì) http.request() 發(fā)出的 HTTP 請(qǐng)求做出響應(yīng)處理,將 HTTP response 轉(zhuǎn)換為 API 網(wǎng)關(guān)標(biāo)準(zhǔn)數(shù)據(jù)結(jié)構(gòu),把它作為回調(diào) callback 的參數(shù),調(diào)用 callback 返回給 API 網(wǎng)關(guān)。
4.在入口函數(shù)中引入適配層代碼并調(diào)用
以上 3 步就將適配層核心代碼完成了,整個(gè)過(guò)程就是:將 API 網(wǎng)關(guān)事件轉(zhuǎn)換成 HTTP 請(qǐng)求,通過(guò)本地 socket 和函數(shù)起 Node.js Server 進(jìn)行通信。
最后我們?cè)谌肟诤瘮?shù)所在文件 index.js 中引入 server.js,先用 Server 構(gòu)建一個(gè) HTTP 代理服務(wù),然后在入口函數(shù) handler 中調(diào)用 server.proxy(event, context, callback); 即可將函數(shù)計(jì)算的請(qǐng)求轉(zhuǎn)發(fā)給 express 應(yīng)用處理。
// index.js
const express = require('express');
const Server = require('./server.js');
const app = express();
app.all('*', (req, res) => {
res.send('express-app hello world!');
});
const server = new Server(app); // 創(chuàng)建一個(gè)自定義 HTTP Server
module.exports.handler = function(event, context, callback) {
server.proxy(event, context, callback); // server.proxy 將函數(shù)計(jì)算的請(qǐng)求轉(zhuǎn)發(fā)到 express 應(yīng)用
};
我們將以上代碼在 FC 上部署、調(diào)用,執(zhí)行成功結(jié)果如下:
HTTP 觸發(fā)的適配層
實(shí)現(xiàn)原理
HTTP 觸發(fā)的情況下,不用對(duì)請(qǐng)求參數(shù)做轉(zhuǎn)換,其它原理與 API 網(wǎng)關(guān)觸發(fā)器一致:通過(guò)適配層將 FaaS 函數(shù)接收到的請(qǐng)求參數(shù)直接轉(zhuǎn)發(fā)到自定義的 Web 服務(wù)內(nèi),最后再將 HTTP 響應(yīng)包裝返回即可,整體工作原理如下圖所示:
核心過(guò)程
同樣我們抽取核心過(guò)程簡(jiǎn)單實(shí)現(xiàn)一個(gè)適配層,與 API 網(wǎng)關(guān)觸發(fā)器原理相同的部分將不再贅述 。
1.創(chuàng)建一個(gè)自定義 HTTP Server,通過(guò)監(jiān)聽(tīng) Unix Domain Socket,啟動(dòng)服務(wù)
server.js 代碼如下:
// server.js
const http = require('http');
const HttpTriggerProxy = require('./http-trigger-proxy');
function Server(requestListener,serverListenCallback) {
this.httpTriggerProxy = new HttpTriggerProxy(this);
this.server = http.createServer(requestListener); // 1.1 創(chuàng)建一個(gè)自定義 HTTP Server
this.socketPathSuffix = getRandomString();
this.server.on("listening", () => {
this.isListening = true;
if (serverListenCallback) serverListenCallback();
});
this.server.on("close", () => {
this.isListening = false;
}).on("error", (error) => {
// 異常處理,例如判讀 socket 是否已被監(jiān)聽(tīng)
});
}
// 暴露給函數(shù)計(jì)算入口函數(shù) handler 調(diào)用的方法
Server.prototype.httpProxy = function (request, response, context) {
this.httpTriggerProxy.handle({ request, response, context });
}
// 1.2 啟動(dòng)服務(wù)
Server.prototype.startServer = function () {
return this.server.listen(this.getSocketPath());
}
Server.prototype.getSocketPath = function () {
/* istanbul ignore if */
/* only running tests on Linux; Window support is for local dev only */
if (/^win/.test(process.platform)) {
const path = require('path');
return path.join('\\\\?\\pipe', process.cwd(), `server-${this.socketPathSuffix}`);
} else {
return `/tmp/server-${this.socketPathSuffix}.sock`;
}
}
function getRandomString() {
return Math.random().toString(36).substring(2, 15);
}
module.exports = Server;
2.將 HTTP request 直接轉(zhuǎn)發(fā)給 Web Server,再將 HTTP response 包裝返回
創(chuàng)建一個(gè) api-trigger-proxy.js 文件如下:
// api-trigger-proxy.js
const http = require('http');
const isType = require('type-is');
const url = require('url');
const getRawBody = require('raw-body');
function HttpTriggerProxy(server) {
this.server = server;
}
HttpTriggerProxy.prototype.handle = function ({
request,
response,
context
}) {
this.server.startServer()
.on('listening', () => {
this.forwardRequestToNodeServer({
request,
response,
context
});
});
}
HttpTriggerProxy.prototype.forwardRequestToNodeServer = function ({
request,
response,
context
}) {
// 封裝 resolver
const resolver = data => {
response.setStatusCode(data.statusCode);
for (const key in data.headers) {
if (data.headers.hasOwnProperty(key)) {
const value = data.headers[key];
response.setHeader(key, value);
}
}
response.send(data.body); // 返回 response body
};
try {
// 透?jìng)?request
const requestOptions = this.mapContextToHttpRequest({
request,
context
});
// 2.將 HTTP request 直接轉(zhuǎn)發(fā)給 Web Server,再將 HTTP response 包裝返回
const req = http.request(requestOptions, response => this.forwardResponse(response, resolver));
req.on('error', error => {
// ...
});
// http 觸發(fā)器類(lèi)型支持自定義 body:可以獲取自定義 body
if (request.body) {
req.write(request.body);
req.end();
} else {
// 若沒(méi)有自定義 body:http 觸發(fā)器觸發(fā)函數(shù),會(huì)通過(guò)流的方式傳輸 body 信息,可以通過(guò) npm 包 raw-body 來(lái)獲取
getRawBody(request, (err, body) => {
req.write(body);
req.end();
});
}
} catch (error) {
// ...
}
}
HttpTriggerProxy.prototype.mapContextToHttpRequest = function ({
request,
context
}) {
const headers = Object.assign({}, request.headers);
headers['x-fc-express-context'] = encodeURIComponent(JSON.stringify(context));
return {
method: request.method,
path: url.format({ pathname: request.path, query: request.queries }),
headers,
socketPath: this.server.getSocketPath()
// protocol: `${headers['X-Forwarded-Proto']}:`,
// host: headers.Host,
// hostname: headers.Host, // Alias for host
// port: headers['X-Forwarded-Port']
};
}
HttpTriggerProxy.prototype.forwardResponse = function (response, resolver) {
const buf = [];
response
.on('data', chunk => buf.push(chunk))
.on('end', () => {
const bodyBuffer = Buffer.concat(buf);
const statusCode = response.statusCode;
const headers = response.headers;
const contentType = headers['content-type'] ? headers['content-type'].split(';')[0] : '';
const isBase64Encoded = this.server.binaryTypes && this.server.binaryTypes.length > 0 && !!isType.is(contentType, this.server.binaryTypes);
const body = bodyBuffer.toString(isBase64Encoded ? 'base64' : 'utf8');
const successResponse = {
statusCode,
body,
headers,
isBase64Encoded
};
resolver(successResponse);
});
}
module.exports = HttpTriggerProxy;
3.入口函數(shù)引入適配層代碼
// index.js
const express = require('express');
const Server = require('./server.js');
const app = express();
app.all('*', (req, res) => {
res.send('express-app-httpTrigger hello world!');
});
const server = new Server(app);
module.exports.handler = function (req, res, context) {
server.httpProxy(req, res, context);
};
同樣地,我們將以上代碼在 FC 上部署、調(diào)用,執(zhí)行成功結(jié)果如下:
看到最后,大家會(huì)發(fā)現(xiàn) API 網(wǎng)關(guān)觸發(fā)器和 HTTP 觸發(fā)器很多代碼邏輯是可以復(fù)用的,大家可以自行閱讀優(yōu)秀的源碼是如何實(shí)現(xiàn)的~
其他部署到 Serverless 平臺(tái)的方案
將傳統(tǒng) Web 框架部署到 Serverless 除了通過(guò)適配層轉(zhuǎn)換實(shí)現(xiàn),還可以通過(guò) Custom Runtime 或者 Custom Container Runtime (https://juejin.cn/post/6981921291980767269#heading-5) ,3 種方案總結(jié)如下:
通過(guò)引入適配層,將函數(shù)計(jì)算接收的事件參數(shù)轉(zhuǎn)換為 HTTP 請(qǐng)求交給自定義的 Web Server 處理
通過(guò) Custom Runtime
本質(zhì)上也是一個(gè) HTTP Server,接管了函數(shù)計(jì)算平臺(tái)的所有請(qǐng)求,包括事件調(diào)用或者 HTTP 函數(shù)調(diào)用等
開(kāi)發(fā)者需要?jiǎng)?chuàng)建一個(gè)啟動(dòng)目標(biāo) Server 的可執(zhí)行文件 bootstrap
通過(guò) Custom Container Runtime
工作原理與 Custom Runtime 基本相同
開(kāi)發(fā)者需要把應(yīng)用代碼和運(yùn)行環(huán)境打包為 Docker 鏡像
小結(jié)
本文介紹了傳統(tǒng) Web 框架如何部署到 Serverless 平臺(tái)的方案:可以通過(guò)適配層和自定義(容器)運(yùn)行時(shí)。其中主要以 Express.js 和阿里云函數(shù)計(jì)算為例講解了通過(guò)適配層實(shí)現(xiàn)的原理和核心過(guò)程,其它 Web 框架 Serverless 化的原理也基本一致,騰訊云也提供了原理一樣的 tencent-serverless-http (https://github.com/serverless-plus/tencent-serverless-http) 方便大家直接使用(但騰訊云不支持 HTTP 觸發(fā)器),大家可以將自己所使用的 Web 框架對(duì)照云廠(chǎng)商函數(shù)計(jì)算的使用方法親自開(kāi)發(fā)一個(gè)適配層實(shí)踐一下~
參考資料
Webserverless - FC Express extension (https://github.com/awesome-fc/webserverless/tree/master/packages/fc-express)
如何將 Web 框架遷移到 Serverless (https://zhuanlan.zhihu.com/p/152391799)
Serverless 工程實(shí)踐 | 傳統(tǒng) Web 框架遷移 (https://developer.aliyun.com/article/790302)
阿里云-觸發(fā)器簡(jiǎn)介 (https://help.aliyun.com/document_detail/53102.html)
前端學(xué) serverless 系列—— WebApplication 遷移實(shí)踐 (https://zhuanlan.zhihu.com/p/72076708)
作者:雪霽
歡迎關(guān)注微信公眾號(hào) :政采云前端團(tuán)隊(duì)