翻譯的是clojure的ring庫文檔,原文來自git:https://github.com/ring-clojure/ring/wiki。不知道這個之前是不是有人翻譯過。初試牛刀,紕漏錯誤之處難免,請指正。
Ring 是一個Clojure編程語言構建web應用程序的底層接口和庫。它類似於Rack之於Ruby,WSGI之於Python,或者Java的Servlet規範。
Getting Started
$ lein new hello-world
$ cd hello-world
(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?
- 可以使用clojure的函數和映射編寫應用程序
- 可以在自動載入的開發服務器中運行應用程序
- 把應用程序編譯成一個java servlet
- 應用程序打包成war包
- 可以利用大量預先編寫好的中間件
- 部署應用程度到雲端環境,譬如Amazon Elastic Beanstalk 和Heroku
Concepts
- Handler
- Request
- Response
- Middleware
Handlers
(defn what-is-my-ip [request]
{:status 200
:headers {"Content-Type" "text/plain"}
:body (:remote-addr request)})
這個函數返回一個map,這個map是ring將其轉化爲一個HTTP響應。這個響應返回一個純文本,這個文本包含一個用來訪問應用程序的IP地址。
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 如果響應主體對應響應的狀態碼時,表示這個響應主體。這個主體可能是下面四個類型之一:
- String 此時主體(body)將直接發往客戶端
- ISeq 序列的每個元素作爲一個字符串發往客戶端
- File 引用文件的內容將被髮往客戶端
- InputStream 流的內容發送到輸送到文件。當這個流耗盡了,關閉之。
Middleware
(defn wrap-content-type [handler content-type]
(fn [request]
(let [response (handler request)]
(assoc-in response [:headers "Content-Type"] content-type))))
這個中間件函數在handler生成的每個響應頭上添加了個“Content-Type”。
(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"))
(use 'ring.middleware.resource)
(def app
(wrap-resource your-handler "public"))
如果你正在使用像leiningen或者cake這樣的clojure構建工具,那麼工程的非源文件的資源將被構建在resources目錄。該目錄下的文件將自動地包含jar或者war包文件。
(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 頭。這能確保瀏覽器知曉被提供的文件類型,並且在有緩存的情況下不用重新請求。
Content Types
(use 'ring.middleware.content-type)
(def app
(wrap-content-type your-handler))
訪問一個樣式:
http://example.com/style/screen.css
那麼 content-type 函數將會添加如下頭:
(use 'ring.middleware.content-type)
(def app
(wrap-content-type
your-handler
{:mime-types {"foo" "text/x-foo"}}))
Parameters
(use 'ring.middleware.params)
(def app
(wrap-params your-handler))
中間件 wrap-params 爲 在查詢字符串或者HTTP請求體中的URL編碼參數提供了支持。
- :encoding 參數字符編碼。缺省使用請求的字符編碼,如果請求沒有設置字符編碼則使用”UTF-8“ 。
- :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)。
http://example.com/demo?x=hello
那麼你的參數map將會如下:
{"x" "hello"}
但是如果你有多個相同鍵值的參數:
http://example.com/demo?x=hello&x=world
那麼參數map將會這樣:
{"x" ["hello", "world"]}
Cookies
(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過期的特定日期和時間
{"secret" {:value "foobar", :secure true, :max-age 3600}}
Sessions
(use 'ring.middleware.session
'ring.util.response)
(defn handler [{session :session}]
(response (str "Hello " (:username session))))
(def app
(wrap-session handler))
(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秒,或者一小時。
(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中
(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
(use 'ring.middleware.params
'ring.middleware.multipart-params)
(def app
(-> your-handler
wrap-params
wrap-multipart-params))
上傳的內容存儲在臨時文件裏,臨時文件將在上傳完成一小時以後刪除。
Interactive Development
用Ring開發時,你可能發現自己需要不重啓開發服務器的情況下重載源文件。
有三種方式:
: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
這個服務器將會自動重新載入在你源目錄下修改過的文件。
:dev-dependencies [[ring-serve "0.1.2"]]
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
Benchmarks
https://github.com/ptaoussanis/clojure-web-server-benchmarks
Examples
Hello World
;; When executed, this file will run a basic web server
;; on http://localhost:8080 that will display the text
;; 'Hello World'.
(ns ring.example.hello-world
(:use ring.adapter.jetty))
(defn handler [request]
{:status 200
:headers {"Content-Type" "text/plain"}
:body "Hello World"})
(run-jetty handler {:port 8080})
Hello World with ring.util.response
;; When executed, this file will run a basic web server
;; on http://localhost:8080 that will display the text
;; 'Hello World'.
(ns ring.example.hello-world-2
(:use ring.util.response
ring.adapter.jetty))
(defn handler [request]
(-> (response "Hello World")
(content-type "text/plain")))
(run-jetty handler {:port 8080})
Form parameters
;; When executed, this file will run a basic web server
;; on http://localhost:8080.
(ns ring.example.params
(:use ring.middleware.params
ring.util.response
ring.adapter.jetty))
(defn page [name]
(str "<html><body>"
(if name
(str "Nice to meet you, " name "!")
(str "<form>"
"Name: <input name='name' type='text'>"
"<input type='submit'>"
"</form>"))
"</body></html>"))
(defn handler [{{name "name"} :params}]
(-> (response (page name))
(content-type "text/html")))
(def app
(-> handler wrap-params))
(run-jetty app {:port 8080})
Sessions
;; When executed, this file will run a basic web server
;; on http://localhost:8080, which will tell you how many
;; times you have visited the page.
(ns ring.example.session
(:use ring.middleware.session
ring.util.response
ring.adapter.jetty))
(defn handler [{session :session, uri :uri}]
(let [n (session :n 1)]
(if (= uri "/")
(-> (response (str "You have visited " n " times"))
(content-type "text/plain")
(assoc-in [:session :n] (inc n)))
(-> (response "Page not found")
(status 404)))))
(def app
(-> handler wrap-session))
(run-jetty app {:port 8080})