Skrivning af Clojure Webapps med Ring

1. Introduktion

Ring er et bibliotek til skrivning af webapplikationer i Clojure. Det understøtter alt, hvad der er nødvendigt for at skrive fuldt udstyrede webapps og har et blomstrende økosystem, der gør det endnu mere kraftfuldt.

I denne vejledning giver vi en introduktion til Ring og viser nogle af de ting, vi kan opnå med det.

Ring er ikke en ramme designet til at oprette REST API'er, som så mange moderne værktøjssæt. Det er en ramme på et lavere niveau til generelt at håndtere HTTP-anmodninger, med fokus på traditionel webudvikling. Imidlertid bygger nogle biblioteker oven på det for at understøtte mange andre ønskede applikationsstrukturer.

2. Afhængigheder

Før vi kan begynde at arbejde med Ring, skal vi føje det til vores projekt. De mindste afhængigheder, vi har brug for, er:

  • ring / ringkerne
  • ring / ring-anløbsadapter

Vi kan tilføje disse til vores Leiningen-projekt:

 : afhængigheder [[org.clojure / clojure "1.10.0"] [ring / ringkerne "1.7.1"] [ring / ring-pier-adapter "1.7.1"]]

Vi kan derefter tilføje dette til et minimalt projekt:

(ns ring.core (: brug ring.adapter.jetty)) (defn handler [anmodning] {: status 200: overskrifter {"Content-Type" "text / plain"}: body "Hello World"}) (defn - main [& args] (run-jetty handler {: port 3000}))

Her har vi defineret en handlerfunktion - som vi snart dækker - som altid returnerer strengen "Hello World". Vi har også tilføjet vores hovedfunktion til at bruge denne handler - den lytter efter anmodninger på port 3000.

3. Kernebegreber

Leiningen har et par kernekoncepter, som alt bygger omkring: Requests, Responses, Handlers og Middleware.

3.1. Anmodninger

Anmodninger er en repræsentation af indgående HTTP-anmodninger. Ring repræsenterer en anmodning som et kort, så vores Clojure-applikation let kan interagere med de enkelte felter. Der er et standardsæt med nøgler på dette kort, inklusive men ikke begrænset til:

  • : uri - Den fulde URI-sti.
  • : forespørgselsstreng - Den fulde forespørgselsstreng.
  • : anmodningsmetode - Anmodningsmetoden, en af : get,: head,: post,: put,: delete eller :muligheder.
  • : overskrifter - Et kort over alle de HTTP-overskrifter, der er leveret til anmodningen.
  • :legeme - En InputStream repræsenterer anmodningsorganet, hvis det er til stede.

Middleware kan også tilføje flere nøgler til dette kort efter behov.

3.2. Svar

Svar er ligeledes en repræsentation af de udgående HTTP-svar. Ring repræsenterer disse også som kort med tre standardtaster:

  • : status - Statuskoden, der skal sendes tilbage
  • :overskrifter - Et kort over alle HTTP-overskrifter, der skal sendes tilbage
  • :legeme - Det valgfrie organ, der skal sendes tilbage

Som før, Middleware kan ændre dette mellem vores handler, der producerer det, og det endelige resultat sendes til klienten.

Ring giver også nogle hjælpere til at gøre det lettere at opbygge svarene.

Den mest basale af disse er ring.util.response / respons funktion, som skaber et simpelt svar med en statuskode på 200 OK:

ring.core => (ring.util.response / svar "Hej") {: status 200,: overskrifter {},: body "Hej"}

Der er et par andre metoder, der følger med dette for almindelige statuskoder - for eksempel, dårlig anmodning, ikke fundet og omdirigere:

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" //ring-clojure.github. io / ring / ") {: status 302,: overskrifter {" Placering "" //ring-clojure.github.io/ring/ "},: body" "}

Vi har også status metode, der konverterer et eksisterende svar til en vilkårlig statuskode:

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

Vi har derefter nogle metoder til at justere andre funktioner i svaret på samme måde - for eksempel, indholdstype, overskrift eller sæt-cookie:

ring.core => (ring.util.response / indholdstype (ring.util.response / svar "Hej") "tekst / almindelig") {: status 200,: overskrifter {"Content-Type" "tekst / almindelig "},: body" Hello "} ring.core => (ring.util.response / header (ring.util.response / response" Hello ")" X-Tutorial-For "" Baeldung ") {: status 200, : overskrifter {"X-Tutorial-For" "Baeldung"},: body "Hello"} ring.core => (ring.util.response / set-cookie (ring.util.response / svar "Hello") "Bruger "" 123 ") {: status 200,: overskrifter {},: body" Hej ",: cookies {" Bruger "{: værdi" 123 "}}}

