什么是“大创意"?在 compoju 路线后面? [英] What's the "big idea" behind compojure routes?

查看:16
本文介绍了什么是“大创意"?在 compoju 路线后面?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我是 Clojure 的新手,一直在使用 Compojure 编写一个基本的 Web 应用程序.不过,我遇到了 Compojure 的 defroutes 语法,我想我需要了解这一切背后的如何"和为什么".

I'm new to Clojure and have been using Compojure to write a basic web application. I'm hitting a wall with Compojure's defroutes syntax, though, and I think I need to understand both the "how" and the "why" behind it all.

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

It seems like a Ring-style application begins with an HTTP request map, then just passes the request through a series of middleware functions until it gets transformed into a response map, which gets sent back to the browser. This style seems too "low level" for developers, thus the need for a tool like Compojure. I can see this need for more abstractions in other software ecosystems as well, most notably with Python's WSGI.

问题是我不理解 Compojure 的方法.让我们采用以下 defroutes S 表达式:

The problem is that I don't understand Compojure's approach. Let's take the following 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 源代码看了很长时间,但就是不明白!这里发生了什么?了解大创意"可能会帮助我回答这些具体问题:

I know that the key to understanding all of this lies within some macro voodoo, but I don't totally understand macros (yet). I've stared at the defroutes source for a long time, but just don't get it! What's going on here? Understanding the "big idea" will probably help me answer these specific questions:

  1. 如何从路由函数(例如 workbench 函数)中访问 Ring 环境?例如,假设我想访问 HTTP_ACCEPT 标头或请求/中间件的其他部分?
  2. 解构 ({form-params :form-params}) 是怎么回事?解构时我可以使用哪些关键字?
  1. How do I access the Ring environment from within a routed function (e.g. the workbench function)? For example, say I wanted to access the HTTP_ACCEPT headers or some other part of the request/middleware?
  2. What's the deal with the destructuring ({form-params :form-params})? What keywords are available for me when destructuring?

我真的很喜欢 Clojure,但我太难了!

I really like Clojure but I am so stumped!

推荐答案

Compojure 解释(在某种程度上)

注意.我正在使用 Compojure 0.4.1(这里是 0.4.1 版本的提交)GitHub).

Compojure explained (to some degree)

NB. I am working with Compojure 0.4.1 (here's the 0.4.1 release commit on GitHub).

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

At the very top of compojure/core.clj, there's this helpful summary of Compojure's purpose:

用于生成 Ring 处理程序的简洁语法.

A concise syntax for generating Ring handlers.

从表面上看,这就是为什么"问题的全部内容.为了更深入一点,让我们来看看 Ring 风格的应用程序是如何运作的:

On a superficial level, that's all there is to the "why" question. To go a bit deeper, let's have a look at how a Ring-style app functions:

  1. 请求到达并根据 Ring 规范转换为 Clojure 映射.

  1. A request arrives and is transformed into a Clojure map in accordance with the Ring spec.

这个映射被汇集到一个所谓的处理函数"中,它预计会产生一个响应(这也是一个 Clojure 映射).

This map is funnelled into a so-called "handler function", which is expected to produce a response (which is also a Clojure map).

响应映射被转换为实际的 HTTP 响应并发回客户端.

The response map is transformed into an actual HTTP response and sent back to the client.

上面的第 2 步是最有趣的,因为检查请求中使用的 URI、检查任何 cookie 等并最终到达适当的响应是处理程序的责任.显然,有必要将所有这些工作分解为一组定义明确的作品;这些通常是基本"处理程序函数和包装它的中间件函数的集合.Compojure 的目的是简化基本处理程序函数的生成.

Step 2. in the above is the most interesting, as it is the handler's responsibility to examine the URI used in the request, examine any cookies etc. and ultimately arrive at an appropriate response. Clearly it is necessary that all this work be factored into a collection of well-defined pieces; these are normally a "base" handler function and a collection of middleware functions wrapping it. Compojure's purpose is to simplify the generation of the base handler function.

Compojure 是围绕路线"的概念构建的.这些实际上是由 Clout 库(Compojure 项目的衍生——很多东西)在更深层次上实现的在 0.3.x -> 0.4.x 转换时移至单独的库).路由由 (1) HTTP 方法 (GET、PUT、HEAD...)、(2) URI 模式(使用 Webby Rubyists 显然熟悉的语法指定)、(3) 中使用的解构形式定义将请求映射到正文中可用的名称的绑定部分,(4) 需要生成有效 Ring 响应的表达式正文(在非平凡的情况下,这通常只是对单独函数的调用).

Compojure is built around the notion of "routes". These are actually implemented at a deeper level by the Clout library (a spinoff of the Compojure project -- many things were moved to separate libraries at the 0.3.x -> 0.4.x transition). A route is defined by (1) an HTTP method (GET, PUT, HEAD...), (2) a URI pattern (specified with syntax which will apparently be familiar to Webby Rubyists), (3) a destructuring form used in binding parts of the request map to names available in the body, (4) a body of expressions which needs to produce a valid Ring response (in non-trivial cases this is usually just a call to a separate function).

这可能是看一个简单示例的好点:

This might be a good point to have a look at a simple example:

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

让我们在 REPL 上测试一下(下面的请求映射是最小的有效 Ring 请求映射):

Let's test this at the REPL (the request map below is the minimal valid Ring request map):

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!).

