内容


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

将 Clojure OpenWhisk 操作连接成有用的序列

通过开发一个库存控制系统来了解实际做法

Comments

系列内容:

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

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

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

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

本系列包含 3 篇教程,通过开发一个库存控制系统来演示 Clojure 和 OpenWhisk,本文是第 2 篇。在第 1 部分中,我解释了如何利用 Node.js 运行时和 ClojureScript 包,来使用 Clojure(一种基于 Lisp 的函数编程语言)编写 OpenWhisk 操作。在第 2 部分中,我将介绍如何使用 OpenWhisk 序列,将操作组合到执行应用程序工作的有用代码块中。第 3 部分将介绍如何与外部数据库进行交互,以及如何使用日志记录在 OpenWhisk 应用程序中来调试 Clojure 代码。

完成第 2 部分后,您将拥有示例应用程序(库存管理系统)的大部分功能。您将能够使用 Clojure 操作生成 HTML 来显示信息,以及使用来自 HTML 表单的信息来修改存储在应用程序中的信息。

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

本教程以本系列第 1 篇教程中的信息为基础:“使用 Clojure 编写 OpenWhisk 教程,第 1 部分:使用 Lisp 方言为 OpenWhisk 编写简明的代码。”此外,您将需要:

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

运行应用程序获取代码

从 JSON 到 HTML

通常,OpenWhisk 操作使用 JSON 通信。但是,浏览器需要 HTML。所以,我们需要找到一种方式来将 JSON 转换为 HTML,并使用 Clojure 以模块化方式完成转换。

在本教程中,您将从 Clojure 和 OpenWhisk 中的单个操作发展为实际与浏览器通信的序列。

将商品数据载入一个表中

第一步是将商品数据载入一个表中。为此,在一个新目录中创建一个新操作。使用 第 1 部分 中创建的相同的 package.jsonmain.js 文件。这一次,唯一不同的文件是 action.cljs

(ns action.core (:require clojure.string))



(defn cljsMain [params] (
    let [
      cljParams (js->clj params)
      data (get cljParams "data")
      dataKeys (keys data)
      rowsAsList (map #(clojure.string/join ["<tr><td>" %
                                    "</td><td>" (get data %) "</td></tr>"])
                      dataKeys)
      rowsAsString (clojure.string/join rowsAsList)

    ]

    {"html" (clojure.string/join
        ["<table><tr><th>Item</th><th>Amt. in Stock</th></tr>"
        rowsAsString
        "</table>"
        ])
    }
  )
)

在上面的 action.cljs 清单中,与之前一样,第一个名称声明名称空间。但是 ns 有一个额外的参数: (:require clojure.string)。此参数导入 clojure.string 库。

cljsMain 函数的编写方式与 第 1 部分 中的函数类似。在一个 let 函数中计算一些中间值,然后创建最终的值:

(defn cljsMain [params] (
    let [
      cljParams (js->clj params)
      data (get cljParams "data")

keys 函数接受一个哈希表并返回一个键列表。在本例中,数据是数据库操作的输出,所以键是商品名称:

      dataKeys (keys data)

下一行使用了函数编程中的一个最重要的函数 map

      rowsAsList (map #(clojure.string/join ["<tr><td>" %
                                    "</td><td>" (get data %) "</td></tr>"])
                      dataKeys)

map 函数接受一个函数和一个列表,并返回列表中每个项目上运行函数的结果列表。

要查看 map 函数在 REPL 网站 上的实际效果,可运行 (map #(* 2 %) '(1 2 3 4))。(有关 REPL 的更多信息,请参阅 第 1 部分。)单引号 (') 表示这只是一个列表,而不是函数调用。

运行 map 的结果
运行 map 的结果

对于 action.cljs,函数是 clojure.string/join。该函数接收一个字符串矢量,返回一个连接它们的字符串。在本例中,它是一个表行,包含两列:商品名称和数量。

map 的结果是一个列表。但是,您想生成单一值,也就是一个字符串。为此,将所有值连接起来。这会得到一个包含所有数据库行的字符串:

      rowsAsString (clojure.string/join rowsAsList)

    ]

最后,添加表标签和标题行:

    {"html" (clojure.string/join
        ["<table><tr><th>Item</th><th>Amt. in Stock</th></tr>"
        rowsAsString
        "</table>"
        ])
    }
  )
)

