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



你可能感兴趣的:(clojure)