If :request-method were :head instead, the response would be nil. We'll return to the question of what nil means here in a minute (but notice that it is not a valid Ring respose!).

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

As is apparent from this example, example-route is just a function, and a very simple one at that; it looks at the request, determines whether it's interested in handling it (by examining :request-method and :uri) and, if so, returns a basic response map.

同样明显的是,路由的主体并不真正需要评估为正确的响应图;Compojure 为字符串(如上所示)和许多其他对象类型提供了合理的默认处理;有关详细信息,请参阅 compojure.response/render 多方法(此处的代码完全是自文档化的).

What is also apparent is that the body of the route does not really need to evaluate to a proper response map; Compojure provides sane default handling for strings (as seen above) and a number of other object types; see the compojure.response/render multimethod for details (the code is entirely self-documenting here).

让我们现在尝试使用 defroutes:

Let's try using defroutes now:

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

对上面显示的示例请求及其带有 :request-method :head 的变体的响应与预期的一样.

The responses to the example request displayed above and to its variant with :request-method :head are like expected.

example-routes 的内部工作原理是依次尝试每条路线;一旦其中一个返回非 nil 响应,该响应就会成为整个 example-routes 处理程序的返回值.为了增加便利,defroutes 定义的处理程序被隐式包装在 wrap-paramswrap-cookies 中.

The inner workings of example-routes are such that each route is tried in turn; as soon as one of them returns a non-nil response, that response becomes the return value of the whole example-routes handler. As an added convenience, defroutes-defined handlers are wrapped in wrap-params and wrap-cookies implicitly.

这是一个更复杂的路线的例子:

Here's an example of a more complex route:

(def echo-typed-url-route
  (GET "*" {:keys [scheme server-name server-port uri]}
    (str (name scheme) "://" server-name ":" server-port uri)))

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

Note the destructuring form in place of the previously used empty vector. The basic idea here is that the body of the route might be interested in some information about the request; since this always arrives in the form of a map, an associative destructuring form can be supplied to extract information from the request and bind it to local variables which will be in scope in the route's body.

以上测试:

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附加到请求中:

The brilliant follow-up idea to the above is that more complex routes may assoc extra information onto the request at the matching stage:

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

这会以 "foo":body 响应上一示例中的请求.

This responds with a :body of "foo" to the request from the previous example.

