ring-clojure

翻譯的是clojure的ring庫文檔,原文來自git:https://github.com/ring-clojure/ring/wiki。不知道這個之前是不是有人翻譯過。初試牛刀,紕漏錯誤之處難免,請指正。


Ring 是一個Clojure編程語言構建web應用程序的底層接口和庫。它類似於Rack之於Ruby,WSGI之於Python,或者Java的Servlet規範。


Getting Started


使用leiningen創建一個新工程。(譯者注:lein之於clojure,相當於maven之於java)
$ lein new hello-world
$ cd hello-world

在project.clj中添加ring-core和ring-jetty-adapter依賴。
(defproject hello-world "1.0.0-SNAPSHOT"
  :description "FIXME: write"
  :dependencies [[org.clojure/clojure "1.5.1"]
                 [ring/ring-core "1.2.0"]
                 [ring/ring-jetty-adapter "1.2.0"]])

然後,編輯src/hello_world/core.clj並添加一個基礎handler。
(ns hello-world.core)

(defn handler [request]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body "Hello World"})

現在我們準備通過一個adapter連接這個handler。使用leiningen啓動REPL。
$ lein repl

然後在REPL中,使用Jetty適配器(adapter)觸發handler。
=> (use 'ring.adapter.jetty)
=> (use 'hello-world.core)
=> (run-jetty handler {:port 3000})

服務器啓動,可訪問: http://localhost:3000/ 。

Why Use Ring?


使用ring作爲web應用程序的基礎有一系列好處:
  • 可以使用clojure的函數和映射編寫應用程序
  • 可以在自動載入的開發服務器中運行應用程序
  • 把應用程序編譯成一個java servlet
  • 應用程序打包成war包
  • 可以利用大量預先編寫好的中間件
  • 部署應用程度到雲端環境,譬如Amazon Elastic Beanstalk 和Heroku

Ring目前是編寫clojure web程序事實上的基礎標準。更高一級別的框架,例如 Compojure,Moustache 和 Noir 都是使用ring作爲基礎。

儘管ring提供的是一個低級別的接口,即使你打算使用更高級別的框架開發,理解ring原理仍是很有用的。沒有對ring的瞭解,你就不可能編寫中間件,你會發現調試程序將更加困難。

Concepts


用ring開發的web應用程序包括四個部分:
  • Handler
  • Request
  • Response
  • Middleware

Handlers

Handlers是一些定義你的web應用程序的函數。Handlers接收一個參數,這個參數是表示一個HTTP請求的MAP,返回一個表示HTTP響應的MAP。
我們看一個例子:
(defn what-is-my-ip [request]
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body (:remote-addr request)})

這個函數返回一個map,這個map是ring將其轉化爲一個HTTP響應。這個響應返回一個純文本,這個文本包含一個用來訪問應用程序的IP地址。

這個handler函數可以通過一系列不同的方法被轉化成一個web應用程序,這些方法將在接下來的部分裏講到。

Requests

如前所述,HTTP請求用Clojure map表示。Requests有很多標準的關鍵字一直存在,但是也包含一些通過中間件添加的自定義關鍵字。
標準的關鍵字有:
  • :server-port   處理請求的端口
  • :server-name   已解析的服務器名稱,或者服務器IP地址
  • :remote-addr  客戶端IP地址,或者發送該請求的最後一級代理
  • :uri   請求的URI (域名之後的全路徑)
  • :query-string   請求的字符串,如果存在
  • :scheme   傳輸協議,:http或者:https
  • :request-method   HTTP請求方法,:get :head :options :put :post :delete其中之一
  • :content-type   請求體的MIME類型,若獲知
  • :content-length   請求體的字節數,若獲知
  • :character-encoding   請求體使用的字符編碼名稱,若獲知
  • :headers   小寫header名稱、header值組成的clojure map
  • :body   請求體的輸入流,若存在

Responses

response map由handler創建,包括3個關鍵字:
  • :status   HTTP狀態碼,例如200,302,404等
  • :headers   是HTTP header名稱組和header值組組成的map。這些值可能是字符串,也可能是發往HTTP響應的名稱/值構成的header,或者字符串的集合,這個集合中名稱/值header將被置於每個值中。
  • :body   如果響應主體對應響應的狀態碼時,表示這個響應主體。這個主體可能是下面四個類型之一:
    1. String 此時主體(body)將直接發往客戶端
    2. ISeq 序列的每個元素作爲一個字符串發往客戶端
    3. File 引用文件的內容將被髮往客戶端
    4. InputStream 流的內容發送到輸送到文件。當這個流耗盡了,關閉之。

Middleware

ring的中間件是handlers上添加了額外功能的更高級的函數。middleware函數的第一個參數是一個handler,並且這個middleware返回一個新的handler函數。
此處一例:
(defn wrap-content-type [handler content-type]
  (fn [request]
    (let [response (handler request)]
      (assoc-in response [:headers "Content-Type"] content-type))))

這個中間件函數在handler生成的每個響應頭上添加了個“Content-Type”。

用這個middleware包裝handler:
(def app
  (wrap-content-type handler "text/html"))

此處定義了一個新的handler,“app” 。這個“app”包括用“wrap-content-type”包裝了的handler “handler” 。

這個宏(->)可以用來把中間件連接到一起:(譯者注:->    後面的函數迭代使用之前的函數結果作爲第一個參數,返回最後一次函數調用的值

(def app
  (-> handler
      (wrap-content-type "text/html")
      (wrap-keyword-params)
      (wrap-params)))

Middleware在ring中經常使用,提供了很多處理原始HTTP請求之上的功能。例如Parameters、sessions還有文件上傳等都是用ring標準庫中的middleware處理的。


Creating responses


你可以手動創建ring響應maps(參照 Concepts),但是同樣 ring.util.response 命名空間包含了一些有用的函數使得整個任務變得更簡單。


這個 response 函數 創建了一個基礎的“200 OK”響應:

(response "Hello World")

=> {:status 200
    :headers {}
    :body "Hello World"}

然後,你可以使用像 content-type 這樣的函數來爲基本的響應添加額外的頭(headers)和其他的組件:
(-> (response "Hello World")
    (content-type "text/plain"))

=> {:status 200
    :headers {"Content-Type" "text/plain"}
    :body "Hello World"}



也存在創建重定向這樣特殊的函數:

(redirect "http://example.com")

=> {:status 302
   :headers {"Location" "http://example.com"}
   :body ""}



也或者返回靜態文件或者資源:

(file-response "readme.html" {:root "public"})

=> {:status 200
    :headers {}
    :body (io/file "public/readme.html")}

(resource-response "readme.html" {:root "public"})

=> {:status 200
    :headers {}
    :body (io/input-stream (io/resource "public/readme.html"))}

這些函數的更多信息和其他內容可以點擊這裏 ring.util.response API documentation 。


Static Resources


web應用程序經常需要提供靜態內容,例如圖片或者CSS樣式。ring提供了倆箇中間件函數來應付這些。


一個 是 wrap-file 。它提供本地文件系統某個目錄的靜態內容:

(use 'ring.middleware.file)
(def app
  (wrap-file your-handler "/var/www/public"))

另一個是 wrap-resource 。它提供JVM類路徑下的靜態內容:
(use 'ring.middleware.resource)
(def app
  (wrap-resource your-handler "public"))

如果你正在使用像leiningen或者cake這樣的clojure構建工具,那麼工程的非源文件的資源將被構建在resources目錄。該目錄下的文件將自動地包含jar或者war包文件。

所以在以上的例子裏,處於“resources/public”目錄的文件,當然會屬於公共目錄,他們將作爲靜態文件被提供。

你會經常將 wrap-file 或者 wrap-resource 跟 wrap-file-info 結合在一起:
(use 'ring.middleware.resource
     'ring.middleware.file-info)

(def app
  (-> your-handler
      (wrap-resource "public")
      (wrap-file-info)))

wrap-file-info 這個中間件函數檢查文件的修改日期和文件擴展名,添加 Content-Type 和 Last-Modified 頭。這能確保瀏覽器知曉被提供的文件類型,並且在有緩存的情況下不用重新請求。

注意這個 wrap-file-info 需要用 wrap-resource 或者 wrap-file 函數包裝(即 跟在他們之後)。

Content Types


你可以使用 wrap-content-type 這個中間件來爲基於文件擴展名的URI添加一個 Content-Type:
(use 'ring.middleware.content-type)

(def app
  (wrap-content-type your-handler))

訪問一個樣式:
http://example.com/style/screen.css

那麼 content-type 函數將會添加如下頭:
Content-Type: text/css

你可以參考下默認的content type,ring-core/src/ring/util/mime_types.clj 。

你可以添加自定義MIME類型 通過使用 :mime-types 選項:
(use 'ring.middleware.content-type)

(def app
  (wrap-content-type
   your-handler
   {:mime-types {"foo" "text/x-foo"}}))


Parameters



使用 URL編碼的參數值是瀏覽器傳值給web應用程序的主要方式。當用戶提交表單時,他們被髮送出去,並且經常在像分頁這種情形下使用。

因爲ring是低層級的接口,除非你使用正確的中間件否則本身不支持參數:
(use 'ring.middleware.params)
(def app
  (wrap-params your-handler))

中間件 wrap-params 爲 在查詢字符串或者HTTP請求體中的URL編碼參數提供了支持。

它不支持文件上傳,文件上傳使用的是 wrap-multipart-params 。關於多重形式的更多信息參照 File Uploads的內容。

wrap-params 函數接收一個可選的選項map。此處現僅有一個可識別關鍵字:
  • :encoding   參數字符編碼。缺省使用請求的字符編碼,如果請求沒有設置字符編碼則使用”UTF-8“ 。
parameter中間件當被應用到handler時,添加三個關鍵字到請求map:
  • :query-params   查詢串的參數map
  • :form-params   提交的表單數據的參數map
  • :params   所有參數融合的map
如例,如果你有個類似這個的請求:
{:http-method :get
 :uri "/search"
 :query-string "q=clojure"}

那麼 wrap-params 將會修改其爲:
{:http-method :get
 :uri "/search"
 :query-string "q=clojure"
 :query-params {"q" "clojure"}
 :form-params {}
 :params {"q" "clojure"}}

通常你僅僅想使用 :params 這個key(關鍵字),但是實際情況是存在其他key的情況下,你需要區分是通過查詢串還是通過提交HTML表單來傳遞的參數(get or post)。

參數的鍵是字符串,如果只有一個參數名稱,值也可以是字符串;如果有一個以上的具有相同名稱的鍵值對,值可以是向量。


例如,你有URL:
http://example.com/demo?x=hello

那麼你的參數map將會如下:
{"x" "hello"}

但是如果你有多個相同鍵值的參數:
http://example.com/demo?x=hello&x=world

那麼參數map將會這樣:
{"x" ["hello", "world"]}

Cookies


給 ring handler添加cookie支持,需要使用中間件 wrap-cookies 包裝這個handler:
(use 'ring.middleware.cookies)
(def app
  (wrap-cookies your-handler))

這裏給請求map添加一個 :cookies的key,包含cookies的map將類似於:
{"username" {:value "alice"}}

{:status 200
 :headers {}
 :cookies {"username" {:value "alice"}}
 :body "Setting a cookie."}

不光設置cookie值,你也可以添加更多屬性:
  • :domain   限制cookie到一個特定的域中
  • :path   限制cookie到一個特定的路徑
  • :secure   若真,限制cookie僅使用HTTPS的URL
  • :http-only   若真,限制cookie僅使用HTTP協議(例如javascript不能訪問)
  • :max-age   cookie過期時間數(以秒計量)
  • :expires   cookie過期的特定日期和時間

你若想使用一小時之後過期的安全的cookie,則:
{"secret" {:value "foobar", :secure true, :max-age 3600}}


Sessions


Ring的Sessions可能跟你預期的所有不同,因爲Ring企圖儘可能的實用。

Session 數據通過請求map的 :session鍵來傳輸。下面的例子打印出來自session的現有username。
(use 'ring.middleware.session
     'ring.util.response)

(defn handler [{session :session}]
  (response (str "Hello " (:username session))))

(def app
  (wrap-session handler))


你可以通過向響應(response)裏添加 :session 鍵 來更新session數據。下面的例子統計當前會話(session)已經訪問過頁面的次數。
(defn handler [{session :session}]
  (let [count   (:count session 0)
        session (assoc session :count (inc count))]
    (-> (response (str "You accessed this page " count " times."))
        (assoc :session session))))

完全刪除session,可以設置response 的 :session 鍵 值爲 nil : (譯者注:clojure語言的nil類似於其他語言的false和null)
(defn handler [request]
  (-> (response "Session deleted.")
      (assoc :session nil)))

你經常想控制會話cookie在用戶瀏覽器的存在時間。你可以通過使用 :cookie-attrs 選項來改變會話cookie屬性:
(def app
  (wrap-session handler {:cookie-attrs {:max-age 3600}}))

這種情形下,cookie的最大生命周長設置爲3600秒,或者一小時。

你也可以使用 :secure 來確保使用 HTTPS 的站點 cookie 不會通過 HTTP協議泄露:
(def app
  (wrap-session handler {:cookie-attrs {:secure true}}))

Session Stores

Session 數據保存在會話存儲(session stores)中。Ring中有倆個存儲器:
  • ring.middleware.session.memory/memory-store  存儲session在內存
  • ring.middleware.session.cookie/cookie-store  存儲加密過的session在cookie中
Ring默認將session數據存在內存,但是可以使用 :store 選項來重寫:
(use 'ring.middleware.session.cookie)

(def app
  (wrap-session handler {:store (cookie-store {:key "a 16-byte secret"})})

你可以通過實現 ring.middleware.session.store/SessionStore 協議來編寫自己的session存儲器:
(use 'ring.middleware.session.store)

(deftype CustomStore []
  SessionStore
  (read-session [_ key]
    (read-data key))
  (write-session [_ key data]
    (let [key (or key (generate-new-random-key))]
      (save-data key data)
      key))
  (delete-session [_ key]
    (delete-data key)
    nil))

注意當編寫一個新session時,key值應當是 nil 的。session store 期待並且生成一個新的隨機key值。這個key不能被猜到,這點很重要,否則惡意用戶將會訪問他人的session 數據。


File Uploads


上傳文件到一個web站點需要多重形式的處理,爲此Ring提供了中間件 wrap-multipart-params:
(use 'ring.middleware.params
     'ring.middleware.multipart-params)

(def app
  (-> your-handler
      wrap-params
      wrap-multipart-params))

上傳的內容存儲在臨時文件裏,臨時文件將在上傳完成一小時以後刪除。

Interactive Development


用Ring開發時,你可能發現自己需要不重啓開發服務器的情況下重載源文件。


有三種方式:


1、使用 Lein-Ring
最簡單的方式是用用leiningen的lein-ring插件。首先,在project.clj文件中添加如下依賴:
:plugins [[lein-ring "0.8.7"]]

然後運行shell命令下載安裝此依賴:
lein deps

然後在 project.clj 文件末尾添加下面的key:
:ring {:handler your-app.core/handler}

這個是告訴lein-ring插件你的主Ring handler的目標位置, 所以你需要替換 your-app.core/handler 用你自己的handler函數的命名空間和符號。

完成這些,你就可以在命令行啓動一個新的服務器了:
lein ring server

這個服務器將會自動重新載入在你源目錄下修改過的文件。

2、使用Ring-Serve

如果你使用集成開發環境,例如安裝了SLIME的Emacs,你可能要在你的開發環境中啓動開發服務器。這將有助於你設置斷點和逐步調試代碼。

 ring-serve 庫提供了此功能。首先,在 project.clj文件添加 ring-serve 依賴:
:dev-dependencies [[ring-serve "0.1.2"]]

然後運行shell命令,下載安裝依賴:
lein deps

現在你可以在REPL環境下通過使用 ring.util.serve/serve 來啓動開發服務器:
user> (require 'your-app.core/handler)
nil
user> (use 'ring.util.serve)
nil
user> (serve your-app.core/handler)
Started web server on port 3000

3、手動
如果你不想使用前述的工具,那可以手動設置。此選項適用於那些知其然的更高級的用戶。

確保如下
  • 你的RIng adapter 運行於後臺進程,並不會阻塞你的REPL
  • 你的handler函數賦給一個變量,這樣當你重載命名空間時它將被更新
例如
user> (defonce server (run-jetty #'handler {:port 8080 :join? false}))


API

http://mmcgrana.github.io/ring/


Third Party Libraries

https://github.com/ring-clojure/ring/wiki/Third-Party-Libraries



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章