詳解基于 Node.js 的輕量級云函數(shù)功能實現(xiàn)
導(dǎo)語
在萬物皆可云的時代,你的應(yīng)用甚至不需要服務(wù)器。云函數(shù)功能在各大云服務(wù)中均有提供,那么,如何用“無所不能”的 node.js 實現(xiàn)呢?
一、什么是云函數(shù)?
云函數(shù)是誕生于云服務(wù)的一個新名詞,顧名思義,云函數(shù)就是在云端(即服務(wù)端)執(zhí)行的函數(shù)。各個云函數(shù)相互獨立,簡單且目的單一,執(zhí)行環(huán)境相互隔離。使用云函數(shù)時,開發(fā)者只需要關(guān)注業(yè)務(wù)代碼本身,其它的諸如環(huán)境變量、計算資源等,均由云服務(wù)提供。
二、為什么需要云函數(shù)?
程序員說不想買服務(wù)器,于是便有了云服務(wù);
程序員又說連 server 都不想寫了,于是便有了云函數(shù)。
Serverless 架構(gòu)
通常我們的應(yīng)用,都會有一個后臺程序,它負責(zé)處理各種請求和業(yè)務(wù)邏輯,一般都需要跟網(wǎng)絡(luò)、數(shù)據(jù)庫等 I/O 打交道。而所謂的無服務(wù)器架構(gòu),就是把除了業(yè)務(wù)代碼外的所有事情,都交給執(zhí)行環(huán)境處理,開發(fā)者不需要知道 server 怎么跑起來,數(shù)據(jù)庫的 api 怎么調(diào)用——一切交給外部,在“溫室”里寫代碼即可。
FaaS
而云函數(shù),正是 serverless 架構(gòu)得以實現(xiàn)的途徑。我們的應(yīng)用,將是一個個獨立的函數(shù)組成,每一個函數(shù)里,是一個小粒度的業(yè)務(wù)邏輯單元。沒有服務(wù)器,沒有 server 程序,“函數(shù)即服務(wù)”(Functions as a Service)。
三、如何實現(xiàn)?
由于本實現(xiàn)是應(yīng)用在一個 CLI 工具里面的,函數(shù)聲明在開發(fā)者的項目文件里,因而大致過程如下:

1、函數(shù)聲明與存儲聲明
我們的目標(biāo)是讓云函數(shù)的聲明和一般的 js 函數(shù)沒什么兩樣:
module.exports = async function (ctx) {
return 'hahha'
}
};
由于云函數(shù)的執(zhí)行通常伴隨著接口的調(diào)用,所以應(yīng)該要能支持聲明 http 方法:
module.exports = {
method: 'POST',
handler: async function (ctx) {
return 'hahha'
}
};
存儲
由于有 method 等配置,因此編譯的時候,需要把上述聲明文件 require 進來,此時,handler 字段是一個 Function 類型的對象??梢哉{(diào)用其 toString 方法,得到字符串類型的函數(shù)體:
const f = require('./func.js');
const method = f.method;
const body = f.handler.toString();
// async function (ctx) {
// return 'hahha'
// }
有了字符串的函數(shù)體,存儲就很簡單了,直接存在數(shù)據(jù)庫 string 類型的字段里即可。
2、函數(shù)執(zhí)行
url
如果用于前端調(diào)用,每個云函數(shù)需要有一個對應(yīng)的 url,以上述聲明文件的文件名為云函數(shù)的唯一名稱的話,可以簡單將 url 設(shè)計為:
/f/:funcname
構(gòu)造獨立作用域(重點)
在 js 世界里,執(zhí)行一個字符串類型的函數(shù)體,有以下這么一些途徑:
eval函數(shù)new Functionvm模塊
那么要選哪一種呢?讓我們回顧云函數(shù)的特點:各自獨立,互不影響,運行在云端。
關(guān)鍵是將每個云函數(shù)放在一個獨立的作用域執(zhí)行,并且沒有訪問執(zhí)行環(huán)境的權(quán)限,因此,最優(yōu)選擇是 nodejs 的 vm 模塊。關(guān)于該模塊的使用,可參考官方文檔。
至此,云函數(shù)的執(zhí)行可以分為三步:
- 從數(shù)據(jù)庫獲取函數(shù)體
- 構(gòu)造
context
// ctx 為 koa 的上下文對象
const sandbox = {
ctx: {
params: ctx.params,
query: ctx.query,
body: ctx.request.body,
userid: ctx.userid,
},
promise: null,
console: console
}
vm.createContext(sandbox);
執(zhí)行函數(shù)得到結(jié)果
const code = `func = ${funcBody}; promise = func(ctx);`;
vm.runInContext(code, sandbox);
const data = await sandbox.promise;
NPM社區(qū)的 vm2 模塊針對 vm 模塊的一些安全缺陷做了改進,也可用此模塊,思路大抵相同。
3、引用
雖然說原則上云函數(shù)應(yīng)當(dāng)互相獨立,各不相欠,但是為了提高靈活性,我們還是決定支持函數(shù)間的相互引用,即可以在某云函數(shù)中調(diào)用另外一個云函數(shù)。
聲明
很簡單,加個函數(shù)名稱的數(shù)組字段就好:
module.exports = {
method: 'POST',
use: ['func1', 'func2'],
handler: async function (ctx) {
return 'hahha'
}
};
注入
也很簡單,根據(jù)依賴鏈把函數(shù)都找出來,全部掛載在 ctx 下就好,深度優(yōu)先或者廣度優(yōu)先都可以。
if (func.use) {
const funcs = {};
const fnames = func.use;
for (let i = 0; i < fnames.length; i++) {
const fname = fnames[i];
await getUsedFuncs(ctx, fname, funcs);
}
const funcCode = `{
${Object.keys(funcs).map(fname => `${fname}:${funcs[fname]}`).join('\n')}
}`;
code = `ctx.methods=${funcCode};$[code]`;
} else {
code = `ctx.methods={};$[code]`;
}
// 獲取所有依賴的函數(shù)
const getUsedFuncs = async (ctx, funcName, methods) => {
const func = getFunc(funcName);
methods[funcName] = func.body;
if (func.use) {
const uses = func.use.split(',');
for (let i = 0; i < uses.length; i++) {
await getUsedFuncs(ctx,uses[i], methods);
}
}
}
依賴循環(huán)
既然可以相互依賴,那必然會可能出現(xiàn) a→b→c→a 這種循環(huán)的依賴情況,所以需要在開發(fā)者提交云函數(shù)的時候,檢測依賴循環(huán)。
檢測的思路也很簡單,在遍歷依賴鏈的過程中,每一個單獨的鏈條都記錄下來,如果發(fā)現(xiàn)當(dāng)前遍歷到的函數(shù)在鏈條里出現(xiàn)過,則發(fā)生循環(huán)。
const funcMap = {};
flist.forEach((f) => {
funcMap[f.name] = f;
});
const chain = [];
flist.forEach((f) => {
getUseChain(f, chain);
});
function getUseChain(f, chain) {
if (chain.includes(f.name)) {
throw new Error(`函數(shù)發(fā)生循環(huán)依賴:${[...chain, f.name].join('→')}`);
} else {
f.use.forEach((fname) => {
getUseChain(funcMap[fname], [...chain, f.name]);
});
}
}
4、性能
上述方案中,每次云函數(shù)執(zhí)行的時候,都需要進行一下幾步:
- 獲取函數(shù)體
- 編譯代碼
- 構(gòu)造作用域和獨立環(huán)境
- 執(zhí)行
步驟3,因為每次執(zhí)行的參數(shù)都不一樣,也會有不同請求并發(fā)執(zhí)行同一個函數(shù)的情況,所以作用域 ctx 無法復(fù)用;步驟4是必須的,那么可優(yōu)化點就剩下了1和2。
代碼緩存
vm 模塊提供了代碼編譯和執(zhí)行分開處理的接口,因此每次獲取到函數(shù)體字符串之后,先編譯成 Script 對象:
// ...get code const script = new vm.Script(code);
執(zhí)行的時候可以直接傳入編譯好的 Script 對象:
// ...get sandbox vm.createContext(sandbox); script.runInContext(sandbox); const data = await sandbox.promise;
函數(shù)體緩存
簡單的緩存,不需要很復(fù)雜的更新機制,定一個時間閾值,超過后拉取新的函數(shù)體并編譯得到 Script 對象,然后緩存起來即可:
const cacheFuncs = {};
// ...get script
cacheFuncs[funcName] = {
updateTime: Date.now(),
script,
};
// cache time: 60 sec
const cacheFunc = cacheFuncs[cacheKey];
if (cacheFunc && (Date.now() - cacheFunc.updateTime) <= 60000) {
const sandbox = { /*...*/ }
vm.createContext(sandbox);
cacheFunc.script.runInContext(sandbox);
const data = await saandbox.promise;
return data;
} else {
// renew cache
}
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Node.js命令行/批處理中如何更改Linux用戶密碼淺析
這篇文章主要給大家介紹了關(guān)于Node.js命令行/批處理中如何更改Linux用戶密碼的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07
NestJS中集成TypeORM進行數(shù)據(jù)庫操作
本文深入探討了如何在NestJS中集成TypeORM進行數(shù)據(jù)庫操作,包括TypeORM的配置和集成、實體設(shè)計和關(guān)系映射、Repository模式的應(yīng)用、事務(wù)處理方案、數(shù)據(jù)庫遷移管理、性能優(yōu)化策略2024-12-12
node?NPM庫promise?異步任務(wù)狀態(tài)管理
這篇文章主要介紹了node?NPM庫promise?異步任務(wù)狀態(tài)管理2023-07-07
詳解如何在nodejs項目中使用Vue的響應(yīng)式API
vue3的響應(yīng)式API大家應(yīng)該都特別熟悉,平時大家都是在vue-cli或者vite創(chuàng)建的vue項目里面使用的這些響應(yīng)式API,今天小編來和大家聊聊如何在node.js項目中使用vue的響應(yīng)式API吧2024-11-11
node?NPM庫string-random生成隨機字符串學(xué)習(xí)使用
這篇文章主要為大家介紹了node?NPM庫string-random生成隨機字符串學(xué)習(xí)使用,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07
Nodejs?http模塊返回內(nèi)容中文亂碼問題及解決
這篇文章主要介紹了Nodejs?http模塊返回內(nèi)容中文亂碼問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12