这个最新示例有两个新内容:"/:fst/*" 和非空绑定向量 [fst].第一个是前面提到的类似 Rails-and-Sinatra 的 URI 模式语法.它比上面例子中明显的更复杂一点,因为它支持对 URI 段的正则表达式约束(例如 ["/:fst/*" :fst #"[0-9]+"] 可以使路由只接受上述 :fst 的全数字值).第二种是在请求映射中的 :params 条目上进行匹配的简化方式,它本身就是一个映射;它对于从请求、查询字符串参数和表单参数中提取 URI 段很有用.一个例子来说明后一点:

Two things are new about this latest example: the "/:fst/*" and the non-empty binding vector [fst]. The first is the aforementioned Rails-and-Sinatra-like syntax for URI patterns. It's a bit more sophisticated than what is apparent from the example above in that regex constraints on URI segments are supported (e.g. ["/:fst/*" :fst #"[0-9]+"] can be supplied to make the route accept only all-digit values of :fst in the above). The second is a simplified way of matching on the :params entry in the request map, which is itself a map; it's useful for extracting URI segments from the request, query string parameters and form parameters. An example to illustrate the latter point:

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

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

This would be a good time to have a look at the example from the question text:

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

我们依次分析每条路线:

Let's analyse each route in turn:

  1. (GET "/" [] (workbench)) -- 当处理带有 :uri "/" 的 GET 请求时code>,调用函数 workbench 并将它返回的任何内容渲染到响应映射中.(回想一下,返回值可能是地图,也可能是字符串等)

  1. (GET "/" [] (workbench)) -- when dealing with a GET request with :uri "/", call the function workbench and render whatever it returns into a response map. (Recall that the return value might be a map, but also a string etc.)

(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 处理程序,这个...)

(POST "/save" {form-params :form-params} (str form-params)) -- :form-params is an entry in the request map provided by the wrap-params middleware (recall that it is implicitly included by defroutes). The response will be the standard {:status 200 :headers {"Content-Type" "text/html"} :body ...} with (str form-params) substituted for .... (A slightly unusual POST handler, this...)

(GET "/test" [& more] (str "<pre> more "</pre>")) -- 这将例如回显字符串如果用户代理要求 "/test?foo=1",则表示地图 {"foo" "1"}.

(GET "/test" [& more] (str "<pre> more "</pre>")) -- this would e.g. echo back the string representation of the map {"foo" "1"} if the user agent asked for "/test?foo=1".

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

(GET ["/:filename" :filename #".*"] [filename] ...) -- the :filename #".*" part does nothing at all (since #".*" always matches). It calls the Ring utility function ring.util.response/file-response to produce its response; the {:root "./static"} part tells it where to look for the file.

(ANY "*" [] ...) -- 一个包罗万象的路线.好的 Compojure 实践总是在 defroutes 表单的末尾包含这样的路由,以确保定义的处理程序始终返回有效的 Ring 响应映射(回想一下路由匹配失败导致 nil).

(ANY "*" [] ...) -- a catch-all route. It is good Compojure practice always to include such a route at the end of a defroutes form to ensure that the handler being defined always returns a valid Ring response map (recall that a route matching failure results in nil).

为什么要这样?

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

Why this way?

One purpose of the Ring middleware is to add information to the request map; thus cookie-handling middleware adds a :cookies key to the request, wrap-params adds :query-params and/or :form-params if a query string / form data is present and so on. (Strictly speaking, all the information the middleware functions are adding must be already present in the request map, since that is what they get passed; their job is to transform it to be it more convenient to work with in the handlers they wrap.) Ultimately the "enriched" request is passed to the base handler, which examines the request map with all the nicely preprocessed information added by the middleware and produces a response. (Middleware can do more complex things than that -- like wrapping several "inner" handlers and choosing between them, deciding whether to call the wrapped handler(s) at all etc. That is, however, outside the scope of this answer.)

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

The base handler, in turn, is usually (in non-trivial cases) a function which tends to need just a handful of items of information about the request. (E.g. ring.util.response/file-response doesn't care about most of the request; it only needs a filename.) Hence the need for a simple way of extracting just the relevant parts of a Ring request. Compojure aims to provide a special-purpose pattern matching engine, as it were, which does just that.

这篇关于什么是“大创意"?在 compoju 路线后面?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