What is Pedestal?什么是立柱?
Pedestal is a collection of interacting libraries that together create a pathway for developing a specific kind of application. It empowers developers to use Clojure to build internet applications requiring real-time collaboration and targeting multiple platforms.
立柱是一组相互作用的程序库,它们共同创造了一种用于开发特定类型应用的途径。它赋予开发人员能够使用Clojure 构建需要实时协作和面向多个平台的互联网应用。
In short: Pedestal provides a better, cohesive way to build rich client web applications in Clojure.
简而言之:立柱提供了一种更好的、富有凝聚力的使用Clojure来构建富客户端Web应用程序的方式。
Who is it for? 适合哪些人呢?
Clojurists looking for a standard way to build internet applications will love Pedestal. Rather than composing art out of found objects, they will now be able to mold a single, consistent form to match their vision.
对于哪些寻找建立互联网应用的标准方式的Clojure程序员来说,一定会喜欢立柱。因为通过使用立柱就用不着使用过去那种组合使用各种语言来建立一个系统的技巧了,现在他们能够使用一个单一的,一致的形式来构造应用,以满足他们的开发需求。
Pedestal may also appeal to developers who have been nervously approaching a functional language but who haven't yet mustered the courage to ask it out on a date. It provides a sterling example of how to use the Clojure ecosystem to its best advantage, reducing the friction usually associated with a language switch.
立柱向哪些一直想但又不敢使用函数式语言的开发者提供了鼓起勇气拥抱函数式语言的机会。它提供了如何使用纯 Clojure生态系统的一个例子,其最大的优势在于减少了常常与语言切换相关的摩擦。
Where do I start? 从什么地方开始?
In the Getting Started section, you will find a walk-through that introduces you to all of Pedestal's moving parts via the creation of a new application. It explains the features you'll find in the pedestal.app namespace.
在入门这一节,通过创建一个新的应用程序,贯穿始终你会发现通过它向您介绍了所有立柱的活动部分。它解释了你会在pedestal.app命名空间中发现的那些特性。
App Docs covers higher-level topics, and pulls back the curtain on the reasoning behind Pedestal's approach.
在App文档这一节,涵盖了更高层次的议题,并回到立柱之所以这样做的幕后理由上。
Service Docs gets down and dirty with the inner workings of the pedestal.service layer.
在Service文档这一节,深入到pedestal.service层的内部,介绍其运作的机理。
What about API Documentation? 关于API文档?
To generate literate-programming-style documentation for the app and service libraries, add the lein plugin for marginalia<https://github.com/fogus/lein-marginalia> to your lein user profile. After installing the pedestal libraries you can then cd into the app or service directories and run lein marg.
要为app 和 service库生成文学编程风格文档,在你的lein用户配置文件中加入lein插件marginalia。在安装底座库之后,你可以通过cd进入到app或service的目录,然后运行lein marg。
cat ~/.lein/profiles.clj
# {:user {:plugins [[lein-marginalia "0.7.1"]]}}
git clone https://github.com/pedestal/pedestal.git
cd pedestal
lein sub install
( cd app && lein marg )
( cd service && lein marg )
This will create the documentation for pedestal.app and pedestal.service in their respective docs directory.
这将在各自的docs目录中创建pedestal.app和pedestal.service的文档。
Hello World Service Hello World服务
This explains how to create a simple “Hello World” service on Pedestal. This is the simplest ever service. As you can easily guess, the service just responds with “Hello, World!” whenever you need a friendly greeting.
这里解释了如何创建立柱上的一个简单的“Hello World”服务。这是一种最简单的服务。正如你可以很容易猜到的,每当你需要一个友好的问候的时候,该服务只是响应“Hello, World!”。
Create a Clojure project for the server side 为服务器端创建一个Clojure项目
mkdir ~/tmp
cd ~/tmp
lein new pedestal-service helloworld
cd helloworld
Edit project.clj 编辑 project.clj
The generated project definition looks like this:
生成的项目定义如下所示:
(defproject helloworld "0.0.1-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.5.1"]
[io.pedestal/pedestal.service "0.1.9"]
;; Remove this line and uncomment the next line to
;; use Tomcat instead of Jetty:
[io.pedestal/pedestal.jetty "0.1.9"]
;; [io.pedestal/pedestal.tomcat "0.1.9"]
;; auto-reload changes
[ns-tracker "0.2.1"]
;; Logging
[ch.qos.logback/logback-classic "1.0.7" :exclusions [org.slf4j/slf4j-api]]
[org.slf4j/jul-to-slf4j "1.7.2"]
[org.slf4j/jcl-over-slf4j "1.7.2"]
[org.slf4j/log4j-over-slf4j "1.7.2"]]
:profiles {:dev {:source-paths ["dev"]}}
:min-lein-version "2.0.0"
:resource-paths ["config", "resources"]
:aliases {"run-dev" ["trampoline" "run" "-m" "dev"]}
:main ^{:skip-aot true} helloworld.server)
You may want to change the description, add dependencies, change the license, or whatever else you'd normally do to project.clj. Once you finish editing the file, run lein deps to fetch any jars you need.
您可能需要更改说明,添加依赖,更改许可,或任何你通常在project.clj文件中所做其他事项。一旦你完成这个文件的编辑,运行lein deps来获取你需要的jar文件。
Edit service.clj 编辑 service.clj
Our project name is helloworld, so the template generated two files under src/helloworld. service.clj defines the logic of our service. server.clj creates a server (a daemon) to host that service.
我们的项目名称为helloworld的,所以在src/helloworld目录下模板生成两个文件。 service.clj定义了我们服务的逻辑。 server.clj创建一个服务器(守护进程)来托管服务。
Of course, if you used a different project name, your service.clj would be src/your-project-name-here/service.clj. Also, the namespace will be your-project-name-here.service instead of helloworld.service.
当然,如果你使用一个不同的项目名称,你service.clj会是这样src/<这里是你的项目名称>/service.clj。此外,该命名空间将是<这里是你的项目名称>.service而不是helloworld.service。
The default service.clj demonstrates a few things, but for now let's replace the default service.clj with the smallest example that will work. Edit src/helloworld/service.clj until it looks like this:
缺省service.clj演示了一些东西,但现在让我们用能够工作的最小的例子来替换掉缺省的service.clj。编辑的src/helloworld/service.clj,直到它看起来像这样:
(ns helloworld.service
(:require [io.pedestal.service.http :as bootstrap]
[io.pedestal.service.http.route.definition :refer [defroutes]]
[ring.util.response :refer [response]]))
(defn home-page
[request]
(response "Hello World!"))
(defroutes routes
[[["/" {:get home-page} ^:interceptors [bootstrap/html-body]]]])
;; Consumed by helloworld.server/create-server
(def service {:env :prod
::bootstrap/routes routes
::bootstrap/type :jetty
::bootstrap/port 8080})
The home-page function defines the simplest HTTP response to the browser. In routes, we map the URL / so it will invoke home-page. Finally, the var service describes how to hook this up to a server. Notice that this is just a map. service doesn't actually start the server up; it defines how the service will look when it gets started later.
home-page函数定义了返回给浏览器的简单的HTTP响应。在路由上,我们映射URL/使得它会调用home-page。最后,变量service定义如何挂钩这件事到服务器。请注意,这只是一个map。服务实际上并没有启动服务器,它定义了当服务器启动之后服务会是什么样子。
There's nothing magic about these function names. There are no required names here. One of our design principles in Pedestal is that all the connections between parts of your application should be evident. You should be able to trace functions from call to definition without any “magic” or “action at a distance” metaprogramming.
关于这些函数的名称没有什么神奇的。这里没有需要的名字。我们在立柱的一个设计原则是,你的应用程序各部分之间的所有连接应该是明显的。你应该能够从调用定义跟踪函数,无需任何“神奇”或“超距作用”的元编程。
Take a peek into src/helloworld/server.clj. We won't be changing it, but it's interesting to look at the create-server function:
快来看一下src/helloworld/server.clj。我们不会改变它,但它是有趣的,看看create-server函数:
(ns helloworld.server
(:require [helloworld.service :as service]
[io.pedestal.service.http :as bootstrap]))
;; ...
(defn create-server
"Standalone dev/prod mode."
[& [opts]]
(alter-var-root #'service-instance
(constantly (bootstrap/create-server (merge service/service opts)))))
;; ...
You can see that create-server calls helloworld.service/service to get that map we just looked at. create-server merges that map with any per-invocation options, and then creates the actual server by calling bootstrap/create-server.
你可以看到,create-server调用helloworld.service/service来获得我们刚才看过的map。create-server将此map 与任何前调用选项进行合并,然后通过调用bootstrap/create-server创建实际的服务器。
Run it in Dev Mode 在开发模式运行此服务
We'll start the server from a repl, which is how we will normally run in development mode.
我们会从一个REPL启动服务器,下面就是我们通常在开发模式下如何运行此服务器的过程。
$ lein repl
nREPL server started on port 52471
REPL-y 0.2.0
Clojure 1.5.1
Docs: (doc function-name-here)
(find-doc "part-of-name-here")
Source: (source function-name-here)
Javadoc: (javadoc java-object-or-class-here)
Exit: Control+D or (exit) or (quit)
To make life easier in the repl, pedestal generated a “dev.clj” file with some convenience functions. We'll use one to start the server:
为了使在REPL生活更轻松,立柱会生成“dev.clj”文件,此文件包含一些便利的函数。我们将使用其中一个启动服务器:
helloworld.server=> (use 'dev)
nil
helloworld.server=> (start)
nil
Now let's see “Hello World!”
现在,让我们来看看“Hello World!”
Go to http://localhost:8080/<http://localhost:8080/> and you'll see a shiny “Hello World!” in your browser.
进入http://localhost:8080/,你会在您的浏览器上看到一个闪亮的“Hello World!”。
Done! Let's stop the server.
做完了!让我们停止服务器。
helloworld.server=> (stop)
nil
Where To Go Next 下一步去哪里
For more about building out the server side, you can look at Routing and Linking <http://pedestal.io/documentation/service-routing/> or Connecting to Datomic.<http://pedestal.io/documentation/connecting-to-datomic/>
欲了解更多有关在服务器端的构建,你可以看看路由和链接<http://pedestal.io/documentation/service-routing/>或链接到Datomic<http://pedestal.io/documentation/connecting-to-datomic/>。
Connecting “Hello World” to Datomic 将“Hello World”连接到Datomic
This tutorial extends the Hello World Service <http://pedestal.io/documentation/hello-world-service/> to use strings retrieved from Datomic<http://www.datomic.com/>. We will start from the “helloworld” service that we created in our first tutorial.
本教程通过使用检索来自Datomic字符串扩展Hello World服务。我们启动在第一个教程中创建的“helloworld”服务。
This tutorial assumes that you have Datomic running already. If not, hop over to the download site<http://www.datomic.com/get-datomic.html> and download the free edition.
本教程假定您有Datomic在运行。如果没有,跳了下载网站和下载免费版。
Add Datomic to the Project 添加Datomic到项目中
There are just a couple of steps to get our service hooked up to Datomic. First, we need to add the dependency to our project.clj.
只需要几个步骤,就可以将我们的服务挂接到Datomic。首先,我们需要将依赖添加到我们的project.clj中。
(defproject helloworld "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.5.0"]
[io.pedestal/pedestal.service "0.1.0"]
[org.slf4j/jul-to-slf4j "1.7.2"]
[org.slf4j/jcl-over-slf4j "1.7.2"]
[org.slf4j/log4j-over-slf4j "1.7.2"]
[com.datomic/datomic-free "0.8.3826"]]
:profiles {:dev {:source-paths ["dev"]}}
:resource-paths ["config"]
:main ^{:skip-aot true} helloworld.server)
Run lein deps to fetch any jars you need.
运行lein deps来获取你需要的任何jar文件。
Write Schema and Seed Data 编写模式和种子数据
On Pedestal app, a suitable place to put a schema is the resources/[your-project-name-here] directory. In this sample, the project name is “helloworld”, so place the Datomic schema file below in resources/helloworld/schema.edn. The schema is also pretty simple: it has just one attribute.
在立柱的应用程序中,一个存放模式的合适地方是resources/<这里是你的项目名称>目录。在此示例中,该项目名称为“helloworld”的,所以将Datomic模式放在resources/helloworld/schema.edn文件内。该模式也很简单:它只有一个属性。
[
{:db/id #db/id[:db.part/db]
:db/ident :hello/color
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/fulltext true
:db/doc "Today's color"
:db.install/_attribute :db.part/db}
]
This application doesn't have any way to write new data into Datomic yet. So, in order to have something to show in a browser, we'll put some seed data into Datomic. This file can also reside under the resources/helloworld directory. Create the file resources/helloworld/seed-data.edn with the following contents.
此应用程序目前没有任何办法写入新的数据到Datomic中。所以,为了有东西在浏览器中显示,我们会放一些种子数据到Datomic中。该文件还可以驻留在resources/helloworld目录下。创建文件resources/helloworld/seed-data.edn包含以下内容。
[
{:db/id #db/id[:db.part/user -1], :hello/color "True Mint"}
{:db/id #db/id[:db.part/user -2], :hello/color "Yellowish White"}
{:db/id #db/id[:db.part/user -3], :hello/color "Orange Red"}
{:db/id #db/id[:db.part/user -4], :hello/color "Olive Green"}
]
The funny looking negative IDs are a way to ask Datomic to assign entity IDs automatically.
你可能觉得有趣,怎么会有负的ID?其实这是一种要求Datomic自动分配项目ID的方法。
Create Some Data Functions 创建一些数据函数
Now we need create functions to establish a connection to Datomic, define the schema, and retrieve data. This code is nothing new, just a simple Datomic sample. Put the following code into src/helloworld/peer.clj.
现在,我们需要创建建立到Datomic的连接,定义模式,和检索数据的函数。这个代码不是什么新鲜事,只是一个简单的Datomic样例。把下面的代码放到src/helloworld/peer.clj中。
(ns helloworld.peer
(:require [datomic.api :as d :refer (q)]))
(def uri "datomic:mem://helloworld")
(def schema-tx (read-string (slurp "resources/helloworld/schema.edn")))
(def data-tx (read-string (slurp "resources/helloworld/seed-data.edn")))
(defn init-db []
(when (d/create-database uri)
(let [conn (d/connect uri)]
@(d/transact conn schema-tx)
@(d/transact conn data-tx))))
(defn results []
(init-db)
(let [conn (d/connect uri)]
(q '[:find ?c :where [?e :hello/color ?c]] (d/db conn))))
Use Database Results in the Service 在我们的服务中使用数据库记录集
Next, we need to use results from the database. For this, we will add a function to service.clj. This new function will use peer.clj to access data.
接下来,我们需要使用数据库记录集。对于这一点,我们将在service.clj中添加一个函数。这个新函数将使用peer.clj来访问数据。
Open up src/helloworld/service.clj again and modify the ns macro to reference helloworld.peer:
再次打开src/helloworld/service.clj和修改ns宏加上对helloworld.peer的引用:
(ns helloworld.service
(:require [io.pedestal.service.http :as bootstrap]
[io.pedestal.service.http.route.definition :refer [defroutes]]
[ring.util.response :refer [response]]
[helloworld.peer :as peer :refer [results]]))
Let's now rewrite the home-page function in service.clj so that we see the output from Datomic.
现在,让我们重写service.clj的home-page函数,使我们看到来自Datomic输出。
(defn home-page
[request]
(response (str "Hello Colors! " (results))))
If you still have the service running from Hello World Service, then you will need to exit the REPL. Restart the service the same way as before: lein repl, (use 'dev), and (start).
如果您仍然在运行来自Hello World Service的服务,那么您将需要退出REPL。以以前相同的方式重新启动该服务:lein repl, (use 'dev), 和 (start)。
Now point your browser at http://localhost:8080/ and you will see the thrilling string:
现在,在你的浏览器的http://localhost:8080/中,你会看到激动人心的字符串:
Hello Colors! [["True Mint"], ["Olive Green"], ["Orange Red"], ["Yellowish White"]]
Because home-page returns a string, the HTTP response will be sent with a content type of “text/plain”, as we can see by using “curl” to access the server.
因为home-page将返回一个字符串,HTTP响应将按照“text/plain”的内容类型发送,正如我们在下面看到的我们可以通过使用“curl”命令来访问服务器。
$ curl -i http://localhost:8080/
HTTP/1.1 200 OK
Date: Fri, 22 Feb 2013 20:31:06 GMT
Content-Type: text/plain
Content-Length: 82
Server: Jetty(8.1.9.v20130131)
Hello Colors! [["True Mint"], ["Orange Red"], ["Olive Green"], ["Yellowish White"]]
Where to go Next 下一步去哪里
For more about Datomic, check out datomic.com<http://www.datomic.com/>.
欲了解更多有关Datomic的信息,请点击datomic.com。
App Docs app文档
Application Rationale 应用原理
The Pedestal application library (pedestal-app) is designed to help developers create interactive web applications.
立柱应用程序库(立柱式应用程序)旨在帮助开发人员创建交互式Web应用程序。
Pedestal reduces application complexity by giving developers tools to model, report and react to change.
立柱通过给开发人员提供建模、报告和应对变化的工具来降低了应用的复杂性。
The problem 问题
Interactive applications constantly receive inputs from multiple sources. This property forces them to be long-running, single-page applications which must have state.
交互式应用程序常常接收来自多个源的输入。这个属性迫使它们成为需要长时间运行、单页的必须有状态的应用程序。
There are three high-level tasks that this kind of application must perform:
有三个高层次的任务,这种应用程序必须执行:
.Receive and process input
.Manage state
.Update the UI when state changes
。接收和处理输入
。管理状态
。当状态发生变化时更新UI
There are hard problems associated with each of these tasks.
存在许多与这些任务相关的难题。
All I/O in the browser is asynchronous. If we receive input from users or services then we must create callback functions to process these inputs. This reduces our ability to control how our program executes, and can make programs hard to understand. This problem is often referred to as Callback Hell.
在浏览器中的所有I/O是异步的。如果我们收到来自用户或服务的输入,那么我们必须创建回调函数来处理这些输入。这减少了我们对程序如何执行的控制能力,并且这可能使程序难以理解。这个问题通常被称为回调地狱。
Inputs supply new information to our application which must be stored in some way. While applying changes to state, which are triggered by asynchronous inputs, we must ensure that we are always seeing a consistent view of state. Additionally, there may be many parts of that model which need to change based on the input. Where does the code which knows about these data dependencies live? If it is the responsibility of the code processing the input message then this code becomes brittle and must be changed every time we add a new feature to the application.
输入为我们的应用提供了新的信息,而应用必须按照某种方式存储这些信息。同时将更改应用的状态,这是由异步输入触发的,我们必须确保我们总是看到状态的一致视图。此外,模型的许多部分还有可能需要根据输入来改变。知道这些数据依赖的代码放在哪里?如果处理输入信息是代码的责任,那么这段代码变得脆弱,并且我们每次添加一个新功能到应用程序中时这段代码便必须修改。
The purpose of a user interface is to be a visual representation of the information model. When the model changes, the UI will need to change. In order to do this efficiently, we need to know what has changed.
用户界面的目的是要成为信息模型的可视化表示。当模型发生变化,用户界面将需要改变。为了有效地做到这一点,我们需要知道发生了什么变化。
These are the problems that Pedestal addresses.
这些便是立柱所设法解决的问题。
Pedestal's solution 立柱的解决方案
Every callback function in Pedestal has one job: convert an event into a message (data) and place this message on the application's input queue. This addresses the two problems of callbacks: it helps us understand how our program works and it gives back control.
在立柱里的每个回调函数有一个工作:将事件转换为一个消息(数据) ,并将此消息放到应用程序的输入队列中。这解决了回调的两个问题:它可以帮助我们了解我们的程序是如何工作的,并且它归还了控制。
Event wiring code is just that, it wires up an event, captures it, converts it to data and puts it on a queue. Callback functions are no longer directly causing any other code in our application to run.
事件连接代码就是这样,它触发一个事件,捕捉它,将它转换成数据,并把它放在一个队列中。回调函数不再直接调用在我们的应用程序中的任何其他代码来运行。
We can now think of all input as being conveyed to our application on a queue. We are in control of how and when the messages on this queue are processed.
现在我们可以把输送到我们的应用程序上的所有输入放在一个队列上。我们只要对这个队列中的消息进行处理的方式和时间进行控制就可以了。
Messages on this queue are processed one at a time. For each input message, a single transaction is run which results in a new state. While this transaction is running, no other input can change the state. This ensures that we have a consistent state within a transaction.
在处理这个队列中的消息时,每次只处理一项。对于每个输入的消息,单个事务被执行而导致一个新的状态。当本次事务正在运行时,没有其他的输入可以改变状态。这使得在一个事务内确保一致的状态。
Within a transaction, each change is defined by a pure function. Each function can focus on making one change. Changes to dependencies in the information model are handled by dataflow. This allows changes to automatically propagate when new inputs are received. New features are added by creating new dataflow functions rather than updating existing ones.
在事务中,每一个变化是由一个纯函数定义。每个函数可以专注一种更改。依赖于信息模型的改变由数据流来处理。这使得当接收到新的输入时变化会自动传播。新功能是通过创建新的数据流函数来加入的,而不必更改现有函数。
For each transaction, the changes which have been made to the information model are translated into instructions which are sent to the renderer. The instructions describe the exact changes which need to be made to the UI. This technique of communicating change decouples rendering from state and allows rendering code to make efficient changes to the view.
对于每个事务,已提交给信息模型中的变化将转换成发送到渲染器的指令。该指令描述了需要对UI所要进行的确切的改变。这种通信改变技术对渲染和状态进行了解耦,使得渲染代码能够有效地改变视图。
Application Introduction 应用介绍
Most applications need to do the same basic things:
大多数应用程序都需要做一些基本相同的事情:
.Receive and process input
.Manage state
.Update the UI when state changes
。接收和处理输入
。管理状态
。当状态发生变化时更新UI
Writing this kind of application in ClojureScript is straightforward.
用ClojureScript来写这类应用是很简单的。
(ns hello-world
(:require [domina :as dom]))
(def application-state (atom 0))
(defn render [old-state new-state]
(dom/destroy-children! (dom/by-id "content"))
(dom/append! (dom/by-id "content")
(str "<h1>" new-state " Hello Worlds</h1>")))
(add-watch application-state :app-watcher
(fn [key reference old-state new-state]
(render old-state new-state)))
(defn receive-input []
(swap! application-state inc)
(.setTimeout js/window #(receive-input) 3000))
(defn ^:export main []
(receive-input))
Here we have all of the basic parts that we need. There is an atom for storing state and Clojure's update semantics for performing state transitions. We can easily watch the atom for changes and call a rendering function, passing in the old and new state.
在这里,存在所有我们需要的基本部分。有一个原子来存储状态以及用Clojure的更新语义进行状态转换。我们可以很容易地观察原子的变化,并通过传入旧的和新的状态调用渲染函数。
If you have ever built a ClojureScript application, you may have started in this way.
如果你曾经建立了一个ClojureScript应用程序,你可能已经早就开始使用这种方式。
As the application above gets more complex, many problems will arise. How many atoms should we have? What is the structure of the data that goes into the atoms? When we are handed an old and new state, how do we figure out what has changed? How do we know if we should care about a change?
当上面的应用程序变得更加复杂,会出现很多问题。我们应该有多少个原子?进入原子的数据结构是什么?当传给我们一个老的和新的状态,我们怎么弄清楚发生了什么改变?我们怎么知道我们关心的改变已经发生?
One of the largest problems with this approach is that the rendering function is being asked to do a lot of work. It is handed an old and new value and asked to render it. It has to figure out what has changed and then figure out the state of the DOM so that it can make the necessary change. There are only two ways to deal with this; both are unacceptable.
其中这种方法的最大问题是:渲染函数是被要求做大量的工作。这是传入一个老的和新的值,要求对其进行渲染。不得不弄清楚发生了什么变化,然后弄清楚DOM的状态,以便对它进行必要的改变。只有两种方法来处理这个,两者都是不可接受的。
The first is to look at the DOM and try to figure out what needs to be modified, the other is to wipe out large sections of the DOM and re-render everything. The first approach means that we put state in the DOM. The second does not perform well.
首先是看看DOM并且弄清楚什么是需要修改的,另一种是消灭大DOM和重新渲染所有的部分。第一种方法意味着,我们把状态存放在DOM中。第二不能很好地执行。
You may have noticed the word “change” a few times in the above paragraphs. The main source of complexity which arises from a large application of this type is dealing with change.
您可能已经注意到了字“变”几次出现在上面的段落。复杂性的主要来源,来自一个大型这种类型的应用程序对变化的处理。
Pedestal helps us deal with this complexity by giving us tools to model, report and react to change.
立柱通过给我们提供进行建模、报告和应对变化的工具帮助我们处理这种复杂性。
First Pedestal application 第一个立柱应用
The example below shows the same application written with Pedestal.
下面的例子显示用立柱写的相同的应用程序。
(ns hello-world
(:require [io.pedestal.app.protocols :as p]
[io.pedestal.app :as app]
[io.pedestal.app.messages :as msg]
[io.pedestal.app.render :as render]
[io.pedestal.app.render.push :as push]
[domina :as dom]))
(defn render-value [renderer [_ path old-value new-value] input-queue]
(let [id (push/get-parent-id renderer path)]
(dom/destroy-children! (dom/by-id id))
(dom/append! (dom/by-id id)
(str "<h1>" new-value " Hello Worlds</h1>"))))
(defn inc-t [old-state message]
(inc old-state))
(def count-app {:version 2
:transform [[:inc [:count] inc-t]]})
(defn receive-input [input-queue]
(p/put-message input-queue {msg/type :inc msg/topic [:count]})
(.setTimeout js/window #(receive-input input-queue) 3000))
(defn ^:export main []
(let [app (app/build count-app)
render-fn (push/renderer "content" [[:value [:*] render-value]])]
(render/consume-app-model app render-fn)
(receive-input (:input app))
(app/begin app)))
Ignoring the required namespaces and the setup code in the main function, this is about the same amount of code.
忽略需要的命名空间和在主函数中的设置代码,这大约有相同的代码量。
The benefits of the Pedestal version include:
立柱版本的好处包括:
.no explicit state management
.application behavior can be defined by pure functions
.detailed change reporting
.没有明确的状态管理
.应用行为可以通过纯函数定义
.详细的变更报告
In the Pedestal version, there is no explicit state management. There is no atom, no watcher and no swap!. Much like a Clojure reference type, the application that is built in main manages state transitions. The inc-t function receives a value and produces a new value without having to produce side effects.
在立柱的版本,没有明确的状态管理。没有原子,没有监护人,没有交换!多像Clojure的引用类型,是内置在主应用程序管理的状态转换。该inc-t函数接收一个值,并产生一个新的值,而不必产生副作用。
Because state transitions are handled by the application engine, most of our application logic can be written as pure functions.
因为状态转换由应用程序引擎处理,我们的大部分应用程序逻辑可以写成纯函数。
The state is no longer a global thing which can be updated from anywhere. All updates to the information model happen in one place. In this small application, they take place within the inc-t function.
状态不再是一个全局性的东西可以从任何地方进行更新。所有更新的信息模型发生在同一个地方。在这个小的应用程序中,在inc-t函数内对信息模型进行更新。
In the Pedestal version, the rendering function is passed a description of the exact change which was made. Rendering code no longer has to find changes, make huge changes to the DOM or look for state in the DOM. It simply makes the change it is told to make. This is usually implemented in a small function which makes a very precise change.
在立柱版本,渲染函数传入做了确切变化的描述。渲染代码不再去寻找变化、对DOM做出巨大的改变或在其中寻找状态。它只是进行被告知要进行的更改。这通常是在一个小函数中实现的,这就使得能够实现一个非常精确的改变。
Application Overview 应用概述
This document is a high-level description of the key ideas behind pedestal-app. Detailed documentation of each feature will be provided as pedestal-app becomes more mature. This document, in addition to the pedestal-app tutorial<https://github.com/pedestal/app-tutorial/wiki> should be enough to get started.
这份文档是立柱式应用程序的背后关键概念的高层次的描述。当立柱应用程序变得更加成熟时会提供每个特性的详细说明文档。这份文档加上立柱式应用程序的教程应该足够上手。
Why Pedestal for applications? 立柱式应用程序存在的理由
Pedestal-app provides a clean architecture for creating large, interactive, single-page applications in the browser.
立柱式应用程序为创建大型,互动,单页的基于浏览器的应用程序提供了一个干净的架构。
Creating large single-page applications is hard for several reasons
创建大型单页应用程序是很难的,存在以下几个原因:
.lots of interdependent state
.views must be kept in sync with state
.the DOM is bad place to keep application state
.events and callbacks can easily lead to a hairball
.testing is essential and yet difficult
.大量的相互依存的状态
.视图必须保持与状态同步
.为了保持应用程序状态,DOM不是一个好地方
.事件和回调很容易导致一个毛球
.测试是必要的,但困难
For large applications, these things are difficult to deal with even when a user interface only has to respond to inputs from a single user. The problem is compounded when we add interactivity. An application can now receive inputs from multiple sources. Change becomes constant and efficiently keeping everything in sync is difficult. Even more important, we must do this in a way that we understand both now and in the future.
对于大型应用程序,即使一个用户界面只需从单个用户响应输入,这些东西也是很难对付的,。当我们添加交互性时问题变得更加复杂。应用程序现在可以接收来自多个源的输入。变化成为不变并有效地保持一切同步是困难的。更重要的是,我们必须以现在和将来都能够理解的方式来处理这些事情。
Existing client-side frameworks may help with one small part of this problem and in some cases can be used along side Pedestal. Most approaches to managing state involve object oriented techniques which lead to unnecessarily confusing abstractions and webs of interconnected mutable objects.
现有的客户端框架可以帮助解决这个问题的一小部分,在某些情况下,可以在立柱的某一端继续使用。管理状态的大多数的方法包括面向对象技术导致不必要的混乱的抽象和相互关联的可变对象的网状结构。
Pedestal's approach is to control this complexity by going after the root of the problem: change. Because data is immutable, we can actually know about change. Pedestal allows us to track fine-grained changes to our application's information model while allowing transactions over the whole model. Pedestal uses simpler tools to solve problems which prevents the introduction of incidental complexity. Tools such as data, functions and queues.
立柱的做法是寻找问题的根源:改变,来控制这种复杂性。由于数据是不可变的,我们实际上可以了解变化。当允许在整个模型上执行事务时,立柱使我们能够跟踪针对我们的应用程序的信息模型细粒度的变化。立柱采用简单的工具来解决问题,防止引进偶然的复杂性。这些工具包括数据,函数和队列。
Pedestal also thinks about an application as a system of interconnected components rather than a single monolithic application. Separating parts and concerns is critical in keeping complexity under control. In Pedestal you will see things like queues and messages used as if we were creating a distributed system. This kind of design allows us to build applications which are flexible, testable and extendable.
立柱也把应用程序看成是互连组件的一个系统,而不是一个单一的整体应用。分离部件和关注点对于保持对复杂性的控制是至关重要的。在立柱中你会看到的使用的东西像队列和消息好像我们正在创建一个分布式系统。这种设计使得我们可以构建灵活的、可测试的和可扩展的应用程序。
Sites vs Apps 网站与应用程序
Pedestal-app is focused on creating single-page applications. The web was designed for documents. Over the past 20 years we have come up with some ingenious ways to build applications on a platform designed for documents. Doing this leads to a lot of incidental complexity that many of us have gotten so used to we don't even see it as complexity. We are perfectly happy writing HTML and making HTTP an essential part of how our applications work.
立柱式应用程序专注于创造单页面的应用程序。Web是为文档而设计的。在过去的20年里,我们想出一些巧妙的方法来为基于文档而设计的平台构建应用程序。这样导致了很多偶然的复杂性,我们许多人已经变得如此习惯以至于我们甚至不把它看作是复杂性。我们心甘情愿编写HTML并且使HTTP成为我们应用程序能够工作的一个重要组成部分。
Focusing on apps allows us to step away from the implementation details of the web and create higher level abstractions.
专注于应用程序可以使我们逐步远离网页的实现细节而创造更高层次的抽象。
We are also interested in creating interactive applications, this kind of application can only be a single-page app. Inputs come from many different sources are not aligned with page loads.
我们对创建交互式应用程序也有兴趣,这类应用只能是一种单页的应用程序。输入来自许多不同的来源与页面加载这种方式不相同。
An application 一个应用程序
At a high level, the core of a Pedestal application can be thought of as a black box with an input and output queue. The input queue conveys messages which transform state (transform messages) to the application and the output queue conveys messages which transform the view or can effect the outside world.
在高层次上,一个立柱应用程序的核心可以被看作是一个具有输入和输出队列的黑盒子。输入队列传递变换状态(转换消息)到应用程序,而输出队列传递变换视图或可以影响外部世界的消息。
All input from a view or from back-end services is sent to the application as a transform message on the input queue. Messages which transform the UI are consumed by a renderer which will modify the view and add event handlers which convert events to transform messages.
被发送到应用程序的所有来自视图或后端服务的输入作为输入队列上的变换消息。其中变换UI的消息由一个渲染器来处理从而修改视图,并添加事件处理程序来将事件转换为变换消息。
In the diagram above, the view could be the DOM, a JavaScript visualization or anything which can draw on a screen and collect inputs.
在上面的图中,视图可以是DOM,JavaScript的可视化,或任何可以绘制在屏幕上,并收集输入的东西。
Why use queues? 为什么要使用队列?
Queues separate application concerns. In Pedestal, they eliminate the direct connection between callback functions which are triggered by events and all of the logic which deals with state transitions. In general, queues separate application concerns from the complexity of I/O in the browser. From the application's perspective, all input comes in as a stream of messages and all output is placed on a queue and quickly forgotten.
队列分离了应用程序的关注点。在立柱中,它们消除了由事件触发的回调函数和所有处理状态转换的逻辑之间的直接连接。一般情况下,队列使得应用程序不再关注浏览器中I/O的复杂性。从应用程序的角度来看,所有输入来自一个消息流,所有输出放置到一个队列中并很快将其遗忘。
The black box above contains all of the logic which determines what this application actually does. The logic inside the black box doesn't know anything about the DOM or even that it is running in a browser. This means that it can be run when the DOM is not available, for example, in a Web Worker. It can also be run on the server and tested from Clojure.
黑盒子上面包含了所有的逻辑,这些逻辑决定了这个应用程序实际上所要做的事情。在黑盒子里面的逻辑不知道DOM的任何事情,即使它在浏览器中运行也是一样。这意味着它可以在DOM不可用时运行,例如,在一个Web Worker中运行。它也可以在服务器上运行,并通过Clojure进行测试。
Testing this logic is now easy. We can send it fabricated data and examine the output data. There are no dependencies on any web technology.
测试这个逻辑现在很容易。我们可以向它发送事先准备的数据并检查输出数据。对任何Web技术没有依赖关系。
Using queues for input and output also allows us to be flexible in what we do with the messages on the queues. We can see when queues are backing up and respond accordingly. Messages on a queue can be filtered, dropped or merged.
输入和输出使用队列也使我们能够灵活地处理队列中的消息。我们可以看到,何时队列被备份以及何时作出了相应的反应。能够对队列中的消息进行过滤、删除或合并。
Information Model 信息模型
In a Pedestal application, application state is stored in a tree. This tree is the application's information model. Imagine that we are creating a hotel information system. A portion of the information model may look something like this
在立柱的应用程序中,应用程序状态被存储在一个树中。这棵树是应用程序的信息模型。试想一下,我们正在创造一个酒店信息服务系统。信息模型的一部分可以是这个样子
All data for a hotel is stored under the root node. We can represent the path to this node as []. All data for rooms is stored under [:rooms] and all data for room 3 is stored under [:rooms 3].
酒店的所有数据存储在根节点下。我们可以表示到该节点的路径为[]。所有客房数据存储在[:rooms]和所有房间3数据被存储在[:rooms 3]下面。
Changes to this information model are managed by the application. The value of the model is immutable, so change takes the form of state transitions which are implemented as pure functions.
改变此信息模型由应用程序进行管理。该模型的值是不变的,所以变更采用状态变换的形式并用纯函数来实现。
The functions which perform these state transitions are run each time a new transform message is delivered on the input queue.
每当一个新的转换消息传递到输入队列中时就会执行这些状态转换函数。
Transform Messages 变换消息
Messages are data, specifically Clojure maps.
消息是数据,特别是Clojure的map。
A transform message is any message which describes a change that the receiver should make. Transform messages will usually have a target, an operation to perform and some arguments.
变换消息是描述一个接收器应该做出变更的任何消息。变换消息通常都会有一个目标,要执行的操作和一些参数。
All messages placed on the input queue are transform messages which have a specific format.
放置在输入队列中的所有消息都是具有特定格式的变换消息。
Each input message has a topic and a type. The topic is a vector which represents a path into the information model where the message will be applied. This is the target. The type is a keyword which is usually mapped to a specific function which will apply the message to the information model. This is the operation. An input message can have any other data. These are the arguments.
每个输入消息有一个主题和类型。主题是一个向量,它表示消息被应用到信息模型的一个路径。这是目标。类型是关键字通常被映射到其中将应用消息到信息模型的特定函数。这是操作。输入消息可以具有其它的任何数据。这些都是参数。
The topic and type keys are namespaced keywords which live in the io.pedestal.app.messages namespace. It is common to require this namespace as msg and use the topic and type vars as a shorthand.
主题和类型键是带命名空间的关键字它们是在io.pedestal.app.messages命名空间中的关键字。通常需要这个命名空间作为msg而使用主题和类型变量作为缩写。
(require '[io.pedestal.app.messages :as msg])
{msg/topic [:rooms 1 :guests] msg/type :add-name :name "Alice"}
In the context of a hotel information system, this message could mean that we are adding “Alice” as a guest who will be staying in room 1.
在酒店信息系统的背景下,这个消息可能意味着,我们将加入“Alice”作为客人,她将会留在房间1。
Processing input with transform functions 用变换函数处理输入
Transform messages enter an application on the input queue and then something happens which causes the information model to change. What happens is that messages are taken off the queue, one at a time, and routed to a function which will apply this message to the information model. These functions are called transform functions because they are used to transform the model. The message type and topic are used to route messages to transform functions.
变换信息进入应用程序的输入队列,然后导致信息模型的改变。当从队列中一次取出一个消息,会发生什么事情呢?此时会路由到函数,此函数将使用此消息对信息模型做出相应的改变。这些函数被称为变换函数,因为它们是用来转换模型的。消息中的类型和主题用于路由消息到相应的转换函数。
A transform function takes two arguments, the old value at the target path and the message. It returns the new value for the target path. The transform function add-name is shown below.
一个变换函数有两个参数,在目标路径上的旧值和消息。它返回在目标路径上的新值。变换函数add-name如下所示。
(defn add-name [old-value message]
((fnil conj []) old-value (:name message)))
This transform function could be used to process the message shown above. It could also be used to process any message which adds a name to a vector.
这个变换函数可以被用来处理上面显示的消息。它也可以被用来处理增加一个名字到一个向量的任何消息。
Messages are routed to transform functions. When we describe a Pedestal application, we provide a routing table which controls how this is done. The table that routes a message to this function might look like this:
消息被路由到变换函数。当我们描述了一个立柱应用,我们提供了一个路由表,它控制如何执行此操作。路由消息给这个函数的路由表可能看起来像这样:
[[:add-name [:rooms :* :guests] add-name]]
The routing table is a vector of vectors. The first matching vector will be used. Each vector contains an operation, target and function to match against.
路由表是向量的向量。第一个匹配的向量将被使用。每个向量包含一个用于匹配的操作、目标和函数。
[op target function]
The op my be a wildcard :* and the target may contain wildcards as the example above does. The function in the first vector with a matching op and target will be used. For the message above this function will be passed the old value at [:rooms 1 :guests]. The update performed on the information model is essentially this
op可能是一个通配符:* 并且target可以像上面的例子一样包含通配符。在匹配op和target的第一向量中的函数将被使用。对于上面的消息将传递在[:rooms 1 :guests]的旧值给这个函数。对信息模型进行更新,基本上是这样
(update-in data-model [:rooms 1 :guests] add-name message)
To read a bit more about transform functions, see the tutorial page that introduces transforms.
要阅读更多关于变换函数的资料,请参阅介绍变换的教程页面<https://github.com/pedestal/app-tutorial/wiki/Making-a-Counter#transform-functions>。
Why route transform functions? 为什么要将消息路由到变换函数呢?
What value does the routing table provide? Messages contain all the information that we need to find a function and update the information model.
在路由表中提供什么样的值?消息中包含所有我们需要的找到一个函数和更新信息模型的信息。
The main benefit of the routing table is that it lets you restrict what operations may be performed on what parts of the information model. Without this table, any operation could be performed anywhere and this may not make sense. It also allows you to clearly define which functions may be used to update the information model.
路由表的主要好处是,它可以限制哪些操作可以对信息模型的哪些部分进行处理。如果没有这个表,可以在任何地方进行任何操作,这可能没有什么意义。它也可以让你清楚地定义哪些函数可用于更新信息模型。
Pedestal is young and this feature may be removed. We are considering changing the transform message format to
立柱目前还比较年轻,以后有可能会移除这一特性。我们正在考虑改变变换消息格式为:
[target op & args]
to better match how this is applied to the data model.
以便更好匹配如何对数据模型进行变换。
(apply update-in data-model [target op arg1 arg2])
This change would also allow us to eliminate the use of the transform routing table.
这种改变也允许我们能够避免对变换路由表的使用。
Delta Detection 增量检测
Transform functions make changes to the information model. The very next thing we need to know is what has changed. pedestal-app provides a mechanism for reporting fine-grained change to the information model. This allows us to have the benefits of transactions on the whole information model without forcing functions that care about change to diff the entire data model each time it is updated.
变换函数更改信息模型。我们需要知道的下一件事情是发生了什么变化。立柱式的应用程序提供一种机制来报告信息模型的细粒度变化。这使得我们具有了在整个信息模型上进行事务的优点,而不会强迫那些关注它的变化的函数在每一次更新时通过对整个数据模型前后变化的对比来获取其差异。
In pedestal-app, we declare dependencies between parts of the data model and functions which should be called only when those parts change. Detecting deltas makes this efficient.
在立柱式的应用程序中,我们声明了数据模型的部分和函数之间的的依赖关系,只有当这些部分发生变更时函数才会被调用。检测增量使得这件事情变得很高效。
In the remaining diagrams, blue arrows will represent state deltas which are used to report change.
在后面的图中,蓝色箭头将代表其用于报告状态改变的增量。
Generating rendering instructions with emit functions 生成渲染指令与发射函数
When the information model changes, something outside of the application's behavior will also need to change. One of those things is the view. We have not really discussed what a renderer is, but for now we can think of it as something which is pulling messages off of the output queue (which we will now call the rendering queue) and modifying the view based on these messages.
当信息模型更改时应用程序的行为之外的东西也需要改变。这些需要改变的东西之一就是视图。我们还没有真正讨论什么是渲染器,但现在我们可以把它看作是从输出队列(我们现在称之为渲染队列)拽出消息的某个东西,并且它基于这些消息来修改视图。
These messages are also transform messages, with an operation, target and arguments, but have a different format from the transform messages described above which are used for input.
这些消息也是变换消息,具有操作、目标和参数,但与上面描述用于输入的变换消息具有不同的格式。
By default, an emitter is configured which will generate rendering instructions when anything in the information model changes. When the following message is placed on the input queue
默认情况下,一个发射器被配置使得在信息模型中发生任何变化时将产生渲染指令。当下面的消息被放置在输入队列时
{msg/topic [:rooms 1 :guests] msg/type :add-name :name "Alice"}
the following messages are placed on the rendering queue
下面的消息就会被放置在渲染队列中
[:node-create [] :map]
[:node-create [:rooms] :map]
[:value [:rooms] nil {1 {:guests ["Alice"]}}]
These three messages encode changes which should be made to the view. The root node [] should be created. Under this, the node [:rooms] should be created and the value for rooms should be changed from nil to {1 {:guests ["Alice"]}}.
这三个消息对使视图发生的变化进行编码。首先创建根节点[]。在这之后,创建节点[:rooms]并且客房的值应从nil改为{1 {:guests ["Alice"]}}。
As a renderer consumes these messages, it will modify the view to reflect them visibly. When it receives the message
当渲染消耗这些消息时,它会以可视的方式修改视图以反映这些消息所代表的变化。当它接收到下列消息
[:node-create [:rooms] :map]
it may create a box in which to display room information. When it receives the message
它可以创建一个用于显示房间信息的框。当它接收到消息
[:value [:rooms] nil {1 {:guests ["Alice"]}}]
it can display which guests are in which room.
它可以显示哪些客人在哪个房间。
There is an assumption being made here about what constitutes an atomic value from the renderer's perspective. When the value at [:rooms] changes, the entire map will be sent in the update. For example, sending this message on the input queue
这里存在一个假设从渲染的角度是什么构成一个原子值。当在[:rooms]的值发生变化时,整个map会在此更新被发送。例如,在输入队列发送下列消息
{msg/topic [:rooms 2 :guests] msg/type :add-name :name "Bob"}
will result in this message on the rendering queue
将导致下列消息在渲染队列中
[:value [:rooms] {1 {:guests ["Alice"]}}
{1 {:guests ["Alice"]}, 2 {:guests ["Bob"]}}]
This is fine if the rendering logic is happy with getting updates at this resolution. Once there are a lot of rooms, the renderer may want to only update the rooms which have actually changed. With these updates, the rendering logic would have to figure out what has changed.
如果渲染逻辑很满意在这个分辨率获取更新这还不错,。但一旦有很多房间,渲染可能希望只更新这实际上改变了的房间。有了这些更新,渲染逻辑就必须弄清楚发生了什么变化。
We can configure an emitter to emit instructions at the resolution that we would like. In the description of our application we could add the following
我们可以配置一个发射器来发射我们希望的分辨率的指令。在我们的应用程序的描述中,我们可以添加以下
[#{[:rooms :*]} (app/default-emitter)]
In most cases where we need to define an emitter we only need to change the resolution at which change is reported. To do this we can use the default emitter in io.pedestal.app and provide a path which describes which branch of the tree to emit instructions for and how far down to go to find atomic values.
在大多数情况下,我们需要定义一个只需改变它的报告分辨率的发射器。要做到这一点,我们可以使用在io.pedestal.app中的默认发射器,并提供描述树的哪个分支发出指令,以及多深去寻找原子值的路径。
With this change, the first message
随着这一变化,第一条消息
{msg/topic [:rooms 1 :guests] msg/type :add-name :name "Alice"}
will cause this to be emitted
将导致发射下列指令
[:node-create [] :map]
[:node-create [:rooms] :map]
[:node-create [:rooms 1] :map]
[:value [:rooms 1] nil {:guests ["Alice"]}]
and the second message
而第二条消息
{msg/topic [:rooms 2 :guests] msg/type :add-name :name "Bob"}
will cause this to be emitted
将导致发射下列指令
[:node-create [:rooms 2] :map]
[:value [:rooms 2] nil {:guests ["Bob"]}]
As mentioned above, you will almost always use the default emitter but you can write your own custom emitter function if you need to.
正如上面所说,你几乎总是使用默认的发射器,但如果你需要时也可以编写自己的自定义发射器函数。
In the diagram above, the emit function will be called when the state deltas report that any of its inputs have changed.
在上面的图中,当状态增量报告报告任何输入已经改变时便会调用发射函数。
The default emitter configuration not only controls the resolutions at which instructions are generated but it also controls which parts of the information model will be watched for changes.
默认的发射器配置,不仅控制了生成指令的分辨率,而且也控制了信息模型的哪些部分发生变化时将被观察到。
By default, this configuration is used
默认情况下,使用下列配置
[[#{[:*]} (app/default-emitter)]]
which will emit change instructions when any node of the tree has changed. By changing this to
这会在在树的任意节点发生改变时发出变更指令。将配置改为
[[#{[:rooms :*]} (app/default-emitter)]]
instructions will only be emitted for the children of the [:rooms] node. This allows us to have parts of the information model which do not directly effect the view.
只对[:rooms]节点及子节点进行发射指令。这使我们能够做到信息模型的某些部分不直接影响视图。
To read more about emitters, see the section of the app-tutorial entitled observing-change<https://github.com/pedestal/app-tutorial/wiki/Increment-the-Counter#observing-change>.
需要阅读更多关于发射器的资料,请参阅该应用程序教程观测变化的章节。
Derived data 导出数据
So far we have seen one way to change the information model, the transform function. This allows us to change the information model based on input from the outside world. Most applications will have some values which depend on other values. For example, in the hotel information system we may want to ensure that a member of the hotel staff is assigned to each occupied room to ensure the comfort of the guests (this is a nice hotel). Each time a room is updated, we will need to ensure that a staff member who is on duty is assigned to that room. We will also need to do this when the status of a staff member changes.
到目前为止,我们已经看到了改变的信息模型的一种方式,变换函数。这使我们能够基于外界输入来改变信息模型。大多数应用程序拥有将依赖于其他值的某些值。例如,在酒店的信息系统,我们可能希望确保,有酒店员工分配到每个有客人的房间,以确保客人的舒适性(这是一个不错的酒店)。每当一个房间被更新时,我们将需要确保员工谁在当班被分配到那个房间。当一名员工的状态发生变化时,我们也需要做到这一点。
In the information model above, the blue area of the model might be updated when receiving messages from the check-in system. Messages like this:
在上面的信息模型中,该模型的蓝色区域可能会从登记系统接收消息并进行更新。消息可能像下列的样子:
{msg/topic [:rooms 1 :guests] msg/type :add-name :name "Alice"}
The orange part might be updated when receiving messages from the system where staff sign in and out for work. Messages like this:
当工作人员因为工作登入和退出系统,会从系统中接收到消息,这时图中的橙色部分可能会更新。消息像如下所列:
{msg/topic [:staff :on-duty] msg/type :sign-in :name "Ann"}
Both of these parts of the model are updated by transform functions. The sign-in transform function is shown below.
模型的两个部分会由变换函数进行更新。该登入变换函数如下所示。
(defn sign-in [old-value message]
(assoc old-value (:name message) {:checkin (js/Date.)}))
The transform routing table would now look like this:
变换路由表现在看起来像这样:
[[:add-name [:rooms :* :guests] add-name]
[:sign-in [:staff :on-duty] sign-in]]
We can now create a function which takes rooms and on-duty staff and returns a new map of staff assignments.
现在,我们可以创建一个函数,它接受房间和值班人员,并返回一个新的人员分配的map。
(defn assign-rooms [_ {:keys [rooms staff]}]
(let [s (sort (keys staff))
r (sort (keys rooms))]
(reduce (fn [a [k v]]
(update-in a [k] (fnil conj #{}) v))
{}
(map #(vector %1 %2) (cycle s) r))))
In Pedestal, such a function would be called a derive function. It derives one value in the information model from other values. There is one output value and there can be multiple input values.
在立柱上,这样的函数会被称为派生函数。它从其他值的派生信息模型的一个值。存在一个输出值,也可能存在多个输入值。
Derive functions update a location in the information model. A derive function takes two arguments: the old value at the location which is being updated and its inputs. In the example above the old value is ignored and the inputs are passed as a map which contains rooms and staff keys. The return value is a map where the keys are staff members and the values are a set of room numbers that this staff member is to look after.
导出函数更新信息模型中的一个位置。一个导出函数有两个参数:一个正准备更新的位置上的旧值和它的输入。在上面的的例子中旧值被忽略,输入作为包含房间和员工键的映射来传入。返回值是一个映射,其中的键是员工,值是一组由这名员工照看房间号。
When we describe a Pedestal application, we provide a set of derive functions. Each derive function is described by a set of inputs, an output path and a function.
当我们描述了一个立柱应用时,我们提供了一组派生函数。每个派生函数通过一组输入,一个输出路径和一个函数加以描述。[#{[:rooms] [:staff :on-duty]}
[:staff :assignments]
assign-rooms]
This will pass a map for the inputs but it is not the map we want. The passed map will contain a complete report of what has changed. To get a map with only the inputs we can use this instead.
这将传入一个输入的映射,但它不是我们想要的映射。传递的映射将包含了一份哪些发生了变化的完整的报告。为了得到一个只含输入的映射,我们可以用这个来代替。
[#{[:rooms] [:staff :on-duty]}
[:staff :assignments]
assign-rooms
:map]
This will give us a map of inputs but the keys will be [:rooms] and [:staff :on-duty]. That is not ideal. We can use a map to specify inputs which allows us to define the keys that will be used.
这将给我们一个输入的映射,但其键将为[:rooms] 和 [:staff :on-duty]。这不够理想。我们可以用一个映射来指定输入,它允许我们定义将要使用的键。
[{[:rooms] :rooms [:staff :on-duty] :staff}
[:staff :assignments]
assign-rooms
:map]
To learn more about derive functions, see the section of the tutorial on derived values<https://github.com/pedestal/app-tutorial/wiki/Derived-Values>.
要了解更多有关派生函数的信息,请参阅本教程上派生值的部分。
Continue functions 持续函数
A continue function is like derive except that it does not directly update the model. Continue functions take a single argument, the map of inputs (the same thing as the second argument to a derive function) and return a collection of transform messages which are processed within the same transaction.
一个持续函数就像是一个派生函数,只不过它不直接更新模型。持续函数需要一个参数,输入的映射(同样的东西在派生函数中它是第二个参数),并返回在同一个事务内处理的一组变换消息。
Continue functions allow for arbitrary recursion.
持续函数允许任意的递归
For an example of a use case for continue functions see the app-tutorial<https://github.com/pedestal/app-tutorial/wiki/Start-a-Game#using-a-continue-function-to-set-focus-based-on-state>.
关于一个持续函数用例的例子请参阅应用程序教程。
Effect functions 效用函数
Effect functions are just like emit functions except that they return a collection of messages which are meant to be sent out of an application. These messages are usually sent to a back-end service.
除了它们返回的是要发到一个应用程序的外部的一组消息这部分不同外,效用函数就像是一个发射函数。这组消息通常发送到后端服务。
Messages produced by effect functions go on a separate queue called the effect queue.
效用函数产生的消息会放在一个称为效用队列的单独的队列中。
For examples of using effect functions, see the section of the app-tutorial on simulating effects<https://github.com/pedestal/app-tutorial/wiki/Simulating-Effects>.
关于使用效用函数的示例,请参阅此应用程序教程中模拟效果的部分。
Why are there five functions? Did we miss one? 为什么只有五类函数?我们有没有错过一些?
We didn't miss one. If anything we have some redundancy here.
我们不但没有错过一个,而且如果有的话,我们这里倒还是有一些冗余度。
One goal of Pedestal is to allow developers to use pure functions as much as possible when writing an application. Each of these functions does one thing. At a minimum, we need to do three things:
立柱的目标之一是让开发者编写应用程序时尽可能使用纯函数。这些函数只做一件事情。至少,我们需要做三件事情:
process input
derive values
generate output
处理输入
派生值
生成输出
We have provided two ways to derive values and generate output:
我们提供了两种方式来派生值和生成输出:
New values can be derived with
新值可以通过下列两种方式派生
derive 派生
continue 持续
Output can be generated with
输出可以通过下列两种方式生成
emit 发射
effect 效用
As with Clojure reference types, we supply the functions which generate new values from old values and we let the reference type take care of the hard work of coordination.
如同Clojure的引用类型,我们提供根据旧值生成新值函数,我们让引用类型来对付协调这种困难的工作。
Possible changes to this model 以后可能改变这种模式
It is likely that a future version of Pedestal will move emit and effect functions out of the core. We may also remove derive and use continue for recursion and dataflow. The output of the core would be the change report which is currently used internally to trigger flow. This will make the core of Pedestal more generally useful.
很可能立柱的未来版本将从核心内移除发射和效果函数。我们也可能删除派生函数并将持续函数用于递归和数据流。核心的输出将是目前在内部用于触发消息流的变化报告。这将使立柱的核心更通用。
Dataflow 数据流
We have now been introduced to the five types of functions: transform, emit, derive, continue and effect. Each is a pure function. transform and derive functions produce values which change the state of part of the information model. derive and emit functions are called when parts of the information model, which are inputs to these functions, are changed. All of the dependencies between functions and the information model are described in a data structure. The application which we have been imagining is described in the map below.
我们现在已经被引入了五种类型的函数:变换,发射,派生,持续和效用。每类都是一个纯函数。变换和派生函数产生哪些改变信息模型某一部分??的状态值。当信息模型的某些部分发生改变时就会调用派生和发射函数,这是输入到这些函数,。函数和信息模型之间的所有依赖关系在一个数据结构中进行描述。我们一直想象的应用在下面的映射中进行了描述。
(require '[io.pedestal.app :as app])
(def hotel-app
{:version 2
:transform [[:add-name [:rooms :* :guests] add-name]
[:sign-in [:staff :on-duty] sign-in]]
:derive #{[{[:rooms] :rooms [:staff :on-duty] :staff}
[:staff :assignments]
assign-rooms
:map]}
:emit [[#{[:rooms :*]
[:staff :assignments :*]} (app/default-emitter)]]})
The :version key is used to indicate the version of the dataflow description which is being used. Even though there is one dataflow engine, the format for describing connections between things may change.
:version键被用来表示正被使用的数据流描述的版本。即使有一个数据流引擎,用于描述事物之间的连接的格式也可能发生改变。
The content of the :derive and :emit sections have been described above. For a more complete example of an application description, see the app-tutorial<https://github.com/pedestal/app-tutorial/blob/master/tutorial-client/app/src/tutorial_client/behavior.clj>.
:derive 和 :emit部分已在上面描述。关于一个应用程序的描述更完整的示例,请参见应用程序教程。
The io.pedestal.app namespace contains a build function which takes the above map as an argument and returns an application.
io.pedestal.app命名空间包含一个build函数,它接受上述映射作为参数并返回一个应用。
(def app (app/build hotel-app))
We can then initialize an app with the begin function and then start to send it messages.
然后,我们可以通过调用begin函数初始化一个应用程序,然后开始给它发送消息。
(require '[io.pedestal.app.protocols :as p])
(app/begin app)
(p/put-message (:input app)
{msg/topic [:rooms 1 :guests] msg/type :add-name :name "Alice"})
Rendering 渲染
In each diagram above there is a box labeled Renderer. As mentioned above, this part of an application is responsible for consuming rendering instructions and making changes to the view. In Pedestal, anything which does this is a renderer.
在上面的各图中有一个方块标示为渲染器。如上所述,这部分的应用程序负责执行渲染指令,并更改视图。在立柱上,任何做这种事情的东西便是一个渲染器。
Pedestal provides a helper function for consuming the rendering queue name consume-app-model which is located in the io.pedestal.app.render namespace. This function takes an app object and a rendering function and will call the rendering function when there are new instructions to render.
立柱提供了一个名为consume-app-model的辅助功能,用来处理渲染队列,它位于io.pedestal.app.render命名空间内。这个函数接受一个应用程序对象和渲染函数,当有新的渲染指令出现时,将调用渲染函数。
A rendering function takes a collection of deltas and the input-queue as arguments.
渲染函数接受增量的集合和输入队列作为参数。
(defn example-renderer [instructions input-queue]
;; modify the DOM and wire events
)
With an app in hand, set this function as the renderer with
用手头的一个应用程序,设置此函数作为渲染器
(io.pedestal.app.render/consume-app-model app example-renderer)
There are many ways to handle rendering in a Pedestal application. Pedestal does provide some library support. For more information see the Rendering<https://github.com/pedestal/app-tutorial/wiki/Rendering> section of the tutorial.
在立柱应用中有很多方法来处理渲染。立柱确实提供了一些对渲染支持的库。欲了解更多信息,请参阅本教程的渲染部分。
Services 服务
Any useful application will need to communicate with back-end services. As mentioned above, effect functions can produce messages which are sent to back-end services. Any messages which are received from back-end services can be turned into transform messages and placed on the input queue.
任何有用的应用程序都需要与后端服务进行通信。正如上面提到的,效用函数可以产生发往后端服务的消息。从后端服务收到任何消息可以变成变换消息,并放置在输入队列中。
Anything which consumes messages from the effect queue and places new messages on the input queue is considered to be a service.
从效用队列接收消息并将在输入队列上放置新的消息的任何东西被认为是一种服务。
Pedestal provides a function to help with consuming the effects queue. This function is named consume-effects and is located in the io.pedestal.app namespace. It takes an app and services function as arguments.
立柱提供了一个函数来帮助消耗效用队列。这个函数被命名为consume-effects并位于io.pedestal.app命名空间中。它需要一个应用程序和服务函数作为参数。
A services function is a function which receives a message and the input queue.
一个服务函数是接收消息和输入队列的函数。
(defn example-services [message input-queue]
;; send message to a back-end service
;; arrange for callback to transform response and place it on the
;; input queue
)
This function can be configured to consume the effects queue with
这个函数可以按下列方式被配置来消耗效用队列
(app/consume-effects app example-services)
For more information, see the Services<https://github.com/pedestal/app-tutorial/wiki/Connecting-to-the-Service> section of the tutorial.
欲了解更多信息,请参见本教程的服务部分。
Service Docs 服务文档
Interceptors 拦截器
Ring Request Processing 振铃请求处理
Any discussion of Interceptors should start with two important facts:
拦截器的任何讨论应该从两个重要的事实开始:
1.Interceptors are more complex to write than ring middlewares.
2.Why would anyone ever choose to embrace this additional complexity?
1.编写拦截器比起振铃中间件更复杂。
2.为什么会有人选择去拥抱这种额外的复杂性?
Let's start by first examining Ring's approach to request processing.
让我们开始先检查振铃对请求处理的做法。
Ring middlewares embrace possibly the simplest abstraction for handling HTTP requests. The incoming request is modeled as a map of data, it is fed to a function which returns a response. The response is interpreted as a map of data, specific keys in the response are extracted and used to build an HTTP response which is sent back to the client. In this model, composition is achieved by using higher order functions of other functions. Conventionally, a wrap-functionality function is written which accepts a function of a request, and returns a new function of a request which exhibits the new composite functionality. Sessions are a good example; the wrap-session ring middleware accepts a handler function. It returns a new function which, when invoked, extracts data from the request to re-establish a map of session data, and associates this session data into the request map. This new request map is then passed to the wrapped handler, eventually producing a response. The response is examined for session data, which gets processed and eventually encoded in additional headers which will be sent back to the client. The modified response is returned out of the wrapping function, providing the composite behavior transparently to the wrapped function.
振铃中间件支持可能是处理HTTP请求最简单的抽象。传入的??请求被建模为数据的映射,它被馈送到一个函数,这个函数返回一个响应。响应被解释为数据的映射,在响应中的特定键被提取并用于生成发送回客户端的HTTP响应。在这个模型中,组合是通过用其它函数的高阶函数来实现的。通常,编写一个包装功能的函数,它接受一个请求的函数,并返回一个请求的新函数,这个函数展示了新的组合功能。会话是一个很好的例子,在包装会话上振铃中间件接受一个处理函数。调用时它返回一个新的函数,从请求中提取数据重建会话数据的一个映射,并将这些会话数据结合到请求的映射中。这个新的请求映射,然后传递到包装处理器,最终产生一个响应。检查响应的会话数据,该数据被处理,并最终增加附加头,其结果将被发送回客户端。修改后的响应由包装函数返回,由此提供了包装函数的复合行为的透明性。
This strategy works well and it is possible to compose many different concerns in web server processing isolated from each other, but it has an important limitation. Because the mechanism of composition is composite evaluation, the total composition of processing a request must occur within the context of one thread. While it is possible to suspend the thread until other processing is finished, there's no convenient mechanism to dissociate the request's processing from the thread which starts servicing it, and resume it later with another different thread. Having a large number of passive requests which can be serviced at a later time (e.g. long polling, server sent events, requests waiting for status from a long running process) consumes a commensurately large number of threads. Maintaining context over the life of the request's processing makes use of closure scopes and the call stack to retain values calculated before request processing to be used after a response has been generated.
这种策略效果很好,它可以构成web服务器处理中许多不同的关注点相互隔离,但它有一个重要的限制。由于组合的机理是复合赋值,处理请求的总组合必须发生在一个线程的上下文中。虽然直到其他处理完成挂起线程这是可能的,没有方便的机制来从启动服务的线程中分解请求的处理,并且从另一个不同的线程来恢复它。有大量的被动请求可以在以后的时间进行服务(如长轮询,服务器发送的事件,从一个长期运行的进程请求等待状态)消耗了相应的大量线程。在请求处理的生命期保持上下文利用封闭范围和调用堆栈来保留在请求处理之前计算的值以便产生响应后可以使用。
To summarize the qualities of ring request processing:
概括振铃请求处理的品质如下:
1.Composition is achieved by middlewares knowing about each other and conditionally calling the other middlewares they know about.
2.The information for chaining is hidden away in closure scope. An outside observer working with a ring middleware chain cannot see where it goes.
3.Responsibility of chaining behavior is diffused through all middlewares. Each middleware is responsible for calling the next in the chain.
4.Execution of the whole chain is bound to one thread.
1.组合由彼此了解,并有条件地调用其他的中间件的中间件来实现。
2.信息链隐藏在封闭的范围中。带振铃中间件链的旁观者看不到信息链在哪里。3.链行为的责任是通过所有中间件扩散的。每个中间件负责调用链的下一个中间件。
4.整个供应链的执行被绑定到一个线程。
Interceptor Execution Stages 拦截器执行阶段
Interceptors aim to explicitly solve the issue of request processing being coupled tightly to one thread. It does this with two mechanisms:
拦截器的目标是明确的解决请求处理的在一个线程紧密耦合的问题。它有两个机制来处理这个问题:
1.Interceptors operate on a context which explicitly retains all data associated with processing one request.
2.Interceptors allow the processing of one context to be paused in one thread, and resumed in another thread.
1.拦截器在其中明确保留与处理一个请求相关的所有数据的上下文中进行操作。
2.拦截器允许暂停在一个线程内进行的上下文的处理,并在另一个线程内恢复。
In order for interceptors to achieve this, they do not operate by invoking each other or by wrapping as higher ordered functions, but instead as members of an execution queue, where each interceptor is invoked and its return value retained to be invoked by the next interceptor. An ordered collection of interceptors to execute is referred to as a path. A path of interceptors will be executed by the interceptor engine by progressing through stages. There are five such stages of interceptor execution:
为了用拦截器来实现这一点,只要不通过彼此调用或通过包裹更高阶函数来操作,而是作为执行队列的成员,这儿每个拦截器被调用,其返回值保留到下一个拦截器被调用。拦截器执行的有序集合称为一个路径。拦截器的路径将被拦截引擎通过分阶段执行。存在拦截器执行的五个这样的阶段:Enter 进入
Leave 离开
Error 错误
Pause 暂停
Resume 恢复
The most conventional stages, and the ones end users are most likely to use, are enter and leave. As an interceptor path is processed, the enter stage of each interceptor is called with context in turn. This continues until calling the enter function of the last interceptor in the path. At this point, the leave stages of the interceptors are called in reverse order, that is, the first interceptor specified in its path will have its leave function called last.
最常规的以及那些最终用户最可能使用的阶段是进入和离开。当处理一个拦截器路径时,每个拦截器的输入阶段与上下文依次被调用。这种情况持续下去,直到调用路径中的最后一个拦截器的输入函数。此时,该拦截器的离开阶段按照相反的顺序被调用,即在此路径中指定的第一个拦截器将会在最后调用其离开函数。
Alternatively, an interceptor may call terminate, which will terminate execution of the path immediately and begin invoking leave stages. If the context contains a terminator predicate, as associated into a context with the terminate-with function, which returns true after the processing of any interceptor, the execution will terminate and the leave stages of interceptors will begin to be invoked.
另外,一个拦截器可以调用terminate,将立即终止执行的路径,并开始调用离开阶段。如果上下文包含一个终止谓词,当用terminate-with函数关联到一个上下文,经任何拦截器处理后将返回true,执行将终止并且拦截器的离开阶段将开始调用。
The error stage is used for exception handling. If during any stage an uncaught exception is thrown, then the interceptor framework will catch the exception, and call the immediately preceding interceptor with the context and the caught exception. If this interceptor rethrows the exception, it will be caught again and provided to the next most immediately preceding interceptor. If the interceptor returns a context, processing will continue by calling the leave functions of preceding interceptors, as if the last interceptor in the path had been reached.
错误阶段用于异常处理。如果在任何阶段未捕获的异常被抛出,那么拦截器框架将捕获该异常,并调用与上下文和捕获异常的前一个拦截器。如果该拦截器重新抛出异常时,它会被再次抓住,并提供给下一个最前一个拦截器。如果拦截器返回一个上下文,通过调用前面拦截器的离开函数处理将继续进行,仿佛路径中的最后一个拦截器已经到达。
During execution, an interceptor may revert to the pause state (most often using the with-pause macro). In this case, each interceptor in the path which has previously had its enter function called, has their pause function called in reverse order. When all of the pause functions have been called, the body of with-pause executes with the context resulting from all of the pause invocations. Finally, interceptor processing terminates in that thread, but the context upon which the interceptors had been processing may be retained in memory.
在执行过程中,一个拦截器可能恢复到暂停状态(通常通过使用with-pause宏)。在这种情况下,在该路径中的每个以前曾经调用其输入函数的拦截器,将按相反的顺序调用它们的暂停函数。当全部的暂停函数已被调用时,with-pause的执行体会从所有的暂停调用所产生的上下文中执行。最后,在该线程上拦截器处理终止,但拦截器已经处理的上下文可能会保留在内存中。
Any thread, including the originating thread, or a different thread which receives the context, may then resume interceptor execution. On resuming, the resume functions of each interceptor are called (in the same order as the enter functions were called), until returning to the point in the path after the interceptor which paused. The enter functions of further interceptors in the path are invoked as if no pause had occurred. A single context may pause and resume an arbitrary number of times.
任何线程,包括原始线程,或者一个接收上下文的不同的线程,可以恢复拦截器执行。在恢复时,每个拦截器的恢复函数被调用(在同一顺序作为输入函数来调用),直到它返回路径中的暂停拦截器后面的那一点。进一步在路径中拦截器输入函数被调用,就好像没有暂停的情况发生一样。一个单一的上下文可以暂停和恢复任意多次。
Request Processing Across Threads 跨线程的请求处理
This architecture allows for processing a single request across multiple threads. The thread which initially begins processing the request invokes the (with-pause) macro, which implicitly invokes the pause stage of all previous interceptors in the path, captures the resultant context, and binds it to the name provided in the binding form before executing the body. In the body, the context is made available to other threads through a concurrency construct (such as a concurrent identity like an atom, ref, or agent, or a concurrent processing form like a future or a delay). The body terminates, and the first thread terminates it's processing entirely. A new thread calls resume on the post pause context, which resumes interceptor execution with the context from the paused thread. The sse interceptor<https://github.com/pedestal/pedestal/blob/master/service/src/io/pedestal/service/http/sse.clj>, which creates a channel for servers to communicate with clients, demonstrates this pattern.
这种架构允许跨多个线程处理一个请求。最初开始处理请求的线程调用(with-pause)宏,它隐式调用路径上所有以前的拦截器的暂停阶段,抓住产生的上下文,绑定它到在执行体之前到绑定的形式提供的名字。在此执行体中,上下文是通过并行结构(如像一个原子,引用,或代理的并发身份,或像一个未来或延迟的并行处理形式)提供给其他线程。该执行体终止,那么第一个线程终止它的整个处理。一个新的线程调用恢复后的停顿上下文,它从暂停线程的上下文上继续执行拦截器。sse拦截器,它创建服务器与客户端沟通的渠道,演示了这种模式。
Adding and Removing Interceptors 添加和删除拦截器
As an interceptor path is traversed, the context is continually re-evaluated to determine what stage of which interceptor should fire next. The return value of one interceptor may itself be a context with a path where more interceptors have been added, where the total interceptor path can be examined or chained, or where additional terminators can be introduced. The routing interceptor<https://github.com/pedestal/pedestal/blob/master/service/src/io/pedestal/service/http/route.clj> uses this feature to add additional interceptors to the interceptor path after examining the incoming request to find a matching path to dispatch requests to.
当对一个拦截器路径进行遍历时,上下文被不断地重新评估,以确定下一步应触发哪个拦截器的哪个阶段。一个拦截器的返回值本身可能为具有路径的上下文,此上下文中添加了多个拦截器,其中总的拦截器路径进行检查或链接,或额外的终止器可以被引入路径的上下文。该路由拦截器使用此功能来添加其他拦截器到此拦截器路径,检查传入的请求以便找到一个匹配的路径派遣请求给它。
Compatibility with Ring 与振铃的兼容性
The Pedestal service infrastructure is designed to be Ring-compatible to the greatest extent possible. Specifically HTTP requests and responses are represented as Ring-style maps, but held in a wrapping Pedestal service context map.
立柱服务基础设施的设计尽最大可能与振铃兼容。具体的HTTP请求和响应表示为振铃式的映射,但由包装的立柱服务上下文映射持有。
All of the middlewares in Ring have been refactored so that in addition to the conventional wrap-xyz function for building a Ring-style middleware chain, there are xyz-request and xyz-response functions. These functions process requests and responses separately. The wrap-xys functions have been refactored to use the separate request and response processing functions.
所有在振铃中的中间件的被重构,使得除了常规的用于建立一个振铃式中间件链的wrap-xyz函数外,有xyz-request和xyz-response函数。这些函数分别处理请求和响应。wrap-xys函数被重构,以便使用分开的请求和响应处理函数。
The io.pedestal.service.http.ring-middlewares namespace defines interceptors that use the new Ring xyz-request and xyz-response functions, making all the standard Ring middlewares usable in Pedestal services.
中间件命名空间io.pedestal.service.http.ring 使用新的振铃式xyz-request和xyz-response函数定义拦截器,使得所有标准的振铃中间件在立柱服务中可以重用。
Compared with Ring Middleware 与振铃中间件的比较
Consider the nature of Pedestal Service's Interceptors as compared with Ring's Middlewares.
作为与振铃的中间件的对比考虑立柱服务的拦截器的下列性质。
1.Composition is achieved by placing a number of interceptors into a queue for execution. This queue is traversed first in first out order.
1.合成是通过放置一些拦截器到执行队列中来实现的。此队列首按照先进先出的顺序进行遍历。
2.Ordering and presence are clearly visible, it is data that can be worked with using all of Clojure's tools for working with seqs and PersistentQueues.
2.次序和呈现都清晰可见,它是一些可以使用所有的能够与seqs和PersistentQueues一道工作的Clojure的工具进行处理的数据。
3.Responsibility of chaining behavior is delegated to the interceptor framework.
3.链的行为的责任委托给拦截器框架。
4.There exist tools for manipulating the chaining behavior at run time (e.g. terminating execution, enqueuing additional interceptors). Implementing consistent chaining behavior does not require diffusing that behavior through each interceptor.
4.在运行时中操纵链行为(如终止执行,额外添加到队列拦截器)存在相应的工具。实施一致的链行为不需要将行为扩散到每个拦截器。
5.Interceptors can know the entire queue of execution as it stands at their time of execution. It is possible to know what the last planned interceptor is before getting there. It's possible to know what the last interceptor which executed is. Most ring middlewares are only aware of what the immediately next middleware is.
5.在拦截器的执行期间,拦截器能够知道执行的整个队列。在计划中的最后的拦截器未执行之前它就可以知道。它也知道已经执行的最后一个拦截器。大多数振铃中间件只知道紧邻的中间件是什么。
6.Execution of the whole chain can be paused in one thread, then resumed in another different thread. A paused interceptor queue is data and does not consume a thread.
6.整条链的执行可以在一个线程中暂停,然后在另一个不同的线程重新执行。暂停的拦截器队列是数据无需占用一个线程。
7.Interceptors do not need any information about any other interceptors to execute correctly. This information is available in the context, but it is not required.
7.拦截器不需要任何其他的拦截器任何正确执行的信息。这些信息在上下文中可以获得,但它不是必需的。
Interceptor Debugging 拦截器调试
The interceptor framework logs the entry of each interceptor's function, in each stage, at the debug log level. The interceptor framework logs the entry and the context it is currently processing at the entry of each interceptor's function in each stage at the trace log level. This is a useful way to determine what exactly is happening between the interceptor framework and the interceptors it is firing, but it is extremely verbose.
当设置为调试日志级别时,在每一个阶段,拦截器框架记录每个拦截器的函数的入口。此时拦截器框架将记录当前正在进行处理的每个拦截器的函数所在每个阶段的条目和上下文。这是一个有用的方法来确定究竟是在拦截器框架中还是它触发的拦截器中发生的事情,但它非常冗长。
Definition 定义
An interceptor is one instance of an Interceptor record or a map with :enter, :leave, :pause, :resume, and :error keys. An interceptor-fn is a function which returns an interceptor.
拦截器是一个拦截记录或者具有:enter, :leave, :pause, :resume, 和:error 键的映射的一个实例。一个interceptor-fn是一个返回一个拦截器的函数。
Pedestal includes macros for defining interceptors, and for defining interceptor-fns. These macros are conveniences for attaching a symbolic name and docstring to an interceptor.
立柱包含用于定义拦截器和定义interceptor-fn的宏。这些宏是方便用于将符号名和文档字符串连接到一个拦截器。
There are functions and macros for constructing interceptors that deal with Ring requests and responses:
存在一些函数和宏用于构建用于处理振铃请求和响应的拦截器:
。The on-request function and defon-request macro define an interceptor with an enter function that takes a Ring request and returns a modified Ring request.
on-request函数和defon-request宏定义一个带有进入函数的拦截器,它接受一个振铃的请求,并返回修改后的振铃请求。
。The on-response function and defon-response macro define an interceptor with a leave function that takes a Ring response and returns a modified Ring response.
on-response函数和defon-response宏定义一个带有离开函数的拦截器,它接受一个振铃的响应,并返回修改后的振铃响应。
。The middleware function and defmiddleware macro define an interceptor with both an enter and a leave function.
middleware函数和defmiddleware宏定义了一个带有进入函数和离开函数的拦截器。
There are equivalent functions and macros for building interceptors that deal directly with context maps, named before and defbefore, after and defafter, and around and defaround.
存在等价的函数和宏用于创建直接处理上下文的映射的拦截器,分别命名为before和defbefore,after和defafter,以及around和defaround。
Existing Ring handler functions used at the end of middleware chains that take a request and return a response can be referred to directly from a service's route table. The routing infrastructure will wrap them in an interceptor using the handler function. Alternatively, you can wrap them yourself using the defhandler macro.
用在中间件链的尾部,获取一个请求并返回一个响应现有的振铃处理函数,可直接从服务的路由表中进行引用。路由基础设施将此处理函数包装成一个拦截器。或者,您可以使用defhandler宏自己包装它们。
These macros also flag the vars they create with metadata identifying them as either interceptors or interceptor-fns. Other pieces in the Pedestal framework make use of this metadata to make intelligent decisions about how to work with these vars.
这些宏也标识与它们通过标识它们为拦截器或interceptor-fn的元数据的变量。在立柱框架的其他部分利用这些元数据来做出关于如何使用这些变量的明智决定。????
Porting Ring Code 移植振铃代码
You can port Ring code to Pedestal by:
你可以通过下列方式移植振铃代码到立柱中:
。Reusing handler functions directly in a route-table (or by wrapping them in a call to handler or defhandler)
。在路由表中直接重用处理器函数(或在调用handler或defhandler时对它们进行包装)
。Refactoring middleware functions into two separate functions, one that modifies a request and one that modifies a response and using them to define an interceptor using the on-request, middleware or on-response functions or the defon-request, defmiddleware or defon-response macros.
。重构中间件函数为两个独立的函数,一个是用于修改请求,一个用于修改响应,并且使用它们定义一个用在on-request、 middleware 或 on-response 函数 或 defon-request、defmiddleware 或 defon-response 宏中的拦截器。
You can build an interceptor that works directly with a context map, providing access to both Ring maps.
您可以构建一个直接与上下文映射协同工作,提供访问两个振铃映射的拦截器。
。If you are using Compojure for routing requests, rewrite your route definitions using the terse routing format (see Service Routing<http://pedestal.io/documentation/service-routing/>). Any Ring middlewares that run before your Compojure routes should be replaced by interceptors that run before routing. Any middlewares specified in your Compojure routes should be replaced by interceptors referenced directly in your route definitions. There are interceptors provided for all the existing Ring middlewares. They are defined in the io.pedestal.service.http.ring-middlewares namespace.
如果您使用的是Compojure路由请求,使用简洁的路由格式(见服务路由)重写你的路由定义。您的Compojure路由之前运行的任何振铃中间件应该由路由之前运行的拦截器所取代。在你的Compojure路由指定的任何中间件应直接在路由定义中由引用拦截器所取代。所有现有的振铃中间件都提供了相应的拦截器。它们在io.pedestal.service.http.ring-middlewares命名空间中定义。
Service Route 服务路由
Introduction 引言
Pedestal's HTTP service plumbing provides a mechanism for routing requests through an ordered list of interceptors that handle them. The same infrastructure supports generating URLs that, when used with the appropriate HTTP verb, cause a request to be routed to a particular interceptor list. This document describes how routing and URL generation work.
立柱的HTTP服务管道提供了一种机制,通过处理它们的拦截器的有序列表的路由请求。相同的基础架构支持生成URL,与相应的HTTP动词使用时,会导致请求被路由到特定的拦截器列表。本文档介绍了路由和生成URL是如何工作的。
Routing Tables 路由表
Pedestal's HTTP routing and URL generation features are driven by a route table. A route table is a sequence of routes. A route is a map containing criteria for matching an HTTP request and an ordered list of interceptors to invoke on a request that matches a particular route.
立柱的HTTP路由和URL生成功能是由路由表驱动的。路由表是路由的序列。路由是一个包含匹配的HTTP请求和拦截器调用的一个特定的路由匹配要求的有序列表的标准映射。
A route matching is based on: 路由匹配的依据是:
URL scheme URL模式
HTTP method HTTP方法
Host header Host标头
URL path URL路径
Constraints on param values in URL path and/or query string 在URL路径和/或查询字符串中参数值的约束
Defining route tables 定义的路由表
A route table is simply a data structure; in our case, it is a sequence of maps. The structure caters to the needs of matching and dispatching of requests, and as such has a great deal of repeated and derived data intended for use in that process. Creating the data structure in its final, verbose form by hand would be very tedious.
路由表是一个简单的数据结构,在我们的例子中,它是映射的序列。结构适合匹配和请求调度的需求,因此存在大量旨在用在这一过程中的重复和派生数据。最后通过手工以详细的形式创建这些数据结构将是非常乏味的。
We've built a simpler, terse form for route tables. The terse form is also a data structure, albeit more explicitly hierarchical; Writing a route table in the terse form is easier because information is not explicitly duplicated. Instead, child nodes implicitly inherit relevant route data from their ancestors.
我们已经为路由表建立了一种简单的,简洁的形式。这种简洁形式也是一种数据结构,尽管具有更明确的分层,以这种简洁形式建立一个路由表更容易,因为信息显然不会重复。相反,子节点隐式地继承它们的祖先相关的路由数据。
It is important to note that the terse form is a convenient way to define route tables, nothing more. It is always expanded to the more verbose structure - a sequence of maps - before use. While a convenient authoring format, it is not directly used to route requests or generate URLs.
简洁的形式是一种定义路由表的方便方式,注意到仅此而已是很重要的。在使用前它总是扩展到更详细的结构 - 映射的序列。而作为便利的制作格式,不能直接用来请求路由或生成URL。
The terse format 简洁的格式
In the terse format, a route table is a vector of vectors, each describing an application. Each application vector can contain the following optional elements:
在简洁的格式内,一个路由表是一个向量的向量,其中每一个都描述一个应用程序。每个应用程序向量可以包含以下可选的元素:
.a keyword identifying the application by name
.required URL scheme(s)
.a required host header value, e.g., example.com
.one or more nested vectors specifying routes
。一个关键字标识该应用程序的名字
。所需的URL模式
。所需的主机标头值,例如,example.com
。一个或多个嵌套向量指定的路由
Here is a simple “Hello World” example:
下面是一个简单的“Hello World”的例子:
[[:hello-world :http "example.com"
["/hello-world" {:get hello-world}]]]
In this case, the following HTTP request:
在这种情况下,下面的HTTP请求:
GET /hello-world HTTP/1.1
Host: example.com
would be routed to the hello-world interceptor.
将被路由到的hello-world拦截器。
A request to a different host (either DNS name or IP address) or using HTTPS would not be routed, unless the application's specification were loosened, like so:
到不同的主机(无论是DNS名称或IP地址)或使用HTTPS的请求不会被路由,除非放松了应用程序的规格,就像这样:
[[:hello-world
["/hello-world" {:get hello-world}]]]
The application's name, :hello-world, is optionally used during URL generation, and can also be omitted, leaving this:
应用程序的名字,:hello-world,在URL生成中可以有选择地使用,并且也可以被省略,留下这样的:
[[["/hello-world" {:get hello-world}]]]
This is the smallest possible example of a useful route table.
这是一个有用的路由表最小的可能的例子。
Verb maps 动词映射
In most cases, a nested vector specifying routes contains a path and a verb map (there are exceptions, explained below). The verb map contains keys corresponding to HTTP verbs. All verbs are supported, along with the special value :any, indicating a match to any HTTP verb. Each verb represents a different route. The values in the verb map represent the “destination interceptor”. Additional intermediate interceptors may also be invoked, as described below.
在大多数情况下,一个指定路由的嵌套向量包含路径和一个动词映射(也有例外,在下面解释)。动词映射包含与HTTP动词相对应的键。所有的动词都支持,连同特殊值:any,表示匹配任何HTTP动词。每个动词都代表不同的路由。在动词映射的值代表了“目标拦截器”。附加的中间拦截器也可以被调用,正如下所述。
The value for a key in a route's verb map specifies a route's destination interceptor. The value can be:
在一个路由的动词映射中一个键的值指定路由的目的地拦截器。该值可以是:
.a symbol that resolves to one of:解析为下列之一的符号:
.a function that accepts a Ring request map and returns a Ring response map (i.e. a Ring handler)
它接受一个振铃请求映射并返回一个振铃响应映射(即振铃处理器)的一个函数
.an interceptor 一个拦截器
.a function that returns an interceptor and is marked with metadata ^{:interceptor-fn true}
一个函数,它返回一个拦截器,并标有元数据^{:interceptor-fn true}
.a vector containing the following: 包含下列部分的向量:
.an optional keyword that names the route, for use in URL generation
一个在URL生成时使用的命名路由的可选的关键字
.a value that is either:一个值,该值可以是下列二者之一:
.a symbol interpreted as described above 一个如上述解释的符号
.a list that evaluates to either:一个列表,可以取下列值之一:
.a function that accepts a Ring request map and returns a Ring response map (i.e. a Ring handler)
它接受一个振铃请求映射并返回一个振铃响应映射(即振铃处理器)的一个函数
.an interceptor 一个拦截器
.an optional vector of interceptors, described below
一个可选拦截器的向量,如下所述
.an optional map of constraints, described below
一个可选约束的向量,如下所述
The following sections explains how these values are used.
以下各节说明了如何使用这些值。
Terse format expansion 简洁的格式扩展
A terse route definition must be expanded to a full route table before it can be used. There are two ways to do this:
在可以使用之前,一个简洁的路由定义必须扩大到全路由表。有两种方法可以做到这一点:
。the io.pedestal.service.http.route.definition/expand-routes function
。函数io.pedestal.service.http.route.definition/expand-routes
。the io.pedestal.service.http.route.definition/defroutes macro
。宏io.pedestal.service.http.route.definition/defroutes
The expand-routes function takes a terse route definition data structure as input and returns a route table. For example:
函数expand-routes需要一个简洁的路由定义的数据结构作为输入并返回一个路由表。例如:
(defn hello-world [req] {:status 200 :body "Hello World!"})
(def route-table
(expand-routes '[[["/hello-world" {:get hello-world}]]]))
Note that the terse data structure is quoted, making hello-world a symbol. It resolves the hello-world function, which takes a Ring request and returns a Ring response.
请注意,用""括起的简洁的数据结构,将hello-world看成一个符号。它解析hello-world函数,此函数接受一个振铃请求并返回一个振铃响应。
The defroutes macro is equivalent to calling expand-routes with a quoted data structure:
该defroutes宏等效于用引号的数据结构调用函数expand-routes:
(defroutes route-table
[[["/hello-world" {:get hello-world}]]])
A quoted terse route definition is read at load time and is static after that. In some cases, you may need to dynamically generate routes. Here is an example:
带引号的简洁路由定义在加载时和加载之后是静态的,故它是只读的。在某些情况下,您可能需要动态生成路由。下面是一个例子:
(defn hello-fn [who]
(fn [req] (ring.util.response/response (str "Hello " who)))
(defn make-routes-for-who [who]
(expand-routes
`[[["/hello" {:get [:hello-who (hello-fn ~who)]}]]]))
(def route-table (make-routes-for-who "World"))
In this case, the make-routes-for-who function takes an argument, who, that it uses to configure the resulting routes. It generates the terse route data structure using Clojure's syntax quote mechanism, splicing in the value of who where it is needed.
在这种情况下,make-routes-for-who函数接受一个参数,who,它用来配置所产生的路由。此函数使用Clojure的语法引用机制,在需要who的值的地方进行拼接生成简洁的路由数据结构。
In some cases, you may want to assemble the terse data structure without quoting it at all. Here is an example:
在某些情况下,您可能需要组装完全无引号的简洁的数据结构。下面是一个例子:
(defn hello-world [req] {:status 200 :body "Hello World!"})
(def route-table
(expand-routes
[[["/hello-world"
{:get [(handler ::hello-world hello-world)]}]]]))
In this case, the hello-world symbol is resolved to the hello-world function as the data structure is built. The handler function (defined in io.pedestal.service.interceptor) takes the function and builds an interceptor from it, to meet the requirement that a value in a verb map must be a symbol, an interceptor, or a list.
在这种情况下,当数据结构已经建立时,符号hello-world被解析为hello-world函数。该处理函数(在io.pedestal.service.interceptor中定义)取此函数,并根据它建立了一个拦截器,以满足在一个动词映射中的值必须是一个符号,一个拦截器,或列表的要求。
Alternatively, hello-world can be defined as an interceptor directly, using the io.pedestal.service.interceptor/defhandler macro:
或者,通过使用io.pedestal.service.interceptor/defhandler宏,可以直接将hello-world定义为拦截器:
(defhandler hello-world [req] {:status 200 :body "Hello World!"})
(def route-table
(expand-routes
[[["/hello-world" {:get hello-world}]]]))
Or, hello-world can be quoted, making it a symbol again:
或者,hello-world可以被引号括起,再次使其成为一个符号:
(defn hello-world [req] {:status 200 :body "Hello World!"})
(def route-table
(expand-routes
[[["/hello-world" {:get 'hello-world]}]]]))
The expand-routes function is more flexible, but also harder to use than the defroutes macro. The latter is preferred in most cases.
此expand-routes函数更灵活,但同时比defroutes宏更难使用。在大多数情况下后者是最好的。
Advanced route definitions 高级路由定义
This section describes path parameters, hierarchical route definitions, intermediate interceptors and constraints.
本节介绍了路径参数,分层路由定义,中间拦截器和约束。
Path parameters 路径参数
Segments of a route's path may be parameterized simply by prepending ':' to the segment's name:
一个路由的路径片段可以简单地在段的名字前面加上':'进行参数化:
(defn hello-who [req]
(let [who (get-in req [:path-params :who])]
(ring.util.response/response (str "Hello " who))))
(defroutes route-table [[["/hello/:who" {:get hello-who}]]])
As with Ring, Rails, etc, the path parameters are parsed and added to the request's param map.
作为具有振铃,Rails等,路径参数被解析并添加到请求的参数映射中。
Splat parameters are also supported. They are defined using a final path segment prepended with '*', like this:
此外,还支持图示参数。它们使用在最后的路径段前面加上'*'的方式来定义,像这样:
[[["/hello/:who" {:get hello-who}]
["/*other" {:get get-other-stuff]]
Hierarchical route definitions 分层路由定义
Route definitions in the terse form are hierarchical. A route definition may contain zero or more child routes. A child route inherits information from it's ancestors.
简洁形式的路由定义是分层的。路由定义可以包含零个或多个子路由。一个子路由从它的祖先继承信息。
Here is an example showing how a path is inherited:
下面是一个显示路径是如何继承的例子:
[[["/order" {:get list-orders :post create-order}
["/:id" {:get view-order :put update-order}]]]]
This defines these four routes:
这个例子定义了这四个路由:
GET /order
POST /order
GET /order/:id
PUT /order/:id
The “/order” path segment is inherited by the child routes.
在“/order”路径段由子路由继承。
It is worth noting that this same structure could be defined without hierarchy:
值得注意的是,这种相同的结构可以不用分层结构来定义:
[[["/order" {:get list-orders :post create-order}]
["/order/:id" {:get view-order :put update-order}]]]
This would produce the same four routes.
这将产生相同的四个路由。
Interceptors 拦截器
Every route definition includes an interceptor path that will be executed for any request that matches the route. By default, a route's interceptor path contains one interceptor, the route's handler. For instance, the four routes defined in the previous section have the following interceptor paths:
每一个路由定义包含了将任何该路由匹配的请求被执行拦截器的路径。默认情况下,路由的拦截器路径中包含一个拦截器,即该路由的处理器。例如,在上一节中定义的四个路由有以下拦截器的路径:
GET /order => [list-orders]
POST /order => [create-order]
GET /order/:id => [view-order]
PUT /order/:id => [update-order]
Route definitions can specify additional interceptors to include in the interceptor path for a given route. These interceptors function as before, after or around filters for specific routes. They are specified using a vector marked with ^:interceptors metadata. The values specified in the interceptors vector may be either a symbol that resolves to one of:
路由定义可以在一个给定路由的拦截器路径中指定其他拦截器。对特定的路由这些拦截器函数作为之前,之后或前后的过滤器。它们由一个标记为^:interceptors元数据的向量来指定。在拦截器向量中指定的值可以是一个符号可解析为下列之一:
。a symbol that resolves to one of: 一个符号可解析为下列之一:
。an interceptor 一个拦截器
。a function that returns an interceptor and is marked with metadata ^{:interceptor-fn true} 一个函数,它返回一个拦截器,并用元数据^{:interceptor-fn true} 来标识
。a function that accepts a Ring request map and returns a Ring response map (i.e. a Ring handler) 一个函数,它接受一个振铃映射的请求并返回一个振铃响应映射(即一个振铃处理器)
。a list that evaluates to either: 一个列表,计算结果为:
。an interceptor 一个拦截器
。a function that accepts a Ring request map and returns a Ring response map (i.e. a Ring handler) 一个函数,它接受一个振铃映射的请求并返回一个振铃响应映射(即一个振铃处理器)
Here is an example:
下面是一个例子:
[[["/order" {:get list-orders :post create-order}
["/:id"
^:interceptors [load-order-from-db]
{:get view-order :put update-order}]]]]
With this additional interceptor specified for the second two routes, the interceptor paths become:
用为第二组的两个路由指定的这个额外的拦截,拦截器路径成为:
GET /order => [list-orders]
POST /order => [create-order]
GET /order/:id => [load-order-from-db view-order]
PUT /order/:id => [load-order-from-db update-order]
Any number of interceptors may be specified as an order sequence:
任何数量的拦截器可以作为一个有序的序列来指定:
[[["/order" {:get list-orders :post create-order}
["/:id"
^:interceptors [load-order-from-db verify-order-ownership]
{:get view-order :put update-order}]]]]
In this case, for requests that match the “/order/:id” route, the load-order-from-db interceptor will run before the verify-order-ownership interceptor, then the appropriate handler, view-order or update-order, will run.
在这种情况下,对于匹配“/order/:id”路由的请求,load-order-from-db拦截器将在verify-order-ownership拦截器之前运行,然后合适的处理程序,view-order或update-order,将运行。
Interceptors may be specified at multiple levels of the hierarchy. Like paths, interceptors are inherited. Inherited interceptors always come first in the interceptor path for a given route.
拦截器可以在多个级别的层次结构中指定。像路径,拦截器可以被继承。对于一个给定的路由来说,继承的拦截器永远是第一位的拦截器路径。
[[["/order"
^:interceptors [verify-request]
{:get list-orders :post create-order}
["/:id"
^:interceptors [verify-order-ownership load-order-from-db]
{:get view-order :put update-order}]]]]
This definition produces the following routes and interceptor paths:
这个定义将产生以下路由和拦截器路径:
GET /order => [verify-request list-orders]
POST /order => [verify-request create-order]
GET /order/:id => [verify-request verify-order-ownership load-order-from-db view-order]
PUT /order/:id => [verify-request verify-order-ownership load-order-from-db update-order]
Inherited interceptors always precede a route definition's own handlers in its interceptor path.
继承的拦截器在它的拦截器路径上总是先于路由定义的自身处理程序。
Constraints 约束
A route may specify constraints on path parameters and query string parameters. Constraints are tested when a request is being matched against a route. If the request does not satisfy a route's constraints, it is not considered a match.
一个路由可以指定路径参数和查询字符串参数的约束。当一个请求与一个路由匹配时便会进行约束测试。如果该请求不符合一个路由的约束,此时不能认为是匹配的。
Constraints are specified as a map marked with ^:constraints metadata. The keys in the map are path parameters or query string parameters. The values are regular expressions used for testing parameter values.
约束通过标记^:constraints元数据的映射来指定。在此映射上的键是路径参数或查询字符串参数。这些值是用于测试参数值的正则表达式。
Here is an example of how constraints can be used:
下面是可以使用怎样的约束的一个示例:
["/user" {:get list-users :post add-user}
["/:user-id"
^:constraints {:user-id #"[0-9]+"}
{:put update-user}
[^:constraints {:view #"long|short"}
{:get view-user}]]]
This defines four routes:
它定义了四种路由:
GET /user => [list-users]
POST /user => [add-user]
PUT /user/:user-id => [update-user], but only if :user-id matches [0-9]+
但前提是::user-id匹配[0-9]+
GET /user/:user-id => [view-user], but only if :user-id matches [0-9]+ and there is a “view” query param whose value is either “long” or “short”
但前提是::user-id匹配[0-9]+并且存在一个“view”查询参数,它的值为“long” 或者 “short”
Note that constraints can be used in addition to or in place of a path when defining a child route. In that case, they must appear as the first item in the child route vector.
注意,当定义的子路由时,约束可以用于附加或代替一个路径。在这种情况下,它们必须出现在该子路由向量的第一项。
Like intermediate interceptors, constraints are inherited by child routes.
像中间拦截器,约束由子路由继承。
Routing 路由
Once a route table is defined, it can be used to create a router. The io.pedestal.service.http.route/router function takes a route table as input and returns an interceptor that handles routing.
一旦一个路由表被定义,它可以被用来创建一个路由处理器。 该io.pedestal.service.http.route/router函数需要一个路由表作为输入,并返回一个处理路由的拦截器。
(defn hello-world [req] {:status 200 :body "Hello World!"})
(defroutes route-table
[[["/hello-world" {:get hello-world}]]])
(def router (router route-table))
When a routing interceptor's enter function is invoked, it attempts to match the incoming request against each route in the route table in turn. If a route matches, the routing interceptor adds all the interceptors for the given route to the current interceptor path. They will be invoked by the interceptor engine after the router's function completes. It also adds the selected route to the interceptor context map so that other interceptors can know which route was selected.
当调用一个路由拦截器的输入函数时,它会按相反次序尝试针对路由表中的每个路由与传入的请求进行匹配。如果匹配,路由拦截器将给定路由的所有的拦截器加到当前的拦截器路径上。在路由处理器函数完成后,它们将被拦截器引擎调用。它也增加了选择的路由到拦截器上下文映射中,这样其他的拦截器能知道哪条路由被选中。
If no route matches, the router simply returns the current interceptor context without modification.
如果没有匹配的路由,路由处理器只是简单地返回当前没有修改的拦截器上下文。
During development it is useful to be able to reprocess route definitions without restarting your server. If you call the router function and pass a function that returns a route table, it will be called every time the routing interceptor is used. This allows your Web server to use the latest compiled routes without restarting.
在开发过程中它能够重新处理路由定义而不需要重新启动您的服务器是非常有用的。如果你调用了路由处理器函数,并传入一个函数,返回一个路由表,每当使用路由拦截器时它都会被调用。这允许您的Web服务器来使用最新编译的路由而无需重新启动。
(def router (router #(deref #'route-table)))
If you are using the Pedestal service template for lein, it provides a default route table and handles setting up a routing interceptor as one of the steps of building a service. It also configures use of the latest compiled routes when running in the repl.
如果您正在使用针对 lein 立柱的的服务模板,它提供了一个默认的路由表,并且将处理建立路由拦截器作为构建服务的步骤之一。它还配置使用在REPL执行时,最新的编译路由。
URL generation URL生成
In addition to routing, route tables are also used for URL generation. You can request a URL for a given route by name and specify parameter values to fill in. This section describes URL generation, starting with how routes are named.
除了路由,路由表也用于URL生成。你可以针对给定的路由按名称和填入指定参数值来请求一个URL。本节介绍URL生成,先从怎样命名路由开始。
Route names 路由命名
Every route has a name, represented as a keyword. Route names are implicit, where possible. For routes that specify destination interceptors using symbols, the name is the fully-qualified symbol name expressed as a keyword.
每个路由都有一个名字,表示为一个关键字。在可能的情况下,路由命名是隐式的。对于指定目的地拦截器的路由使用符号,名字是表示为一个关键字的完全限定符号名。
For routes that specify destination interceptors directly as interceptor values, the route-name is the name of the interceptor.
对于指定目的地拦截器直接作为拦截器值的路由,该路由的名字是拦截器的名字。
For interceptors defined using the defbefore, defafter, defaround, defon-request, defhandler and defon-response macros in the io.pedestal.service.interceptor namespace, the name is the interceptor's fully-qualified symbol name expressed as a keyword.
在io.pedestal.service.interceptor命名空间中用defbefore, defafter, defaround, defon-request, defhandler 宏定义的拦截器,其名字是表示为一个关键字的拦截器的完全限定符号名。
For interceptors defined using the before, after, around, on-request, handler and on-response functions in the io.pedestal.service.interceptor namespace, the name is the keyword passed to the function, if any.
在io.pedestal.service.interceptor命名空间内使用before, after, around, on-request, handler 和on-response 函数定义的拦截器,其名字是传递给函数的关键字,如果有的话。
For routes that specify interceptors indirectly as lists to be evaluated, no route name can be implicitly assigned.
对于间接通过被求值的列表指定拦截器的路由而言,不能隐式指派路由的名字。
You can specify an explicit route name for any route by adding a keyword as the first item in the vector specified as the value of a given HTTP verb for a given route. Explicit route names take precedence over implicit names. For routes that cannot be given an implicit name, an explicit name must be provided or an exception will be thrown during route expansion.
您可以通过添加一个关键字来为一个给定的路由作为给定的HTTP动词值向量中的第一项指定明确的路由名字。显式路由的名字优先于隐式名字。对于不能给出一个隐式的名字的路由,必须提供一个明确的名字否则在路由扩展时将抛出异常。
Here is an example.
下面是一个例子。
(require '[orders :as o])
(defroutes routes
[[["/order"
^:interceptor [verify-request]
{:get o/list-orders
:post [:make-an-order o/create-order]}
["/:id"
^:interceptors [o/verify-order-ownership o/load-order-from-db]
{:get o/view-order
:put o/update-order}]]]])
In this case, the destination interceptors are all specified as symbols in the orders namespace. The route names are listed below:
在这种情况下,目标拦截器都被指定为在该命名空间中的命令符号。路由名字如下:
GET /order => :orders/list-orders
POST /order => :make-an-order
GET /order/:id => :orders/view-order
POST /order/:id => :orders/update-order
The second route specified an explicit route name, :make-an-order, which takes precedence over the implicit name for that route, :orders/create-order.
第二条路由指定了一个显式路由名字,:make-an-order,这将优先于这条路由的隐式名字,:orders/create-order。
The io.pedestal.service.http.route/print-routes helper function prints route verbs, paths and names at the repl. When in doubt, you can use it to find route names.
在REPL中io.pedestal.service.http.route/print-routes辅助函数打印路由动词,路径和名字。如有疑问,您可以用它来寻找路由的名字。
URL generation URL生成
The io.pedestal.service.http.route/url-for-routes function takes a route table returns a function that accepts a route-name (and optional arguments) and returns a URL that can be used in a hyperlink.
io.pedestal.service.http.route/url-for-routes函数接受一个路由表返回一个函数,此函数接受一个路由名字(和可选参数),并返回可以在超链接中使用的URL。
(def url-for (route/url-for-routes route-table))
(url-for ::o/list-orders) ;; use keyword derived from symbol to name route
;;使用符号派生的关键字命名路由
;; => "/order"
(url-for :make-an-order) ;; use specified route name 使用指定路由名字
;; => "/order"
An url-for function can populate parameters in a route. Parameter values are passed as additional arguments:
一个url-for函数可以用一个路由填充参数。参数值作为额外的参数来传递:
(url-for :view-order :params {:id 10})
;; => "/order/10"
Entries in the :params map that do not correspond to parameter values in a route's path are added to the returned URL as query string parameters. Alternatively, :path-params and :query-params can be used to specify parameter values independently.
在不对应于一个路由的路径参数值的:params映射中的条目被添加到返回的URL作为查询字符串参数。或者,:path-params和:query-params可以用来独立地指定参数值。
Request-specific URL generation 请求特定的URL生成
A route table provides the basis for URL generation. A request map can act as an additional basis. This allows for the generation of absolute vs relative URLs, depending on the URL a request was sent to and how specific a route-table is about an application's host name and supported schemes.
路由表提供了基础URL生成。请求映射可以作为一个额外的基础。这允许绝对的和相对的URL的生成,这取决于请求发送的URL和如何指定一个关于应用程序的主机名和支持模式的的路由表。
When the routing interceptor matches a request to a route, it creates a new URL generator function that closes over the request. It adds the function to the interceptor context and the Ring request map, using the key :url-for.
当路由拦截器匹配对一个路由的请求时,它会创建一个新的URL生成函数,此函数结束此请求。使用键值:url-for将此函数加到拦截器上下文和振铃请求映射中。
The request-specific URL generator function is also dynamically bound to a private var in the io.pedestal.service.http.route namespace. The io.pedestal.service.http.route/url-for function calls the dynamically bound function.
请求特定的URL生成函数也动态地绑定到一个私有变量中,此变量在io.pedestal.service.http.route命名空间中。io.pedestal.service.http.route/url-for函数调用这个动态绑定的函数。
The io.pedestal.service.http.route/url-for function can be called from any thread that is currently executing an interceptor. If you need to use a request-specific URL generator function elsewhere, extract :url-for from the context or request map and propagate it as needed.
io.pedestal.service.http.route/url-for函数可以从任何当前正在执行一个拦截器的线程内调用。如果您需要其他地方使用一个请求特定的URL生成函数,从上下文或请求映射提取:url-for,并根据需要传播它。
Verb smuggling 谓词走私
The url-for functions only return URLs. The io.pedestal.service.http.route/form-action-for-routes function takes a route table returns a function that accepts a route-name (and optional arguments) and returns a map containing a URL and an HTTP verb.
url-for函数只返回URL。io.pedestal.service.http.route/form-action-for-routes函数输入一个路由表返回一个函数,此函数接受一个路径名(和可选的参数),并返回包含一个URL和HTTP谓词的映射。
(def form-action (route/form-action-for-routes routes-table))
(form-action :make-an-order)
;; => {:action "/order" :method :post}
A form action function will (by default) convert verbs other than GET or POST to POST, with the actual verb added as a query string parameter named _method:
表格操作函数(默认情况下)转换成谓词,而不是GET或POST到POST,与增加实际谓词作为名为_method的查询字符串参数:
(form-action ::o/update-order :params {:id 20})
;; => {:action "/order/20?_method=put" :method :post}
This behavior can be disabled (or enabled for url-for functions) and the query string parameter name can be changed. All of these settings can be modified when an url-for or form-action function is created or when it is invoked.
这种行为可以被禁用(或启用通过url-for函数)和查询字符串参数名称可以更改。当一个url-for或form-action函数创建或调用时,所有这些设置都可以进行修改。
Asynchronous processing 异步处理
The interceptor infrastructure supports asynchronous processing. When processing a request, you can pause and release the current thread. Later, another thread can resume processing and deliver a response. This allows long running work to occur without blocking a Web server thread.
拦截器基础架构支持异步处理。当处理一个请求时,你可以暂停并释放当前线程。然后,另一个线程可以继续处理,并提供一个响应。这允许发生长时间运行的工作出现而不会阻塞Web服务器线程。
Here is some code that needs to wait for something to happen:
下面是一些代码,需要等待一些事情发生:
(interceptors/defhandler takes-time [req]
(response (wait-for-something-that-takes-a-long-time req)))
(defroutes routes
[[["/takes-time" {:get takes-time}]]])
Requests for /takes-time will block until the wait call completes, stopping the Web server thread from handling other work.
直到等待调用完成,/takes-time的请求将会阻塞,因为处理其他工作导致停止Web服务器线程。
This code can be made rewritten so that it releases the Web server thread, as shown below:
这段代码可重写使得它释放Web服务器线程,如下所示:
(interceptors/defbefore takes-time [{req :request :as context}]
;; give back the web server thread after doing work in body
(io.pedestal.service.impl.interceptor/with-pause
[paused-context context]
;; kick off another thread to wait
(future
;; wait
(let [result (wait-for-something-that-takes-a-long-time req)]
;; resume with context that includes response
(io.pedestal.service.impl.interceptor/resume
(assoc paused-context :response (response result)))))))
(defroutes routes
[[["/takes-time" {:get takes-time}]]])
The io.pedestal.service.impl.interceptor/with-pause macro pauses interceptor processing and returns the calling thread. The io.pedestal.service.impl.interceptor/resume function resumes processing on another thread.
io.pedestal.service.impl.interceptor/with-pause宏暂停拦截处理,并返回调用的线程。io.pedestal.service.impl.interceptor/resume函数在另一个线程恢复处理。
It is important to note that pausing and resuming an interceptor path only affects how threads are used. While paused, the context for the ongoing request still holds the pipe back to the client.
重要的是要注意到,暂停和恢复一个拦截器路径只影响线程是如何被使用的。暂停时,上下文进行中的请求仍占用返回给客户端的管道。
The simple handler function that took a Ring request map and returned a Ring response map is replaced with a before function, that takes and returns a Pedestal interceptor context. This is required because the with-pause macro and resume functions work with contexts, not Ring requests and response.
输入一个振铃请求映射并返回一个振铃响应映射的简单的handler函数被替换为before函数,此函数输入和返回一个立柱拦截器上下文。这是必需的,因为with-pause宏和resume函数需要此上下文,没有振铃的请求和响应。
In this example, the Web server's thread is freed, but another worker thread is blocked. This is not a requirement. Here is an example that releases the Web server thread without using another thread.
在此示例中,Web服务器的线程被释放,但另一个工作线程被阻塞。这不是必须的。下面是释放Web服务器线程而无需使用另一个线程的例子。
(defn resume-fn [context result]
(io.pedestal.service.impl.interceptor/resume
(assoc context :response (response result))))
(interceptors/defbefore takes-time [{:request req :as context}]
;; give back the web server thread after doing work in body
(io.pedestal.service.impl.interceptor/with-pause
[paused-context context]
;; give context to some other code that will resume it
;; by extracting resume-fn and calling it, passing result
(give-context-to-code-that-will-store-it
(assoc paused-context :resume-fn (partial resume-fn paused-context)))))
(defroutes routes
[[["/takes-time" {:get takes-time}]]])
In this case, the assumption is that some other piece of code will use the stored context to complete the request at some point in the future, perhaps in response to an event that has occurred. This approach can be used to implement long-polling. In fact, this is how Pedestal's built in support for server-sent events works.
在这种情况下,假设是在将来的某个时候一些其他的代码段将使用存储的上下文来完成请求,或许响应已发生的事件。这种方法可以被用来实现长轮询。事实上,这是立柱对server-sent事件如何处理的内置支持。
(Note that both these examples use functions in the io.pedestal.service.impl.inteceptor namespace. The code in this namespace is subject to change.)
(请注意,这两个例子中使用在io.pedestal.service.impl.inteceptor命名空间中的函数。此命名空间中的代码今后可能会有所变更。)
For more about streaming, see Streaming Responses<http://pedestal.io/documentation/service-streaming>. For more about SSE, see Server-Sent Events<http://pedestal.io/documentation/service-sse>.
欲了解更多有关流的信息,见流响应。欲了解更多有关SSE的信息,请参见服务器发送的事件。
Server-Sent Events 服务器发送的事件
The Pedestal service library includes support for Server-Sent Events, or SSE. The SSE protocol defines a mechanism for sending event notifications from a server to a client using a form of long polling. However, unlike conventional long polling, SSE does not send each event as a separate response, with an expectation that the client will make a new request in between each one. Rather, SSE sends all its events as part of a single response stream. The stream is kept alive over time by sending events and/or periodic heart-beat data. In the event that the stream is closed for some reason, the client can send a request to re-open it and events notifications can continue. All modern browsers have built in support for SSE via the EventSource API.
这一立柱服务库包括对服务器发送的事件,即SSE的支持。SSE协议定义了一种机制,使用长轮询的形式从服务器到客户端发送事件通知。然而,与传统的长轮询不同,SSE不发送每个事件作为一个单独的回应,并期望客户端将在每一个之间会发一个新的请求。相反,SSE将其所有的事件,作为一个单一的响应流的一部分。随着时间的推移,通过发送事件和/或定期的心跳数据,流维持其活动。由于某些原因流可能被关闭,此时客户端可以发送请求重新打开它,流的事件通知可以继续进行下去。所有现代的浏览器都通过EventSource API内置了对SSE支持。
You can setup an event source endpoint by defining a route that maps requests to an interceptor returned from the io.pedestal.service.http.sse/start-event-stream function. The resulting SSE interceptor processes a request by:
你可以通过定义一个路由来建立事件源端点,此路由映射请求到一个从io.pedestal.service.http.sse/start-event-stream函数返回的拦截器。由此产生的SSE拦截器通过下列方式处理一个请求:
。pausing interceptor execution (see Service Async<http://pedestal.io/documentation/service-async>)
。暂停拦截器执行(见服务异步)
。sending the appropriate HTTP response headers to tell the client that an event stream is starting.
。发送相应的HTTP响应头告诉客户端一个事件流正在启动。
。initiating a timed heartbeat to keep the connection alive
。启动一个定时心跳保持连接活动
and
和
。passing the current interceptor context to the stream-ready-fn function that was passed as an argument to start-event-stream (previously called sse-setup, which is still supported for backward compatibility).
。传递当前拦截器上下文给stream-ready-fn函数,而此函数被作为参数传递给start-event-stream(以前称为sse-setup,为了向后兼容仍然支持)。
The stream-ready-fn is responsible for using the context or storing it for later use by some other piece of code.
stream-ready-fn负责使用此上下文,或将其存储起来以便在以后一些其他的代码段能够使用它。
Events can be sent to the client using the io.pedestal.service.http.sse/send-event function. It takes the context passed to the stream-ready-fn, an event name and event data as arguments.
事件可以通过使用io.pedestal.service.http.sse/send-event函数发送给客户端。这需要传递上下文以及事件名称和事件数据作为参数给stream-ready-fn。
Note that in the current implementation the data must be a string. This restriction will be removed in the future.
需要注意的是在当前的实现中数据必须是一个字符串。在将来会移除这一限制。
If a client closes its connection, the call to send-event will throw a java.io.IOException. The calling code should catch it and clean up the streaming context.
如果客户端关闭它的连接,调用send-event将抛出一个IOException。调用代码应该抓住它,并清理流的上下文。
When a streaming context is no longer needed, either because there are no more events to send or the connection was broken by the client, it must be cleaned up by calling the io.pedestal.service.http.sse/end-event-stream function.
当一个流上下文不再需要,要么是因为没有更多的事件来发送,要么连接已由客户端断开,必须通过调用io.pedestal.service.http.sse/end-event-stream函数将其清理掉。
Here is an example that shows how an SSE streaming context is created and used.
下面是一个例子,说明如何创建和使用SSE流上下文。
(def a-stored-streaming-context (atom nil))
(defn clean-up []
(when-let [streaming-context @a-stored-streaming-context]
(reset! a-stored-streaming-context nil)
(end-event-stream streaming-context)))
(defn notify [event-name event-data]
(when-let [streaming-context @a-stored-streaming-context]
(try
(send-event streaming-context event-name event-data)
(catch java.io.IOException ioe
(clean-up)))))
(defn store-streaming-context [streaming-context]
(reset! a-stored-stream-context streaming-context))
(defroutes route-table
[[["/events" {:get [::events (start-event-stream store-streaming-context)]}]]])
The store-streaming-context function is passed to start-event-stream. It is called when the streaming context is ready. It stores the streaming context in the a-stored-streaming-context atom. (A more sophisticated implementation would store it in a map keyed by some other information in the context, e.g., a cookie.)
store-streaming-context函数传入start-event-stream。当流上下文准备好时它被调用。它存储的流上下文在原子a-stored-streaming-context中。 (一个更复杂的实现是将其存储在一个映射中,此映射包含此上下文中一些其他信息作为键,例如,一个cookie。)
The notify function is used to send an event to the client attached to the stream. If an IOException is thrown, it catches it and cleans up the streaming context by calling clean-up. The clean-up function can also be used directly when there are no more events to send.
notify函数,用于发送一个事件到连接到流的客户端。如果抛出IOException,它捕获它,并通过调用clean-up函数清理流上下文。当没有更多的事件来发送时,可以直接调用clean-up函数。
It is important to understand that the server-sent events infrastructure uses the low-level streaming mechanism described here<http://pedestal.io/documentation/service-streaming>. As such, it is subject to the major limitation of that approach: once events have been streamed, any interceptors that post-process the response from the SSE interceptor will not be able change what was sent on the wire. Interceptor paths that use the SSE interceptor should not include interceptors that expect to alter the response, e.g., by setting cookies. If they do, the interceptor that handles writing response data to the HTTP output stream will log an exception indicating that the data could not be sent. An interceptor can use the io.pedestal.http.impl.servlet-interceptor/response-sent? function to determine whether a response has already been sent by an SSE (or equivalent) interceptor.
理解在server-sent事件基础结构使用此处<>所描述的低级别的数据流的机制是很重要的。因此,它会具有这种方法的主要局限性:一旦事件被放入流中,任何对来自SSE拦截器的响应进行后处理的拦截器将无法改变被送往电线上的信息上的任何东西。使用SSE拦截器的拦截器路径不应包括期望改变响应的拦截器,例如,设置cookie的拦截器。如果这样做,处理响应写入数据到HTTP输出流的拦截器将记录一个异常,表明该数据无法发送。拦截器可以使用io.pedestal.http.impl.servlet-interceptor/response-sent?函数来确定是否响应已经发送了一个SSE(或等价的)的拦截器。
Streaming Responses 流响应
In some cases, you may want to stream large responses back to clients. Streaming makes more efficient use of space when you are dealing with large response bodies. It also allows a client to start consuming data as quickly as possible.
在某些情况下,您可能需要将大的响应传回给客户端。当你正在处理较大的响应体时,流可以更有效地利用空间。它还允许客户端尽可能快地处理数据。
Here are some examples of potentially large values that you might want to stream back to clients:
下面是你可能要使用流这种方式将大量数据传回客户端潜在的一些示例:
。the contents of a lazy sequence
。一个惰性序列的内容
。a file or resource stored on disk
。存储在磁盘上的文件或资源
。a byte stream retrieved from some service, e.g., an image on S3
。从一些服务(例如S3上的一张图片)检索出的字节流
In these cases, an interceptor can return a context containing a Ring response map whose body is an instance of a type that supports streaming:
在这些情况下,拦截器可以返回一个上下文,此上下文包含一个振铃响应的映射,其主体是一个支持流的类型的实例:
。clojure.lang.IPersistentCollection - the collection is printed directly into the HTTP output stream. Lazy sequences implement IPersistentCollection.
。clojure.lang.IPersistentCollection - 此集合直接打印到HTTP输出流。惰性序列实现IPersistentCollection接口。
。clojure.lang.Fn - the function is invoked when the response body is written. The function is passed the HTTP output stream and can write directly to it. You can use the io.pedestal.service.http.impl.servlet-interceptor/write-body-to-stream function to do this, but you don't have to.
。clojure.lang.Fn - 当写入响应体时此函数被调用。此函数传入的HTTP输出流并且可以直接写入此流中。您可以使用io.pedestal.service.http.impl.servlet-interceptor/write-body-to-stream函数来做到这一点,但这不是必须的。
。java.io.File or java.io.InputStream - the contents are copied in chunks to the HTTP output stream in chunks using clojure.java.io/copy when the response body is written.
。java.io.File or java.io.InputStream - 当响应体块被写入时,内容将使用clojure.java.io/copy按块的方式复制到HTTP输出流中。
。io.pedestal.service.http.impl.servlet-interceptor/WriteableBody - the instance writes itself to the HTTP output stream.
。io.pedestal.service.http.impl.servlet-interceptor/WriteableBody - 该实例本身写入到HTTP输出流。
The thread completing the interceptor path will write the body out to the HTTP response stream. If the request is processed synchronously, the work will be done on the Web server thread. If the request is processed asynchronously, the work will be done on whatever thread resumed processing (see Service Async<http://pedestal.io/documentation/service-async>).
完成拦截器路径的线程会写body到HTTP响应流。如果请求是同步处理,该工作将在Web服务器的线程上完成。如果请求是异步处理,该工作将由任何进行恢复处理的线程来完成(见服务异步)。
Here is an example of an interceptor that returns an arbitrarily large volume of data.
下面是返回任意大数据量的一个拦截器的例子。
(defn range [req]
(let [limit (get-in req [:query-params :limit] 10)]
(response (range limit))))
(defroutes route-table
[[["/range" {:get range}]]])
Because the return value is an IPersistentCollection, it is streamed out to the client.
因为返回值是一个IPersistentCollection,它流出来给客户端。
Low-level streaming 低级流
In order to use the streamed response body types described in the previous section, you must complete processing of a request. In doing so, you are dedicating a thread (either a Web server thread or your own worker thread) to copying data into the HTTP output stream. After that streaming process completes, no more data can be sent.
为了使用上一节中描述的流响应体的类型,你必须完成一个请求的处理。在这样做时,你使用一个专门的线程(无论是Web服务器的线程或您自己的工作线程)来将数据复制到HTTP输出流。该流处理完成后,没有更多的数据可以发送。
There may be cases, however, where you want to stream data back but then continue processing. You can do that from within an interceptor by invoking the io.pedestal.service.http.impl.servlet-interceptor/write-response and io.pedestal.service.http.impl.servlet-interceptor/write-response-body functions.
但是,也可能存在这种情况,在这里你需要数据流回来,但然后继续处理。你可以在一个拦截器中通过调用io.pedestal.service.http.impl.servlet-interceptor/write-response和io.pedestal.service.http.impl.servlet-interceptor/write-response-body函数来做这件事。
The write-response function takes a context map a Ring response map. It sets the HTTP response status and headers. It also writes the body, if any, to the HTTP output stream.
write-response函数接受一个上下文映射、一个振铃响应映射。它设置HTTP响应状态和头。如果有的话,它也写体到HTTP输出流中。
The write-response-body function takes a context map and an object to write to it.
write-response-body函数接受一个上下文映射和一个对象,并对此对象进行改写。
It is important to understand that writing response data this way essentially removes you from the normal interceptor processing path, in the sense that, while the interceptors in your path may post-process your response, no changes they make to it will be sent on the wire. So, for instance, you cannot use this technique with an interceptor that post-processes your response and sets a cookie value. The updated cookie will never be sent.
重要的是要明白,写响应数据的这种方式基本上不能在正常拦截器处理路径上进行,在这个意义上,当在你的路径中的拦截器虽说可以对你的响应进行后处理,但这不会对在流上发送的数据有任何改变。所以,举例来说,你不能使用这种拦截器技术,对您的响应进行后处理并设置一个cookie值。更新后的cookie将永远不会被发送。
If an interceptor returns a response after a response has been streamed out, the interceptor that handles writing response data to the HTTP output stream will log an exception indicating that the data could not be sent. An interceptor can use the io.pedestal.http.impl.servlet-interceptor/response-sent? function to determine whether a response has already been sent by an SSE (or equivalent) interceptor.
如果一个拦截器返回一个在已经流出去的响应之后的响应,处理响应写入数据到HTTP输出流的拦截器将记录一个异常,表明该数据无法发送。一个拦截器可以使用io.pedestal.http.impl.servlet-interceptor/response-sent?函数来确定是否响应已经通过一个SSE(或等价的)的拦截器发送了。
This functionality is used to implement Server-Sent Events, documented here<http://pedestal.io/documentation/service-sse>.
这一功能是用来实现Server-Sent事件,文档在这里。
Deploying a WAR File 部署WAR文件
Java web applications are commonly deployed to web containers, also called Servlet containers or application servers. Web containers manage the runtime environment and lifecycle of an application. Popular web containers include Jetty<http://www.eclipse.org/jetty/>, Tomcat<http://tomcat.apache.org/>, and JBoss<http://www.jboss.org/>. Some hosting services such as AWS Elastic Beanstalk<http://aws.amazon.com/elasticbeanstalk/> also function as web containers.
Java Web应用程序通常被部署到Web容器中,Web容器也称为Servlet容器或应用服务器。 Web容器管理运行环境和应用程序的生命周期。流行的Web容器包括Jetty,Tomcat和JBoss。一些托管服务,例如AWS弹性魔豆也可作为Web容器。
To deploy an application to a web container, you must package it as a Web Archive<http://docs.oracle.com/javaee/6/tutorial/doc/bnaby.html> (WAR) file. A WAR file is just a Java JAR<http://docs.oracle.com/javase/tutorial/deployment/jar/> file with a particular internal layout and a .war extension.
要将应用程序部署到Web容器中,必须将其打包为Web归档(WAR)文件。 WAR文件也是一个一个Java JAR文件,其具有一个特定的内部布局和一个.war扩展名的文件。
Building a WAR File 构建一个WAR文件
In the future, Pedestal will include automated tools to accomplish this, but for now you can follow these steps to build a WAR file:
在未来,立柱将包括自动化工具来完成这件事,但现在你可以按照以下步骤来构建一个WAR文件:
Step One: Prepare Your Project 步骤一:准备好你的项目
A WAR file contains all the dependencies of your application, but it should not contain any dependencies which would conflict with the web container itself.
WAR文件包含你的应用程序的所有依赖关系,但它不应该包含会与Web容器本身冲突的任何依赖关系。
By default, new Pedestal applications have dependencies declared for running an “embedded” web container, either Tomcat or Jetty. You need to remove those lines before building the WAR.
默认情况下,新的立柱应用声明对运行的“内嵌式”Web容器的依赖,无论是Tomcat还是Jetty。您需要构建WAR文件前删除这些行。
Edit your project.clj file, find the vector after :dependencies, and delete the following entries (if they exist):
编辑您的project.clj文件,找到在:dependencies后的向量,并删除以下条目(如果存在的话):
[io.pedestal/pedestal.jetty "0.1.0"]
[io.pedestal/pedestal.tomcat "0.1.0"]
Step Two: Configure Your Servlets 第二步:配置您的Servlet
A WAR file must contain a special file called web.xml which tells the web container how to configure the application.
一个WAR文件必须包含名为web.xml的特殊文件,这个文件告诉Web容器如何配置应用程序。
In the root directory of your project, create a file called web.xml like the following example:
在您的项目的根目录下,创建一个名为web.xml类似于下面的示例文件:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0"
metadata-complete="true">
<description>Pedestal HTTP Servlet</description>
<display-name>Pedestal HTTP Servlet</display-name>
<servlet>
<servlet-name>PedestalServlet</servlet-name>
<servlet-class>io.pedestal.servlet.ClojureVarServlet</servlet-class>
<init-param>
<param-name>init</param-name>
<param-value>YOUR_APP_SERVER_NAMESPACE/servlet-init</param-value>
</init-param>
<init-param>
<param-name>service</param-name>
<param-value>YOUR_APP_SERVER_NAMESPACE/servlet-service</param-value>
</init-param>
<init-param>
<param-name>destroy</param-name>
<param-value>YOUR_APP_SERVER_NAMESPACE/servlet-destroy</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>PedestalServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
Replace YOUR_APP_SERVER_NAMESPACE with the “server” namespace generated for your application by the Pedestal template. If your app is called “foo” then this would be foo.server. The template-generated source file at src/foo/server.clj should contain functions named servlet-init, servlet-destroy, and servlet-service.
用由立柱模板生成的应用程序中的“server”命名空间替换YOUR_APP_SERVER_NAMESPACE。如果您的应用程序被称为“foo”,那么这将是foo.server。在SRC/foo/server.clj模板生成的源文件应包含名为servlet-init,servlet-destroy和servlet-service的函数。
Note: The url-pattern in the XML above must match the routes your Pedestal application is expected to handle and must match the conventions of your web container. For example, if you are deploying an application in the file foo.war, a typical web container will expect it to have <url-pattern>/foo/*</url-pattern>. Therefore your application should have routes that begin with /foo/.
注:在上面的XML文件中的url-pattern必须与你的立柱应用程序期望处理的路由相匹配并且与您的Web容器的约定相匹配。例如,如果您正在文件foo.war上部署一个应用程序,一个典型的Web容器都期望拥有<url-pattern>/foo/*</url-pattern>。因此,您的应用程序应该有一个以/foo/开始的路由。
If your application will be deployed as the “root” web application in the container, then you should have <url-pattern>/*</url-pattern> as shown in the example above. Consult the documentation of your web container for more details on Servlet mappings.
如果您的应用程序在容器中作为“root”Web应用程序来部署的话,那么你应该如上面的例子那样有<url-pattern>/*</url-pattern>。请咨询您的Web容器的有关servlet映射的详细信息的文档。
Step Three: Build the WAR 第三步:构建WAR
Working from a standard Unix shell (or Cygwin on Windows), run the following commands in your project directory.
从一个标准的Unix shell(或者Cygwin的Windows上)上进行工作,在在你的项目目录下运行下面的命令。
You will need both Leiningen<https://github.com/technomancy/leiningen> and Maven<http://maven.apache.org/> installed to run these commands.
您将需要安装Leiningen和Maven来运行这些命令。
Getting dependencies: 获取相关的依赖:
lein clean
lein pom
mvn dependency:copy-dependencies -DoutputDirectory=target/war/WEB-INF/lib
Copying your application files:
复制您的应用程序文件:
mkdir - target/war/WEB-INF/classes
cp -R src/* config/* target/war/WEB-INF/classes
Copying web.xml:
复制web.xml:
cp web.xml target/war/WEB-INF
Creating the WAR file:
创建WAR文件:
jar cvf target/YOUR_APPLICATION.war -C target/war WEB-INF
Replace YOUR_APPLICATION with the name of your application.
用您的应用程序的名字替换YOUR_APPLICATION。
Step Four: Deploy 第四步:部署
Consult the documentation of your web container to find out how to deploy an application from a WAR file. For many web containers, it may be a simple matter of copying the .war file into a “webapps” directory.
请咨询您的Web容器的文档,以了解如何用一个WAR文件部署应用程序。对于许多Web容器,它可能是一件简单的事情,就是将.war文件复制到一个“webapps”目录中。