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

我是Clojure的新手,已经使用Compojure编写基本的Web应用程序。 不过,我用Compojure的defroutes语法打了一堵墙,而且我认为我需要了解它背后的“如何”和“为什么”。

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

问题是我不了解Compojure的方法。 让我们来看看下面的defroutes Sexpression式:

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

我知道理解这一切的关键在于一些macros观的巫术,但我不完全理解macros(还)。 我已经盯了很久了,但不明白! 这里发生了什么? 了解“大创意”可能会帮助我回答这些具体问题:

  1. 如何从路由function(例如workbenchfunction)中访问Ring环境? 例如,假设我想要访问HTTP_ACCEPT头或请求/中间件的其他部分?
  2. 与解构有什么关系( {form-params :form-params} )? 什么关键字可供我解构时使用?

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

Compojure解释(在某种程度上)

NB。 我正在使用Compojure 0.4.1( 这里是GitHub的0.4.1发行版)。

为什么?

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

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

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

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

  2. 这个映射被集成到所谓的“处理函数”中,这个函数有望产生一个响应(这也是一个Clojure映射)。

  3. 响应映射被转换成一个实际的HTTP响应并发送回客户端。

上面的第2步是最有趣的,因为处理程序有责任检查请求中使用的URI,检查任何cookie等,最终得到适当的响应。 显然,所有这些工作都必须被分解成一系列明确的部分。 这些通常是一个“基本”处理函数和包含它的中间件函数的集合。 Compojure的目的是简化基础处理函数的生成。

怎么样?

Compojure围绕“路线”的概念而build立。 这些实际上是通过Clout库(Compojure项目的一个分支 – 在0.3.x – > 0.4.x的过渡中将许多东西移动到不同的库)在更深层次上实现的。 一个路由由(1)一个HTTP方法(GET,PUT,HEAD …),(2)一个URI模式(使用语法来指定,这个模式显然是Webby Rubyists所熟悉的),(3)将请求映射的部分绑定到正文中的可用名称;(4)需要产生有效响应的expression式体(在非平凡的情况下,这通常只是对单独函数的调用)。

看一个简单的例子可能是一个很好的观点:

 (def example-route (GET "/" [] "<html>...</html>")) 

让我们在REPL中testing这个(下面的请求映射是最小的有效响铃请求映射):

 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 Respose!)。

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

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

现在尝试使用defroutes

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

上面显示的对示例请求的响应以及对其变体的:request-method :head预期的那样。

example-routes的内部工作是这样的,每个路线依次尝试; 只要其中一个返回非零响应,该响应就成为整个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))) 

注意解构forms代替之前使用的空向量。 这里的基本思想是,路由的主体可能会对请求的某些信息感兴趣; 因为这总是以地图的forms到达,所以可以提供关联解构forms来从请求中提取信息,并将其绑定到局部variables,这些局部variables将在path的主体范围内。

以上的testing:

 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"} 

上述的辉煌后续的想法是,更复杂的路线可能在匹配阶段将额外的信息结合到请求上:

 (def echo-first-path-component-route (GET "/:fst/*" [fst] fst)) 

这对上一个例子的请求使用"foo" :body进行响应。

这个最新的例子有两个新东西: "/:fst/*"和非空绑定向量[fst] 。 第一种是URI模式的Rails-and-Sinatra-like语法。 它比上面例子中显而易见的要复杂得多,因为支持URI段的正则expression式约束(例如["/:fst/*" :fst #"[0-9]+"]可以被提供来使路线只接受上面的全部数字值:fst )。 第二种方法是在请求映射中的:params条目上进行匹配的简化方法,该映射本身就是一个映射; 从请求中提取URI段,查询string参数和表单参数非常有用。 举一个例子来说明后一点:

 (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并将返回的任何内容渲染到响应映射中。 (回想一下,返回值可能是一个地图,但也是一个string等)

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

  3. (GET "/test" [& more] (str "<pre> more "</pre>")) – 例如,如果用户代理回显地图{"foo" "1"}的string表示要求"/test?foo=1"

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

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

为什么这样?

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

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

James Reeves(Compojure的作者)的booleanknot.com网站上有一篇很棒的文章,并且阅读它让我“点击了”,所以我在这里重新翻译了一些(真的就是我所做的)。

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

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

 A concise syntax for generating Ring handlers. 

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

很简单,但也相当低级。 上面的处理程序可以使用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提供了一个defroutesmacros:

 (defroutes ab-routes a-route b-route) ;; is identical to (def ab-routes (routes a-route b-route)) 

Compojure提供了其他的macros,比如GETmacros:

 (GET "/a" [] "Alpha") ;; will expand to (fn [request#] (if (and (= (:request-method request#) ~http-method) (= (:uri request#) ~uri)) (let [~bindings request#] ~@body))) 

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

请务必查看詹姆斯的post ,因为它更详细的解释。

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

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

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

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

Vector binding-exprs允许您将名称绑定到顺序事物的部分(而不仅仅是向量),如向量,列表,seqs,string,数组以及任何支持nth的东西。 基本的顺序forms是绑定forms的向量,绑定forms将绑定到init-expr中的连续元素,通过第n个查找。 此外,并且可选地,随后是绑定forms将导致绑定forms被绑定到序列的剩余部分,即尚未绑定的那部分,通过nthnext查找。 最后也是可选的:如果后面跟着一个符号,则会将该符号绑定到整个init-expr:

 (let [[abc & d :as e] [1 2 3 4 5 6 7]] [abcde]) ->[1 2 3 (4 5 6 7) [1 2 3 4 5 6 7]] 

Vector binding-exprs允许您将名称绑定到顺序事物的部分(而不仅仅是向量),如向量,列表,seqs,string,数组以及任何支持nth的东西。 基本的顺序forms是绑定forms的向量,绑定forms将绑定到init-expr中的连续元素,通过第n个查找。 此外,并且可选地,随后是绑定forms将导致绑定forms被绑定到序列的剩余部分,即尚未绑定的那部分,通过nthnext查找。 最后也是可选的,如果后面跟着一个符号,则会将该符号绑定到整个init-expr:

 (let [[abc & d :as e] [1 2 3 4 5 6 7]] [abcde]) ->[1 2 3 (4 5 6 7) [1 2 3 4 5 6 7]] 

与解构有什么关系({form-params:form-params})? 什么关键字可供我解构时使用?

可用的键是input映射中的键。 解构可以在let和doseqforms里面,或者在fn或defn的参数里面

下面的代码将有希望提供信息:

 (let [{a :thing-a c :thing-c :as things} {:thing-a 0 :thing-b 1 :thing-c 2}] [ac (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和打印结果(或结果的键),您可以更好地了解可以访问的其他数据。