《Clojure Web开发实战》——第2章,第2.3节应用架构

2.3 应用架构
典型的Compojure开发Web程序方式可能不同于你之前使用的方式。多数框架偏好使用模型-视图-控制器(MVC,model-view-controller)模式使用逻辑分离思想将视图、控制、模式严格分开。这里,Compojure并没有明确分离视图和控制。
相反,我们为程序中每个路由创建了独立的handler,这些handler用于处理来自客户端的HTTP请求,Compojure正是以这种思路来分派任务的。handler驱动模型负责处理域逻辑。这种方法提供了一个彻底的域逻辑分离模式,并不牵涉应用程序的表示层,也没有任何不必要的联系。
尽管如此,Clojure的Web栈设计得还是比较灵活,它甚至允许你以任何喜好的方式来组织,如果你非要在程序中使用传统MVC风格,也不会有什么麻烦。
仅通过几个逻辑部件就能一览典型应用(这是指我们前面做的那个留言簿程序的结构)。那我们再看看别的一些特性,多数应用被拆分为如下几个方面。
• handler——此命名空间负责处理请求、响应。
• routes——路由涵盖我们程序的核心内容,譬如维护读取页面和处理客户端请求的逻辑关系。
• model——此命名空间保留给数据模型和持久化层。
• views——此命名空间包含通用逻辑以构成应用层。
程序的handler
handler是功能入口,它通常用于定义handler命名空间。它负责将程序的所有路由汇聚起来,并且定义所有的处理过程,用于封装必要的中间件。
handler命名空间也为程序定义一些基础路由,但不用于任何特定的工作流。我们留言簿程序中的那个handler,有两条路由:一条用于处理静态资源;还有一条用于捕获其他所有路由都未定义的URI请求。
`(defroutes app-routes
 (route/resources "/")
 (route/not-found "Not Found"))`
路由里具体的工作流,比如在留言簿里发布和浏览消息的路由处理,都组织在与它们功能相关的特定命名空间里。每一条都供routes命名空间访问。
handler命名空间也提供init和destroy方法,它们在程序起停时被调用。任何需要在始末阶段调用的代码,都要分别放在这两个函数里面执行。
举个例子说明吧,我们在留言簿程序里就用上了,init函数用来检查数据库连接是否可用。
`(defn init []
 (println "guestbook is starting")
 (if-not (.exists (java.io.File. "./db.sq3"))
  (db/create-guestbook-table)))`
接下来,我们定义入口点,在调用app函数时,程序将开始处理所有路由请求。
(def app (handler/site (routes home-routes app-routes)))
这段代码,compojure.handler/site函数用于生成Ring handler,用中间件支撑一个典型网站。
site函数仅仅创建一个handler,并将其封装进一些通用中间件,来支持通用网站。中间件由如下封装器构成。
• wrap-session。
• wrap-flash。
• wrap-cookies。
• wrap-multipart-params。
• wrap-params。
• wrap-nested-params。
• wrap-keyword-params。
在project.clj里,程序的handler、init函数、destroy函数,都绑定在:ring键下面,具体参见我们的留言簿程序(“第1章起步”)。
`:ring {:handler guestbook.handler/app
    :init  guestbook.handler/init
    :destroy guestbook.handler/destroy}`
