Clojure Web开发实战(txt+pdf+epub+mobi电子书下载)


发布时间:2020-05-18 02:29:08

点击下载

作者:[美] Dmitri Sotnikov 肖特尼科夫

出版社:人民邮电出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Clojure Web开发实战

Clojure Web开发实战试读:

版权信息书名:Clojure Web开发实战作者:[美] Dmitri Sotnikov 肖特尼科夫排版:辛萌哒出版社:人民邮电出版社出版时间:2015-11-01ISBN:9787115398932本书由人民邮电出版社授权北京当当科文电子商务有限公司制作与发行。— · 版权所有 侵权必究 · —第1章起步

在简介部分,我们谈到了在编写应用程序时,采用函数式编程风格能够获得诸多好处。当然,想要学会一门语言,仅仅通过阅读是远远不够的,只有亲手编写一些代码,你才能获得真切的体验。

在本章中,我们将会介绍如何开发一个简单的留言簿应用,用户可以使用它给他人留言。通过它,我们能够了解Web应用的基本结构,并且尝试一些高效的Clojure开发工具。如果你是一个Clojure新手,那我建议你先跳到“附录2 Clojure入门”,快速了解一下Clojure的基本概念和语法。1.1 环境设置

Clojure需要Java虚拟机(JVM,Java Virtual Machine)才能运行,此外,你还需要一份1.6或是更高版本的Java开发工具包[1](JDK,Java Development Kit)用于开发。Clojure是作为一个JAR包来分发的,你只需简单地将其包含在工程的class-path中即可。你可以使用任何常规的Java工具来构建Clojure应用,比方说Maven[2]或者Ant[3]。不过,我强烈建议你使用Leiningen[4],它是专为Clojure定制的。使用Leiningen管理工程

借助Leiningen,你可以建立、构建、测试、打包和部署工程。也就是说,它能为你提供工程管理方面的一站式服务。

Maven是一个非常流行的Java依赖关系管理工具,而Leiningen就相当于Clojure世界中的Maven。重点是,Leiningen与Maven兼容,因此它可以毫无障碍地访问那些得到精心维护,且存放着海量Java类库的存储中心。此外,Clojure的库通常可以在Clojars[5]这个存储中心找到。所以,默认情况下Leiningen是启用了Clojars的。

使用Leiningen,你不用手动去下载那些在工程中需要用到的库。你只需要简单地声明一下工程的顶级依赖,剩下的事情Leiningen就会帮你自动搞定。

Leiningen的安装实在是小菜一碟,只需要从官方主页[6]上下载并执行安装脚本即可。

不如动手试试看。我们会通过执行下列命令,来下载这个脚本,并创建一个全新的Clojure工程:wget https://raw.github.com/technomancy/leiningen/stable/bin/leinchmod +x leinmv lein ~/binlein new myapp

由于这是我们第一次运行lein这个命令,它做的第一件事情是安装它自己。一切顺利的话,你将会看到下面的输出:Generating a project called myapp based on the 'default' template.To see other templates (app, lein plug-in, etc), try `lein help new`.

一个新的文件夹myapp就创建好了,里面是应用程序的骨架。应用程序的代码存放在src文件夹中。其中有另外一个myapp文件夹,这个文件夹中只有一个文件,名为core.clj。文件内容如下:(ns myapp.core)(defn foo  "I don't do a whole lot."   [x]  (println x "Hello, World!"))

请注意命名空间的声明,与其文件夹结构是相匹配的。由于命名空间core位于myapp目录当中,所以它的名字就是myapp.core。Leiningen工程文件一瞥

在工程文件夹myapp里有一个project.clj文件。这个文件包含了应用程序的描述信息,你可以仔细观察一下,就会发现这个文件是用标准的Clojure语法编写的,描述了应用的名称、版本、网址、许可证信息和依赖项,如下所示。(defproject myapp "0.1.0-SNAPSHOT":description "FIXME: write description":url "http://example.com/FIXME":license {:name "Eclipse Public License"     :url "http://www.eclipse.org/legal/epl-v10.html"}:dependencies [[org.clojure/clojure "1.5.1"]])

通过修改这个project.clj文件,能让我们控制应用程序的方方面面。例如,我们可以通过添加:main关键字,将myapp.core命名空间下的foo函数设置为应用的入口点:(defproject myapp "0.1.0-SNAPSHOT":description "FIXME: write description":url "http://example.com/FIXME":license {:name "Eclipse Public License"     :url "http://www.eclipse.org/legal/epl-v10.html"}:dependencies [[org.clojure/clojure "1.5.1"]];;this will set foo as the main function:main myapp.core/foo)

