Clojure Ring Web 开发

Ring框架

Python有Django。PHP有Drupal。当然Ruby有所有web框架之王,Ruby on Rails。

在Clojure里有大量的web框架,但是初学者应该把他们自己的服务器栈移动到Ring生态系统。

在Clojure里你应该使用什么框架?实际上这个问题是难以回答的。外面有很多web框架了。有人把 Compojure 叫做框架,虽然它真正是一个类库。 lib-noir 为你做了大量工作。然而有属于你的真正框架,像 Pedestal 或 Hoplon ,它们提供基础功能和解决web开发的抽象。所有这些项目是伟大的,但是对于初学者,推荐建立你自己的web栈,从Ring开始。

Compojure实际上只是一个路由类库,而不是框架。虽然有 playnice , bidi , Route One 和 gudu 等其它替代品,但是你能够用它满足路由需要。如果你不想下决定,那就使用Compojure。它使用广泛、表现优秀。如果你想深入,可以看看其他文档。它们针对不同的场景各有优点。

lib-noir 来自于 Noir ,后者是一个web框架(现在废弃了)。它比较容易,还为你提供了一些管道,因此你刚好借助建好的大量基础设施来开始一个项目。lib-noir是以类库形式存在的基础设施。我还没有用过,但是很多人喜欢它。然而,当我研究它的时候,我发现它提供了太多我不需要的东西,或太过琐碎。如果得到了大规模的应用(像Rails),你就能得到生态系统的效应,这通常是良性的,但是还没有这样。lib-noir被应用了,只是完全不占优势。

Pedestal 有很多支持者。它的目标是通过提供使用ClojureScript、消息队列形式的、一个明智的前端环境来处理单页app。如果你需要“实时app”,它或许为是你准备的。尽管如此,我仍然警告你,它不适合Clojure初学者。Pedestal引入了大量新概念,甚至有经验的Clojure程序员也不得不去学习。 这个教程 又长又费力。如果你不了解Clojure,你去学习Pedestal会遇到问题的。

Hoplon 也是为web app设计的。它为你提供了用ClojureScript实现的DOM(包括自定义组件),数据流编程(像电子表格)和客户端-服务器端通信。这是勇敢的一步,但是再一次,需要你接受花很长时间才能理解的编程模型。如果你还不熟悉Clojure,你就是在自找麻烦。

外面还有其它框架。但是我推荐你考虑自己条件。如果你在学习Clojure,掌握web app如何工作的最好方法就是得到一个配置了一些基本handler的 Ring Jetty适配器。根据需要添加中间件。写一些自己的中间件。使用Compojure做路由。使用 Hiccup 生成HTML。这个安装将让你学到很多。

Ring仅仅是个函数。借助一些基本概念和Ring SPEC,你可以快速建立正是你想要的web服务器,你能够全面理解它。自己建立的经历能够让你在框架如何整合上受益良多。 况且,Ring有优势。大多数人写功能(以中间件或handler的形式)是以Ring为假设、而不是其它。因此保持靠近本质,你就会接近庞大的彼此兼容的、预编写的类库池。Ring就是Clojure web生态系统的所在地。

Ring 的设计思想

ring 的设计思想是仅仅处理核心的 request, response 映射问题(其 session, cookie 等实现都可以随意替换),compojure 这样的 url 解析框架是 ring 有意留空的领域。换句话说,ring 的设计思路是有其他补充性的库来让其变得更加易用。正因为如此,它成为了所有 clojure web 框架的基础(目前 manifold 打算补充 ring 语义对异步处理的遗漏,但总的思想仍然与 ring 兼容)。用 ring 作者的话说:one ring to bind them all。这个魔戒之王统率其他 url 解析、加密解密、web 服务器兼容等各层其他库。

这个思路上可以生长出非常繁盛的生态系统。例如,对于 url 解析就有 compojure, bidi, silk, mastache 等许多库,其目标接近,手段互不相同;对于显示层生成有 enlive, laser, hiccup, selmar 等多种生成技术。这种创造力对于初涉者往往看起来难以掌握,但它类似 unix 命令行,威力在于互相结合,但有较陡峭的学习曲线。

Ring 之 HelloWorld

传统的JavaEE使用Servlet接口来划分服务器和应用程序的界限,应用程序负责提供实现Servlet接口的类,服务器负责处理HTTP连接并转换为Servlet接口所需的HttpServletRequest和HttpServletResponse。Servlet接口定义十分复杂,再加上Filter,所需的XML配置复杂度很高,而且测试困难。

