复合路线背后的“大创意”是什么?


109

我是Clojure的新手,并且一直在使用Compojure编写基本的Web应用程序。不过,我正在用Compojure的defroutes语法碰壁,我想我需要同时理解所有背后的“如何”和“为什么”。

环形应用程序似乎以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,但是我很沮丧!

Answers:


212

Compojure的解释(一定程度上)

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

为什么?

在的最上方compojure/core.clj,是Compojure目的的有用摘要:

生成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)需要产生有效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相反,他们的回应是nilnil一分钟后,我们将回到这里是什么意思的问题(但请注意,这不是有效的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-defined处理程序被包裹在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))

这与响应:body"foo",从前面的例子请求。

关于此最新示例,有两件事是新的:"/: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>"))

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

  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 "*" [] ...)-全面路线。最好在defroutes表单末尾包含这样的路由,以确保定义的处理程序始终返回有效的Ring响应图(请注意,路由匹配失败会导致nil),这是Compojure的良好做法。

为什么这样呢?

Ring中间件的目的之一是向请求映射中添加信息。因此cookie处理中间件:cookies向请求添加了密钥,wrap-params添加:query-params和/或:form-params如果存在查询字符串/表单数据,等等。(严格来说,中间件功能要添加的所有信息必须已经存在于请求映射中,因为这是它们传递的内容;它们的工作是将其转换为使其在包装的处理程序中使用更方便。)最终,“丰富的”请求被传递到基本处理程序,该处理程序检查请求映射以及由中间件添加的所有经过很好预处理的信息,并产生响应。(中间件可以做的比这更复杂的事情-例如包装多个“内部”处理程序并在它们之间进行选择,确定是否完全调用被包装的处理程序等。但是,这不在此答案的范围内。)

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


3
“为增加便利,defroutes定义的处理程序被隐式包装在wrap-params和wrap-cookie中。” -从0.6.0版开始,您必须明确添加这些内容。编号github.com/weavejester/compojure/commit/…–
Dan Midwood

3
非常好。该答案应该在Compojure的主页上。
Siddhartha Reddy '02

2
必须阅读Compojure的任何新手。我希望有关该主题的所有Wiki和博客文章都以指向该主题的链接开头。
jemmons

7

James Reeves(Compojure的作者)在booleanknot.com上有一篇出色的文章,对它的阅读对我来说是“单击”的,所以我在这里重新转录了其中的一部分(真的就是我所做的一切)。

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

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


4

对于仍在努力寻找路线进展的任何人,可能像我一样,您也不了解解构的想法。

实际阅读文档let有助于弄清整个“魔术价值从何而来?” 题。

我要粘贴以下相关部分:

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

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

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

3

谢谢,这些链接绝对有帮助。我今天大部分时间都在研究这个问题,并且处在一个更好的位置……我将尝试在某个时候发布后续报告。
肖恩·伍兹

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.