此时我们就可以通过执行lein run这个命令来运行应用了。由于foo函数要求传入一个参数,我们只得遵命行事:lein run FirstFirst Hello, World!

在前面这个例子中,我们创建的应用非常简单,只有一个依赖项:Clojure运行时。如果我们直接以此为基础来开发Web应用的话,就免不了要编写大量的样板代码,才能让它运行起来。下面就让我们看看如何利用Leiningen的模板,来创建一个开箱即用的Web应用吧。Leiningen的模板

当把模板的名称提供给lein脚本时,就可以根据其对应的模板来初始化工程骨架。其实模板自身也不过是使用了lein-newnew插件[7]的Clojure工程罢了。稍后我们将看到如何创建自己的模板。

眼下,我们将会使用compojure-app模板[8]来初始化下一个应用。执行lein脚本时,模板的名称是作为参数传给new关键字的,紧接其后的是工程名称。为了创建一个Web应用,而不是之前那样的默认工程,我们只需执行以下命令即可:lein new compojure-app guestbook

这样Leiningen就知道创建留言簿应用时,应该使用compojure-app模板了。此类应用需要启动一个Web服务才能运行。其实这很容易,我们只需要使用lein ring server来替代lein run即可。

当我们运行这个应用时,控制台会输出如下信息,与此同时还会弹出一个打开了应用主页的浏览器窗口。lein ring serverguestbook is starting2013-07-14 18:21:06.603:INFO:oejs.Server:jetty-7.6.1.v201202152013-07-14 18:21:06.639:INFO:oejs.AbstractConnector:StartedSelectChannelConnector@0.0.0.0:3000Started server on port 3000

喔,现在我们已经知道如何创建和运行应用了,接下来不妨考虑一下应该选用什么样的编辑器。

你多半已经留意到,Clojure代码中有大量的括号。保持它们起止对应很快就会成为一种挑战,所幸Clojure编辑器会替我们收拾这个摊子,否则会令人产生严重的挫败感。

事实上,这些编辑器不仅仅能平衡括号,其中的一些甚至能够感知其结构。这就意味着编辑器能够理解一个表达式是从什么地方开始,又到什么地方结束的。因此,我们可以根据逻辑上的代码块来导航和选取,而非简单针对文本行号。

在本章中,我们将会选用Light Table[9]来开发留言簿应用。获取并运行Light Table是非常容易的,这样我们就能尽快投入到代码的编写中了。然而,它的功能还比较有限,在较大的工程中,你对此可能有较深的体会。“附录1选择IDE”中还有对其他开发环境的讨论。使用Light Table

Light Table不需要安装,下载完成后即可直接运行。

Light Table的外观相当简洁。默认情况下,它仅在编辑器窗格中显示了几行欢迎信息,如图1-1所示。图1-1 Light Table工作区

为了显示workspace面板,我们可以在菜单中选择View →Workspace,或是按下Ctrl+T(Windows/Linux)组合键或Cmd+T(OS X)组合键。

如图1-2所示,我们可以在workspace的folder标签页中打开留言簿工程。图1-2 打开工程

一旦工程被选中,我们就可以浏览整个工程树,并选择我们想要编辑的文件,如图1-3所示。

现在,开发环境已经就绪,看起来我们终于可以为留言簿应用添加一些功能了。图1-3 Light Table的工程1.2 你的第一个工程

你的留言簿应该已经在控制台运行了,可以通过http://localhost:3000/来访问。在控制台终端按下Ctrl+C,就能停止它的运行。既然我们已经在Light Table的工作区打开了这个工程,不妨就直接在编辑器中运行它吧。

我们现在要更进一步,创建一个 “读取—求值—打印循环”(REPL,Read-Evaluate-Print Loop),将Light Table连接至我们的工程。菜单View →Connections可以打开连接标签页。如图1-4所示,让我们点击标签页中的Add Connection按钮。图1-4 Light Table的连接

此时,会弹出一个列表,列出了几种不同的连接选项。如图1-5所示,接下来选择Clojure。然后,让我们找到留言簿工程所在的文件夹,并且选中project.clj文件。图1-5 Light Table连接Clojure

一旦我们的工程与Light Table建立了连接,我们就可以直接在编辑器中对代码进行求值了。

说不如做,你可以立刻挑选一个函数,然后按下Ctrl+Enter(Windows/Linux)组合键或是Cmd+Enter(OS X)组合键。如果我们选择的是home函数,那么打印出来的内容应该是这样:#'guestbook.routes.home/home

这意味着这个函数已经在REPL中进行了求值,随时可用了。

另外,按下Ctrl+spacebar组合键后输入repl,就能打开一个即时repl。在这个新打开的编辑器窗格中,我们可以随意运行任何代码,如图1-6所示。图1-6 Light Table的即时repl

