《Clojure Web开发实战》——第2章,第2.3节应用架构
本節(jié)書摘來自異步社區(qū)《Clojure Web開發(fā)實(shí)戰(zhàn)》一書中的第2章,第2.3節(jié)應(yīng)用架構(gòu),作者[美]Dmitri Sotnikov,更多章節(jié)內(nèi)容可以訪問云棲社區(qū)“異步社區(qū)”公眾號(hào)查看
2.3 應(yīng)用架構(gòu)
典型的Compojure開發(fā)Web程序方式可能不同于你之前使用的方式。多數(shù)框架偏好使用模型-視圖-控制器(MVC,model-view-controller)模式使用邏輯分離思想將視圖、控制、模式嚴(yán)格分開。這里,Compojure并沒有明確分離視圖和控制。
相反,我們?yōu)槌绦蛑忻總€(gè)路由創(chuàng)建了獨(dú)立的handler,這些handler用于處理來自客戶端的HTTP請(qǐng)求,Compojure正是以這種思路來分派任務(wù)的。handler驅(qū)動(dòng)模型負(fù)責(zé)處理域邏輯。這種方法提供了一個(gè)徹底的域邏輯分離模式,并不牽涉應(yīng)用程序的表示層,也沒有任何不必要的聯(lián)系。
盡管如此,Clojure的Web棧設(shè)計(jì)得還是比較靈活,它甚至允許你以任何喜好的方式來組織,如果你非要在程序中使用傳統(tǒng)MVC風(fēng)格,也不會(huì)有什么麻煩。
僅通過幾個(gè)邏輯部件就能一覽典型應(yīng)用(這是指我們前面做的那個(gè)留言簿程序的結(jié)構(gòu))。那我們?cè)倏纯磩e的一些特性,多數(shù)應(yīng)用被拆分為如下幾個(gè)方面。
? handler——此命名空間負(fù)責(zé)處理請(qǐng)求、響應(yīng)。
? routes——路由涵蓋我們程序的核心內(nèi)容,譬如維護(hù)讀取頁(yè)面和處理客戶端請(qǐng)求的邏輯關(guān)系。
? model——此命名空間保留給數(shù)據(jù)模型和持久化層。
? views——此命名空間包含通用邏輯以構(gòu)成應(yīng)用層。
程序的handler
handler是功能入口,它通常用于定義handler命名空間。它負(fù)責(zé)將程序的所有路由匯聚起來,并且定義所有的處理過程,用于封裝必要的中間件。
handler命名空間也為程序定義一些基礎(chǔ)路由,但不用于任何特定的工作流。我們留言簿程序中的那個(gè)handler,有兩條路由:一條用于處理靜態(tài)資源;還有一條用于捕獲其他所有路由都未定義的URI請(qǐng)求。
`(defroutes app-routes
(route/resources "/")
(route/not-found "Not Found"))`
路由里具體的工作流,比如在留言簿里發(fā)布和瀏覽消息的路由處理,都組織在與它們功能相關(guān)的特定命名空間里。每一條都供routes命名空間訪問。
handler命名空間也提供init和destroy方法,它們?cè)诔绦蚱鹜r(shí)被調(diào)用。任何需要在始末階段調(diào)用的代碼,都要分別放在這兩個(gè)函數(shù)里面執(zhí)行。
舉個(gè)例子說明吧,我們?cè)诹粞圆境绦蚶锞陀蒙狭?#xff0c;init函數(shù)用來檢查數(shù)據(jù)庫(kù)連接是否可用。
`(defn init []
(println "guestbook is starting")
(if-not (.exists (java.io.File. "./db.sq3"))
(db/create-guestbook-table)))`
接下來,我們定義入口點(diǎn),在調(diào)用app函數(shù)時(shí),程序?qū)㈤_始處理所有路由請(qǐng)求。
(def app (handler/site (routes home-routes app-routes)))
這段代碼,compojure.handler/site函數(shù)用于生成Ring handler,用中間件支撐一個(gè)典型網(wǎng)站。
site函數(shù)僅僅創(chuàng)建一個(gè)handler,并將其封裝進(jìn)一些通用中間件,來支持通用網(wǎng)站。中間件由如下封裝器構(gòu)成。
? wrap-session。
? wrap-flash。
? wrap-cookies。
? wrap-multipart-params。
? wrap-params。
? wrap-nested-params。
? wrap-keyword-params。
在project.clj里,程序的handler、init函數(shù)、destroy函數(shù),都綁定在:ring鍵下面,具體參見我們的留言簿程序(“第1章起步”)。
`:ring {:handler guestbook.handler/app
:init guestbook.handler/init
:destroy guestbook.handler/destroy}`
以上描述用于引導(dǎo)程序核心部分。接下來,我們一起看看怎樣添加一些別的路由,來滿足應(yīng)用程序的具體功能。
路由請(qǐng)求
此前我們討論過,程序路由表現(xiàn)為URI,由客戶端請(qǐng)求,由服務(wù)端執(zhí)行。客戶端請(qǐng)求的URI由路由程序?qū)?yīng)的處理函數(shù)做相應(yīng)回應(yīng)。
現(xiàn)實(shí)當(dāng)中沒有哪個(gè)應(yīng)用只有一條路由。比如,在我們的留言簿程序中,有兩個(gè)獨(dú)立路由,各自執(zhí)行不同的操作:
`guestbook/src/guestbook/routes/home.clj
(defroutes home-routes
(GET "/" [](home))
(POST "/" name message))`
第一條路由被綁定于/,用于從數(shù)據(jù)庫(kù)檢索消息,并用此消息創(chuàng)建一張表單,最終呈現(xiàn)整幅頁(yè)面給客戶端。
第二條路由會(huì)處理用戶輸入。如果輸入驗(yàn)證通過,接下來這條消息就會(huì)被存入數(shù)據(jù)庫(kù);否則,頁(yè)面將呈現(xiàn)錯(cuò)誤描述。
其實(shí)這兩條路由功能有交集:存儲(chǔ)和顯示用戶信息,它們也算是同一工作流的兩個(gè)部分。
當(dāng)你發(fā)現(xiàn)程序的工作流有明確所屬,那么可以將此工作流的邏輯關(guān)系合并,放在一起處理。程序中的routes包之下的命名空間正是為這種特殊工作流預(yù)留的。
由于我們的留言簿應(yīng)用很小。除了在guestbook.routes.home命名空間里有幾個(gè)輔助函數(shù),定義一套路由就夠用了。
當(dāng)程序包含多個(gè)頁(yè)面,為便于維護(hù)代碼,我們會(huì)創(chuàng)建額外的命名空間。接下來我們用Compojure提供的routes宏,在每個(gè)獨(dú)立的命名空間下創(chuàng)建獨(dú)立的路由,并將處理放在handler命名空間。
routes宏可以將多個(gè)路由合并,最終創(chuàng)建handler。有一點(diǎn)要注意,路由之間存在覆蓋關(guān)系。由于我們的app-routes調(diào)用了(route/not-found "Not Found"),務(wù)必把它置為最后一條,否則在not-found路由后面的所有路由將被覆蓋。
應(yīng)用模型
稍稍復(fù)雜一些的應(yīng)用,都需要建立在某種模型之上。模型用于描述應(yīng)用程序如何存儲(chǔ)數(shù)據(jù)、單個(gè)數(shù)據(jù)元素之間的內(nèi)在關(guān)系。我們的留言簿程序模型由用戶表和消息表構(gòu)成。
處理模型和持久層的所有命名空間,慣例上屬于models包。我們?cè)谙乱徽聲?huì)用大篇幅重點(diǎn)講述。
應(yīng)用視圖
views包用于為頁(yè)面提供可視布局和其他的通用控件,其下有預(yù)設(shè)的layout命名空間。這個(gè)命名空間為我們包含了common布局聲明,用于生成基礎(chǔ)頁(yè)面模板。
common布局用于填充頁(yè)面頭、填寫標(biāo)題標(biāo)簽、打包資源(如CSS)及添加負(fù)載內(nèi)容。由于內(nèi)容使用html5宏封裝,common布局被調(diào)用之后,將自動(dòng)創(chuàng)建HTML文本串,這個(gè)處理直接將結(jié)果反饋給客戶端。
這種方式常用于創(chuàng)建通用布局,以及提供基本頁(yè)面結(jié)構(gòu),也使用它定義個(gè)別頁(yè)面。亦可創(chuàng)建通用頁(yè)面元素,比如頁(yè)眉、頁(yè)腳、菜單,并會(huì)得到統(tǒng)一維護(hù)。我們每次創(chuàng)建的頁(yè)面,都需要使用定義的布局簡(jiǎn)單將內(nèi)容包裹起來。
定義頁(yè)面
創(chuàng)建路由的同時(shí)也就定義了頁(yè)面,通過接受請(qǐng)求參數(shù)來生成各種特殊的響應(yīng),比如用來返回HTML元素,執(zhí)行服務(wù)端操作,重定向到另一個(gè)頁(yè)面;或者返回特殊類型的數(shù)據(jù),比如數(shù)據(jù)交換格式(JSON,JavaScript Object Notation)字符串或文件。
通常,一張頁(yè)面由多條路由組成。其中有一條接受GET請(qǐng)求,并返回HTML供瀏覽器渲染的路由。還有其他情況,比如在客戶端用戶與頁(yè)面交互時(shí),生成并提交了表單,這時(shí)會(huì)有其他路由來處理此請(qǐng)求。
無論我們選擇如何處理,都能創(chuàng)建頁(yè)面,Compojure并不關(guān)心我們使用的具體方法,這恰好為選擇模板庫(kù)留有余地。可選的方案不少,這里介紹幾個(gè)流行的庫(kù):Hiccup14、Enlive15、Selmer16、Stencil17。
Hiccup能使用原生Clojure數(shù)據(jù)結(jié)構(gòu),通過它定義表情并生成相適應(yīng)的HTML;Enlive反其道而行,使用純HTML定義頁(yè)面而不用特殊處理標(biāo)簽。適配器將特定模型和域變換為HTML模板。
與Hiccup和Enlive不一樣,Stencil和Selmer都是基于外部模板系統(tǒng),而不是基于Clojure。Stencil是實(shí)現(xiàn)了Mustache(這是個(gè)流行的無邏輯模板系統(tǒng)),Selmer是模仿Django模板系統(tǒng)在Python上的實(shí)現(xiàn)。
本書重點(diǎn)關(guān)注并使用Hiccup,因?yàn)樗恍枰~外學(xué)習(xí)任何語(yǔ)法,直接使用Clojure函數(shù)即可。此外,我們?cè)诤竺孢€會(huì)學(xué)習(xí)用Selmer模板來取代Hiccup創(chuàng)建的應(yīng)用。
別的選擇徹底沒有考慮使用服務(wù)端模板,你需要在客戶端處理模板來接管這些工作,挑個(gè)流行的JavaScript庫(kù),并使用Ajax與服務(wù)通訊。當(dāng)然,這樣也能勝任。好處是這可以讓客戶端服務(wù)端的界限明確、清晰,有助于擴(kuò)充其他形式的客戶端,比如移動(dòng)應(yīng)用接口。在編寫單頁(yè)應(yīng)用18時(shí),這還是通行手段。
無論你喜歡何種模板策略,最佳實(shí)踐都不會(huì)去聚合域邏輯和視圖。通過合理構(gòu)架的程序,是可以輕松替換模板引擎的。
Hiccup處理模板化頁(yè)面
現(xiàn)在開始介紹一些Hiccup使用基礎(chǔ),以及通過它如何生成適當(dāng)?shù)捻?yè)面元素。
剛才提到,用原生Clojure就能編寫Hiccup模板,所以你就不需要去學(xué)習(xí)特定領(lǐng)域語(yǔ)言(DLS,domain-specific language)就能駕馭它。
Hiccup用Clojure vector(向量表)表示HTML元素,其屬性使用map描述,這種結(jié)構(gòu)表達(dá)方式與生成的HTML標(biāo)簽在結(jié)構(gòu)上比較吻合,示例如下。
`[:tag-name {:attribute-key "attribute value"} tag body]
attribute-key="attribute value">tag body`
如果我們想要?jiǎng)?chuàng)建一個(gè)包含圖片的div標(biāo)簽,可以創(chuàng)建一個(gè)vector,第一個(gè)元素為:div關(guān)鍵字,緊隨其后是一個(gè)map(包含div ID和div的class)。余下部分是以vector表示圖片的內(nèi)容構(gòu)成。
[:div {:id "hello", :class "content"} [:p "Hello world!"]]
我們使用hiccup.core/html宏將vector轉(zhuǎn)換為HTML文本:
(html [:div {:id "hello", :class "content"} [:p "Hello world!"]])
Hello world!
由于Hiccup允許你通過map設(shè)置元素屬性,如有必要,你還可以使用元素內(nèi)聯(lián)樣式。盡管如此,你還是應(yīng)該抵御這種誘惑,使用CSS樣式化元素取代之,這可以確保結(jié)構(gòu)和描述分離。
由于對(duì)元素設(shè)置ID和設(shè)置class是常用操作,Hiccup還提供便捷的CSS樣式化處理。我們可以如下簡(jiǎn)化編寫我們的div,取代之前的代碼:
[:div#hello.content [:p "Hello world!"]]
Hiccup同樣提供一些輔助函數(shù),用來定義常用元素,比如表單、鏈接、圖像。所有這些函數(shù)輸出的vector,由Hiccup預(yù)先定義的格式描述。
當(dāng)一個(gè)函數(shù)在使用中并不能滿足需求時(shí),你當(dāng)然可以寫下元素的文本描述,還可以調(diào)整輸出來滿足需要。描述HTML元素的函數(shù)可以配置,其第一個(gè)參數(shù)可以接受可選屬性的map。我們?cè)倭私庖恍┏S玫腍iccup輔助函數(shù),來改善使用體驗(yàn)。
首先,我們來看看怎么用link-to輔助函數(shù)創(chuàng)建一個(gè)標(biāo)簽:
(link-to {:align "left"} "http://google.com" "google")
這段代碼將生成以下vector:
[:a {:align "left", :href #http://google.com>} ("google")]
我們已有一個(gè)關(guān)鍵字:a作為第一項(xiàng),緊隨其后的map表示屬性,以及表示內(nèi)容的list。
還是如此,將link-to函數(shù)封裝在html宏里面,我們可以基于此vector輸出HTML:
(html (link-to {:align "left"} "http://google.com" "google"))
還有一個(gè)常用的函數(shù)form-to,用來生成HTML表單,我們用此函數(shù)實(shí)現(xiàn)上一章創(chuàng)建的表單,并將信息提交給服務(wù)端。
`(form-to [:post "/"]
[:p "Name:" (text-field "name")]
[:p "Message:" (text-area {:rows 10 :cols 40} "message")]
(submit-button "comment"))`
這個(gè)輔助函數(shù)接受一個(gè)vector,第一個(gè)元素是HTTP請(qǐng)求類型的關(guān)鍵字,第二個(gè)元素是URL字符串。余下參數(shù)也為vector,通過求值可以表示為HTML元素。當(dāng)調(diào)用html宏后,前面的代碼會(huì)被轉(zhuǎn)化為以下HTML:
`
Name:
Message:
還有一個(gè)實(shí)用的輔助宏defhtml。我們?cè)诙x一個(gè)函數(shù)同時(shí),通過參數(shù)內(nèi)容悄悄生成HTML。這意味著在構(gòu)造頁(yè)面時(shí),我們不需要用html宏作用每一個(gè)獨(dú)立元素。
`(defhtml page [& body]
[:html
[:head
[:title "Welcome"]]
[:body body]])`
同樣,在hiccup.page命名空間里,Hiccup提供若干生成特定HTML變體的宏,比如HTML4、HTML5和XHTML。看,我們?cè)诹粞圆境绦蚶锸褂玫木褪莌tml5宏。
`(defn common [& body]
(html5
[:head
[:title "Welcome to guestbook"]
(include-css "/css/screen.css")]
[:body body]))`
添加資源
現(xiàn)實(shí)中,大型網(wǎng)站的頁(yè)面必然涉及加載JavaScript和CSS。在hiccup.page 命名空間里,Hiccup提供幾個(gè)實(shí)用函數(shù)來達(dá)到這個(gè)目的。你可以使用include-css去引用任何CSS文件,include-js來加載JavaScript資源。這里有個(gè)在常用布局中包含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都能接受多個(gè)字符串,每個(gè)參數(shù)指定一個(gè)URI資源。它們的輸出必然是一個(gè)Hiccupvector,最終會(huì)被轉(zhuǎn)換為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提供一個(gè)名為image的輔助函數(shù)去加載圖片:
(image "/img/test.jpg")
[:img {:src #}]
(image "/img/test.jpg" "alt text")
[:img {:src #, :alt "alt text"}]
Hiccup API一覽
你已經(jīng)見識(shí)了一些常用的函數(shù),其實(shí)還有一些更有用的。大多數(shù)輔助函數(shù)可以在element和form命名空間里找到。這些函數(shù)用于定義元素,比如圖像、鏈接、腳本標(biāo)簽、復(fù)選框、下拉工具欄以及輸入欄。
如你所見,Hiccup提供一套簡(jiǎn)明API去生成HTML模板,此外還有字面量vector表達(dá)式。既然你已經(jīng)領(lǐng)悟到了Hiccup的精髓,那我們回過來對(duì)此前的留言簿程序進(jìn)行更深入的剖析。
回顧留言簿程序
我們現(xiàn)在換個(gè)角度去看待那些定義在home命名空間的函數(shù)。當(dāng)你試著運(yùn)行程序,并來回瀏覽時(shí),順便查閱頁(yè)面的HTML輸出和在代碼里的定義。
首先,我們用show-guests函數(shù)去生成一個(gè)無序清單。它遍歷數(shù)據(jù)庫(kù)的消息,然后為每一個(gè)消息創(chuàng)建一個(gè)列表項(xiàng)。
(defn show-guests []
[:ul.guests
(for [{:keys [message name timestamp]} (db/read-guests)]
[:li
[:blockquote message]
[:p "-" [:cite name]]
[:time (format-time timestamp)]])])
這里有個(gè)輔助函數(shù),可以用于顯示格式化時(shí)間戳。此函數(shù)使用java.text.SimpleDate Format將日期對(duì)象轉(zhuǎn)化為格式化字符串。我們使用流化(->)宏去執(zhí)行格式化器去格式化文本,接下來使用此方法處理從數(shù)據(jù)庫(kù)獲取的時(shí)間戳。
(defn format-time [timestamp]
(-> "dd/MM/yyyy"
(java.text.SimpleDateFormat.)
(.format timestamp)))
你可能已經(jīng)發(fā)現(xiàn)目前的home函數(shù)編寫得有點(diǎn)復(fù)雜,因?yàn)樗€有一些用來指導(dǎo)用戶提交表單的額外描述。
這里有一點(diǎn)值得一提:錯(cuò)誤處理行的代碼用于顯示錯(cuò)誤鍵值,由控制器填充,最終交由show-guests函數(shù)去呈現(xiàn)內(nèi)容。
home函數(shù)使用layout/common封裝內(nèi)容,為頁(yè)面生成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創(chuàng)建頁(yè)面模板,同時(shí)也便于通過關(guān)聯(lián)模板定義生成輸出元素。
我們就此完成了路由定義,Compojure路由得以完善。
(defroutes home-routes
(GET "/" name message error)
(POST "/" name message))
到目前為止,我們已完成創(chuàng)建路由并由此呈現(xiàn)頁(yè)面,還能處理來自客戶端的請(qǐng)求表單。正如我們先前提到的,除了由Ring和Compojure提供的,真實(shí)的應(yīng)用還需要添加一些別的元素。接下來,讓我們看看如何為我們的應(yīng)用添加更多功能。
總結(jié)
以上是生活随笔為你收集整理的《Clojure Web开发实战》——第2章,第2.3节应用架构的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 组合(Combination)
- 下一篇: android屏幕录制