Compojure 解释(在某种程度上)
注意。我正在使用 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
在一分钟内回到这里意味着什么的问题(但请注意,它不是有效的 Ring 响应!)。
从这个例子中可以明显看出,example-route
它只是一个函数,而且是一个非常简单的函数;它查看请求,确定是否有兴趣处理它(通过检查:request-method
and :uri
),如果是,则返回基本响应映射。
同样明显的是,路线的主体并不真正需要评估为适当的响应图;Compojure 为字符串(如上所示)和许多其他对象类型提供了合理的默认处理;有关详细信息,请参阅compojure.response/render
多方法(此处的代码完全是自记录的)。
让我们现在尝试使用defroutes
:
(defroutes example-routes
(GET "/" [] "get")
(HEAD "/" [] "head"))
对上面显示的示例请求及其变体的响应与:request-method :head
预期的一样。
的内部工作原理example-routes
是依次尝试每条路线;一旦其中一个返回非nil
响应,该响应就成为整个example-routes
处理程序的返回值。作为一个额外的便利, -defroutes
定义的处理程序被包装wrap-params
并wrap-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>"))
让我们依次分析每条路线:
(GET "/" [] (workbench))
-- 处理带有 的GET
请求时:uri "/"
,调用该函数workbench
并将它返回的任何内容呈现到响应映射中。(回想一下,返回值可能是一个映射,也可能是一个字符串等)
(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
处理程序,这个......)
(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 旨在提供一个特殊用途的模式匹配引擎,它可以做到这一点。