您好,登錄后才能下訂單哦!
今天就跟大家聊聊有關(guān) Node.js 實(shí)現(xiàn)輕量級(jí)云函數(shù)功能,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。
一、什么是云函數(shù)?
云函數(shù)是誕生于云服務(wù)的一個(gè)新名詞,顧名思義,云函數(shù)就是在云端(即服務(wù)端)執(zhí)行的函數(shù)。各個(gè)云函數(shù)相互獨(dú)立,簡(jiǎn)單且目的單一,執(zhí)行環(huán)境相互隔離。使用云函數(shù)時(shí),開發(fā)者只需要關(guān)注業(yè)務(wù)代碼本身,其它的諸如環(huán)境變量、計(jì)算資源等,均由云服務(wù)提供。
二、為什么需要云函數(shù)?
程序員說(shuō)不想買服務(wù)器,于是便有了云服務(wù);
程序員又說(shuō)連 server 都不想寫了,于是便有了云函數(shù)。
Serverless 架構(gòu)
通常我們的應(yīng)用,都會(huì)有一個(gè)后臺(tái)程序,它負(fù)責(zé)處理各種請(qǐng)求和業(yè)務(wù)邏輯,一般都需要跟網(wǎng)絡(luò)、數(shù)據(jù)庫(kù)等 I/O 打交道。而所謂的無(wú)服務(wù)器架構(gòu),就是把除了業(yè)務(wù)代碼外的所有事情,都交給執(zhí)行環(huán)境處理,開發(fā)者不需要知道 server 怎么跑起來(lái),數(shù)據(jù)庫(kù)的 api 怎么調(diào)用——一切交給外部,在“溫室”里寫代碼即可。
FaaS
而云函數(shù),正是 serverless 架構(gòu)得以實(shí)現(xiàn)的途徑。我們的應(yīng)用,將是一個(gè)個(gè)獨(dú)立的函數(shù)組成,每一個(gè)函數(shù)里,是一個(gè)小粒度的業(yè)務(wù)邏輯單元。沒有服務(wù)器,沒有 server 程序,“函數(shù)即服務(wù)”(Functions as a Service)。
三、如何實(shí)現(xiàn)?
由于本實(shí)現(xiàn)是應(yīng)用在一個(gè) CLI 工具里面的,函數(shù)聲明在開發(fā)者的項(xiàng)目文件里,因而大致過(guò)程如下:
1、函數(shù)聲明與存儲(chǔ)聲明
我們的目標(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' } };
存儲(chǔ)
由于有 method
等配置,因此編譯的時(shí)候,需要把上述聲明文件 require
進(jìn)來(lái),此時(shí),handler
字段是一個(gè) Function
類型的對(duì)象。可以調(diào)用其 toString
方法,得到字符串類型的函數(shù)體:
const f = require('./func.js'); const method = f.method; const body = f.handler.toString(); // async function (ctx) { // return 'hahha' // }
有了字符串的函數(shù)體,存儲(chǔ)就很簡(jiǎn)單了,直接存在數(shù)據(jù)庫(kù) string
類型的字段里即可。
2、函數(shù)執(zhí)行
url
如果用于前端調(diào)用,每個(gè)云函數(shù)需要有一個(gè)對(duì)應(yīng)的 url,以上述聲明文件的文件名為云函數(shù)的唯一名稱的話,可以簡(jiǎn)單將 url 設(shè)計(jì)為:
/f/:funcname
構(gòu)造獨(dú)立作用域(重點(diǎn))
在 js 世界里,執(zhí)行一個(gè)字符串類型的函數(shù)體,有以下這么一些途徑:
eval
函數(shù)
new Function
vm
模塊
那么要選哪一種呢?讓我們回顧云函數(shù)的特點(diǎn):各自獨(dú)立,互不影響,運(yùn)行在云端。
關(guān)鍵是將每個(gè)云函數(shù)放在一個(gè)獨(dú)立的作用域執(zhí)行,并且沒有訪問(wèn)執(zhí)行環(huán)境的權(quán)限,因此,最優(yōu)選擇是 nodejs 的 vm
模塊。關(guān)于該模塊的使用,可參考官方文檔。
至此,云函數(shù)的執(zhí)行可以分為三步:
從數(shù)據(jù)庫(kù)獲取函數(shù)體
構(gòu)造 context
// ctx 為 koa 的上下文對(duì)象 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
模塊針對(duì) vm
模塊的一些安全缺陷做了改進(jìn),也可用此模塊,思路大抵相同。
3、引用
雖然說(shuō)原則上云函數(shù)應(yīng)當(dāng)互相獨(dú)立,各不相欠,但是為了提高靈活性,我們還是決定支持函數(shù)間的相互引用,即可以在某云函數(shù)中調(diào)用另外一個(gè)云函數(shù)。
聲明
很簡(jiǎn)單,加個(gè)函數(shù)名稱的數(shù)組字段就好:
module.exports = { method: 'POST', use: ['func1', 'func2'], handler: async function (ctx) { return 'hahha' } };
注入
也很簡(jiǎn)單,根據(jù)依賴鏈把函數(shù)都找出來(lái),全部掛載在 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)
既然可以相互依賴,那必然會(huì)可能出現(xiàn) a→b→c→a 這種循環(huán)的依賴情況,所以需要在開發(fā)者提交云函數(shù)的時(shí)候,檢測(cè)依賴循環(huán)。
檢測(cè)的思路也很簡(jiǎn)單,在遍歷依賴鏈的過(guò)程中,每一個(gè)單獨(dú)的鏈條都記錄下來(lái),如果發(fā)現(xiàn)當(dāng)前遍歷到的函數(shù)在鏈條里出現(xiàn)過(guò),則發(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í)候,都需要進(jìn)行一下幾步:
獲取函數(shù)體
編譯代碼
構(gòu)造作用域和獨(dú)立環(huán)境
執(zhí)行
步驟3,因?yàn)槊看螆?zhí)行的參數(shù)都不一樣,也會(huì)有不同請(qǐng)求并發(fā)執(zhí)行同一個(gè)函數(shù)的情況,所以作用域 ctx
無(wú)法復(fù)用;步驟4是必須的,那么可優(yōu)化點(diǎn)就剩下了1和2。
代碼緩存
vm
模塊提供了代碼編譯和執(zhí)行分開處理的接口,因此每次獲取到函數(shù)體字符串之后,先編譯成 Script
對(duì)象:
// ...get code const script = new vm.Script(code);
執(zhí)行的時(shí)候可以直接傳入編譯好的 Script
對(duì)象:
// ...get sandbox vm.createContext(sandbox); script.runInContext(sandbox); const data = await sandbox.promise;
函數(shù)體緩存
簡(jiǎn)單的緩存,不需要很復(fù)雜的更新機(jī)制,定一個(gè)時(shí)間閾值,超過(guò)后拉取新的函數(shù)體并編譯得到 Script
對(duì)象,然后緩存起來(lái)即可:
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)容,你們對(duì) Node.js 實(shí)現(xiàn)輕量級(jí)云函數(shù)功能有進(jìn)一步的了解嗎?如果還想了解更多知識(shí)或者相關(guān)內(nèi)容,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝大家的支持。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。