默认情况下,一旦进行任何修改,都会使得即时repl中的所有内容被重新求值。这被称为live实时模式。现在,让我们载入guestbook.repl命名空间,然后执行start-server函数。(use 'guestbook.repl)(start-server)

一旦上述代码完成求值,就会启动HTTP服务,同时打开一个新的浏览器窗口,指向了应用的主页,如图1-7所示。图1-7 在即时repl中运行服务

显然我们不希望start-server被反复调用,因此记得从即时repl里删除之前的代码。

另外,我们还可以关闭实时求值功能,只要点击右上角的live图标即可。禁用了实时模式后,我们可以通过Alt-Enter来进行选择性的求值。

下面,如图1-8所示,让我们执行(use 'guestbook.routes.home)来导入home命名空间,然后调用home函数。

如你所见,对home的调用只是简单生成了我们的HTML主页,一个字符串。这就是我们访问http://localhost:3000时,浏览器为我们呈现出来的内容。图1-8 使用REPL

值得注意的是,在我们的代码中使用了Clojure的vector(矢量表)来表达相应的HTML标签。如果我们添加一些新的标签,并在浏览器中刷新页面的话,立刻就能看到变化。例如,让我们对home函数稍事修改,让它能够显示标题,并提供一个用于录入消息的表单。(defn home [] (layout/common [:h1 "Guestbook"] [:p "Welcome to my guestbook"] [:hr] [:form  [:p "Name:"]  [:input]  [:p "Message:"]  [:textarea {:rows 10 :cols 40}]]))

好了,刷新一下页面,看到变化了吧,如图1-9所示。图1-9 留言簿

你可能已经猜到了,紧接着home函数的那几行代码,就是负责将“/”路由和处理函数home绑到一块儿的。(defroutes home-routes (GET "/" [] (home)))

此处,我们使用defroutes来定义guestbook.routes.home命名空间中的路由。每个路由都代表着一个应用会响应的URI地址。路由定义的起始位置是HTTP请求的类型,例如,GET或者POST,接下来则是参数和主体部分。

我们还会为这个工程添加更多的功能,在此之前,让我们了解一下Leiningen模板为我们生成了哪些文件吧。了解应用程序的结构

在Workspace标签页中展开我们的工程之后,看上去应该是这样的:guestbook/ resources/  public/   css/    screen.css   img/   js/ src  guestbook/  models/  routes/   home.clj  views/   layout.clj  handler.clj  repl.cljtest/ guestbook/ test/   hanlder.cljproject.cljREADME.md

位于工程根目录下的project.clj文件是用于配置和构建应用的。

还有几个文件夹,src用来存放应用的代码。resources文件夹则用来存放与应用相关的静态资源,比如CSS、图片和JavaScript脚本。最后,在test文件夹中,我们可以为应用添加一些测试。

Clojure命名空间遵循Java的打包约定,也就是说,如果命名空间包含前缀,则其存放的文件夹路径必须与前缀相匹配。需要注意的是,如果一个命名空间包含“-”,则体现在文件夹路径和文件名上时,“-”必须转换为“_”。

这是因为Java的包名中不允许出现“-”。而Clojure代码最终会被编译为JVM字节码,所以也必须遵守这个规则。

由于我们把自己的应用叫作guestbook,因此它所有的命名空间都被放置在了src/guestbook文件夹下。让我们看看都有些什么吧。首先,我们在src/guestbook/handler.clj文件中找到了guestbook.handler命名空间。这个命名空间包含了应用程序的入口点,此外还定义了被用来处理所有请求的handler。

在src/guestbook/repl.clj文件中的是guestbook.repl命名空间,调用里面的函数,就可以在REPL中启动和停止服务。我们可以借助它直接从编辑器中启动我们的应用,而不必非得通过lein来运行。

接下来,我们有一个名为models的文件夹。这是留给应用的模型层的。里面的命名空间也负责连接数据库、定义表结构,还有访问记录等。

在routes文件夹下,是那些负责定义路由的命名空间。这些路由构成了我们将要实现的工作流的入口点。

目前,我们只有一个被称为guestbook.routes.home的命名空间,应用的主页就是在这里定义的。这个命名空间位于src/guestbook/routes/home.clj文件中。

接下来的文件夹是views,里面的命名空间通常负责应用的界面布局。其自带的命名空间guestbook.views.layout定义了页面的基本结构。显而易见,这个命名空间对应的文件就是src/guestbook/views/layout.clj。添加一些功能

让我们来看看如何为留言簿应用创建用户界面(UI,user interface)吧。即使你阅读这些代码会感觉有点吃力,也不必担心,在后面的章节中你还有机会弄明白。相比纠缠于每个函数的细枝末节,目前应把注意力放在如何组织我们的应用,以及如何拆分应用逻辑更为重要。

在前面,我们曾经用纯手工的方式创建了一个录入表单。现在,我们打算用一个更好的实现来替代它,这会用到Hiccup[10]库提供的辅助函数。

为了使用这些函数,需要把库导入我们的命名空间,像下面这样修改命名空间的声明:(ns guestbook.routes.home  (:require [compojure.core :refer :all]       [guestbook.views.layout :as layout]       [hiccup.form :refer :all]))

首先我们创建一个函数,用来呈现已有的消息。这个函数会生成一个包含了现有消息的HTML列表。就目前来说,我们先简单地硬编码几条消息就行。(defn show-guests [] [:ul.guests  (for [{:keys [message name timestamp]}     [{:message "Howdy" :name "Bob" :timestamp nil}      {:message "Hello" :name "Bob" :timestamp nil}]]  [:li    [:blockquote message]    [:p "-" [:cite name]]    [:time timestamp]])])

接下来,我们对home函数进行调整,使顾客可以看到前面那些顾客留下的消息。当然,还得提供一个表单用来创建新的消息。(defn home [& [name message error]] (layout/common  [:h1 "Guestbook"]  [:p "Welcome to my guestbook"]  [:p error]  ;here we call our show-guests function  ;to generate the list of existing comments  (show-guests)  [:hr]  ;here we create a form with text fields called "name" and "message"  ;these will be sent when the form posts to the server as keywords of  ;the same name  (form-to [:post "/"]   [:p "Name:"]   (text-field "name" name)   [:p "Message:"]   (text-area {:rows 10 :cols 40} "message" message)   [:br]   (submit-button "comment"))))

切换到浏览器,可以看到两条测试消息连同表单一块儿都显示出来了。请留意,现在home函数多了几个可选参数。我们会把这些参数的值显示到页面上。如果这些参数为nil,那么在进行显示时,会把它们视作空字符串。

我们创建的这个表单会向“/”发送HTTP的POST请求,所以我们再添加一个路由来处理它吧:这个路由将会调用一个名为save-message的辅助函数,我们稍后会给出其定义。guestbook/src/guestbook/routes/home.clj(defroutes home-routes(GET "/" [] (home))(POST "/" [name message] (save-message name message)))

save-message函数会检查name和message这两个参数,然后就去调用home函数。倘若两个参数都没问题,那么消息会被打印到控制台;否则,将会生成一条出错信息。(defn save-message [name message] (cond  (empty? name)  (home name message "Some dummy forgot to leave a name")  (empty? message)  (home name message "Don't you have something to say?")  :else  (do   (println name message)   (home))))

来,在留言簿中留一次言试试看,你会看到名字和消息在控制台里打印出来了。接下来,将name或者message留白,看看有没有显示出错消息。

现在,视图部分已经具备了通过UI显示和提交消息的能力。但此时此刻,我们还没有能真正存放这些消息的地方。添加数据模型

既然我们的应用需要保存访客们的留言,那我们在project.clj[11]文件中加入对JDBC和SQLite的依赖项吧。添加完毕后的,:dependencies看起来应该是下面这样子的::dependencies [[org.clojure/clojure "1.5.1"]         [compojure "1.1.5"]         [hiccup "1.0.4"]         [ring-server "0.3.0"]         ;;JDBC dependencies         [org.clojure/java.jdbc "0.2.3"]         [org.xerial/sqlite-jdbc "3.7.2"]]

因为添加了新的依赖项,我们需要将工程与REPL重新连接。首先打开Connect标签页并且点击disconnect按钮,然后按照先前介绍过的步骤来连接一个新的REPL实例,如图1-10所示。图1-10 断开REPL

一旦重新连上了REPL,我们就需要在即时repl中执行(start-server),早些时候我们曾经做过一次,还记得吗?

OK,万事俱备,只欠数据模型了。我们会在src/guestbook/models文件夹下创建一个新的命名空间。我们把这个命名空间称为guestbook.models.db。具体做法是:在工作区中,右键单击models文件夹,并且选择New File选项,然后将这个文件命名为db.clj。

正如其名称所暗示的,db命名空间将负责应用的数据模型,并且提供从数据库读取或是向数据库写入数据的功能。

首先,我们需要添加命名空间声明,以及导入数据库依赖项。下面是这个命名空间的声明:guestbook/src/guestbook/models/db.clj(ns guestbook.models.db (:require [clojure.java.jdbc :as sql]) (:import java.sql.DriverManager))

请注意,导入其他Clojure命名空间时,我们使用的是:require关键字,而导入Java类时,我们则用了:import。

下一步,我们将要创建数据库连接的定义。这个定义其实就是一个简单的map,包含了JDBC驱动的类型、协议,以及SQLite数据库的文件名。guestbook/src/guestbook/models/db.clj(def db {:classname "org.sqlite.JDBC",     :subprotocol  "sqlite",     :subname     "db.sq3"})

声明了数据库连接之后,我们还需要编写一个函数,创建用于保存访客留言的数据表。guestbook/src/guestbook/models/db.clj(defn create-guestbook-table [] (sql/with-connection  db  (sql/create-table   :guestbook   [:id "INTEGER PRIMARY KEY AUTOINCREMENT"]   [:timestamp "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"]   [:name "TEXT"]   [:message "TEXT"])  (sql/do-commands "CREATE INDEX timestamp_index ON guestbook (timestamp)")))

这个函数使用了with-connection语句,这样就能确保数据库连接在使用完毕后能够得到恰当的清理。在其内部,我们调用create-table函数来创建数据表,表名用关键字表示,而表示字段则使用了vector。为了完整起见,我们还为timestamp字段创建了索引。

在即时repl中执行(create-guestbook-table)之前,我们首先要导入它的命名空间,前面我们曾经对guestbook.routes.home也这么做过,还记得吗?(use 'guestbook.models.db)(create-guestbook-table)

现在你就可以在即时repl中执行create-guestbook-table,把数据表给创建出来了。但有一点需要注意,如果你开启了实时模式,那么最好将其禁用;否则每次即时repl临时缓冲的变动,都会导致create-guestbook-table被调用并产生错误。

数据表创建完毕,接下来我们就可以编写从数据库中读取留言的函数了。guestbook/src/guestbook/models/db.clj(defn read-guests [] (sql/with-connection  db  (sql/with-query-results res    ["SELECT * FROM guestbook ORDER BY timestamp DESC"]    (doall res))))

此处我们使用with-query-results与来执行select语句,并返回其结果。之所以要在返回之前调用doall,是因为res是惰性的,不会把所有结果都加载到内存中。

通过调用doall,我们强制对res进行了完全求值。如果不这么做的话,一旦离开了函数的作用范围,我们的数据库连接就会被关闭,于是便无法在函数之外访问结果数据了。

我们还需要创建另外一个函数,用来把消息保存到留言簿的数据表中。这个函数会调用insert-values,并且接受访客的名字和消息作为参数进行保存。guestbook/src/guestbook/models/db.clj(defn save-message [name message] (sql/with-connection  db  (sql/insert-values    :guestbook    [:name :message :timestamp]    [name message (new java.util.Date)])))

用于读取和保存消息的函数已经写好,现在我们可以在REPL中尝试一下了。我们需要在即时repl中重新执行一遍(use 'guestbook.models.db),这样才能访问这几个新添加的函数。然而,在guestbook.models.db和guestbook.routes.home这两个命名空间中都定义了名为save-message的函数。

如果尝试重新加载guestbook.models.db命名空间,我们会得到一个错误,指出save-message已经从guestbook.routes.home命名空间导入过了。为了避免这个问题,在执行(use 'guestbook.models.db)之前,我们需要在即时repl中先执行ns-unmap,移除当前对save-message的引用。(ns-unmap 'user 'save-message)(use 'guestbook.models.db)

现在我们可以尝试运行下面的代码,看看保存和读取消息的逻辑是否符合预期:(save-message "Bob" "hello")(read-guests)

将留言保存到数据库,然后读取出来以后,我们应该能看到图1-11所示的输出。

有了持久层,我们就可以回过头去修改home命名空间,将先前那些硬编码的假数据统统扔掉了。组合起来

现在我们可以把对db的依赖项添加到home路由的命名空间声明中了。guestbook/src/guestbook/routes/home.clj(ns guestbook.routes.home (:require [compojure.core :refer :all]      [guestbook.views.layout :as layout]      [hiccup.form :refer :all]      [guestbook.models.db :as db]))

接下来,我们需要修改show-guests函数,让它去调用db/read-guests:图1-11 测试保存功能(defn show-guests [] [:ul.guests  (for [{:keys [message name timestamp]} (db/read-guests)]  [:li    [:blockquote message]    [:p "-" [:cite name]]    [:time timestamp]])])

最后,我们还得修改save-message函数,让它调用db/save-message,而不是简单地把参数打印出来:guestbook/src/guestbook/routes/home.clj(defn save-message [name message] (cond  (empty? name)  (home name message "Some dummy forgot to leave a name")  (empty? message)  (home name message "Don't you have something to say?")  :else  (do   (db/save-message name message)    (home))))

完成这些修改之后,我们都迫不及待地要打开浏览器中看看效果如何啦。不出所料,先前我们在REPL中添加到数据库中的那条消息显示出来了,如图1-12所示。图1-12 真正的留言

我们还可以试着多录入几条消息,以确认留言簿的功能确实符合预期。

你也许注意到了,页面上消息的显示是存在缺陷的。时间只是简单的显示为毫秒数。这实在是太不友好了,所以,让我们添加一个改善其格式的函数吧。

为此,我们将会创建一个Java的SimpleDateFormat对象,用来对时间戳进行格式化。guestbook/src/guestbook/routes/home.clj(defn format-time [timestamp] (-> "dd/MM/yyyy"    (java.text.SimpleDateFormat.)    (.format timestamp)))(defn show-guests [] [:ul.guests  (for [{:keys [message name timestamp]} (db/read-guests)]   [:li     [:blockquote message]     [:p "-" [:cite name]]     [:time (format-time timestamp)]])])收尾

我们的留言簿应用已接近完成,还剩下最后一个问题。

由于我们需要先创建数据库,其后才能访问,所以还需要往handler命名空间中添加一些代码。首先,我们需要在handler中导入命名空间db。(ns guestbook.handler ... (:require ...       [guestbook.models.db :as db]))

接下来修改init函数,检查数据库是否存在,如果不存在则创建之。guestbook/src/guestbook/handler.clj(defn init [] (println "guestbook is starting") (if-not (.exists (java.io.File. "./db.sq3"))  (db/create-guestbook-table)))

由于应用加载时会调用init函数,因此就能确保数据库在真正开始运行之前便已经准备妥当了。你学到了什么

通过前面这个例子,我们体验了如何使用Clojure来开发Web应用。你也许已经注意到了,你只是编写了极少的代码,就得到了一个可用的程序。而且,你几乎没有编写任何样板代码。

阅读至此,你对程序结构、主要组件,以及如何将它们组合到一起应该相当熟悉了。

回顾一下,我们的应用包含了以下几个命名空间。

命名空间guestbook.handler的职责是启动服务,并创建一个handler,负责把来自客户端的请求传递给应用。

然后是命名空间guestbook.routes.home。我们在这里建立了留言功能的工作流程,同时大部分应用逻辑也都位于此处。如果需要添加更多的工作流,你需要在guestbook.routes下创建新的命名空间。例如,你可能会创建guestbook.routes.auth命名空间,用来处理用户注册和认证。

通常,routes文件夹下的每个命名空间都封装着应用中一个自包含的工作流程。所有与之相关的代码都位于同一个地方,并且与其他的路由保持独立。此处工作流表示的可能是用户认证,也可能是内容编辑,或是事务管理等。

命名空间guestbook.views.layout负责管理应用的界面布局。我们会在这里放置一些代码,用来生成页面的公共元素,以及控制页面的结构。一般来说,布局方面需要考虑的内容包括:组织静态资源,比如页面需要的CSS和JavaScript文件;设置公共元素,比如页眉和页脚等。

最后,还有命名空间guestbook.models.db,它负责整个应用的数据模型。联系例子中定义的数据表,它描述了数据的类型,以及哪些用户的数据需要持久化。

当我们着手构建更大规模的应用时,这些东西应该牢记于胸。一个结构良好的Clojure应用会易于理解,也方便维护。对于有些编程语言,当代码规模较大时,你得费尽心思才能理清其复杂的层次结构。而在Clojure应用的整个生命周期中,你都不会有类似的烦恼,这真是太美妙了。

我们使用了Light Table来开发留言簿应用。虽然它相当易用,但仍需更多打磨,还缺乏一些其他集成开发环境(IDE,Integrated Development Environments)提供的有用特性。这些特性包括代码完成、结构化的代码编辑,以及集成的依赖管理。

所以,我建议你花些时间去尝试一下那些更为成熟的开发环境,例如Eclipse[12]或者Emacs[13]。本书的剩余部分假定以Eclipse作为我们的开发环境,不过,无论你选用的是哪种编辑器,都没有任何问题。如需了解其他可选的IDE,不妨参考“附录1 选择IDE”。

你会发现,在开发应用的过程中,我们大量使用了REPL。因此,对于Clojure开发环境而言,是否集成了REPL可谓有着天壤之别。能在REPL中执行代码,就意味着你能获得更快的反馈周期,从而显著地提升生产力。

在本章中,我们演示了如何设置开发环境,以及如何搭建一个典型的Clojure Web应用。下一章,我们将关注那些构成Clojure Web栈的核心库。你将会了解到以下内容:请求响应的生命周期、定义路由、会话管理,以及利用中间件强化核心处理请求功能。

[1]http://www.oracle.com/technetwork/java/javase/downloads/index.html

[2]http://maven.apache.org/

[3]http://ant.apache.org/

[4]http://leiningen.org/

[5]https://clojars.org/

[6]http://leiningen.org/#install

[7]https://github.com/Raynes/lein-newnew

[8]https://github.com/yogthos/compojure-template

[9]http://www.lighttable.com/

[10]https://github.com/weavejester/hiccup

[11]http://www.sqlite.org/

[12]http://www.eclipse.org/

[13]http://www.gnu.org/software/emacs/第2章Clojure的Web技术栈

在上一章,我们直接构建了一个简单的应用。通过它,我们对工程的结构有了初步印象,同时也熟悉了开发环境。现在,我们将节奏放缓,先往后退一步,了解一下所有这些组件的运作细节。

Clojure社区崇尚简单和灵活,而不是循规蹈矩或是一成不变。实际上,Web栈中的所有组件,都有为数众多的替代品。你可以根据自己的风格,以及你开发的应用类型做出选择。本书中,我们把重点放在流行的Ring/Compojure栈,现实当中,许多案例都是用它创建的。

前面的章节中我们介绍了一个简单的应用,用户可以留言并且能够看到其他用户的留言。我们介绍了工程的目录结构和主要文件,以及它们的用途。然而,我们还没有真正关注这些文件中的代码。在本章中,你将学习一些必要的背景知识,以便能够充分地理解我们的这个留言板应用。

由于Clojure Web栈是建立在Java HTTP Servlet API[1]之上的,所以可以将应用部署到任意的servlet容器中,比如Jetty[2]、GlassFish[3]或者是Tomcat[4]。

你可以选择让Clojure应用独立运行,也可以将它和其他Java应用一块儿部署在一个应用服务器上。

由于众多云服务都支持Java虚拟机,你也可以考虑把应用部署到云端。这些服务包括亚马逊的AWS[5]、谷歌的App Engine[6],此外还有Heroku[7]和Jelastic[8]等。

servlets用于处理任意的网络请求/相应交换,这意味着HTTP servlets会按照RFC描述的有关内容处理HTTP交换。servlet容器调用servlet类的接口并传入对应的数据作为请求,并且servlet返回用于servlet容器的数据作为响应。这套API提供了诸多Web开发中需要用到的核心功能,比如cookies、会话,以及URL重写。然而,servlets是专为Java语言设计的,如果直接在Clojure中使用,未免有些别扭。

不像许多其他平台(比如Rails或Django),Clojure的Web栈并没有提供那种自以为完备的整体框架。相反,你可以把各种库糅合在一起,来构建你自己的应用。本书中,我们仅专注几个常用的Web开发库。

作为起点,让我们先了解一下Ring和Compojure这两个提供了原生Clojure Servlet API的库吧。Ring封装了Java的servlet API,而Compojure则用来把请求处理函数映射到指定的URL。应用本身则位于栈顶,使用这些库来与客户端交互,以及管理应用的状态。2.1 使用Ring来路由请求

Ring的目标是把HTTP的细节抽象为简单且模块化的API,可以用来构建类型广泛的应用。如果你曾经使用Python或是Ruby开发过Web应用的话,那么你会发现Ring与Python的WSGI[9]和Ruby的Rake[10]非常类似。

对于构建Web应用,Ring已经成为了事实上的标准,因此诞生了很多周边的工具和中间件。尽管在大多数情况下你都无需直接与Ring打交道,但高屋建瓴地了解一下其设计,将对后续的开发和排错有颇多益处。

基于Ring的应用都包含以下这四个基本组件:处理器(handler)、请求(request)、响应(response)和中间件(middleware)。来分别了解它们一下吧。请求处理

Ring使用标准的Clojuremap来表示客户端请求以及服务端响应。而所谓handler,不过是一组用于处理客户端请求的函数罢了。这些函数的参数是请求map,返回值则是响应map。下面是一个非常简单的Ring handler:(defn handler [request-map] {:status 200  :headers {"Content-Type" "text/html"}  :body (str " your IP is: "        (:remote-addr request-map)        "")})

如你所见,它的参数是一个表示HTTP请求的map,返回了一个表示HTTP响应的map。至于说如何将HTTP servlet请求对象转换为map,以及如何将map转换为响应对象,那就是Ring操心的问题了。

前面的这个handler只是简单地生成了一段内容为客户端IP地址的HTML字符串,并将响应的状态码置为200。由于类似这种操作实在是太常见了,于是Ring就提供了一个辅助函数用来生成这样的响应:(defn handler [request-map] (response  (str " your IP is: "     (:remote-addr request-map)     "")))

如果想要创建自己的响应,你只用编写一个函数,处理传入的请求map,并返回用于表示你自己响应的map即可。下面让我们来了解一下这两个map的格式吧。请求map和响应map

请求map和响应map都包含了诸如服务端口、URI、对端地址、负载类型以及实际的负载数据。这些map的键名源自于servlet API和官方的HTTP RFC标准文档[11]。请求map的内容

请求map中定义了下列的标准键。注意,此处列出的键,并不一定会出现在所有的请求中,比如:ssl-client-cert。● :server-port——用于处理该请求的服务端口。● :server-name——服务器的IP地址或是主机名。● :remote-addr——客户端的IP地址。● :query-string——请求的查询字符串。● :scheme——协议的类型,可以是HTTP或者HTTPS。● :request-method——请求的方法,比如:

get、:head、:options、:put、:post或:delete。● :request-string——请求的查询字符串。● :content-type——请求消息体的MIME类型。● :content-length——请求消息体的字节数。● :character-encoding——请求采用的字符编码名称。● :headers——包含了请求头部的map。● :body——可用于读取请求消息体的输入流。● :context——当应用没有作为根来部署时,其所处的上下文。● :uri——服务端的URI全路径,包含了:context(如果存在)的部

分。● :ssl-client-cert——客户端的SSL证书。

除了上述由Ring规定的标准键之外,请求map中还有可能会出现由中间件函数添加的其他一些特定于应用的键。怎么才能做到?别着急,本章后面会讨论这个话题。响应map的内容

响应map仅包含三个键,就足以描述HTTP响应了:● :status——响应的HTTP状态码。● :headers——返回给客户端的HTTP头部。● :body——响应的消息体。

status是一个数字,表示HTTP RFC标准中定义的一个状态码,规定其最小值为100。

headers是一个map,包含所有表示HTTP头部的键值对。头部可以是字符串,也可以是字符串的序列,在这种情况下,序列中的每个字符串都会作为单独的键和值来发送。

最后,响应消息体可以是一个字符串、一个序列、一个文件或者是一个输入流。此外,消息体还应该与响应的状态码对应。

当响应的消息体是一个字符串时,它会被原样发送给客户端。而如果它是一个序列的话,那么发送给客户端的将是每一个元素的字符串表达。最后,如果响应是一个文件,或者是一个输入流,那么服务器会将其中的内容发送给客户端。通过中间件扩充功能

所谓中间件,就是一些用来封装处理器(handler)的函数,这些函数能够更改处理请求的方式。中间件函数通常被用于扩展Ring的基本功能,以满足应用的实际需要。

中间件本身就是一个函数,它接受一个现有的handler和一些其他的可选参数,并返回一个新的handler,只不过这个新的handler将具有一些新的行为特征。下例就是这样的一个函数:(defn handler [request] (response  (str " your IP is: "     (:remote-addr request)     "")))(defn wrap-nocache [handler] (fn [request]   (let [response (handler request)]     (assoc-in response [:headers "Pragma"] "no-cache"))))(def app (wrap-nocache handler))

以上例子里封装了一个函数,它接受一个handler,并返回一个handler形式的函数。由于这个返回函数封装在局部,也就可以在处理内部引用handler。当函数被调用,它就将请求参数传递给handler并在回应的map里添加Pragma:no-cache。

这种封装处理称为闭包(closure),因为它隐蔽了handler函数的参数内容,使之易于处理返回。

面对程序中的任何具体问题,我们都可以用刚才这种手法创建小函数(small functions)来解决。再将他们通过各种组合,使应用程序可以轻松应对任何复杂的真实环境。适配器是什么

适配器位于handler和HTTP框架协议之间,它们组织并提供一些必要的内容,比如端口映射、解析HTTP请求,还能通过handler返回的map构造HTTP响应。不过,你并不太需要直接和适配器打交道,我们就此不提了。2.2 定义Compojure路由

Compojure是构建在Ring之上的路由库,它提供的方式非常简洁,用来关联处理URL和HTTP方法。Compojure路由基本上是这样子的:(GET "/:id" [id] (str "

the id is: " id "

" ))

其路由函数名与HTTP方法名直接对应,比如GET、POST、PUT、DELETE和HEAD。还有一个称为ANY的路由会响应客户端任何方法。URI是包含冒号的键名,对应的那些值可以用作路由参数,Rails[12]和Sinatra[13]就是使用类似的处理机制,而Compojure正是受到这种特性的启发。上面的Ring回应描述中会自动包含路由回应。

其实在我们的实际应用中,可能会存在多条路由,Compojure提供了路由功能,能从多条路由中创建一个Ring处理。假设我们有/foo路由和/:id项,那么我们可以使用单条处理进行如下合并:(defn foo-handler [] "foo called")(defn bar-handler [id] (str "bar called, id is: " id))

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载