Escrevendo Clojure Webapps com anel

Escrevendo Clojure Webapps com anel

1. Introdução

Ring is a library for writing web applications in Clojure. Ele suporta tudo o que é necessário para criar aplicativos da Web com todos os recursos e possui um ecossistema próspero para torná-lo ainda mais poderoso.

Neste tutorial, daremos uma introdução ao Ring e mostraremos algumas das coisas que podemos alcançar com ele.

O Ring não é uma estrutura projetada para criar APIs REST, como tantos kits de ferramentas modernos. It’s a lower-level framework to handle HTTP requests in general, com foco no desenvolvimento tradicional da web. No entanto, algumas bibliotecas são construídas sobre ela para oferecer suporte a muitas outras estruturas de aplicativos desejadas.

2. Dependências

Antes de começarmos a trabalhar com o Ring, precisamos adicioná-lo ao nosso projeto. The minimum dependencies we need are:

Podemos adicioná-los ao nosso projeto de Leiningen:

  :dependencies [[org.clojure/clojure "1.10.0"]
                 [ring/ring-core "1.7.1"]
                 [ring/ring-jetty-adapter "1.7.1"]]

Podemos então adicionar isso a um projeto mínimo:

(ns ring.core
  (:use ring.adapter.jetty))

(defn handler [request]
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body "Hello World"})

(defn -main
  [& args]
  (run-jetty handler {:port 3000}))

Aqui, definimos uma função de manipulador - que abordaremos em breve - que sempre retorna a string "Hello World". Além disso, adicionamos nossa função principal para usar este manipulador - ele ouvirá solicitações na porta 3000.

3. Conceitos principais

Leiningen tem alguns conceitos principais em torno dos quais tudo se desenvolve: Solicitações, Respostas, Manipuladores e Middleware.

3.1. solicitações de

Solicitações são uma representação de solicitações HTTP recebidas. Ring represents a request as a map, allowing our Clojure application to interact with the individual fields easily. Há um conjunto padrão de chaves neste mapa, incluindo, mas não se limitando a:

  • :uri - O caminho URI completo.

  • :query-string – A string de consulta completa.

  • :request-method - o método de solicitação, um de:get, :head, :post, :put, :delete ou:options.

  • :headers - Um mapa de todos os cabeçalhos HTTP fornecidos para a solicitação.

  • :body - UmInputStream que representa o corpo da solicitação, se presente.

Middleware may add more keys to this map as well conforme necessário.

3.2. Respostas

Da mesma forma, as respostas são uma representação das respostas HTTP de saída. Ring also represents these as maps with three standard keys:

  • :status - O código de status para enviar de volta

  • : _headers_ - Um mapa de todos os cabeçalhos HTTP para enviar de volta

  • : _body_ - o corpo opcional para enviar de volta

Como antes,Middleware may alter this between our handler producing it and the final result getting sent to the client.

Ring also provides some helpers to make building the responses easier.

A mais básica delas é a funçãoring.util.response/response, que cria uma resposta simples com um código de status de200 OK:

ring.core=> (ring.util.response/response "Hello")
{:status 200, :headers {}, :body "Hello"}

There are a few other methods that go along with this for common status codes - por exemplo,bad-request,not-found eredirect:

ring.core=> (ring.util.response/bad-request "Hello")
{:status 400, :headers {}, :body "Hello"}
ring.core=> (ring.util.response/created "/post/123")
{:status 201, :headers {"Location" "/post/123"}, :body nil}
ring.core=> (ring.util.response/redirect "https://ring-clojure.github.io/ring/")
{:status 302, :headers {"Location" "https://ring-clojure.github.io/ring/"}, :body ""}

Também temos o métodostatus que converterá uma resposta existente para qualquer código de status arbitrário:

ring.core=> (ring.util.response/status (ring.util.response/response "Hello") 409)
{:status 409, :headers {}, :body "Hello"}

We then have some methods to adjust other features of the response similarly - por exemplo,content-type, header orset-cookie:

ring.core=> (ring.util.response/content-type (ring.util.response/response "Hello") "text/plain")
{:status 200, :headers {"Content-Type" "text/plain"}, :body "Hello"}
ring.core=> (ring.util.response/header (ring.util.response/response "Hello") "X-Tutorial-For" "example")
{:status 200, :headers {"X-Tutorial-For" "example"}, :body "Hello"}
ring.core=> (ring.util.response/set-cookie (ring.util.response/response "Hello") "User" "123")
{:status 200, :headers {}, :body "Hello", :cookies {"User" {:value "123"}}}

