What's the "big idea" behind compojure routes?

我是 Clojure 的新手,一直在使用 Compojure 编写一个基本的 Web 应用程序。不过,我在 Compojure 的 defroutes语法方面遇到了困难,我认为我需要理解这一切背后的“如何”和“为什么”。

这看起来像是 Ring 风格的应用程序从一个 HTTP 请求映射开始,然后只是通过一系列中间件函数传递请求,直到它被转换成一个响应映射,然后被发送回浏览器。这种风格对于开发人员来说似乎太“低级”了,因此需要像 Compojure 这样的工具。我可以看到在其他软件生态系统中也需要更多的抽象,最显著的是 Python 的 WSGI。

问题是我不理解 Compojure 的方法:

(defroutes main-routes
(GET "/"  [] (workbench))
(POST "/save" {form-params :form-params} (str form-params))
(GET "/test" [& more] (str "<pre>" more "</pre>"))
(GET ["/:filename" :filename #".*"] [filename]
(response/file-response filename {:root "./static"}))
(ANY "*"  [] "<h1>Page not found.</h1>"))

我知道理解所有这些的关键在于一些宏巫术,但是我还没有完全理解宏。我已经盯着 defroutes源很长时间了,但就是不明白!这是怎么回事?理解“大想法”可能会帮助我回答这些具体的问题:

  1. How do I access the Ring environment from within a routed function (e.g. the workbench function)? For example, say I wanted to access the HTTP_ACCEPT headers or some other part of the request/middleware?
  2. 解构({form-params :form-params})是怎么回事? 解构时有哪些关键词可用?

我真的很喜欢 Clojure,但是我被难住了!

21888 次浏览

(在某种程度上)解释清楚

注意,我正在使用 Compojure 0.4.1(给你是 GitHub 上的0.4.1发行版提交)。

为什么?

compojure/core.clj的最顶端,有一个对 Compojure 目的的有用总结:

生成 Ring 处理程序的简洁语法。

从表面上看,这就是“为什么”这个问题的全部。更深入一点,让我们来看看 Ring 风格的应用程序是如何工作的:

  1. A request arrives and is transformed into a Clojure map in accordance with the Ring spec.

  2. 这个映射被导入到所谓的“处理程序函数”中,这个函数需要产生一个响应(也是一个 Clojure 映射)。

  3. 响应映射被转换为实际的 HTTP 响应并发送回客户端。

第二步。在上面是最有趣的,因为它是处理程序的责任,检查在请求中使用的 URI,检查任何 cookie 等,并最终得到一个适当的响应。显然,有必要将所有这些工作分解成一个定义良好的片段集合; 这些片段通常是一个“基本”处理程序函数和一个包装它的中间件函数集合。Compojure 的目的是简化基本处理程序函数的生成。

How?

Compojure 是围绕“路线”的概念构建的。这些实际上是由 影响力库在更深层次上实现的(Compojure 项目的一个衍生物——许多东西在0.3. x-> 0.4. x 转换时被移动到单独的库)。一个路由由(1)一个 HTTP 方法(GET,PUT,HEAD...) ,(2)一个 URI 模式(用 Webby Rubyists 熟悉的语法指定) ,(3)一个用于将请求映射的某些部分绑定到主体中可用名称的解构形式,(4)一个需要产生有效 Ring 响应的表达式主体(在非平凡的情况下,这通常只是对一个单独函数的调用)。

这可能是一个很好的观点,看看一个简单的例子:

(def example-route (GET "/" [] "<html>...</html>"))

让我们在 REPL (下面的请求映射是最小有效 Ring 请求映射)测试一下:

user> (example-route {:server-port 80
:server-name "127.0.0.1"
:remote-addr "127.0.0.1"
:uri "/"
:scheme :http
:headers {}
:request-method :get})
{:status 200,
:headers {"Content-Type" "text/html"},
:body "<html>...</html>"}

如果 :request-method:head,那么响应应该是 nil。稍后我们将回到 nil在这里的含义这个问题(但是请注意,它不是一个有效的 Ring 响应!).

从这个例子中可以明显看出,example-route只是一个函数,而且非常简单; 它查看请求,确定是否有兴趣处理它(通过检查 :request-method:uri) ,如果有兴趣,返回一个基本的响应映射。

同样显而易见的是,路由的主体并不真正需要求值到一个正确的响应映射; Compojure 为字符串(如上所示)和许多其他对象类型提供了合理的默认处理; 有关详细信息,请参阅 compojure.response/render多方法(这里的代码完全是自我文档化的)。

现在让我们尝试使用 defroutes:

(defroutes example-routes
(GET "/" [] "get")
(HEAD "/" [] "head"))

对上面显示的示例请求及其 :request-method :head变体的响应与预期的一样。

example-routes的内部工作原理是依次尝试每个路由; 只要其中一个路由返回非 nil响应,该响应就成为整个 example-routes处理程序的返回值。为了方便起见,defroutes定义的处理程序隐式地封装在 wrap-paramswrap-cookies中。

Here's an example of a more complex route:

(def echo-typed-url-route
(GET "*" {:keys [scheme server-name server-port uri]}
(str (name scheme) "://" server-name ":" server-port uri)))

注意解构形式代替了前面使用的空向量。这里的基本思想是,路由的主体可能对关于请求的一些信息感兴趣; 因为这些信息总是以映射的形式到达,所以可以提供一个关联的解构形式来从请求中提取信息,并将其绑定到路由主体范围内的局部变量。

以上测试:

user> (echo-typed-url-route {:server-port 80
:server-name "127.0.0.1"
:remote-addr "127.0.0.1"
:uri "/foo/bar"
:scheme :http
:headers {}
:request-method :get})
{:status 200,
:headers {"Content-Type" "text/html"},
:body "http://127.0.0.1:80/foo/bar"}

The brilliant follow-up idea to the above is that more complex routes may assoc extra information onto the request at the matching stage:

(def echo-first-path-component-route
(GET "/:fst/*" [fst] fst))

This responds with a :body of "foo" to the request from the previous example.

这个最新示例有两个新特性: "/:fst/*"和非空绑定向量 [fst]。第一种是前面提到的 URI 模式的 Rails-and-Sinatra 式语法。它比上面的例子要复杂一些,因为它支持 URI 段上的正则表达式约束(例如,可以提供 ["/:fst/*" :fst #"[0-9]+"]使路由只接受上面的 :fst的全数字值)。第二种是对请求映射中的 :params条目进行匹配的简化方法,它本身就是一个映射; 它对于从请求、查询字符串参数和表单参数中提取 URI 段非常有用。举例说明后一点:

(defroutes echo-params
(GET "/" [& more]
(str more)))


user> (echo-params
{:server-port 80
:server-name "127.0.0.1"
:remote-addr "127.0.0.1"
:uri "/"
:query-string "foo=1"
:scheme :http
:headers {}
:request-method :get})
{:status 200,
:headers {"Content-Type" "text/html"},
:body "{\"foo\" \"1\"}"}

现在是时候看看问题文本中的例子了:

(defroutes main-routes
(GET "/"  [] (workbench))
(POST "/save" {form-params :form-params} (str form-params))
(GET "/test" [& more] (str "<pre>" more "</pre>"))
(GET ["/:filename" :filename #".*"] [filename]
(response/file-response filename {:root "./static"}))
(ANY "*"  [] "<h1>Page not found.</h1>"))

Let's analyse each route in turn:

  1. (GET "/" [] (workbench))——当使用 :uri "/"处理 GET请求时,调用函数 workbench并将它返回的任何内容呈现到响应映射中。(回想一下,返回值可能是一个 map,但也可能是一个字符串等。)

  2. (POST "/save" {form-params :form-params} (str form-params))—— :form-params是由 wrap-params中间件提供的请求映射中的一个条目(回想一下,它是由 defroutes隐式包含的)。响应将是标准的 {:status 200 :headers {"Content-Type" "text/html"} :body ...},用 (str form-params)代替 ...。(一个稍微不同寻常的 POST处理程序,这个...)

  3. 例如,如果用户代理要求使用 "/test?foo=1",那么回显映射 {"foo" "1"}的字符串表示。

  4. (GET ["/:filename" :filename #".*"] [filename] ...)—— :filename #".*"部分什么也不做(因为 #".*"总是匹配)。它调用 Ring 实用程序函数 ring.util.response/file-response来产生响应; {:root "./static"}部分告诉它在哪里查找文件。

  5. (ANY "*" [] ...)——一条包罗万象的路线。Compojure 的良好实践总是在 defroutes表单的末尾包含这样的路由,以确保定义的处理程序总是返回有效的 Ring 响应映射(回想一下,路由匹配失败导致 nil)。

为什么走这条路?

Ring 中间件的目的之一是向请求映射添加信息; 因此 cookie 处理中间件向请求添加一个 :cookies键,如果存在查询字符串/表单数据,则 wrap-params添加 :query-params和/或 :form-params,等等。(严格地说,中间件函数添加的所有信息必须已经存在于请求映射中,因为这是它们传递的信息; 它们的任务是将其转换为更便于在它们包装的处理程序中使用的信息。)最终,“丰富的”请求被传递给基本处理程序,它使用中间件添加的所有经过精心预处理的信息检查请求映射,并生成响应。(中间件可以做更复杂的事情——比如包装几个“内部”处理程序并在它们之间进行选择,决定是否调用包装的处理程序等等。然而,这超出了这个答案的范围。)

反过来,基本处理程序通常(在非平凡的情况下)是一个函数,它往往只需要关于请求的少量信息项。(例如,ring.util.response/file-response不关心大部分请求; 它只需要一个文件名。)因此需要一种简单的方法来提取 Ring 请求的相关部分。Compojure 的目标是提供一种特殊用途的模式匹配引擎。

对于那些仍在努力寻找路线的人来说,可能就像我一样,你不理解解构的概念。

Actually reading let的文件 helped clear up the whole "where do the magic values come from?" question.

我正在粘贴下面的相关部分:

Clojure supports abstract structural 约束,通常被称为解构, in let binding lists, fn parameter 列表,以及任何展开为 一个 let 或 fn。基本思想是 Bind-form 可以是数据结构 包含符号的文字 的各部分 这个绑定是抽象的 that a vector literal can bind to 任何连续的东西,而 Map 值可以绑定到任何 是联想的。

Vector binding-exprs allow you to bind 连续事物部分的名称 (不仅仅是向量) ,像向量, 列表、序列、字符串、数组和 任何支持... 的东西 sequential form is a vector of 绑定窗体,它将绑定到 连续的元素 通过 n 向上查找 附加,可选,& 形式会导致 要绑定到 remainder of the sequence, i.e. that part not yet bound, looked up via 最后,也是可选的 后面跟着一个符号会导致 符号绑定到整个 Init-expr:

(let [[a b c & d :as e] [1 2 3 4 5 6 7]]
[a b c d e])
->[1 2 3 (4 5 6 7) [1 2 3 4 5 6 7]]

向量绑定-exps 允许您绑定 names to parts of sequential things (不仅仅是向量) ,像向量, 列表、序列、字符串、数组和 任何支持... 的东西 序列形式是 绑定窗体,它将绑定到 连续的元素 init-expr, looked up via nth. In 附加,可选,& 形式会导致 要绑定到 remainder of the sequence, i.e. that 部分尚未装订,通过向上看 最后,也是可选的 后面跟着一个符号会导致 符号绑定到整个 Init-expr:

(let [[a b c & d :as e] [1 2 3 4 5 6 7]]
[a b c d e])
->[1 2 3 (4 5 6 7) [1 2 3 4 5 6 7]]

解构是怎么回事({ form-params: form-params }) ? 解构时有哪些关键字可用?

可用的键是那些在输入映射中的键。解构可以在 let 和 doseq 表单中使用,也可以在 fn 或 defn 的参数中使用

希望以下代码能够提供信息:

(let [{a :thing-a
c :thing-c :as things} {:thing-a 0
:thing-b 1
:thing-c 2}]
[a c (keys things)])


=> [0 2 (:thing-b :thing-a :thing-c)]

一个更先进的例子,显示了嵌套的解体:

user> (let [{thing-id :id
{thing-color :color :as props} :properties} {:id 1
:properties {:shape
"square"
:color
0xffffff}}]
[thing-id thing-color (keys props)])
=> [1 16777215 (:color :shape)]

如果使用得当,解构可以避免样板数据访问,从而简化代码。通过使用: as 并打印结果(或结果的键) ,您可以更好地了解可以访问哪些其他数据。

Booleanknot.com上有一篇来自 James Reeves (Compojure 的作者)的优秀文章,阅读它让我“点击”,所以我在这里重新转录了一些(真的,这就是我所做的全部)。

也有一个幻灯片 来自同一个作者,它回答了这个确切的问题。

Compojure 基于 戒指,它是 http 请求的抽象。

A concise syntax for generating Ring handlers.

那么,这些 指挥官是什么:

;; Handlers are functions that define your web application.
;; They take one argument, a map representing a HTTP request,
;; and return a map representing the HTTP response.


;; Let's take a look at an example:


(defn what-is-my-ip [request]
{:status 200
:headers {"Content-Type" "text/plain"}
:body (:remote-addr request)})

很简单,但也很低级。 使用 ring/util库可以更简洁地定义上述处理程序。

(use 'ring.util.response)


(defn handler [request]
(response "Hello World"))

现在,我们希望根据请求调用不同的处理程序。 我们可以这样做一些静态路由:

(defn handler [request]
(or
(if (= (:uri request) "/a") (response "Alpha"))
(if (= (:uri request) "/b") (response "Beta"))))

然后像这样重构它:

(defn a-route [request]
(if (= (:uri request) "/a") (response "Alpha")))


(defn b-route [request]
(if (= (:uri request) "/b") (response "Beta"))))


(defn handler [request]
(or (a-route request)
(b-route request)))

The interesting thing that James notes then is that this allows nesting routes, because "the result of combining two or more routes together is itself a route".

(defn ab-routes [request]
(or (a-route request)
(b-route request)))


(defn cd-routes [request]
(or (c-route request)
(d-route request)))


(defn handler [request]
(or (ab-routes request)
(cd-routes request)))

到目前为止,我们已经开始看到一些代码,看起来可以使用宏进行因数分解。Compojure 提供了一个 defroutes宏:

(defroutes ab-routes a-route b-route)


;; is identical to


(def ab-routes (routes a-route b-route))

Compojure 提供了其他宏,比如 GET宏:

(GET "/a" [] "Alpha")


;; will expand to


(fn [request#]
(if (and (= (:request-method request#) ~http-method)
(= (:uri request#) ~uri))
(let [~bindings request#]
~@body)))

最后生成的函数看起来像我们的处理程序!

请务必检查 James Post,因为它进入了更详细的解释。