组合路线背后的"大创意"是什么?

Sea*_*ods 107 clojure compojure

我是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>"))
Run Code Online (Sandbox Code Playgroud)

我知道理解所有这些的关键在于一些宏观伏都教,但我还没有完全理解宏(还).我已经盯着defroutes源头很长一段时间了,但就是不明白!这里发生了什么?理解"大创意"可能会帮助我回答这些具体问题:

  1. 如何从路由功能(例如workbench功能)中访问Ring环境?例如,假设我想访问HTTP_ACCEPT标头或请求/中间件的其他部分?
  2. 什么是解构({form-params :form-params})?在解构时我可以使用哪些关键字?

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

Mic*_*zyk 209

Compojure解释(在某种程度上)

NB.我正在使用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)需要产生有效Ring响应的表达式主体(在非平凡的情况下,这通常只是对单独函数的调用).

这可能是一个很好的观察一个简单的例子:

(def example-route (GET "/" [] "<html>...</html>"))
Run Code Online (Sandbox Code Playgroud)

让我们在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>"}
Run Code Online (Sandbox Code Playgroud)

如果:request-method:head,反应将是nil.我们将nil在一分钟内回到这里意味着什么的问题(但请注意它不是一个有效的Ring respose!).

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

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

我们defroutes现在尝试使用:

(defroutes example-routes
  (GET "/" [] "get")
  (HEAD "/" [] "head"))
Run Code Online (Sandbox Code Playgroud)

上面显示的示例请求及其变体的响应与: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)))
Run Code Online (Sandbox Code Playgroud)

注意解构形式代替先前使用的空向量.这里的基本思想是路线的主体可能对有关请求的一些信息感兴趣; 因为它总是以地图的形式到达,所以可以提供关联解构形式以从请求中提取信息并将其绑定到将在路线主体范围内的局部变量.

以上测试:

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"}
Run Code Online (Sandbox Code Playgroud)

以上的出色后续想法是,更复杂的路线可能会assoc在匹配阶段向请求提供额外信息:

(def echo-first-path-component-route
  (GET "/:fst/*" [fst] fst))
Run Code Online (Sandbox Code Playgroud)

这与响应: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\"}"}
Run Code Online (Sandbox Code Playgroud)

现在是查看问题文本示例的好时机:

(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>"))
Run Code Online (Sandbox Code Playgroud)

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

  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表单的末尾包含这样的路由,以确保定义的处理程序始终返回有效的响应响应映射(回想一下路由匹配失败导致nil).

为什么这样?

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

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

  • "作为一个额外的便利,defroutes定义的处理程序隐含地包裹在wrap-params和wrap-cookies中." - 从版本0.6.0开始,您必须明确添加这些内容.参考https://github.com/weavejester/compojure/commit/73a2eed60dd6230b80ba608806916e66d4c99eb7#README.md (3认同)
  • 非常好.这个答案应该在Compojure的主页上. (3认同)
  • 对Compojure新手的必读内容.我希望每个关于该主题的wiki和博客文章都以此链接开头. (2认同)

nha*_*nha 7

来自James Reeves(Compojure的作者)的booleanknot.com上有一篇很好的文章,阅读它让我"点击"了,所以我在这里重新翻译了一些(真的就是我所做的一切).

同一位作者还有一个幻灯片,可以回答这个问题.

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

A concise syntax for generating Ring handlers.
Run Code Online (Sandbox Code Playgroud)

那么,那些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)})
Run Code Online (Sandbox Code Playgroud)

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

(use 'ring.util.response)

(defn handler [request]
  (response "Hello World"))
Run Code Online (Sandbox Code Playgroud)

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

(defn handler [request]
  (or
    (if (= (:uri request) "/a") (response "Alpha"))
    (if (= (:uri request) "/b") (response "Beta"))))
Run Code Online (Sandbox Code Playgroud)

并重构它像这样:

(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)))
Run Code Online (Sandbox Code Playgroud)

詹姆斯当时注意到的有趣的事情是,这允许嵌套路线,因为"​​将两条或更多条路线组合在一起的结果本身就是一条路线".

(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)))
Run Code Online (Sandbox Code Playgroud)

到目前为止,我们开始看到一些看起来像是可以使用宏的因素.Compojure提供了一个defroutes宏:

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

;; is identical to

(def ab-routes (routes a-route b-route))
Run Code Online (Sandbox Code Playgroud)

Compojure提供其他宏,如GET宏:

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

;; will expand to

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

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

请务必查看詹姆斯的帖子,因为它会有更详细的解释.