Noter det det sæt-cookie metode tilføjer en helt ny post til svarkortet. Dette har brug for wrap-cookies mellemvare at behandle det korrekt, så det fungerer.

3.3. Handlere

Nu hvor vi forstår anmodninger og svar, kan vi begynde at skrive vores handlerfunktion for at binde den sammen.

En handler er en simpel funktion, der tager den indgående anmodning som parameter og returnerer det udgående svar. Hvad vi gør i denne funktion er helt op til vores ansøgning, så længe det passer til denne kontrakt.

På det meget enkle kunne vi skrive en funktion, der altid returnerer det samme svar:

(defn handler [anmodning] (ring.util.response / svar "Hej"))

Vi kan også interagere med anmodningen efter behov.

For eksempel kunne vi skrive en behandler for at returnere den indgående IP-adresse:

(defn check-ip-handler [anmodning] (ring.util.response / indholdstype (ring.util.response / respons (: remote-addr anmodning)) "tekst / almindelig"))

3.4. Middleware

Middleware er et navn, der er almindeligt på nogle sprog, men mindre i Java-verdenen. Konceptuelt ligner de Servlet-filtre og Spring Interceptors.

I Ring refererer middleware til enkle funktioner, der omslutter hovedhåndteringen og justerer nogle aspekter af det på en eller anden måde. Dette kan betyde at mutere den indgående anmodning, før den behandles, mutere det udgående svar, efter at det er genereret eller potentielt ikke gøre mere end at logge, hvor lang tid det tog at behandle.

Generelt, middleware-funktioner tager en første parameter i handleren til at pakke og returnerer en ny handlerfunktion med den nye funktionalitet.

Middleware kan bruge så mange andre parametre som nødvendigt. For eksempel kunne vi bruge følgende til at indstille Indholdstype header på hvert svar fra den indpakkede handler:

(defn wrap-content-type [handler content-type] (fn [anmodning] (lad [respons (handler anmodning)] (assoc-in svar [: overskrifter "Content-Type"] indholdstype))))

Når vi læser igennem det, kan vi se, at vi returnerer en funktion, der tager en anmodning - dette er den nye handler. Dette vil derefter ringe til den leverede handler og derefter returnere en muteret version af svaret.

Vi kan bruge dette til at producere en ny handler ved blot at kæde dem sammen:

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

Clojure tilbyder også en måde at kæde mange sammen på en mere naturlig måde - ved hjælp af trådmakroer. Dette er en måde at give en liste over funktioner til at ringe til, hver med output fra den foregående.

Vi ønsker især makroen Tråd først, ->. Dette giver os mulighed for at kalde hver middleware med den angivne værdi som den første parameter:

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

Dette har derefter produceret en handler, der er den oprindelige handler pakket i tre forskellige middleware-funktioner.

4. Writing Handlers

Nu hvor vi forstår de komponenter, der udgør en Ring-applikation, er vi nødt til at vide, hvad vi kan gøre med de faktiske håndtere. Disse er kernen i hele applikationen, og det er her størstedelen af ​​forretningslogikken vil gå.

Vi kan placere den kode, vi ønsker, i disse håndterere, inklusive databaseadgang eller opkald til andre tjenester. Ring giver os nogle yderligere evner til at arbejde direkte med de indgående anmodninger eller udgående svar, som også er meget nyttige.

4.1. Serverer statiske ressourcer

En af de enkleste funktioner, som enhver webapplikation kan udføre, er at tjene statiske ressourcer. Ring giver to middleware-funktioner for at gøre det let - wrap-fil og wrap-ressource.

Det wrap-fil middleware tager et bibliotek på filsystemet. Hvis den indgående anmodning matcher en fil i denne mappe, returneres filen i stedet for at ringe til handlerfunktionen:

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

På en meget lignende måde, det wrap-ressource middleware tager et klassesti-præfiks, hvor det søger efter filerne:

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

I begge tilfælde den indpakkede handlerfunktion kaldes kun nogensinde, hvis der ikke findes en fil, der vender tilbage til klienten.

