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

我是Clojure的新手,并且一直在使用Compojure编写基本的Web应用程序。 尽管如此,我用Compojure的defroutes语法打了一堵墙,我认为我需要了解它背后的“如何”和“为什么”。

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

问题是我不了解Compojure的方法。 我们来看看下面的defroutes S-expression:

(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来源很长一段时间了,但不明白! 这里发生了什么? 理解“大创意”可能会帮助我回答这些具体问题:

  • 如何从路由功能(例如workbench功能)中访问Ring环境? 例如,假设我想访问HTTP_ACCEPT头或请求/中间件的其他部分?
  • 与解构有什么关系( {form-params :form-params} )? 解构时可用的关键字是什么?
  • 我非常喜欢Clojure,但我很难过!


    Compojure解释说(在某种程度上)

    NB。 我正在使用Compojure 0.4.1(这里是GitHub上的0.4.1发布提交)。

    为什么?

    compojure/core.clj顶部,Compojure的目的是这个有用的总结:

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

    从表面上看,这就是“为什么”的问题。 要深入一点,让我们来看看Ring风格的应用程序的功能:

  • 请求到达并根据Ring规范转换为Clojure地图。

  • 这张地图被集成到一个所谓的“处理函数”中,这个函数预计会产生一个响应(这也是一个Clojure地图)。

  • 响应映射被转换为实际的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意义的问题(但请注意,这不是一个有效的戒指!)。

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

    同样显而易见的是,路线的主体并不需要评估一个适当的响应地图; Compojure提供了对字符串(如上所示)和许多其他对象类型的理性默认处理; 有关详细信息,请参阅compojure.response/render multimethod(代码完全自行记录在此处)。

    现在尝试使用defroutes

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

    上面显示的对示例请求的响应以及其变体with :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] 。 第一种是URI模式的上述Rails-and-Sinatra-like语法。 它比上面例子中显而易见的更复杂一点,它支持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>"))
    

    我们依次分析每条路线:

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

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

  • (GET "/test" [& more] (str "<pre> more "</pre>")) - 例如,如果用户代理回显了映射{"foo" "1"}的字符串表示形式要求"/test?foo=1"

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

  • (ANY "*" [] ...) - 一条全路径。 Compojure的做法总是在defroutes表单末尾包含这样一个路由,以确保定义的处理程序总是返回一个有效的Ring响应图(回想一下匹配失败的路由结果nil )。

  • 为什么这样?

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

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


    James Reeves(Compojure的作者)在booleanknot.com上发表了一篇优秀的文章,并且阅读它为我“点击”,所以我在这里重新编写了一些内容(这些都是我所做的)。

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

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

    A concise syntax for generating Ring handlers.
    

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

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

    詹姆斯注意到的有趣之处在于,这允许嵌套路线,因为“​​将两条或更多条路线组合在一起的结果本身就是一条路线”。

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


    对于那些仍然在努力研究路线上发生的事情的人来说,可能就像我一样,你不理解解构的想法。

    实际上阅读文档let澄清了整个“魔法值从何而来?” 题。

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

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

    向量绑定表达式允许您将名称绑定到顺序事物的某些部分(而不仅仅是向量),如向量,列表,序列,字符串,数组以及任何支持nth的东西。 基本的顺序形式是绑定形式的向量,绑定形式将绑定到init-expr中的连续元素,通过第n个查找。 另外,可选地,紧接着绑定形式将导致绑定形式被绑定到序列的其余部分,即尚未绑定的那部分,通过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]]
    

    向量绑定表达式允许您将名称绑定到顺序事物的某些部分(而不仅仅是向量),如向量,列表,序列,字符串,数组以及任何支持nth的东西。 基本的顺序形式是绑定形式的向量,绑定形式将绑定到init-expr中的连续元素,通过第n个查找。 另外,可选地,紧接着绑定形式将导致绑定形式被绑定到序列的其余部分,即尚未绑定的那部分,通过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]]
    
    链接地址: http://www.djcxy.com/p/56969.html

    上一篇: What's the "big idea" behind compojure routes?

    下一篇: Which version of PostgreSQL am I running?