112

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

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

问题是我不理解 Compojure 的方法。让我们采用以下defroutesS 表达式:

(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. 如何从路由函数(例如workbench函数)中访问 Ring 环境?例如,假设我想访问 HTTP_ACCEPT 标头或请求/中间件的其他部分?
  2. 解构 ( {form-params :form-params}) 是怎么回事?解构时可以使用哪些关键字?

我真的很喜欢 Clojure,但我很难过!

4

5 回答 5

216

Compojure 解释(在某种程度上)

注意。我正在使用 Compojure 0.4.1(这里是 GitHub 上的 0.4.1 版本提交)。

为什么?

在 的最顶部compojure/core.clj,有对 Compojure 目的的有用总结:

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

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

  1. 请求到达并根据 Ring 规范转换为 Clojure 映射。

  2. 这个映射被汇集到一个所谓的“处理函数”中,预计会产生一个响应(这也是一个 Clojure 映射)。

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

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

如何?

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

这可能是看一个简单示例的好点:

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

让我们在 REPL 上测试一下(下面的请求图是最小的有效环请求图):

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-methodand :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隐含。

这是更复杂路线的示例:

(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"}

上面的精彩后续想法是,更复杂的路由可能会assoc在匹配阶段为请求提供额外的信息:

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

这会以 of 响应上一个:body示例"foo"中的请求。

这个最新的例子有两点是新的:"/:fst/*"和非空绑定向量[fst]。第一个是前面提到的类似 Rails 和 Sinatra 的 URI 模式语法。它比上面的例子更复杂一点,因为它支持对 URI 段的正则表达式约束(例如["/:fst/*" :fst #"[0-9]+"],可以提供以使路由只接受:fst上面的全数字值)。第二种是:params在request map中的entry上进行匹配的一种简化方式,它本身就是一个map;它对于从请求、查询字符串参数和表单参数中提取 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>"))

让我们依次分析每条路线:

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

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

  3. (GET "/test" [& more] (str "<pre> more "</pre>")){"foo" "1"}-如果用户代理要求,这将例如回显地图的字符串表示"/test?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 旨在提供一个特殊用途的模式匹配引擎,它可以做到这一点。

于 2010-08-16T03:51:04.073 回答
9

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

同一作者这里还有一个幻灯片,可以回答这个确切的问题。

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

A concise syntax for generating Ring handlers.

那么,那些Ring 处理程序是什么?从文档中提取:

;; 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)))

James 注意到的有趣的事情是,这允许嵌套路由,因为“将两个或多个路由组合在一起的结果本身就是一个路由”。

(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 的帖子,因为它有更详细的解释。

于 2014-11-29T13:40:29.190 回答
4

对于那些仍在努力找出路线发生了什么的人来说,可能像我一样,你不理解解构的概念。

实际上阅读文档let有助于清除整个“神奇值来自哪里?” 问题。

我在下面粘贴相关部分:

Clojure 在 let 绑定列表、fn 参数列表以及任何扩展为 let 或 fn 的宏中支持抽象结构绑定,通常称为解构。基本思想是绑定形式可以是包含符号的数据结构文字,这些符号绑定到 init-expr 的各个部分。绑定是抽象的,因为向量文字可以绑定到任何顺序的东西,而映射文字可以绑定到任何关联的东西。

Vector binding-exprs 允许您将名称绑定到顺序事物的一部分(不仅仅是向量),例如向量、列表、序列、字符串、数组以及任何支持 nth 的东西。基本的顺序形式是绑定形式的向量,它将绑定到 init-expr 中的连续元素,通过 nth 查找。此外,可选地, & 后跟绑定形式将导致绑定形式绑定到序列的其余部分,即尚未绑定的部分,通过 nthnext 查找。最后,也是可选的,:as 后跟一个符号将导致该符号绑定到整个 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]]

Vector binding-exprs 允许您将名称绑定到顺序事物的一部分(不仅仅是向量),例如向量、列表、序列、字符串、数组以及任何支持 nth 的东西。基本的顺序形式是绑定形式的向量,它将绑定到 init-expr 中的连续元素,通过 nth 查找。此外,可选地, & 后跟绑定形式将导致绑定形式绑定到序列的其余部分,即尚未绑定的部分,通过 nthnext 查找。最后,也是可选的,:as 后跟一个符号将导致该符号绑定到整个 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]]
于 2010-11-29T17:36:10.507 回答
3

我还没有开始使用 clojure 网络的东西,但是,我会的,这是我收藏的东西。

于 2010-08-16T00:39:46.170 回答
1

解构({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 并打印结果(或结果的键),您可以更好地了解您可以访问哪些其他数据。

于 2013-04-23T18:50:16.437 回答