Ring leverer også yderligere middleware for at gøre disse renere til brug via HTTP API:

(brug 'ring.middleware.resource' ring.middleware.content-type 'ring.middleware.not-modified) (def app-handler (-> your-handler (wrap-resource "public") wrap-content-type wrap) -ikke-modificeret)

Det wrap-content-type middleware vil automatisk bestemme Indholdstype header at indstille baseret på den ønskede filtypenavn. Det wrap-not-modified middleware sammenligner Hvis ikke-ændret header til Sidst ændret værdi til understøttelse af HTTP-caching, kun returnering af filen, hvis det er nødvendigt.

4.2. Adgang til anmodningsparametre

Ved behandling af en anmodning er der nogle vigtige måder, hvorpå klienten kan give oplysninger til serveren. Disse omfatter forespørgselsstrengparametre - inkluderet i URL- og formularparametre - indsendt som anmodningens nyttelast til POST- og PUT-anmodninger.

Før vi kan bruge parametre, skal vi bruge wrap-params mellemvare til at pakke handleren. Dette parser korrekt parametrene, understøtter URL-kodning og gør dem tilgængelige for anmodningen. Dette kan valgfrit specificere den tegnkodning, der skal bruges, som standard UTF-8, hvis ikke angivet:

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

Når du er færdig, anmodningen opdateres for at gøre parametrene tilgængelige. Disse går ind i passende taster i den indgående anmodning:

  • : forespørgsel-params - Parametrene, der er parset ud af forespørgselsstrengen
  • : form-params - Parametrene parset ud af formen body
  • : params - Kombinationen af ​​begge : forespørgsel-params og : form-params

Vi kan gøre brug af dette i vores anmodningshåndterer nøjagtigt som forventet.

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

Denne handler returnerer et svar, der indeholder værdien fra parameteren input.

Parametre tilordnes til en enkelt streng, hvis der kun er én værdi, eller til en liste, hvis der er flere værdier.

For eksempel får vi følgende parameterkort:

// / echo? input = hej {"input" hej "} // / echo? input = hej & navn = Fred {" input "hej" "navn" "Fred"} // / echo? input = hej & input = verden {" input ["hej" "verden"]}

4.3. Modtagelse af filoverførsler

Ofte vil vi være i stand til at skrive webapplikationer, som brugerne kan uploade filer til. I HTTP-protokollen håndteres dette typisk ved hjælp af Multipart-anmodninger. Disse giver mulighed for en enkelt anmodning om at indeholde både formularparametre og et sæt filer.

Ring kommer med en middleware kaldet wrap-multipart-params til at håndtere denne form for anmodning. Dette svarer til den måde, som wrap-params analyserer enkle anmodninger.

wrap-multipart-params afkoder automatisk og gemmer alle uploadede filer på filsystemet og fortæller handleren, hvor de skal arbejde sammen med dem:

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

Som standard, de uploadede filer gemmes i det midlertidige systemkatalog og slettes automatisk efter en time. Bemærk, at dette kræver, at JVM stadig kører i den næste time for at udføre oprydningen.

Hvis det foretrækkes, der er også en lagerhukommelse, selvom det selvfølgelig risikerer at løbe tør for hukommelse, hvis store filer bliver uploadet.

Vi kan også skrive vores lagermotorer, hvis det er nødvendigt, så længe det opfylder API-kravene.

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

Når denne middleware er konfigureret, de uploadede filer er tilgængelige på det indgående anmodningsobjekt under params nøgle. Dette er det samme som at bruge wrap-params mellemvare. Denne post er et kort med de detaljer, der er nødvendige for at arbejde med filen, afhængigt af den anvendte butik.

For eksempel returnerer den standard midlertidige filbutik værdier:

 {"fil" {: filnavn "words.txt": indholdstype "text / plain": tempfile #object [java.io.File ...]: størrelse 51}}

Hvor er : tempfile indgangen er en java.io-fil objekt, der direkte repræsenterer filen på filsystemet.

4.4. Arbejde med cookies

Cookies er en mekanisme, hvor serveren kan levere en lille mængde data, som klienten fortsætter med at sende tilbage på efterfølgende anmodninger. Dette bruges typisk til session-id'er, adgangstokener eller vedvarende brugerdata såsom de konfigurerede lokaliseringsindstillinger.

Ring har middleware, der giver os mulighed for nemt at arbejde med cookies. Dette analyserer automatisk cookies på indgående anmodninger og giver os også mulighed for at oprette nye cookies på udgående svar.

Konfiguration af denne mellemvare følger de samme mønstre som før:

(def app-handler (-> din wrap-cookies til din handler))

På dette tidspunkt, alle indgående anmodninger får deres cookies analyseret og sat i : cookies indtast anmodningen. Dette indeholder et kort over cookienavn og værdi:

{"session_id" {: værdi "session-id-hash"}}

Vi kan derefter tilføje cookies til udgående svar ved at tilføje : cookies nøgle til det udgående svar. Vi kan gøre dette ved at oprette svaret direkte:

{: status 200: overskrifter {}: cookies {"session_id" {: værdi "session-id-hash"}}: body "Indstilling af en cookie."}

Der er også en hjælperfunktion, som vi kan bruge til at tilføje cookies til svarene, på en lignende måde som hvor tidligere vi kunne indstille statuskoder eller overskrifter:

(ring.util.response / set-cookie (ring.util.response / respons "Indstilling af en cookie.") "session_id" "session-id-hash")

Cookies kan også have yderligere indstillingerefter behov for HTTP-specifikationen. Hvis vi bruger sæt-cookie så giver vi disse som en kortparameter efter nøglen og værdien. Nøglerne til dette kort er:

  • :domæne - Domænet, som cookien skal begrænses til
  • :sti - Stien til at begrænse cookien til
  • :sikkerrigtigt for kun at sende cookien på HTTPS-forbindelser
  • : kun httprigtigt for at gøre cookien utilgængelig for JavaScript
  • : max-alder - Antallet af sekunder, hvorefter browseren sletter cookien
  • : udløber - En bestemt tidsstempel, hvorefter browseren sletter cookien
  • : samme sted - Hvis indstillet til :streng, så sender browseren ikke denne cookie tilbage med anmodninger på tværs af websteder.
(ring.util.response / set-cookie (ring.util.response / respons "Indstilling af en cookie.") "session_id" "session-id-hash" {: sikker sand: http-kun sand: max-alder 3600} )

4.5. Sessioner

Cookies giver os mulighed for at gemme bits af information, som klienten sender tilbage til serveren på hver anmodning. En mere kraftfuld måde at opnå dette på er at bruge sessioner. Disse gemmes udelukkende på serveren, men klienten vedligeholder identifikatoren, der bestemmer, hvilken session der skal bruges.

Som med alt andet her, sessioner implementeres ved hjælp af en middleware-funktion:

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

Som standard, dette gemmer sessionsdata i hukommelsen. Vi kan ændre dette, hvis det er nødvendigt, og Ring leveres med en alternativ butik, der bruger cookies til at gemme alle sessionsdata.

Som ved upload af filer, Vi kan levere vores lagerfunktion, hvis det er nødvendigt.

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

Vi kan også justere detaljerne i den cookie, der bruges til at gemme sessionsnøglen.

For eksempel for at gøre det, så sessionscookien fortsætter i en time, kunne vi gøre:

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

Cookieattributterne her er de samme som understøttes af wrap-cookies mellemvare.

Sessioner kan ofte fungere som datalagre at arbejde med. Dette fungerer ikke altid så godt i en funktionel programmeringsmodel, så Ring implementerer dem lidt anderledes.

I stedet, vi får adgang til sessionsdataene fra anmodningen, og vi returnerer et kort med data, der skal gemmes i det som en del af svaret. Dette er hele sessionstilstanden, der skal gemmes, ikke kun de ændrede værdier.

For eksempel holder følgende løbende optælling af, hvor mange gange handleren er blevet anmodet om:

(defn handler [{session: session}] (lad [count (: count session 0) session (assoc session: count (inc count)))] - -> (svar (str "Du har fået adgang til denne side" count "gange." )) (assoc: session session))))

At arbejde på denne måde kan vi fjern data fra sessionen ved simpelthen ikke at medtage nøglen. Vi kan også slette hele sessionen ved at vende tilbage nul til det nye kort.

(defn handler [anmodning] (-> (svar "Session slettet.") (assoc: session nul)))

5. Leiningen-plugin

Ring leverer et plugin til Leiningen-byggeværktøjet til at hjælpe både udvikling og produktion.

Vi opretter pluginet ved at tilføje de korrekte plugindetaljer til projekt.clj fil:

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

Det er vigtigt, at versionen af lein-ring er korrekt for versionen af ​​Ring. Her har vi brugt Ring 1.7.1, hvilket betyder, at vi har brug for lein-ring 0,12,5. Generelt er det sikreste at bare bruge den nyeste version af begge, set på Maven central eller med leinsøgning kommando:

$ lein search ring-core Søger clojars ... [ring / ring-core "1.7.1"] Ringkernebiblioteker. $ lein search lein-ring Søger clojars ... [lein-ring "0.12.5"] Leiningen Ring plugin

Det : handler parameter til :ring call er det fuldt kvalificerede navn på den handler, som vi vil bruge. Dette kan omfatte enhver middleware, som vi har defineret.

Brug af dette plugin betyder, at vi ikke længere har brug for en hovedfunktion. Vi kan bruge Leiningen til at køre i udviklingstilstand, ellers kan vi bygge en produktionsartefakt til implementeringsformål. Vores kode kommer nu nøjagtigt ned på vores logik og intet mere.

5.1. Opbygning af en produktionsartefakt

Når dette er konfigureret, Vi kan nu oprette en WAR-fil, som vi kan distribuere til enhver standard servletcontainer:

$ lein ring uberwar 2019-04-12 07: 10: 08.033: INFO :: main: Logning initialiseret @ 1054ms til org.eclipse.jetty.util.log.StdErrLog Oprettet ./clojure/ring/target/uberjar/ring-0.1 .0-SNAPSHOT-standalone.war

Vi kan også oprette en uafhængig JAR-fil, der kører vores handler nøjagtigt som forventet:

$ lein ring uberjar Kompilering ring.core 2019-04-12 07: 11: 27.669: INFO :: main: Logning initialiseret @ 3016ms til org.eclipse.jetty.util.log.StdErrLog Oprettet ./clojure/ring/target/uberjar /ring-0.1.0-SNAPSHOT.jar Oprettet ./clojure/ring/target/uberjar/ring-0.1.0-SNAPSHOT-standalone.jar

Denne JAR-fil inkluderer en hovedklasse, der starter handleren i den integrerede container, som vi inkluderede. Dette vil også ære en miljøvariabel på HAVN giver os mulighed for let at køre det i et produktionsmiljø:

PORT = 2000 java -jar ./clojure/ring/target/uberjar/ring-0.1.0-SNAPSHOT-standalone.jar 2019-04-12 07: 14: 08.954: INFO :: main: Logning initialiseret @ 1009ms til org. eclipse.jetty.util.log.StdErrLog ADVARSEL: kan ses? refererer allerede til: # 'clojure.core / seqable? i navneområdet: clojure.core.incubator, erstattes af: # 'clojure.core.incubator / seqable? 2019-04-12 07: 14: 10.795: INFO: oejs.Server: main: jetty-9.4.z-SNAPSHOT; bygget: 2018-08-30T13: 59: 14.071Z; git: 27208684755d94a92186989f695db2d7b21ebc51; jvm 1.8.0_77-b03 12-04-2019 07: 14: 10.863: INFO: oejs.AbstractConnector: main: Startet [email protected] {HTTP / 1.1, [http / 1.1]} {0.0.0.0:2000} 2019- 04-12 07: 14: 10.863: INFO: oejs.Server: main: Startet @ 2918ms Startet server på port 2000

5.2. Kører i udviklingstilstand

Til udviklingsformål Vi kan køre handleren direkte fra Leiningen uden at skulle bygge og køre den manuelt. Dette gør tingene lettere for at teste vores applikation i en rigtig browser:

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

Dette ærer også HAVN miljøvariabel, hvis vi har indstillet det.

Derudover der er et Ring Development-bibliotek, som vi kan føje til vores projekt. Hvis dette er tilgængeligt, så udviklingsserveren forsøger automatisk at genindlæse eventuelle registrerede kildeændringer. Dette kan give os en effektiv arbejdsgang med at ændre koden og se den live i vores browser. Dette kræver ring-udvikling afhængighed tilføjer:

[ring / ring-udvikling "1.7.1"]

6. Konklusion

I denne artikel gav vi en kort introduktion til Ring-biblioteket som et middel til at skrive webapplikationer i Clojure. Hvorfor ikke prøve det på det næste projekt?

Eksempler på nogle af de begreber, vi har dækket her, kan ses i GitHub.


$config[zx-auto] not found$config[zx-overlay] not found