备注:要将操作发送到 OpenWhisk,有一个类似下面这样的脚本很有用。它执行所有步骤,包括删除已存在的操作。这个脚本用于调试;您常常需要尝试多个潜在的解决方案才能找到有效的解决方案。

#! /bin/sh
# Send the action to OpenWhisk

npm install
zip -r action.zip package.json main.js action.cljs node_modules
wsk action delete inventory_json2html
wsk action create inventory_json2html action.zip --kind nodejs:6

将操作放入序列中

下一步是将您拥有的两个操作组合为一个序列 - 模拟数据库和转换为 HTML 表。为此,执行以下步骤:

  1. 切换到 IBM Cloud 中的 OpenWhisk 控制台。
  2. 单击边栏上的 Develop,然后单击操作 inventory_dbase
  3. 单击 Link into a Sequence
  4. 单击 MY ACTIONS 磁贴。
  5. 选择 inventory_json2html 并单击 + Add to Sequence。这将创建一个包含 2 个操作的序列。
  6. 然后单击 → This Looks Good
  7. 将该序列命名为“ShowItems”,然后单击 Save Action Sequence
  8. 单击 ShowItems,然后单击 Run this Sequence 并使用以下参数:
    {
     "action": "getAll"
    }
  9. 确认您在响应中获得了一个包含一个表的 HTML 值。

创建一个完整的 HTML 文件

现在可以生成一个表。在真实应用程序中,人们想要的多得多。例如,他们想要链接来展示他们可以执行的其他操作。要提供这些链接,可以创建另一个操作(将它称为“inventory_table2html”)。与之前一样,唯一不同的文件是 action.cljs

(ns action.core (:require clojure.string))

(def header (js* "require('fs').readFileSync(__dirname + '/../../../header')"))
(def footer (js* "require('fs').readFileSync(__dirname + '/../../../footer')"))


(defn cljsMain [params] (
    let [
      cljParams (js->clj params)
      htmlTable (get cljParams "html")
      bootstrapTable (clojure.string/replace htmlTable
          "<table>"
          "<table class=\"table table-striped\"> ")
      delme (prn "Parameter HTML:")
      delme (prn htmlTable)
    ]

    {"html" (clojure.string/join [header (clojure.string/replace bootstrapTable "$$$" "\"") footer])}
  )
)

