天天看點

《Clojure Web開發實戰》——第2章,第2.3節應用架構

本節書摘來自異步社群《clojure web開發實戰》一書中的第2章,第2.3節應用架構,作者[美]dmitri sotnikov,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視

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函數時,程式将開始處理所有路由請求。

<code>(def app (handler/site (routes home-routes app-routes)))</code>

這段代碼,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))

第一條路由被綁定于/,用于從資料庫檢索消息,并用此消息建立一張表單,最終呈現整幅頁面給用戶端。

第二條路由會處理使用者輸入。如果輸入驗證通過,接下來這條消息就會被存入資料庫;否則,頁面将呈現錯誤描述。

其實這兩條路由功能有交集:存儲和顯示使用者資訊,它們也算是同一工作流的兩個部分。

當你發現程式的工作流有明确所屬,那麼可以将此工作流的邏輯關系合并,放在一起處理。程式中的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"&gt;tag body`

如果我們想要建立一個包含圖檔的div标簽,可以建立一個vector,第一個元素為:div關鍵字,緊随其後是一個map(包含div id和div的class)。餘下部分是以vector表示圖檔的内容構成。

<code>[:div {:id "hello", :class "content"} [:p "hello world!"]]</code>

我們使用hiccup.core/html宏将vector轉換為html文本:

<code>(html [:div {:id "hello", :class "content"} [:p "hello world!"]])</code>

hello world!

由于hiccup允許你通過map設定元素屬性,如有必要,你還可以使用元素内聯樣式。盡管如此,你還是應該抵禦這種誘惑,使用css樣式化元素取代之,這可以確定結構和描述分離。

由于對元素設定id和設定class是常用操作,hiccup還提供便捷的css樣式化處理。我們可以如下簡化編寫我們的div,取代之前的代碼:

<code>[:div#hello.content [:p "hello world!"]]</code>

hiccup同樣提供一些輔助函數,用來定義常用元素,比如表單、連結、圖像。所有這些函數輸出的vector,由hiccup預先定義的格式描述。

當一個函數在使用中并不能滿足需求時,你當然可以寫下元素的文本描述,還可以調整輸出來滿足需要。描述html元素的函數可以配置,其第一個參數可以接受可選屬性的map。我們再了解一些常用的hiccup輔助函數,來改善使用體驗。

首先,我們來看看怎麼用link-to輔助函數建立一個标簽:

(link-to {:align "left"} "http://google.com" "google")

這段代碼将生成以下vector:

[:a {:align "left", :href #http://google.com&gt;} ("google")]

我們已有一個關鍵字:a作為第一項,緊随其後的map表示屬性,以及表示内容的list。

還是如此,将link-to函數封裝在html宏裡面,我們可以基于此vector輸出html:

(html (link-to {:align "left"} "http://google.com" "google"))

<a href="http://google.com">google</a>

還有一個常用的函數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 [&amp; body]

 [:html

   [:head

   [:title "welcome"]]

   [:body body]])`

同樣,在hiccup.page命名空間裡,hiccup提供若幹生成特定html變體的宏,比如html4、html5和xhtml。看,我們在留言簿程式裡使用的就是html5宏。

`(defn common [&amp; 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 [&amp; content]

  [: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将日期對象轉化為格式化字元串。我們使用流化(-&gt;)宏去執行格式化器去格式化文本,接下來使用此方法處理從資料庫擷取的時間戳。

(defn format-time [timestamp]

 (-&gt; "dd/mm/yyyy"

   (java.text.simpledateformat.)

   (.format timestamp)))

你可能已經發現目前的home函數編寫得有點複雜,因為它還有一些用來指導使用者送出表單的額外描述。

這裡有一點值得一提:錯誤處理行的代碼用于顯示錯誤鍵值,由控制器填充,最終交由show-guests函數去呈現内容。

home函數使用layout/common封裝内容,為頁面生成html。

(defn home [&amp; [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路由得以完善。

到目前為止,我們已完成建立路由并由此呈現頁面,還能處理來自用戶端的請求表單。正如我們先前提到的,除了由ring和compojure提供的,真實的應用還需要添加一些别的元素。接下來,讓我們看看如何為我們的應用添加更多功能。