什么是“大主意”后面compojure路由? [英] What's the "big idea" behind compojure routes?

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

问题描述

我是Clojure的新手,并且一直在使用Compojure编写一个基本的Web应用程序。我用Compojure的 defroutes 语法击中了一堵墙,但我想我需要了解如何和为什么。 >

看起来像一个Ring样式应用程序从HTTP请求映射开始,然后只是通过一系列中间件函数传递请求,直到它转换为响应映射,发送返回浏览器。这种风格似乎太低级的开发人员,因此需要一个工具,如Compojure。我可以看到这种需要在其他软件生态系统中的更多的抽象,最值得注意的是使用Python的WSGI。



问题是我不明白Compojure的方法。让我们采取以下 defroutes S表达式:

  route 
(GET/[](workbench))
(POST/ save{form-params:form-params}测试[& more](str< pre>more< / pre>))
(GET [/:filename:filename#。* $ b(response / file-response filename {:root./static}))
(ANY*[]< h1>页面未找到< / h1>))

我知道理解所有这些的关键在于一些宏观的巫术,但我不完全理解宏(还)。我盯着 defroutes 源很长一段时间,但只是不能得到它!这里发生了什么?了解大想法可能会帮助我回答这些具体问题:


  1. 如何从路由功能例如 workbench 函数)?例如,假设我想访问HTTP_ACCEPT头部或请求/中间件的其他部分?

  2. 解构的处理是什么( {form- params: form-params} )?


  3. 我真的很喜欢Clojure,但我很累!

    解决方案

    Compojure解释(在一定程度上)



    我正在使用Compojure 0.4.1(这里是0.4.1版本提交在GitHub上)。



    为什么?



    > compojure / core.clj ,这里有对Compojure目的的有用总结:


    处理程序。


    在一个表面层面,这就是为什么的问题。为了更深入一点,让我们看看一个Ring样式的应用程序的功能:


    1. 请求到达并被转换


    2. 这个地图被导入一个所谓的处理函数,这个函数预期产生一个响应(它也是一个Clojure映射)。


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

      li>

    步骤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>))

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

      user> (example-route {:server-port 80 
    :server-name127.0.0.1
    :remote-addr127.0.0.1
    :uri/
    :方案:http
    :headers {}
    :request-method:get})
    {:status 200,
    :headers {Content-Typetext / html} ,
    :body< html> ...< / html>}

    如果:request-method 的是:head ,则响应将是 nil 。我们将在一分钟内回到这里的问题: nil (但是注意它不是有效的回铃音。)



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



    还显而易见的是,路径的主体不需要真正需要评估为适当的响应图; Compojure为字符串(如上所示)和许多其他对象类型提供了正常的默认处理;请参阅 compojure.response / render multimethod了解详情(此处的代码完全是自我记录的)。



    尝试使用 defroutes 现在:

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

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



    example-routes 的内部工作方式是依次尝试每个路由;一旦它们中的一个返回非 nil 响应,该响应就成为整个的返回值 example-routes 处理程序。为了更方便, defroutes 定义的处理程序被包装在 wrap-params



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

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

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



    上述测试:

      user> (echo-typed-url-route {:server-port 80 
    :server-name127.0.0.1
    :remote-addr127.0.0.1
    :uri bar
    :scheme:http:
    :headers {}
    :request-method:get})
    {:status 200,
    :headers text / html},
    :bodyhttp://127.0.0.1:80/foo/bar}

    对于上述的辉煌的后续构想是更复杂的路由可以在匹配阶段将 assoc 额外信息添加到请求上: p>

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

    这会回应:body foo来自上一个示例的请求。



    这个最新示例有两件新内容: /: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-name127.0.0.1
    :remote-addr127.0.0.1
    :uri/
    :query-stringfoo = 1
    :scheme:http:
    :headers {}
    :request-method:get})
    {:status 200,
    :headers {Content-Typetext / html},
    :body{\foo\\1\}}

    这是一个很好的时候来看看问题文本的例子:

     (defroutes main-routes 
    (GET/[](workbench))
    (POST/ save{form- params:form param))
    (GET/ test[& more](str< pre>more< / pre> /:filename:filename#。*] [filename]
    (response / file-response filename {:root./static}))
    (ANY* < h1>未找到页面。< / h1>))

    turn:


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


    2. (POST/ save{form -params:form-params}(str form-params)) - :form-params 是请求映射中的一个条目 wrap-params 中间件(记住它被 defroutes 隐含包含)。响应将是的标准 {:status 200:headers {Content-Typetext / html}:body ...} (str form-params)替换为 ... 。 (稍微不寻常的 POST 处理程序,此...)


    3. GET/ test[& more](str< pre> more< / pre>)) - 这将回送地图的字符串表示 {foo1} 如果用户代理要求/ 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旨在提供一个特殊用途的模式匹配引擎,就像这样。


    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.

    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.

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

    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. 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?

    I really like Clojure but I am so stumped!

    解决方案

    Compojure explained (to some degree)

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

    Why?

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

    A concise syntax for generating Ring handlers.

    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. A request arrives and is transformed into a Clojure map in accordance with the Ring spec.

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

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

    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.

    How?

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

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

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

    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.

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

    Let's try using defroutes now:

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

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

    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.

    A test of the above:

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

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

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

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

    2. (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...)

    3. (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".

    4. (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.

    5. (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).

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

    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.

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

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