Clojure的Web实现最常用的是Ring。Ring的设计来自Python的WSGI和Ruby的Rack,以WSGI为例,其接口设计十分简单,仅一个函数:


def application(env, start_response)

其中env是一个字典,start_response是响应函数。由于WSGI接口本身是纯函数,因此无需Filter接口就可以通过高阶函数对其包装,完成所有Filter的功能。

Ring在内部把Java标准的Servlet接口转换为简单的函数接口:


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

上述函数就完成了Servlet实现类的功能。其中request是一个map,返回值也是一个map,由:status、:headers和:body关键字指定HTTP的返回码、头和内容。

把一系列handler函数串起来就形成了一个处理链,每个链都可以对输入和输出进行处理,链的最后一个处理函数负责根据URL进行路由,这样,完整的Web处理栈就可以构造出来。

Ring把handler称为middleware,middleware基于Clojure的函数式编程模型,利用Clojure自带的->宏就可以直接串起来。

一个完整的Web程序只需要定义一个handler函数,并启动Ring内置的Jetty服务器即可:

;; hello.clj
(ns cljweb.hello
  (:require [ring.adapter.jetty :as jetty]))

(defn handler [request]
      {:status 200,
       :headers {"Content-Type" "text/html"}
       :body "<h1>Hello, world.</h1>"})

(defn start-server []
  (jetty/run-jetty handler {:host "localhost",
                            :port 3000}))

(start-server)

项目配置文件 project.clj

main 函数配置:

:main org.lightsword.clj_web

项目依赖配置:

(defproject org.lightsword/clj-web "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :main org.lightsword.clj_web
  :dependencies [
                 [org.clojure/clojure "1.6.0"]
                 [org.clojure/data.json "0.2.5"]
                 [org.clojure/java.jdbc "0.3.6"]
                 [mysql/mysql-connector-java "5.1.25"]
                 [korma "0.3.0"]
                 [selmer "0.7.2"]
                 [ring "1.3.1"]
                 [ring/ring-json "0.3.1"]
                 [compojure "1.2.1"]
                 ])

运行

lein pom

生成 pom.xml 文件, 然后 maven update, 下载完依赖jar包.

运行lein run,将启动内置的Jetty服务器,然后,打开浏览器,在地址栏输入http://localhost:3000/

就可以看到响应.

输出 request json

;; hello world
(ns org.lightsword.clj_web
  (:require [ring.adapter.jetty :as jetty]))

(defn handler [request] {
                        :status 200,
                        :headers {"Content-Type" "text/html"}
                        :body (str request "\n" "<h1>Hello, World!</h1>")
                        })

(defn start-server []
  (jetty/run-jetty handler {
                            :host "localhost"
                            :port 3000
                            }))



(defn -main [& args]
  (start-server))

在浏览器输入http://localhost:3000/

可以看到输出


{:ssl-client-cert nil, :remote-addr "127.0.0.1", :headers {"host" "localhost:3000", "cookie" "JSESSIONID=4Z6cECnYKywcLQNJWpFW-He2g8b0mXNIpqtApaNW", "accept-language" "zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4", "user-agent" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36", "accept-encoding" "gzip, deflate, sdch", "cache-control" "max-age=0", "connection" "keep-alive", "accept" "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"}, :server-port 3000, :content-length nil, :content-type nil, :character-encoding nil, :uri "/", :server-name "localhost", :query-string nil, :body #, :scheme :http, :request-method :get}

Hello, World!

curl命令看一下


curl -I http://localhost:3000
HTTP/1.1 200 OK
Date: Mon, 11 Jul 2016 16:12:21 GMT
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 392
Server: Jetty(7.6.13.v20130916)




$ curl http://localhost:3000
{:ssl-client-cert nil, :remote-addr "127.0.0.1", :headers {"host" "localhost:3000", "user-agent" "curl/7.43.0", "accept" "*/*"}, :server-port 3000, :content-length nil, :content-type nil, :character-encoding nil, :uri "/", :server-name "localhost", :query-string nil, :body #<HttpInput org.eclipse.jetty.server.HttpInput@18c8579e>, :scheme :http, :request-method :get}
<h1>Hello, World!</h1>

results matching ""

    No results matching ""