以上描述用于引导程序核心部分。接下来,我们一起看看怎样添加一些别的路由,来满足应用程序的具体功能。
路由请求
此前我们讨论过,程序路由表现为URI,由客户端请求,由服务端执行。客户端请求的URI由路由程序对应的处理函数做相应回应。
现实当中没有哪个应用只有一条路由。比如,在我们的留言簿程序中,有两个独立路由,各自执行不同的操作:
`guestbook/src/guestbook/routes/home.clj
(defroutes home-routes
(GET "/" [] (home))
(POST "/" [name message] (save-message name message)))`
第一条路由被绑定于/,用于从数据库检索消息,并用此消息创建一张表单,最终呈现整幅页面给客户端。
第二条路由会处理用户输入。如果输入验证通过,接下来这条消息就会被存入数据库;否则,页面将呈现错误描述。
其实这两条路由功能有交集:存储和显示用户信息,它们也算是同一工作流的两个部分。
当你发现程序的工作流有明确所属,那么可以将此工作流的逻辑关系合并,放在一起处理。程序中的routes包之下的命名空间正是为这种特殊工作流预留的。
由于我们的留言簿应用很小。除了在guestbook.routes.home命名空间里有几个辅助函数,定义一套路由就够用了。
当程序包含多个页面,为便于维护代码,我们会创建额外的命名空间。接下来我们用Compojure提供的routes宏,在每个独立的命名空间下创建独立的路由,并将处理放在handler命名空间。
routes宏可以将多个路由合并,最终创建handler。有一点要注意,路由之间存在覆盖关系。由于我们的app-routes调用了(route/not-found "Not Found"),务必把它置为最后一条,否则在not-found路由后面的所有路由将被覆盖。
应用模型
稍稍复杂一些的应用,都需要建立在某种模型之上。模型用于描述应用程序如何存储数据、单个数据元素之间的内在关系。我们的留言簿程序模型由用户表和消息表构成。
处理模型和持久层的所有命名空间,惯例上属于models包。我们在下一章会用大篇幅重点讲述。
应用视图
views包用于为页面提供可视布局和其他的通用控件,其下有预设的layout命名空间。这个命名空间为我们包含了common布局声明,用于生成基础页面模板。
common布局用于填充页面头、填写标题标签、打包资源(如CSS)及添加负载内容。由于内容使用html5宏封装,common布局被调用之后,将自动创建HTML文本串,这个处理直接将结果反馈给客户端。
这种方式常用于创建通用布局,以及提供基本页面结构,也使用它定义个别页面。亦可创建通用页面元素,比如页眉、页脚、菜单,并会得到统一维护。我们每次创建的页面,都需要使用定义的布局简单将内容包裹起来。
定义页面
创建路由的同时也就定义了页面,通过接受请求参数来生成各种特殊的响应,比如用来返回HTML元素,执行服务端操作,重定向到另一个页面;或者返回特殊类型的数据,比如数据交换格式(JSON,JavaScript Object Notation)字符串或文件。
通常,一张页面由多条路由组成。其中有一条接受GET请求,并返回HTML供浏览器渲染的路由。还有其他情况,比如在客户端用户与页面交互时,生成并提交了表单,这时会有其他路由来处理此请求。
无论我们选择如何处理,都能创建页面,Compojure并不关心我们使用的具体方法,这恰好为选择模板库留有余地。可选的方案不少,这里介绍几个流行的库:Hiccup14、Enlive15、Selmer16、Stencil17。
Hiccup能使用原生Clojure数据结构,通过它定义表情并生成相适应的HTML;Enlive反其道而行,使用纯HTML定义页面而不用特殊处理标签。适配器将特定模型和域变换为HTML模板。
与Hiccup和Enlive不一样,Stencil和Selmer都是基于外部模板系统,而不是基于Clojure。Stencil是实现了Mustache(这是个流行的无逻辑模板系统),Selmer是模仿Django模板系统在Python上的实现。
本书重点关注并使用Hiccup,因为它不需要额外学习任何语法,直接使用Clojure函数即可。此外,我们在后面还会学习用Selmer模板来取代Hiccup创建的应用。
别的选择彻底没有考虑使用服务端模板,你需要在客户端处理模板来接管这些工作,挑个流行的JavaScript库,并使用Ajax与服务通讯。当然,这样也能胜任。好处是这可以让客户端服务端的界限明确、清晰,有助于扩充其他形式的客户端,比如移动应用接口。在编写单页应用18时,这还是通行手段。
无论你喜欢何种模板策略,最佳实践都不会去聚合域逻辑和视图。通过合理构架的程序,是可以轻松替换模板引擎的。
Hiccup处理模板化页面
现在开始介绍一些Hiccup使用基础,以及通过它如何生成适当的页面元素。
刚才提到,用原生Clojure就能编写Hiccup模板,所以你就不需要去学习特定领域语言(DLS,domain-specific language)就能驾驭它。
Hiccup用Clojure vector(向量表)表示HTML元素,其属性使用map描述,这种结构表达方式与生成的HTML标签在结构上比较吻合,示例如下。
`[:tag-name {:attribute-key "attribute value"} tag body]
attribute-key="attribute value">tag body`
如果我们想要创建一个包含图片的div标签,可以创建一个vector,第一个元素为:div关键字,紧随其后是一个map(包含div ID和div的class)。余下部分是以vector表示图片的内容构成。
[:div {:id "hello", :class "content"} [:p "Hello world!"]]
我们使用hiccup.core/html宏将vector转换为HTML文本:
(html [:div {:id "hello", :class "content"} [:p "Hello world!"]])