我本可以指定 HTML 代码放在表之前和之后的字符串常量中。但是,这需要我对双引号 (") 进行转义,而双引号在 HTML 非常常见。相反,为 HTML 创建两个位于表前和表后的 文件(名为 header 和 footer)更容易。只要将它们添加到 action.zip 文件,它们就将与 action.cljs 文件位于同一个目录中,而且 action.cljs 文件将能够读取它们。

但是,如果尝试仅使用文件名 __dirname + "/header"(在 JavaScript 代码中,下面将会解释),则会出错。计算 Clojure 文件时,从这个日志片段可以看出,该系统实际上是 3 个更深的目录:

此屏幕截图显示了日志
此屏幕截图显示了日志

此错误是从系统调用(对操作系统内核的调用)open 返回的。错误代码 ENOENT 表示“Error: NO ENTry”,因为该目录中不存在该文件。错误代码后第 2 行上的文件名向我们显示了该目录。

这个 action.cljs 文件中有多个新商品:

  • 以下两行读取 header 和 footer 文件。我没有将读取文件的 JavaScript 代码转换为 Clojure,而决定简单地使用 js*。对于库函数调用,我想这样更清晰。
    (def header (js* "require('fs').readFileSync(__dirname + '/../../../header')"))
    (def footer (js* "require('fs').readFileSync(__dirname + '/../../../footer')"))
  • 我从上一个操作获得的表是一个标准 HTML 表。 但是,完整的网页使用了 Bootstrap 模板,这要求将类添加到表标签。解决方案是使用 clojure.string/replace 函数,它可以将一个字符串替换为另一个(它也可以使用正则表达式):
          bootstrapTable (clojure.string/replace htmlTable
              "<table>"
              "<table class=\"table table-striped\"> ")
  • 对于调试用途,生成日志条目很有用。用于写到标准输出的函数是 prn。但是要在 let 内使用任何函数,您需要将结果分配到某处。这是使用它的代码:
    delme (prn "Parameter HTML:")
    delme (prn h tmlTable)

    要查看这些消息,可以单击 OpenWhisk 控制台中的 Show Logs。然后您应该会看到一个类似这样的页面:

    此屏幕截图显示了日志
    此屏幕截图显示了日志

将该 HTML 保存到 Internet

现在可以生成 HTML。但是,它被隐藏在一个 JSON 结构内。可以编写一个 index.html 来读取它,这已在另一篇教程“使用 IBM Cloud 和 Node.js 构建面向用户的 OpenWhisk 应用程序”中解释了。 但是也可以将一个序列直接提供给浏览器:

  1. 返回到 OpenWhisk。
  2. 单击 Develop,然后单击 ShowItems 序列。
  3. 单击 Extend 并选择 MY ACTIONS > inventory_table2html
  4. 单击 + Add to Sequence
  5. 单击 Save Your Changes
  6. 单击左侧边栏上的 APIs
  7. 单击 Create Managed API +
  8. 使用这些参数创建一个 API:
    API 名称 Clojure Inventory
    API 的基础路径 /clj_inventory
    CORS Selected(启用 CORS)
  9. 使用这些参数在该 API 中创建一个操作:
    路径 /showItems
    动词 GET/clj_inventory
    包含操作的包 Default
    操作 ShowItems
    响应内容类型 text/html
  10. 单击 Save & expose
  11. 单击左侧边栏上的 API Explorer,单击该 API 中的一个操作 (getShowitems),然后复制来自 curl 命令的 URL。
  12. 将该 URL 粘贴到浏览器中,在后面加上
    ?action=getAll
    确认您获得了一个包含所有商品的表。
  13. action=getAll 替换为 action=getAvailable
    确认您获得了一个仅包含有货的商品的表。

将浏览器输入放入一个序列中

在上一节中,我使用了一个 URL 查询参数来将信息发送到 OpenWhisk 操作。在仅有一个简单参数时,比如 action,此方法很有效。但是,当发送大量信息时,该方法看起来很糟。最好使用 POST,且发送消息时不将它显示给用户。

可通过两种方式实现此目的:可以在浏览器中使用 JavaScript(最好使用 Angular 之类的库)来创建要提交的 JSON,或者可以创建普通的 HTML 表单并在 OpenWhisk 中执行转换。出于学习 OpenWhisk 上的 Clojure 的目的,第二种方法显然更好。

实际修改数据的应用程序由两个序列组成。第一个序列创建一个包含所有可用商品(和库存数量)的表单,第二个序列处理表单结果。本教程解释的是 point-of-sale 应用程序。另外两个应用程序(reorder 和 correction)基本上差不多。

创建表单

要创建表单,可以创建一个名为 inventory_purchaseForm 的新操作(参见源代码)。此操作与 inventory_json2HTML 非常相似,但该表嵌入在一个表单中,而且有单独一列来表示购买的数量。

一个问题是,表单需要使用双引号 (")。不幸的是,由于 clojure.string/replace 的操作方式 - 将此字符放在字符串中(无论是否转义)- 会让 inventory_table2HTML 操作难以理解。可能有更优雅的解决方案,但出于时间关系,我仅使用 3 个美元符号 ($$$) 代替,然后在从 inventory_table2HTML 发出响应前替换它们,如这行代码所示:

{"html" (clojure.string/join [header (clojure.string/replace bootstrapTable "$$$" "\"") footer])}

接下来,创建一个名为 PurchaseForm 的序列。此序列类似于 ShowItems,但中间操作是 inventory_purchaseForm。然后通过路径 /purchase、动词 GET 和回复内容类型 text/html 来将它添加到 API。

处理提交的表单

要查看提交的准确内容,以及如何获取该信息,我创建了一个回送操作 并将它配置为一个 /purchasePOST 动作的回复。这是响应:

{
  "T-shirt L": "1",
  "__ow_method": "post",
  "__ow_headers": {
      ...
  },
  "__ow_path": "",
  "T-shirt S": "",
  "T-shirt XL": "3",
  "action": "getAvailable"
}

现在您从 purchase 表单获得了参数,但也获得了 action(因为我们没有更改 URL,该 URL 中包含查询)和 3 个不需要的内部参数: __ow_method__ow_headers__ow_path。另外,如果任何行未填充,您将得到一个空字符串。

要将此表单转换为 inventory_dbase 操作能够理解的 JSON,可以创建一个名为 inventory_processPurchase 的新操作。可以在这里查看源代码。大部分代码都与我们之前完成的代码类似,但有两处不同。

  • 为了删除不属于该数据的键,我使用了 filter。但是,条件比较复杂。它使用了 or 函数和 5 个不同的条件。
    	realKeys (filter #(not (or (= % "action")
    				     (= % "__ow_method")
                                       (= % "__ow_headers")
                                       (= % "__ow_path")
    				     (= (get usefulParams %) "")
                               )))

    在 Clojure(和其他 LISP 系列语言)中,大多数计算函数都能够接受和处理多个值。要查看此命令在 REPL 网站上的实际效果,可以运行 (* 2 3 4 5 6 7)此屏幕截图显示了命令的结果
    此屏幕截图显示了命令的结果

    可以看到,所有数字相乘得到了 5040。
  • 另一个不同是,使用了 reduce 来创建数据哈希表。此函数有一个函数作为第一个参数,然后接受一个集合(比如列表、矢量等)。它将该集合转换为单个值,方法是在前两个值上运行该函数,然后在结果和第三个值上运行该函数,依此类推。如果用数学符号表示,它类似于 f(f(f(v1,v2),v3),v4)。要查看此命令在 REPL 网站上的实际效果,可以运行 (reduce #(+ %1 %2) '(1 2 3 4 5 6 7))此屏幕截图显示了命令的结果
    此屏幕截图显示了命令的结果

    将这些数字相加,得到 28。

要创建该哈希表,可以使用 assoc 函数,该函数接受一个哈希表作为第一个参数。这意味着,该列表需要在最初有一个空哈希表。为此,您可以使用 list* 函数,该函数接受参数并将它们放在列表(函数获取该列表作为它的最后一个参数)的开头。

data (reduce #(assoc %1 %2 (get usefulParams %2)) (list* {} realKeys))

要查看此命令在 REPL 网站上的实际效果,可以运行 (assoc {} :a :b):您获得了一个哈希表,该项的键为 :a ,值为 :b。

然后运行 (list* 1 2 3 4 5 6 '(7 8)),可以看到结果为一个包含数字 1-8 的列表:

此屏幕截图显示了命令的结果
此屏幕截图显示了命令的结果

接下来,创建序列。以下是一些操作和我们使用它们的原因:

操作用途
inventory_processPurchase 为数据库创建数据和操作
inventory_dbase 实际处理数据,返回商品列表
inventory_json2html 将商品列表转换为一个 HTML 表
inventory_table2html 将该 HTML 表转换为一个完整网页

最后,为路径 /purchase、动词 POST 和回复内容类型 text/html 创建一个 API 操作,以调用该序列。这允许您处理结果。

另外两个页面几乎相同,它们允许用户向现有库存中补货或在数据库中设置数量。不同之处在于:

  • 用于启动它们的查询(是 action=getAll 而不是 action=getAvailable
  • 处理表单后的 action 参数值
  • API 的路径

另外,在处理一次再订购时,有必要将这些值转换为字符串。否则,plus 函数会“看到”两个字符串,而且它实际上是 JavaScript plus,所以它会串联这两个字符串。完成此工作的最佳位置是在数据库操作中,使用 cljs.reader/parse-int 函数来完成。

可以查阅存储库中的代码,如果您感兴趣,可以自行研究。请注意,不需要一个新序列来显示该表单,而且处理它的序列仅使用一个不同的操作来支持对数据库的不同命令。可通过多种方式使用同一个操作,但它们会增加不必要的复杂性。

结束语

通过学习本教程,您已从 Clojure 和 OpenWhisk 中的单个操作发展为实际与浏览器通信的序列。该应用程序现在已完成,但完全没有用。信息存储在一个“数据库”中,这是一个变量,在 OpenWhisk 每次断定数据库操作一段时间未使用,它占用的内存最好用于另一种用途时,就会删除该变量。在本系列的最后一篇教程中,我将介绍如何使用一种更加永久的存储选项,并介绍其他一些琐碎的概念。


相关主题


评论

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Cloud computing
ArticleID=1055281
ArticleTitle=使用 Clojure 编写 OpenWhisk 操作,第 2 部分: 将 Clojure OpenWhisk 操作连接成有用的序列
publish-date=12122017