天天看點

移植 express.js 應用到函數計算

移植 express.js 應用到函數計算

背景

目前有很多 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 參數:

  1. 函數 event 參數中包含的 isBase64Encoded 參數
  2. 函數傳回值中包含的 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 項目,預置了編譯、打包、調試和釋出等開箱即用的功能,可以參考另一篇文章:

繼續閱讀