在万物皆可云的时代,你的应用甚至不须要服务器。云函数功能在各大云服务中均有提供,那么,如何用“无所不能”的 node.js 实现呢?javascript
云函数是诞生于云服务的一个新名词,顾名思义,云函数就是在云端(即服务端)执行的函数。各个云函数相互独立,简单且目的单一,执行环境相互隔离。使用云函数时,开发者只须要关注业务代码自己,其它的诸如环境变量、计算资源等,均由云服务提供。html
程序员说不想买服务器,因而便有了云服务;
程序员又说连 server 都不想写了,因而便有了云函数。前端
一般咱们的应用,都会有一个后台程序,它负责处理各类请求和业务逻辑,通常都须要跟网络、数据库等 I/O 打交道。而所谓的无服务器架构,就是把除了业务代码外的全部事情,都交给执行环境处理,开发者不须要知道 server 怎么跑起来,数据库的 api 怎么调用——一切交给外部,在“温室”里写代码便可。java
而云函数,正是 serverless 架构得以实现的途径。咱们的应用,将是一个个独立的函数组成,每个函数里,是一个小粒度的业务逻辑单元。没有服务器,没有 server 程序,“函数即服务”(Functions as a Service)。node
因为本实现是应用在一个 CLI 工具里面的,函数声明在开发者的项目文件里,于是大体过程以下:程序员
咱们的目标是让云函数的声明和通常的 js 函数没什么两样:数据库
module.exports = async function (ctx) { return 'hahha' } };
因为云函数的执行一般伴随着接口的调用,因此应该要能支持声明 http 方法:api
module.exports = { method: 'POST', handler: async function (ctx) { return 'hahha' } };
因为有 method
等配置,所以编译的时候,须要把上述声明文件 require
进来,此时,handler
字段是一个 Function
类型的对象。能够调用其 toString
方法,获得字符串类型的函数体:数组
const f = require('./func.js'); const method = f.method; const body = f.handler.toString(); // async function (ctx) { // return 'hahha' // }
有了字符串的函数体,存储就很简单了,直接存在数据库 string
类型的字段里便可。promise
若是用于前端调用,每一个云函数须要有一个对应的 url,以上述声明文件的文件名为云函数的惟一名称的话,能够简单将 url 设计为:
/f/:funcname
在 js 世界里,执行一个字符串类型的函数体,有如下这么一些途径:
eval
函数new Function
vm
模块那么要选哪种呢?让咱们回顾云函数的特色:各自独立,互不影响,运行在云端。
关键是将每一个云函数放在一个独立的做用域执行,而且没有访问执行环境的权限,所以,最优选择是 nodejs 的 vm
模块。关于该模块的使用,可参考官方文档。
至此,云函数的执行能够分为三步:
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);
const code = `func = ${funcBody}; promise = func(ctx);`; vm.runInContext(code, sandbox); const data = await sandbox.promise;
NPM社区的 vm2
模块针对 vm
模块的一些安全缺陷作了改进,也可用此模块,思路大抵相同。
虽说原则上云函数应当互相独立,各不相欠,可是为了提升灵活性,咱们仍是决定支持函数间的相互引用,便可以在某云函数中调用另一个云函数。
很简单,加个函数名称的数组字段就好:
module.exports = { method: 'POST', use: ['func1', 'func2'], handler: async function (ctx) { return 'hahha' } };
也很简单,根据依赖链把函数都找出来,所有挂载在 ctx
下就好,深度优先或者广度优先均可以。
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}`; } // 获取全部依赖的函数 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); } } }
既然能够相互依赖,那必然会可能出现 a→b→c→a 这种循环的依赖状况,因此须要在开发者提交云函数的时候,检测依赖循环。
检测的思路也很简单,在遍历依赖链的过程当中,每个单独的链条都记录下来,若是发现当前遍历到的函数在链条里出现过,则发生循环。
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(`函数发生循环依赖:${[...chain, f.name].join('→')}`); } else { f.use.forEach((fname) => { getUseChain(funcMap[fname], [...chain, f.name]); }); } }
上述方案中,每次云函数执行的时候,都须要进行一下几步:
步骤3,由于每次执行的参数都不同,也会有不一样请求并发执行同一个函数的状况,因此做用域 ctx
没法复用;步骤4是必须的,那么可优化点就剩下了1和2。
vm
模块提供了代码编译和执行分开处理的接口,所以每次获取到函数体字符串以后,先编译成 Script
对象:
// ...get code const script = new vm.Script(code);
执行的时候能够直接传入编译好的 Script
对象:
// ...get sandbox vm.createContext(sandbox); script.runInContext(sandbox); const data = await sandbox.promise;
简单的缓存,不须要很复杂的更新机制,定一个时间阈值,超事后拉取新的函数体并编译获得 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 }
腾讯云 - 无服务云函数
阿里云 - 函数计算
AWS - Lambda
Azure - Azure Functions