【问题标题】:What's the "big idea" behind compojure routes?compojure 路线背后的“大创意”是什么?
【发布时间】:2011-03-30 03:16:48
【问题描述】:

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

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

问题是我不理解 Compojure 的方法。让我们采取以下defroutes S-表达式:

(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,但我很难过!

【问题讨论】:

    标签: clojure compojure


    【解决方案1】:

    【讨论】:

    • 谢谢,这些链接肯定很有帮助。我在一天的大部分时间里一直在解决这个问题,并且在一个更好的地方......我会尝试在某个时候发布后续跟进。
    【解决方案2】:

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

    还有一个幻灯片here from the same author,可以回答这个确切的问题。

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

    A concise syntax for generating Ring handlers.
    

    那么,那些 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)))
    

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

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

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

        实际上阅读the docs for let有助于理清整个“魔法值从何而来?”问题。

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

        Clojure 支持抽象结构 绑定,通常称为解构, 在 let 绑定列表中,fn 参数 列表,以及任何扩展为 let 或 fn。基本思想是 binding-form 可以是数据结构 包含得到的符号的文字 绑定到相应的部分 初始化表达式。绑定是抽象的 向量文字可以绑定到 任何顺序的,而 映射文字可以绑定到任何 是关联的。

        Vector binding-exprs 允许你绑定 顺序事物的一部分的名称 (不仅仅是向量),像向量, 列表、序列、字符串、数组和 任何支持 nth 的东西。基础的 顺序形式是一个向量 绑定形式,将绑定到 连续的元素 init-expr,通过 nth 查找。在 加法和可选的 & 跟在后面 通过绑定形式将导致 binding-form 被绑定到 序列的其余部分,即 部分尚未绑定,通过查找 下一个。最后,也是可选的,:as 后跟一个符号会导致 符号绑定到整个 初始化表达式:

        (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 查找。在 加法和可选的 & 跟在后面 通过绑定形式将导致 binding-form 被绑定到 序列的其余部分,即 部分尚未绑定,通过查找 下一个。最后,也是可选的,:as 后跟一个符号会导致 符号绑定到整个 初始化表达式:

        (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]]
        

        【讨论】:

          【解决方案5】:

          Compojure 解释(在某种程度上)

          注意。我正在使用 Compojure 0.4.1(here 是 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-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 中。

          下面是一个更复杂的路线示例:

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

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

          这个最新的例子有两点是新的:"/:fst/*" 和非空绑定向量[fst]。第一个是前面提到的类似 Rails 和 Sinatra 的 URI 模式语法。它比上面示例中明显的要复杂一些,因为支持对 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)) -- 使用:uri "/" 处理GET 请求时,调用函数workbench 并将它返回的任何内容渲染到响应映射中。 (回想一下,返回值可能是一个映射,也可能是一个字符串等)

          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. (GET "/test" [&amp; more] (str "&lt;pre&gt; more "&lt;/pre&gt;")) -- 例如如果用户代理请求"/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 不关心大部分请求;它只需要一个文件名。)因此需要一种简单的方法来提取环请求的相关部分。 Compojure 旨在提供一个特殊用途的模式匹配引擎,它可以做到这一点。

          【讨论】:

          • “为了方便起见,defroutes 定义的处理程序隐式地包装在 wrap-params 和 wrap-cookies 中。” - 从 0.6.0 版开始,您必须明确添加这些内容。参考github.com/weavejester/compojure/commit/…
          • 说得很好。这个答案应该在 Compojure 的主页上。
          • Compojure 新手必读。我希望每篇关于该主题的 wiki 和博客文章都以指向此主题的链接开头。
          • 当您讨论“catch-all”路由时,您的意思是在有多个路由匹配请求的情况下,有某种优先级顺序来决定选择哪个路由。这是纯粹根据路由排序完成的,还是有一些规则来确定哪个是“最佳”匹配?
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-01-09
          • 2012-06-13
          • 1970-01-01
          相关资源
          最近更新 更多