详解基于 Node.js 的轻量级云函数功能实现


Posted in Javascript onJuly 08, 2019

导语

在万物皆可云的时代,你的应用甚至不需要服务器。云函数功能在各大云服务中均有提供,那么,如何用“无所不能”的 node.js 实现呢?

一、什么是云函数?

云函数是诞生于云服务的一个新名词,顾名思义,云函数就是在云端(即服务端)执行的函数。各个云函数相互独立,简单且目的单一,执行环境相互隔离。使用云函数时,开发者只需要关注业务代码本身,其它的诸如环境变量、计算资源等,均由云服务提供。

二、为什么需要云函数?

程序员说不想买服务器,于是便有了云服务;
程序员又说连 server 都不想写了,于是便有了云函数。

Serverless 架构

通常我们的应用,都会有一个后台程序,它负责处理各种请求和业务逻辑,一般都需要跟网络、数据库等 I/O 打交道。而所谓的无服务器架构,就是把除了业务代码外的所有事情,都交给执行环境处理,开发者不需要知道 server 怎么跑起来,数据库的 api 怎么调用——一切交给外部,在“温室”里写代码即可。

FaaS

而云函数,正是 serverless 架构得以实现的途径。我们的应用,将是一个个独立的函数组成,每一个函数里,是一个小粒度的业务逻辑单元。没有服务器,没有 server 程序,“函数即服务”(Functions as a Service)。

三、如何实现?

由于本实现是应用在一个 CLI 工具里面的,函数声明在开发者的项目文件里,因而大致过程如下:

详解基于 Node.js 的轻量级云函数功能实现

1、函数声明与存储声明

我们的目标是让云函数的声明和一般的 js 函数没什么两样:

module.exports = async function (ctx) {
  return 'hahha'
 }
};

由于云函数的执行通常伴随着接口的调用,所以应该要能支持声明 http 方法:

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 类型的字段里即可。

2、函数执行

url

如果用于前端调用,每个云函数需要有一个对应的 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 模块的一些安全缺陷做了改进,也可用此模块,思路大抵相同。

3、引用

虽然说原则上云函数应当互相独立,各不相欠,但是为了提高灵活性,我们还是决定支持函数间的相互引用,即可以在某云函数中调用另外一个云函数。

声明

很简单,加个函数名称的数组字段就好:

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]);
    });
  }
}

4、性能

上述方案中,每次云函数执行的时候,都需要进行一下几步:

  • 获取函数体
  • 编译代码
  • 构造作用域和独立环境
  • 执行

步骤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
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
javaScript 读取和设置文档元素的样式属性
Apr 14 Javascript
映彩衣的js随笔(js图片切换效果)
Jul 31 Javascript
js变换显示图片的实例
Apr 16 Javascript
完美解决AJAX跨域问题
Nov 01 Javascript
javascript Array.prototype.slice的使用示例
Nov 14 Javascript
JS实现5秒钟自动封锁div层的方法
Feb 20 Javascript
Node.js的项目构建工具Grunt的安装与配置教程
May 12 Javascript
基于BootStrap的Metronic框架实现页面链接收藏夹功能按钮移动收藏记录(使用Sortable进行拖动排序)
Aug 29 Javascript
JavaScript如何获取一个元素的样式信息
Jul 29 Javascript
Ant Design Vue 添加区分中英文的长度校验功能
Jan 21 Javascript
vue scroll滚动判断的实现(是否滚动到底部、滚动方向、滚动节流、获取滚动区域dom元素)
Jun 11 Javascript
jquery插件懒加载的示例
Oct 24 jQuery
使用 node.js 模仿 Apache 小部分功能
Jul 07 #Javascript
echarts统计x轴区间的数值实例代码详解
Jul 07 #Javascript
vue + typescript + video.js实现 流媒体播放 视频监控功能
Jul 07 #Javascript
详解django模板与vue.js冲突问题
Jul 07 #Javascript
django中使用vue.js的要点总结
Jul 07 #Javascript
Vue使用lodop实现打印小结
Jul 06 #Javascript
cordova+vue+webapp使用html5获取地理位置的方法
Jul 06 #Javascript
You might like
最简单的PHP程序--记数器
2006/10/09 PHP
typecho插件编写教程(四):插件挂载
2015/05/28 PHP
PHP中的类型提示(type hinting)功能介绍
2015/07/01 PHP
模仿百度三维地图的js数据分享
2011/05/12 Javascript
基于jquery创建的一个图片、视频缓冲的效果样式插件
2012/08/28 Javascript
兼容IE和Firefox火狐的上下、左右循环无间断滚动JS代码
2013/04/19 Javascript
JS判断文本框内容改变事件的简单实例
2014/03/07 Javascript
JQuery中extend使用介绍
2014/03/13 Javascript
浅谈javascript 函数内部属性
2015/01/21 Javascript
全系IE支持Bootstrap的解决方法
2015/10/19 Javascript
javascript实现拖动元素交换位置
2015/11/29 Javascript
轻松学习jQuery插件EasyUI EasyUI实现树形网络基本操作(2)
2015/11/30 Javascript
JavaScript判断变量是否为数组的方法(Array)
2016/02/24 Javascript
浅谈JS读取DOM对象(标签)的自定义属性
2016/11/21 Javascript
SVG动画vivus.js库使用小结(实例代码)
2017/09/14 Javascript
详解使用jQuery.i18n.properties实现js国际化
2018/05/04 jQuery
vue - props 声明数组和对象操作
2020/07/30 Javascript
vue使用echarts图表自适应的几种解决方案
2020/12/04 Vue.js
[46:20]CHAOS vs Alliacne 2019国际邀请赛小组赛 BO2 第二场 8.15
2019/08/16 DOTA
[45:06]完美世界DOTA2联赛PWL S2 Magma vs InkIce 第二场 11.28
2020/12/02 DOTA
python下函数参数的传递(参数带星号的说明)
2010/09/19 Python
Django集成百度富文本编辑器uEditor攻略
2014/07/04 Python
python实现图片变亮或者变暗的方法
2015/06/01 Python
使用Python的Dataframe取两列时间值相差一年的所有行方法
2018/07/10 Python
python实现自动网页截图并裁剪图片
2018/07/30 Python
Python发送邮件封装实现过程详解
2020/05/09 Python
python语言中有算法吗
2020/06/16 Python
Python 利用flask搭建一个共享服务器的步骤
2020/12/05 Python
雅高酒店中国:Accorhotels.com China
2018/03/26 全球购物
会计实习自我鉴定
2013/12/04 职场文书
小学教师节活动方案
2014/01/31 职场文书
乡党委干部党的群众路线教育实践活动个人对照检查材料思想汇报
2014/10/01 职场文书
2015年秋季小班开学寄语
2015/05/27 职场文书
消防安全培训工作总结
2015/10/23 职场文书
2016年学校党支部公开承诺书
2016/03/25 职场文书
关于党风廉政建设宣传教育月的活动总结!
2019/08/08 职场文书