Hello world!

由于Hiccup允许你通过map设置元素属性,如有必要,你还可以使用元素内联样式。尽管如此,你还是应该抵御这种诱惑,使用CSS样式化元素取代之,这可以确保结构和描述分离。
由于对元素设置ID和设置class是常用操作,Hiccup还提供便捷的CSS样式化处理。我们可以如下简化编写我们的div,取代之前的代码:
[:div#hello.content [:p "Hello world!"]]
Hiccup同样提供一些辅助函数,用来定义常用元素,比如表单、链接、图像。所有这些函数输出的vector,由Hiccup预先定义的格式描述。
当一个函数在使用中并不能满足需求时,你当然可以写下元素的文本描述,还可以调整输出来满足需要。描述HTML元素的函数可以配置,其第一个参数可以接受可选属性的map。我们再了解一些常用的Hiccup辅助函数,来改善使用体验。
首先,我们来看看怎么用link-to辅助函数创建一个标签:
(link-to {:align "left"} "http://google.com" "google")
这段代码将生成以下vector:
[:a {:align "left", :href #http://google.com>} ("google")]
我们已有一个关键字:a作为第一项,紧随其后的map表示属性,以及表示内容的list。
还是如此,将link-to函数封装在html宏里面,我们可以基于此vector输出HTML:
(html (link-to {:align "left"} "http://google.com" "google"))
google
还有一个常用的函数form-to,用来生成HTML表单,我们用此函数实现上一章创建的表单,并将信息提交给服务端。
`(form-to [:post "/"]
     [:p "Name:" (text-field "name")]
     [:p "Message:" (text-area {:rows 10 :cols 40} "message")]
     (submit-button "comment"))`
这个辅助函数接受一个vector,第一个元素是HTTP请求类型的关键字,第二个元素是URL字符串。余下参数也为vector,通过求值可以表示为HTML元素。当调用html宏后,前面的代码会被转化为以下HTML:
`
 
Name:

 
Message:
 

还有一个实用的辅助宏defhtml。我们在定义一个函数同时,通过参数内容悄悄生成HTML。这意味着在构造页面时,我们不需要用html宏作用每一个独立元素。
`(defhtml page [& body]
 [:html
   [:head
   [:title "Welcome"]]
   [:body body]])`
同样,在hiccup.page命名空间里,Hiccup提供若干生成特定HTML变体的宏,比如HTML4、HTML5和XHTML。看,我们在留言簿程序里使用的就是html5宏。
`(defn common [& body]
 (html5
  [:head
  [:title "Welcome to guestbook"]
  (include-css "/css/screen.css")]
  [:body body]))`
添加资源
现实中,大型网站的页面必然涉及加载JavaScript和CSS。在hiccup.page 命名空间里,Hiccup提供几个实用函数来达到这个目的。你可以使用include-css去引用任何CSS文件,include-js来加载JavaScript资源。这里有个在常用布局中包含CSS 和JavaScript资源的例子:
`(defn common [& content]
 (html5
  [:head
  [:title "My App"]
  (include-css "/css/mobile.css"
         "/css/screen.css")
  (include-js "//code.jquery.com/jquery-1.10.1.min.js"
         "/js/uielements.js")]
  [:body content]))`
如你所见,include-css和include-js都能接受多个字符串,每个参数指定一个URI资源。它们的输出必然是一个Hiccupvector,最终会被转换为HTML。
;;output of include-css
([:link
 {:type "text/css", :href #, :rel "stylesheet"}]
[:link
 {:type "text/css", :href #, :rel "stylesheet"}])
;;output of include-js
([:script
 {:type "text/javascript",
  :src
  #}]
[:script {:type "text/javascript", :src #}])
同样,在hiccup.element命名空间,Hiccup提供一个名为image的辅助函数去加载图片:
(image "/img/test.jpg")
[:img {:src #}]
(image "/img/test.jpg" "alt text")
[:img {:src #, :alt "alt text"}]
Hiccup API一览
你已经见识了一些常用的函数,其实还有一些更有用的。大多数辅助函数可以在element和form命名空间里找到。这些函数用于定义元素,比如图像、链接、脚本标签、复选框、下拉工具栏以及输入栏。
如你所见,Hiccup提供一套简明API去生成HTML模板,此外还有字面量vector表达式。既然你已经领悟到了Hiccup的精髓,那我们回过来对此前的留言簿程序进行更深入的剖析。
回顾留言簿程序
我们现在换个角度去看待那些定义在home命名空间的函数。当你试着运行程序,并来回浏览时,顺便查阅页面的HTML输出和在代码里的定义。
首先,我们用show-guests函数去生成一个无序清单。它遍历数据库的消息,然后为每一个消息创建一个列表项。
(defn show-guests []
[:ul.guests
  (for [{:keys [message name timestamp]} (db/read-guests)]
  [:li
    [:blockquote message]
    [:p "-" [:cite name]]
    [:time (format-time timestamp)]])])
这里有个辅助函数,可以用于显示格式化时间戳。此函数使用java.text.SimpleDate Format将日期对象转化为格式化字符串。我们使用流化(->)宏去执行格式化器去格式化文本,接下来使用此方法处理从数据库获取的时间戳。
(defn format-time [timestamp]
 (-> "dd/MM/yyyy"
   (java.text.SimpleDateFormat.)
   (.format timestamp)))
你可能已经发现目前的home函数编写得有点复杂,因为它还有一些用来指导用户提交表单的额外描述。
这里有一点值得一提:错误处理行的代码用于显示错误键值,由控制器填充,最终交由show-guests函数去呈现内容。
home函数使用layout/common封装内容,为页面生成HTML。
(defn home [& [name message error]]
 (layout/common
  [:h1 "Guestbook"]
  [:p "Welcome to my guestbook"]
  [:p error]
 (show-guests)
 [:hr]
 (form-to [:post "/"]
  [:p "Name:" (text-field "name" name)]
  [:p "Message:" (text-area {:rows 10 :cols 40} "message" message)]
  (submit-button "comment"))))
如你所见,仅需少许代码,就能使用Hiccup创建页面模板,同时也便于通过关联模板定义生成输出元素。
我们就此完成了路由定义,Compojure路由得以完善。
(defroutes home-routes
 (GET "/" [name message error] (home name message error))
 (POST "/" [name message] (save-message name message)))
到目前为止,我们已完成创建路由并由此呈现页面,还能处理来自客户端的请求表单。正如我们先前提到的,除了由Ring和Compojure提供的,真实的应用还需要添加一些别的元素。接下来,让我们看看如何为我们的应用添加更多功能。

时间: 2024-09-11 10:29:02

《Clojure Web开发实战》——第2章,第2.3节应用架构的相关文章

《Clojure Web开发实战》——第1章,第1.2节你的第一个工程

1.2 你的第一个工程 你的留言簿应该已经在控制台运行了,可以通过http://localhost:3000/来访问.在控制台终端按下Ctrl+C,就能停止它的运行.既然我们已经在Light Table的工作区打开了这个工程,不妨就直接在编辑器中运行它吧. 我们现在要更进一步,创建一个 "读取-求值-打印循环"(REPL,Read-Evaluate-Print Loop),将Light Table连接至我们的工程.菜单View →Connections可以打开连接标签页.如图1-4所示

《Clojure Web开发实战》——导读

目 录第1章 起步 第1章第1节环境设置第1章第2节你的第一个工程第2章 Clojure的Web技术栈 第2章第1节使用Ring来路由请求第2章第2节定义Compojure路由第2章第3节应用架构第2章第4节Compojure和Ring之后第2章第5节你学到什么第3章 服务组件Liberator第4章 访问数据库第5章 相册第6章 收尾第7章 混合附录1 选择IDE附录2 Clojure入门附录3 面向文档的数据库访问

《Clojure Web开发实战》——第2章,第2.4节Compojure和Ring之后

2.4 Compojure和Ring之后不少程序库能有效应对各种处理任务,比如会话管理.输入验证.身份认证.你依旧可以随意挑拣适合你的部件.我们选择lib-noir19作为接下来的关注重点,因为应对Web程序的绝大多数任务,它都能胜任.我们之前通过介绍Hiccup的API,学习了它的一些特性及常见功能,同样,我们也来看看lib-noir是如何用的.首先,为了能启用lib-noir,我们需将其添入项目描述文件project.clj.具体是在依赖项的vector里添加[lib-noir "0.7.6

《Clojure Web开发实战》——第2章,第2.2节定义Compojure路由

2.2 定义Compojure路由Compojure是构建在Ring之上的路由库,它提供的方式非常简洁,用来关联处理URL和HTTP方法.Compojure路由基本上是这样子的:(GET "/:id" [id] (str "<p>the id is: " id "</p>" ))其路由函数名与HTTP方法名直接对应,比如GET.POST.PUT.DELETE和HEAD.还有一个称为ANY的路由会响应客户端任何方法.URI是

《Clojure Web开发实战》——第2章,第2.5节你学到什么

2.5 你学到什么在这一章,我们见识了如何通过Clojure搭建Web栈,以及一些常用程序库.我们谈及了如何与Ring.Compojure.lib-noir交互,通过完成比如输入验证和会话管理的任务来说明它们之间如何相互作用.但愿你已能顺畅阅读,并理解在留言簿项目(我们在"第1章 起步"创建的那个项目)的代码.如果你还有疑惑,我强烈建议你去重新阅读"第1章",并在REPL环境中尝试自己搭建这个例子.如果你还没来得及做,再提一点,借此机会把本章的例子带入留言簿程序做一

《HTML5移动Web开发实战》—— 第1章 HTML5与移动网站

第1章 HTML5与移动网站 HTML5移动Web开发实战 本章内容包括: 准备好你的移动设备 仿真器与模拟器 搭建移动开发环境 在移动网站中使用HTML5 跨浏览器兼容HTML5 适用于移动设备的设计 确定你的核心移动设备 定义一个内容策略

《高性能响应式Web开发实战》一第2章 响应式中要面对的问题

第2章 响应式中要面对的问题 高性能响应式Web开发实战响应式设计的主要工作就是要让网页适配当下种类繁多的设备,使页面在不同设备上仍然看上去友好并且可用.但是细想,当在设法让一个页面同时适配三星Galaxy S6和iPhone 6时,我们究竟是在适配什么?Galaxy S6和iPhone 6究竟存在哪些影响页面展现的差异因素?以上这些问题都可以归纳为:当谈论设备的时候我们究竟在谈论什么? 不同设备间的差异有很多种,我们不关心设备的制造厂商,不关心CPU功耗,不关心生产工艺,只关心会影响页面在屏幕

《高性能响应式Web开发实战》一第1章 概述及任务介绍

第1章 概述及任务介绍 高性能响应式Web开发实战本章向读者大致描述整本书的轮廓.希望通过阅读本章内容,读者能够了解这本书涉及的技术范围.写作风格.写作思路以及贯穿全文的线索.我相信这对读者阅读接下来的内容会很有帮助,不至于让读者觉得某些章节的安排比较突兀. 当然,读者也可以跳过本章内容,直接进入下一章,开始实战技术的学习.

《高性能响应式Web开发实战》一导读

前 言 高性能响应式Web开发实战 为什么写这样一本书 作为一名程序员,写书也好,写博客也罢,其实都和写开源程序的性质是一样的,都是想要把自己的知识分享出去.分享是一件非常有成就感同时也是很快乐的事情,因为我们在此过程中会有很多新的想法,会迫不及待地想去实现,也会有很多人来和我们进行交流,探讨其他的一些可能性.最重要的是,对于做分享的人而言,做好分享很难!首先,分享者要对自己讲解的技术有足够的了解,不仅仅是了解如何用它,还要了解它的过去和未来:其次,分享者要能够娓娓道来,要站在受众的立场上考虑他