翻译的是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})