Observe quethe set-cookie method adds a whole new entry to the response map. This needs the wrap-cookies middleware para processá-lo corretamente para que funcione.

3.3. Manipuladores

Agora que entendemos solicitações e respostas, podemos começar a escrever nossa função de manipulador para amarrá-la.

A handler is a simple function that takes the incoming request as a parameter and returns the outgoing response. O que fazemos nesta função depende inteiramente de nossa aplicação, desde que se ajuste a este contrato.

No mais simples, poderíamos escrever uma função que sempre retorna a mesma resposta:

(defn handler [request] (ring.util.response/response "Hello"))

Também podemos interagir com a solicitação, conforme necessário.

Por exemplo, poderíamos escrever um manipulador para retornar o endereço IP de entrada:

(defn check-ip-handler [request]
    (ring.util.response/content-type
        (ring.util.response/response (:remote-addr request))
        "text/plain"))

3.4. Middleware

Middleware is a name that’s common in some languages but less so in the Java world. Conceitualmente, eles são semelhantes aos Servlet Filters e Spring Interceptors.

No Ring, o middleware se refere a funções simples que envolvem o manipulador principal e ajustam alguns aspectos dele de alguma maneira. Isso pode significar a mutação da solicitação de entrada antes de ser processada, a mutação da resposta de saída após ser gerada ou potencialmente não fazer nada mais do que registrar quanto tempo levou para processar.

Em geral,middleware functions take a first parameter of the handler to wrap and returns a new handler function with the new functionality.

The middleware can use as many other parameters as needed. Por exemplo, poderíamos usar o seguinte para definir o cabeçalhoContent-Type em cada resposta do manipulador empacotado:

(defn wrap-content-type [handler content-type]
  (fn [request]
    (let [response (handler request)]
      (assoc-in response [:headers "Content-Type"] content-type))))

Lendo isso, podemos ver que retornamos uma função que leva uma solicitação - este é o novo manipulador. Isso chamará o manipulador fornecido e retornará uma versão mutada da resposta.

Podemos usar isso para produzir um novo manipulador simplesmente encadeando-os:

(def app-handler (wrap-content-type handler "text/html"))

Clojure also offers a way to chain many together in a more natural way – by the use of Threading Macros. Essa é uma maneira de fornecer uma lista de funções para chamar, cada uma com a saída da anterior.

In particular, we want the Thread First macro,*→*. Isso nos permitirá chamar cada middleware com o valor fornecido como o primeiro parâmetro:

(def app-handler
  (-> handler
      (wrap-content-type "text/html")
      wrap-keyword-params
      wrap-params))

Isso então produziu um manipulador que é o manipulador original envolvido em três funções de middleware diferentes.

4. Manipuladores de escrita

Agora que entendemos os componentes que compõem um aplicativo Ring, precisamos saber o que podemos fazer com os manipuladores reais. These are the heart of the entire application and is where the majority of the business logic will go.

Podemos colocar qualquer código que desejamos nesses manipuladores, incluindo acesso ao banco de dados ou chamar outros serviços. O Ring nos fornece algumas habilidades adicionais para trabalhar diretamente com as solicitações de entrada ou respostas de saída que também são muito úteis.

4.1. Servindo recursos estáticos

Uma das funções mais simples que qualquer aplicativo da Web pode executar é servir recursos estáticos. Ring provides two middleware functions to make this easy – wrap-file and wrap-resource.

The wrap-file middleware takes a directory on the filesystem. Se a solicitação recebida corresponder a um arquivo nesse diretório, esse arquivo será retornado em vez de chamar a função de manipulador:

(use 'ring.middleware.file)
(def app-handler (wrap-file your-handler "/var/www/public"))

De maneira muito semelhante,the wrap-resource middleware takes a classpath prefix in which it looks for the files:

(use 'ring.middleware.resource)
(def app-handler (wrap-resource your-handler "public"))

Em ambos os casos,the wrapped handler function is only ever called if a file isn’t found to return to the client.

O Ring também fornece middleware adicional para tornar esses filtros mais limpos na API HTTP:

(use 'ring.middleware.resource
     'ring.middleware.content-type
     'ring.middleware.not-modified)

(def app-handler
  (-> your-handler
      (wrap-resource "public")
      wrap-content-type
      wrap-not-modified)

O middlewarewrap-content-type determinará automaticamente o cabeçalhoContent-Type a ser definido com base na extensão de nome de arquivo solicitada. O middlewarewrap-not-modified compara o cabeçalhoIf-Not-Modified ao valorLast-Modified para suportar o cache HTTP, retornando o arquivo apenas se for necessário.

4.2. Acessando parâmetros de solicitação

Ao processar uma solicitação, existem algumas maneiras importantes pelas quais o cliente pode fornecer informações ao servidor. Isso inclui parâmetros de sequência de consulta - incluídos nos parâmetros de URL e formulário - enviados como carga útil para solicitações POST e PUT.

Before we can use parameters, we must use the wrap-params middleware to wrap the handler. Isso analisa corretamente os parâmetros, suportando a codificação de URL e os disponibiliza para a solicitação. Opcionalmente, isso pode especificar a codificação de caracteres a ser usada, com o padrão UTF-8, se não for especificado:

(def app-handler
  (-> your-handler
      (wrap-params {:encoding "UTF-8"})
  ))

Uma vez feito isso,the request will get updated to make the parameters available. Eles entram nas chaves apropriadas na solicitação recebida:

  • :query-params - Os parâmetros analisados ​​fora da string de consulta

  • :form-params - Os parâmetros analisados ​​fora do corpo do formulário

  • :params - A combinação de:query-params e:form-params

Podemos usar isso em nosso manipulador de solicitações exatamente como esperado.

(defn echo-handler [{params :params}]
    (ring.util.response/content-type
        (ring.util.response/response (get params "input"))
        "text/plain"))

Este manipulador retornará uma resposta contendo o valor do parâmetroinput.

Parameters map to a single string if only one value is present, or to a list if multiple values are present.

Por exemplo, obtemos os seguintes mapas de parâmetros:

// /echo?input=hello
{"input "hello"}

// /echo?input=hello&name=Fred
{"input "hello" "name" "Fred"}

// /echo?input=hello&input=world
{"input ["hello" "world"]}

4.3. Recebendo uploads de arquivos

Muitas vezes, queremos poder escrever aplicativos da Web nos quais os usuários podem fazer upload de arquivos. No protocolo HTTP, isso geralmente é tratado usando solicitações de várias partes. Isso permite que uma única solicitação contenha parâmetros de formulário e um conjunto de arquivos.

Ring comes with a middleware called wrap-multipart-params to handle this kind of request. Isso é semelhante à maneira quewrap-params analisa solicitações simples.

wrap-multipart-params decodifica e armazena automaticamente quaisquer arquivos carregados no sistema de arquivos e diz ao manipulador onde eles estão para trabalhar com eles:

(def app-handler
  (-> your-handler
      wrap-params
      wrap-multipart-params
  ))

Por padrão,the uploaded files get stored in the temporary system directory and automatically deleted after an hour. Observe que isso requer que a JVM ainda esteja em execução pela próxima hora para executar a limpeza.

Se preferir,there’s also an in-memory store, embora obviamente, haja o risco de ficar sem memória se arquivos grandes forem carregados.

Também podemos gravar nossos mecanismos de armazenamento, se necessário, desde que cumpram os requisitos da API.

(def app-handler
  (-> your-handler
      wrap-params
      (wrap-multipart-params {:store ring.middleware.multipart-params.byte-array/byte-array-store})
  ))

Assim que este middleware estiver configurado,the uploaded files are available on the incoming request object under the params key. Isso é o mesmo que usar o smiddlewarewrap-params . Esta entrada é um mapa que contém os detalhes necessários para trabalhar com o arquivo, dependendo da loja usada.

Por exemplo, o armazenamento de arquivo temporário padrão retorna valores:

  {"file" {:filename     "words.txt"
           :content-type "text/plain"
           :tempfile     #object[java.io.File ...]
           :size         51}}

Onde o:tempfile entinela é um objetojava.io.File que representa diretamente o arquivo no sistema de arquivos.

4.4. Trabalhando com Cookies

Cookies are a mechanism where the server can provide a small amount of data that the client will continue to send back on subsequent requests. Isso geralmente é usado para IDs de sessão, tokens de acesso ou dados persistentes do usuário, como as configurações de localização definidas.

Ring has middleware that will allow us to work with cookies easily. This will automatically [.veryhardreadability] # analisa cookies em solicitações de entrada e também nos permite criar novos cookies em respostas de saída #.

A configuração deste middleware segue os mesmos padrões de antes:

(def app-handler
  (-> your-handler
      wrap-cookies
  ))

Neste ponto,all incoming requests will have their cookies parsed and put into the :cookies key in the request. Isso conterá um mapa do nome e valor do cookie:

{"session_id" {:value "session-id-hash"}}

We can then add cookies to outgoing responses by adding the :cookies key to the outgoing response. Podemos fazer isso criando a resposta diretamente:

{:status 200
 :headers {}
 :cookies {"session_id" {:value "session-id-hash"}}
 :body "Setting a cookie."}

There’s also a helper function that we can use to add cookies to responses, de forma semelhante à forma como antes poderíamos definir códigos de status ou cabeçalhos:

(ring.util.response/set-cookie
    (ring.util.response/response "Setting a cookie.")
    "session_id"
    "session-id-hash")

Cookies can also have additional options set on them, conforme necessário para a especificação HTTP. Se estivermos usandoset-cookie, então os forneceremos como um parâmetro de mapa após a chave e o valor. As chaves deste mapa são:

  • :domain - O domínio ao qual restringir o cookie

  • :path - O caminho para restringir o cookie para

  • :secure -true para enviar o cookie apenas em conexões HTTPS

  • :http-only -true para tornar o cookie inacessível para JavaScript

  • :max-age - O número de segundos após os quais o navegador exclui o cookie

  • :expires - um carimbo de data / hora específico após o qual o navegador exclui o cookie

  • :same-site - se definido como:strict, o navegador não enviará esse cookie de volta com solicitações entre sites.

(ring.util.response/set-cookie
    (ring.util.response/response "Setting a cookie.")
    "session_id"
    "session-id-hash"
    {:secure true :http-only true :max-age 3600})

4.5. Sessões

Os cookies nos permitem armazenar bits de informações que o cliente envia de volta ao servidor a cada solicitação. Uma maneira mais poderosa de conseguir isso é usar sessões. Eles são armazenados inteiramente no servidor, mas o cliente mantém o identificador que determina qual sessão usar.

Como com tudo aqui,sessions are implemented using a middleware function:

(def app-handler
  (-> your-handler
      wrap-session
  ))

Por padrão,this stores session data in memory. Podemos alterar isso, se necessário, e o Ring vem com um armazenamento alternativo que usa cookies para armazenar todos os dados da sessão.

Tal como acontece com o upload de arquivos,we can provide our storage function if needed.

(def app-handler
  (-> your-handler
      wrap-cookies
      (wrap-session {:store (cookie-store {:key "a 16-byte secret"})})
  ))

We can also adjust the details of the cookie used to store the session key.

Por exemplo, para que o cookie da sessão persista por uma hora, poderíamos fazer:

(def app-handler
  (-> your-handler
      wrap-cookies
      (wrap-session {:cookie-attrs {:max-age 3600}})
  ))

Os atributos do cookie aqui são os mesmos suportados pelo middlewarewrap-cookies.

As sessões geralmente podem atuar como repositórios de dados para trabalhar. Isso nem sempre funciona bem em um modelo de programação funcional, então o Ring os implementa de maneira um pouco diferente.

Em vez disso,we access the session data from the request, and we return a map of data to store into it as part of the response. Esse é o estado inteiro da sessão a ser armazenado, não apenas os valores alterados.

Por exemplo, o seguinte mantém uma contagem contínua de quantas vezes o manipulador foi solicitado:

(defn handler [{session :session}]
  (let [count   (:count session 0)
        session (assoc session :count (inc count))]
    (-> (response (str "You accessed this page " count " times."))
        (assoc :session session))))

Trabalhando dessa forma, podemosremove data from the session simply by not including the key. Também podemos deletar a sessão inteira retornandonil para o novo mapa.

(defn handler [request]
  (-> (response "Session deleted.")
      (assoc :session nil)))

5. Leiningen Plugin

O Ring fornece um plugin paraLeiningen build tool para auxiliar no desenvolvimento e produção.

Configuramos o plugin adicionando os detalhes corretos do plugin ao arquivoproject.clj:

  :plugins [[lein-ring "0.12.5"]]
  :ring {:handler ring.core/handler}

It’s important that the version of lein-ring is correct for the version of Ring. Aqui, estamos usando o Anel 1.7.1, o que significa que precisamos delein-ring 0.12.5. Em geral, é mais seguro usar apenas a versão mais recente de ambos, como visto no Maven central ou com o comandolein search:

$ lein search ring-core
Searching clojars ...
[ring/ring-core "1.7.1"]
  Ring core libraries.

$ lein search lein-ring
Searching clojars ...
[lein-ring "0.12.5"]
  Leiningen Ring plugin

O parâmetro:handler para a chamada:ring é o nome totalmente qualificado do manipulador que desejamos usar. Isso pode incluir qualquer middleware que definimos.

Using this plugin means that we no longer need a main function. Podemos usar Leiningen para executar no modo de desenvolvimento, ou então podemos construir um artefato de produção para fins de implantação. Our code now comes down exactly to our logic and nothing more.

5.1. Construindo um Artefato de Produção

Assim que estiver configurado,we can now build a WAR file that we can deploy to any standard servlet container:

$ lein ring uberwar
2019-04-12 07:10:08.033:INFO::main: Logging initialized @1054ms to org.eclipse.jetty.util.log.StdErrLog
Created ./clojure/ring/target/uberjar/ring-0.1.0-SNAPSHOT-standalone.war

We can also build a standalone JAR file that will run our handler exactly as expected:

$ lein ring uberjar
Compiling ring.core
2019-04-12 07:11:27.669:INFO::main: Logging initialized @3016ms to org.eclipse.jetty.util.log.StdErrLog
Created ./clojure/ring/target/uberjar/ring-0.1.0-SNAPSHOT.jar
Created ./clojure/ring/target/uberjar/ring-0.1.0-SNAPSHOT-standalone.jar

This JAR file will include a main class that will start the handler in the embedded container that we included. Isso também respeitará uma variável de ambiente dePORT, permitindo-nos executá-lo facilmente em um ambiente de produção:

PORT=2000 java -jar ./clojure/ring/target/uberjar/ring-0.1.0-SNAPSHOT-standalone.jar
2019-04-12 07:14:08.954:INFO::main: Logging initialized @1009ms to org.eclipse.jetty.util.log.StdErrLog
WARNING: seqable? already refers to: #'clojure.core/seqable? in namespace: clojure.core.incubator, being replaced by: #'clojure.core.incubator/seqable?
2019-04-12 07:14:10.795:INFO:oejs.Server:main: jetty-9.4.z-SNAPSHOT; built: 2018-08-30T13:59:14.071Z; git: 27208684755d94a92186989f695db2d7b21ebc51; jvm 1.8.0_77-b03
2019-04-12 07:14:10.863:INFO:oejs.AbstractConnector:main: Started [email protected]{HTTP/1.1,[http/1.1]}{0.0.0.0:2000}
2019-04-12 07:14:10.863:INFO:oejs.Server:main: Started @2918ms
Started server on port 2000

5.2. Executando em modo de desenvolvimento

Para fins de desenvolvimento,we can run the handler directly from Leiningen without needing to build and run it manually. Isso facilita as coisas para testar nosso aplicativo em um navegador real:

$ lein ring server
2019-04-12 07:16:28.908:INFO::main: Logging initialized @1403ms to org.eclipse.jetty.util.log.StdErrLog
2019-04-12 07:16:29.026:INFO:oejs.Server:main: jetty-9.4.12.v20180830; built: 2018-08-30T13:59:14.071Z; git: 27208684755d94a92186989f695db2d7b21ebc51; jvm 1.8.0_77-b03
2019-04-12 07:16:29.092:INFO:oejs.AbstractConnector:main: Started [email protected]{HTTP/1.1,[http/1.1]}{0.0.0.0:3000}
2019-04-12 07:16:29.092:INFO:oejs.Server:main: Started @1587ms

Isso também honra a variável de ambientePORT se tivermos definido isso.

Além disso,there’s a Ring Development library that we can add to our project. Se estiver disponível, entãothe development server will attempt to automatically reload any detected source changes. Isso pode nos fornecer um fluxo de trabalho eficiente para alterar o código e vê-lo ao vivo em nosso navegador. Isso requer a adição de dependênciaring-devel:

[ring/ring-devel "1.7.1"]

6. Conclusão

Neste artigo, demos uma breve introdução à biblioteca Ring como um meio de escrever aplicativos da Web no Clojure. Por que não experimentá-lo no próximo projeto?

Exemplos de alguns dos conceitos que cobrimos aqui podem ser vistos emGitHub.