Compojure 解释(在某种程度上)
注意。我正在使用 Compojure 0.4.1(here 是 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 和 :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))
这会以"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>"))
让我们依次分析每条路线:
(GET "/" [] (workbench)) -- 使用:uri "/" 处理GET 请求时,调用函数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>")) -- 例如如果用户代理请求"/test?foo=1",则回显映射的字符串表示{"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 不关心大部分请求;它只需要一个文件名。)因此需要一种简单的方法来提取环请求的相关部分。 Compojure 旨在提供一个特殊用途的模式匹配引擎,它可以做到这一点。