内容


使用 Clojure 编写 OpenWhisk 操作,第 3 部分

改进您的 OpenWhisk Clojure 应用程序

通过开发一个库存控制系统来了解该如何做

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 使用 Clojure 编写 OpenWhisk 操作,第 3 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:使用 Clojure 编写 OpenWhisk 操作,第 3 部分

敬请期待该系列的后续内容。

在前两篇教程中,您学习了如何使用 Clojure(一种基于 Lisp 的函数式编程语言)编写一个基本的 OpenWhisk 应用程序,从而为 OpenWhisk 应用程序创建操作。本教程是本系列的最后一部分,将展示如何改进任何这类应用程序。首先,您将学习如何支持包含双引号的参数。然后,我将展示如何使用永久型数据库 (Cloudant) 代替变量来存储信息。

构建您的应用程序需要做的准备工作

本教程以“使用 Clojure 编写 OpenWhisk 操作”系列的前两篇教程(第 1 部分:使用 Lisp 方言为 OpenWhisk 编写简明的代码第 2 部分:将 Clojure OpenWhisk 操作连接为有用的序列)中的信息为基础,所以建议您先阅读这两篇教程。此外,您将需要:

  • OpenWhisk 和 JavaScript 的基本知识(Clojure 是可选的,在您需要它时,本教程会介绍您需要掌握的知识)
  • 一个 IBM Cloud 帐户(在此处注册

运行应用程序获取代码

函数式编程没有副作用,将它们与业务逻辑分离。这样做可以得到更加模块化、更容易测试、更容易调试的应用程序。

包含引号的参数

在第 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"} 的表达式和语法错误。从理论上讲,要解决此问题,可以使用 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 变量):

  1. 在 IBM Cloud 控制台中,单击 Menu 图标并转到 Services > Data & Analytics
  2. 单击 Create Data & Analytics service,并选择 Cloudant NoSQL DB
  3. 将服务命名为“OpenWhisk-Inventory-Storage”并单击 Create
  4. 创建该服务后,打开它并单击 Service credentials > New credential
  5. 将新凭证命名为“Inventory-App”并单击 Add
  6. 单击 View credentials 并将该凭证复制到一个文本文件中。
  7. 选择 Manage > Launch
  8. 单击数据库图标和 Create Database
  9. 将该数据库命名为“openwhisk_inventory”。
  10. 单击 All Documents 行中的加号图标并选择 New Doc
  11. dbase.json 的内容复制到文本区并单击 Create Document

可通过两种方式将 Cloudant 数据库与应用程序相结合。可以将 Cloudant 操作添加到序列中,或者修改 inventory_dbase 操作。我选择了第二种解决方案,因为它允许我更改单个操作(因为所有数据库工作都集中在这里)。

  1. 用于 Cloudant 的 npm 库添加到 package.json 中的依赖项中,并更新 inventory_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。它接受两个参数。一个是在成功时调用的函数(一个参数,即该操作的结果)。另一个是在失败时调用的函数(也是一个参数,即 error 对象)。在本例中,该函数获取 dbase 文档,然后使用 success 函数和该文档来调用 promise-resolve。该匿名函数的第一个参数 (#(promise-resolve resolve %2)) 是 error(如果有)。因为这是一个演示程序,为了简便起见,我们忽略了错误。参见第 71-73 行:

    (defn promise-func [resolve reject]
      (.get mydb "dbase" #(promise-resolve resolve %2))
    )

promise-funcpromise-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 代码

目前,只要 Node.js 应用程序重新启动,我们就会将 Clojure 代码发送到 OpenWhisk,并在那里编译它。另一个选择是在本地将 Clojure 一次性编译为 JavaScript,并发送已编译好的版本。如果您感兴趣的话,可以在这里了解它的工作原理。但是,此方法不会显著提高性能。

在下面的屏幕截图中可以看到,尽管使用了编译后的 ClojureScript 代码,第一次调用该操作所花的时间仍比后续调用长得多。

屏幕截图显示了对操作的第一次调用
屏幕截图显示了对操作的第一次调用

缓慢的原因在于,实际编译对资源的使用不是很密集。创建 Clojure 环境是 Clojure 使用过程中的资源密集部分,而且即使 Clojure 代码本身已编译为 JavaScript,也需要这么做。编译器生成的 JavaScript 使用了一些大型的库。

结束语

这个介绍 Clojure 中的 OpenWhisk 操作的教程系列到此就结束了。希望我向您展示了使用函数式编程来实现函数即服务 (FaaS) 的一些优势。到最后,几乎每个应用程序都需要在某个时刻使用副作用,但函数式编程没有副作用,将它们与业务逻辑分离。这样做可以得到更加模块化、更容易测试、更容易调试的应用程序。通过将应用程序逻辑分离为操作和序列,很容易让应用程序的大型结构变得更清晰,并编写单元测试,将它们作为 REST 调用来运行。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Cloud computing
ArticleID=1055542
ArticleTitle=使用 Clojure 编写 OpenWhisk 操作,第 3 部分: 改进您的 OpenWhisk Clojure 应用程序
publish-date=12192017