背景
目前有很多 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) | 當你的函數通過 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 項目,預置了編譯、打包、調試和釋出等開箱即用的功能,可以參考另一篇文章: