在前两个教程中,您学习了如何使用Clojure(一种基于Lisp的功能编程语言)编写基本的OpenWhisk应用程序,从而为OpenWhisk应用程序创建操作。 在本教程中,我将通过向您展示如何改进任何此类应用程序来结束本系列。 首先,您将学习如何支持包含双引号的参数。 然后,我将向您展示如何使用永久数据库(Cloudant)而不是变量来存储信息。
构建应用程序所需的条件
本教程以“使用Clojure编写OpenWhisk动作”系列的前两个教程中的信息为基础, 第1部分,使用此Lisp方言为OpenWhisk编写简洁明了的代码 , 第2部分,将Clojure OpenWhisk动作连接到有用的序列中 ,因此我建议先阅读这些内容。 此外,您将需要:
- OpenWhisk和JavaScript的基本知识(Clojure是可选的;本教程说明了需要时所需要的内容)
- 一个免费的Bluemix帐户( 在此处注册 )
“函数式编程鼓励您隔离副作用,并将其与业务逻辑分开。 这导致应用程序具有更高的模块化,更易于测试和易于调试。 ”
包含引号的参数
在第1部分中,我介绍了main.js JavaScript:
// Get a Clojure environment
var cljs = require('clojurescript-nodejs');
// Evaluate the action code
cljs.evalfile(__dirname + "/action.cljs");
// The main function, the one called when the action is invoked
var main = function(params) {
var clojure = "(ns action.core)\n ";
var paramsString = JSON.stringify(params);
paramsString = paramsString.replace(/"/g, '\\"');
clojure += '(clj->js (cljsMain (js* "' + paramsString + '")))';
var retVal = cljs.eval(clojure);
return retVal;
};
exports.main = main;
该脚本使用非常简单的解决方案将参数传递给Clojure:
var paramsString = JSON.stringify(params);
paramsString = paramsString.replace(/"/g, '\\"');
…
'(js* "' + paramsString + '")))';
此解决方案可以产生一个字符串,例如{\"num\": 5, \"str\": \"whatever\"}
。 双反斜杠( \\
)转换为单个反斜杠(即转义字符)。 生成的Clojure代码是(js* " {\"num\": 5, \"str\": \"whatever\"}")
。 由于js*
评估它作为JavaScript获得的字符串参数,因此使我们回到了原始参数{"num": 5, "str": "whatever"}
。 问题是,如果字符串参数之一已经包含双引号( "
),则它将与引号完全一样地对待,从而导致诸如{"num": 5, "str": "what"ever"}
这样的表达式{"num": 5, "str": "what"ever"}
和语法错误。 从理论上讲,您可以通过使用js/<var name>
语法来访问带有参数的变量来解决此问题,但是由于某些原因,这在OpenWhisk中不起作用。
在第3部分中,我介绍了fixHash
函数,该函数遍历参数(包括任何嵌套的数据结构)并查找字符串。 在字符串中,它将所有双引号替换为\\x22
。 第一个反斜杠转义第二个反斜杠,因此实际值为\x22
。 该值最终会转换为ASCII字符0x22(十进制34,即双引号),但是这种情况会在以后发生,因此replace
方法不会修改这些字符。
// Fix a hash table so it won't have internal double quotes
var fixHash = function(hash) {
if (typeof hash === "object") {
if (hash === null) return null;
if (hash instanceof Array) {
for (var i=0; i<hash.length; i++)
hash[i] = fixHash(hash[i]);
return hash;
}
var keys = Object.keys(hash)
for (var i=0; i<keys.length; i++)
hash[keys[i]] = fixHash(hash[keys[i]]);
return hash;
}
if (typeof hash === "string") {
return hash.replace(/"/g, '\\x22');
}
return hash;
};
保存到数据库
在几分钟不使用时重置为初始值的库存管理系统不是很有用。 因此,下一步是设置一个对象存储实例以存储数据库值( inventory_dbase
操作中的dbase
变量):
- 在Bluemix控制台中,单击Menu图标,然后转到Services> Data&Analytics 。
- 单击创建数据和分析服务,然后选择Cloudant NoSQL DB 。
- 将服务命名为“ OpenWhisk-Inventory-Storage”,然后单击“ 创建” 。
- 创建服务后,将其打开,然后单击服务凭证>新建凭证 。
- 将新凭据命名为“库存应用”,然后点击添加 。
- 单击查看凭据,然后将凭据复制到文本文件中。
- 选择管理>启动 。
- 单击数据库图标,然后单击创建数据库 。
- 将数据库命名为“ openwhisk_inventory”。
- 单击“ 所有文档”行中的加号图标,然后选择“ 新建文档” 。
- 将dbase.json的内容复制到文本区域,然后单击创建文档 。
有两种方法可以将Cloudant数据库与应用程序结合。 您可以Cloudant行动要么添加到序列,或修改inventory_dbase
行动。 我选择了第二个解决方案,因为它使我可以更改单个操作(因为所有数据库工作都集中在此)。
- 将Cloudant的npm库添加到package.json中的依赖项,并更新ventory_dbase操作的action.cljs文件:
(ns action.core) (def cloudant-fun (js/require "cloudant")) (def cloudant (cloudant-fun "url goes here")) (def mydb (.use (aget cloudant "db") "openwhisk_inventory")) ; Process an action with its parameters and the existing database ; return the result of the action (defn processDB [action dbase data] (case action "getAll" {"data" dbase} "getAvailable" {"data" (into {} (filter #(> (nth % 1) 0) dbase))} "processCorrection" (do (def dbaseNew (into dbase data)) {"data" dbaseNew} ) "processPurchase" (do (def dbaseNew (merge-with #(- %1 %2) dbase data)) {"data" dbaseNew} ) "processReorder" (do (def dbaseNew (merge-with #(+ (- %1 0) (- %2 0)) dbase data)) {"data" dbaseNew} ) {"error" "Unknown action"} ) ; end of case ) ; end of processDB (defn cljsMain [params] ( let [ cljParams (js->clj params) action (get cljParams "action") data (get cljParams "data") updateNeeded (or (= action "processReorder") (= action "processPurchase") (= action "processCorrection")) ] ; Because promise-resolve is here, it can reference ; action (defn promise-resolve [resolve param] (let [ dbaseJS (aget param "dbase") dbaseOld (js->clj dbaseJS) result (processDB action dbaseOld data) rev (aget param "_rev") ] (if updateNeeded (.insert mydb (clj->js {"dbase" (get result "data"), "_id" "dbase", "_rev" rev}) #(do (prn result) (prn (get result "data")) (resolve (clj->js result))) ) (resolve (clj->js result)) ) ) ; end of let ) ; end of defn promise-resolve (defn promise-func [resolve reject] (.get mydb "dbase" #(promise-resolve resolve %2)) ) (js/Promise. promise-func) ) ; end of let ) ; end of cljsMain
让我们看一下action.cljs的一些更有趣的部分。
您需要获取数据库。 在JavaScript中,您可以这样编码:
var cloudant_fun = require("cloudant ");
var cloudant = cloudant_fun(<<<URL>>>);
var mydb = cloudant.db.use("openwhisk_inventory ");
相同代码的Clojure版本相似,但有一些区别。 首先, require
是一个JavaScript函数。 要访问它,您需要使用js
名称空间对其进行限定(上述步骤12中清单3的行):
(def cloudant-fun (js/require "cloudant"))
下一行(第5行)非常标准。 URL是数据库凭据中的URL参数:
(def cloudant (cloudant-fun <<URL GOES HERE>>))
要从JavaScript对象获取成员,请使用aget
。 要使用对象的方法,可以使用(.<method> <object> <other parameters>)
。 参见第7行:
(def mydb (.use (aget cloudant "db") "openwhisk_inventory"))
从数据库读取和写入都是异步操作。 这意味着您不能简单地运行它们并将结果返回给调用方(OpenWhisk系统)。 相反,您需要返回Promise对象 。 该构造函数接受一个参数-调用该函数以启动需要结果的进程。 从Clojure调用JavaScript对象的构造函数的语法是(js/<object name>. <parameters>)
。 参见第75行:
(js/Promise. promise-func)
提供给Promise对象的构造promise-func
的函数是promise-func
。 它接收两个参数。 一种是在成功的情况下调用的函数(一个参数,即操作的结果)。 另一个是发生故障时要调用的函数(也是一个参数,错误对象)。 在这种情况下,该函数获取dbase
文档,然后使用成功函数和该文档调用promise-resolve
。 匿名函数( #(promise-resolve resolve %2)
)的第一个参数是错误(如果有)。 因为这是一个演示程序,所以为了简单起见,我们忽略了错误。 参见第71-73行:
(defn promise-func [resolve reject]
(.get mydb "dbase" #(promise-resolve resolve %2))
)
promise-func
和promise-resolve
都在cljsMain中定义。 原因是promise-resolve
需要action参数的值。 通过在cljsMain中定义这些函数,您可以仅使用该局部变量,而不必将其拖向整个调用链。 参见第52-55行:
(defn promise-resolve [resolve param] (let
[
dbaseJS (aget param "dbase")
dbaseOld (js->clj dbaseJS)
获取或修改数据的函数是processDB
。 此函数封装了第1部分中介绍的功能。请参见第56行:
result (processDB action dbaseOld data)
由于Cloudant使用算法来确保状态一致性,因此修订( _rev
)是更新数据库所必需的。 阅读文档时,会得到当前的修订版( _rev
)。 编写更新的版本时,必须向Cloudant提供要更新的修订。 同时,如果另一个进程更新了文档,则版本将不匹配,并且更新将失败。 参见第57行:
rev (aget param "_rev")
如果数据已修改,请更新Cloudant。 参见第59行:
(if updateNeeded
向数据库提供新数据,文档名称和要更新的修订版。 参见第60-62行:
(.insert mydb (clj->js {"dbase" (get result "data"),
"_id" "dbase",
"_rev" rev})
更新完成后(我们仅假设更新成功;这是一个教育示例,而不是生产代码),请运行以下功能。 注意添加调试打印输出的机制。 使用do
可以计算多个表达式,可以调用任意数量的prn
函数来打印所需的信息,然后将实际需要的表达式放在最后。
在这种情况下,您调用通过Promise对象获得的resolve
函数。 由于此函数在JavaScript中,因此需要接收一个JavaScript对象,而不是Clojure对象,因此请使用clj->js
。 参见第63-64行:
#(do (prn result) (prn (get result "data")) (resolve (clj->js result)))
)
如果不需要更新Cloudant,只需运行resolve
功能。 参见第65-66行:
(resolve (clj->js result))
)
编译Clojure代码
当前,我们将Clojure代码发送到OpenWhisk,并在Node.js应用程序重新启动时在此处进行编译。 另一种选择是一次将Clojure本地编译为JavaScript,然后发送已编译的版本。 如果您有兴趣,可以在这里查看它的工作方式。 但是,此方法不能显着提高性能。
从下面的屏幕截图中可以看到,即使使用已编译的ClojureScript代码,该操作的第一次调用仍比后续操作花费更多的时间。
速度慢的原因是实际编译不是很耗费资源。 Clojure环境的创建是使用Clojure的资源密集型部分,即使Clojure代码本身已编译为JavaScript,这也是必需的。 编译器生成JavaScript使用一些繁重的库。
结论
到此为止,Clojure中的OpenWhisk操作系列结束了。 我希望我已经向您展示了将函数式编程用于函数即服务(FaaS)的一些优点。 归根结底,几乎每个应用程序都需要在某个时候使用副作用,但是功能编程鼓励您隔离这些副作用并将它们与业务逻辑分开。 这导致应用程序具有更高的模块化,更易于测试和易于调试。 通过将应用程序逻辑分为动作和序列,可以很容易地使应用程序的大规模结构更清晰,并编写单元测试以作为REST调用运行。
翻译自: https://www.ibm.com/developerworks/cloud/library/cl-clojure-openwhisk3/index.html