Skriver Clojure Webapps med Ring

1. Introduksjon

Ring er et bibliotek for å skrive webapplikasjoner i Clojure. Den støtter alt som trengs for å skrive fullt utstyrte webapper og har et blomstrende økosystem som gjør det enda kraftigere.

I denne opplæringen vil vi gi en introduksjon til Ring, og vise noen av tingene vi kan oppnå med den.

Ring er ikke et rammeverk designet for å lage REST APIer, som så mange moderne verktøysett. Det er et rammeverk på lavere nivå for å håndtere HTTP-forespørsler generelt, med fokus på tradisjonell webutvikling. Imidlertid bygger noen biblioteker på toppen av det for å støtte mange andre ønskede applikasjonsstrukturer.

2. Avhengigheter

Før vi kan begynne å jobbe med Ring, må vi legge det til i prosjektet vårt. Minimumsavhengighetene vi trenger er:

  • ring / ringkjerne
  • ring / ring-brygger-adapter

Vi kan legge disse til i Leiningen-prosjektet vårt:

 : avhengigheter [[org.clojure / clojure "1.10.0"] [ring / ringkjerne "1.7.1"] [ring / ring-bryggeadapter "1.7.1"]]

Vi kan deretter legge dette til et minimalt prosjekt:

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

Her har vi definert en behandlerfunksjon - som vi snart vil dekke - som alltid returnerer strengen "Hello World". Vi har også lagt til vår hovedfunksjon for å bruke denne håndtereren - den vil lytte etter forespørsler på port 3000.

3. Kjernekonsepter

Leiningen har noen kjernekonsepter som alt bygger rundt: Forespørsler, svar, håndtere og mellomvare.

3.1. Forespørsler

Forespørsler er en representasjon av innkommende HTTP-forespørsler. Ring representerer en forespørsel som et kart, slik at vår Clojure-applikasjon enkelt kan samhandle med de enkelte feltene. Det er et standard sett med nøkler på dette kartet, inkludert men ikke begrenset til:

  • : uri - Hele URI-banen.
  • : spørringsstreng - Hele spørringsstrengen.
  • : forespørselsmetode - Forespørselsmetoden, en av : get,: head,: post,: put,: delete eller : alternativer.
  • : overskrifter - Et kart over alle HTTP-overskriftene som ble gitt til forespørselen.
  • :kropp - En InputStream som representerer anmodningsorganet, hvis det er til stede.

Middleware kan også legge til flere nøkler til dette kartet etter behov.

3.2. Svar

Tilsvar er svar en representasjon av de utgående HTTP-svarene. Ring representerer også disse som kart med tre standardnøkler:

  • :status - Statuskoden du skal sende tilbake
  • :topptekster - Et kart over alle HTTP-overskriftene du skal sende tilbake
  • :kropp - Valgfri kropp å sende tilbake

Som før, Middleware kan endre dette mellom vår behandler som produserer det og det endelige resultatet blir sendt til klienten.

Ring gir også noen hjelpere for å gjøre det lettere å bygge svarene.

Den mest grunnleggende av disse er ring.util.response / respons funksjon, som skaper et enkelt svar med en statuskode på 200 OK:

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

Det er noen andre metoder som følger med dette for vanlige statuskoder - for eksempel, dårlig forespørsel, ikke funnet 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,: headers {" Location "" //ring-clojure.github.io/ring/ "},: body" "}

Vi har også status metode som vil konvertere et eksisterende svar til en vilkårlig statuskode:

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

Vi har noen metoder for å justere andre funksjoner i svaret på samme måte - for eksempel, innholdstype, topptekst eller sett-informasjonskapsel:

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 "" Baeldung ") {: status 200, : headers {"X-Tutorial-For" "Baeldung"},: body "Hello"} ring.core => (ring.util.response / set-cookie (ring.util.response / response "Hello") "Bruker "" 123 ") {: status 200,: headers {},: body" Hello ",: cookies {" User "{: value" 123 "}}}

Noter det de sett-informasjonskapsel metoden legger til en helt ny oppføring i responskartet. Dette trenger wrap-cookies mellomvare å behandle det riktig for at det skal fungere.

3.3. Handlere

Nå som vi forstår forespørsler og svar, kan vi begynne å skrive behandlerfunksjonen vår for å knytte den sammen.

En behandler er en enkel funksjon som tar innkommende forespørsel som parameter og returnerer det utgående svaret. Hva vi gjør i denne funksjonen er helt opp til vår søknad, så lenge det passer denne kontrakten.

På det aller enkleste kan vi skrive en funksjon som alltid returnerer det samme svaret:

(defn handler [forespørsel] (ring.util.response / svar "Hei"))

Vi kan også kommunisere med forespørselen etter behov.

For eksempel kan vi skrive en behandler for å returnere den innkommende IP-adressen:

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

3.4. Middleware

Middleware er et navn som er vanlig på noen språk, men mindre i Java-verdenen. Konseptuelt ligner de Servlet-filtre og vårinterceptorer.

I Ring refererer mellomvare til enkle funksjoner som pakker hovedbehandleren og justerer noen aspekter av den på en eller annen måte. Dette kan bety å mutere den innkommende forespørselen før den behandles, mutere det utgående svaret etter at den er generert eller potensielt ikke gjøre noe mer enn å logge hvor lang tid det tok å behandle.

Generelt, mellomvarefunksjoner tar den første parameteren til behandleren for å pakke inn og returnerer en ny behandlerfunksjon med den nye funksjonaliteten.

Midtvaren kan bruke så mange andre parametere som nødvendig. For eksempel kan vi bruke følgende til å stille inn Innholdstype overskrift på hvert svar fra den innpakket behandleren:

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

Når vi leser gjennom det, kan vi se at vi returnerer en funksjon som tar en forespørsel - dette er den nye behandleren. Dette vil da ringe til den medfølgende håndtereren og deretter returnere en mutert versjon av svaret.

Vi kan bruke dette til å produsere en ny handler ved å bare kjede dem sammen:

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

Clojure tilbyr også en måte å kjede mange sammen på en mer naturlig måte - ved bruk av trådmakroer. Dette er en måte å gi en liste over funksjoner du kan ringe, hver med utgangen fra den forrige.

Spesielt ønsker vi Thread First-makroen, ->. Dette vil tillate oss å ringe hver mellomvare med den oppgitte verdien som den første parameteren:

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

Dette har da produsert en behandler som er den opprinnelige behandleren pakket inn i tre forskjellige mellomvarefunksjoner.

4. Skrivehandlere

Nå som vi forstår komponentene som utgjør en Ring-applikasjon, må vi vite hva vi kan gjøre med de faktiske håndtererne. Dette er hjertet i hele applikasjonen, og det er her flertallet av forretningslogikken vil gå.

Vi kan legge inn hvilken kode vi ønsker i disse håndtererne, inkludert databasetilgang eller ringe andre tjenester. Ring gir oss noen ekstra evner for å jobbe direkte med innkommende forespørsler eller utgående svar som også er veldig nyttige.

4.1. Serverer statiske ressurser

En av de enkleste funksjonene som en hvilken som helst webapplikasjon kan utføre, er å betjene statiske ressurser. Ring har to mellomvarefunksjoner for å gjøre dette enkelt - vikle-fil og vikle-ressurs.

De vikle-fil middleware tar en katalog på filsystemet. Hvis den innkommende forespørselen samsvarer med en fil i denne katalogen, blir den filen returnert i stedet for å ringe behandlerfunksjonen:

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

På en veldig lik måte, de vikle-ressurs middleware tar et klassesti-prefiks der det ser etter filene:

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

I begge tilfeller, den innpakket behandlerfunksjonen blir alltid ringt hvis en fil ikke blir funnet å returnere til klienten.

Ring gir også ekstra mellomvare for å gjøre disse renere å bruke via HTTP API:

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

De wrap-content-type mellomvare vil automatisk bestemme Innholdstype header å angi basert på ønsket filtype. De wrap-not-modified mellomvare sammenligner Hvis ikke-modifisert topptekst til Sist endret verdi for å støtte HTTP-hurtigbufring, og returnerer bare filen hvis det er nødvendig.

4.2. Få tilgang til forespørselsparametere

Når du behandler en forespørsel, er det noen viktige måter som klienten kan gi informasjon til serveren. Disse inkluderer spørringsstrengparametere - inkludert i URL- og skjemaparametere - sendt inn som forespørsel om nyttelast for POST- og PUT-forespørsler.

Før vi kan bruke parametere, må vi bruke wrap-params mellomvare for å pakke inn håndtereren. Dette analyserer parametrene riktig, støtter URL-koding, og gjør dem tilgjengelige for forespørselen. Dette kan valgfritt spesifisere tegnkodingen som skal brukes, som standard UTF-8 hvis ikke spesifisert:

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

Når du er ferdig, forespørselen blir oppdatert for å gjøre parametrene tilgjengelige. Disse går inn på passende nøkler i den innkommende forespørselen:

  • : query-params - Parametrene analysert fra spørringsstrengen
  • : form-params - Parametrene analysert fra skjemaet
  • : params - Kombinasjonen av begge deler : query-params og : form-params

Vi kan bruke dette i vår forespørselsbehandler nøyaktig som forventet.

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

Denne behandleren returnerer et svar som inneholder verdien fra parameteren inngang.

Parametere tilordnes til en enkelt streng hvis bare en verdi er til stede, eller til en liste hvis flere verdier er til stede.

For eksempel får vi følgende parameterkart:

// / echo? input = hallo {"input" hallo "} // / echo? input = hallo & name = Fred {" input "hallo" "name" "Fred"} // / echo? input = hallo & input = world {" input ["hei" "verden"]}

4.3. Motta filopplastinger

Ofte ønsker vi å kunne skrive webapplikasjoner som brukere kan laste opp filer til. I HTTP-protokollen håndteres dette vanligvis ved hjelp av flere del-forespørsler. Disse tillater at en enkelt forespørsel inneholder både skjemaparametere og et sett med filer.

Ring kommer med en mellomvare som heter wrap-multipart-params å håndtere denne typen forespørsler. Dette ligner på den måten wrap-params analyserer enkle forespørsler.

wrap-multipart-params dekoder og lagrer automatisk opplastede filer på filsystemet og forteller behandleren hvor de er for at den skal jobbe med dem:

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

Som standard, de opplastede filene lagres i den midlertidige systemkatalogen og slettes automatisk etter en time. Merk at dette krever at JVM fortsatt kjører den neste timen for å utføre oppryddingen.

Hvis ønsket, det er også en minnebutikk, selv om det åpenbart risikerer å gå tom for minne hvis store filer blir lastet opp.

Vi kan også skrive lagringsmotorer om nødvendig, så lenge den oppfyller API-kravene.

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

Når denne mellomvaren er satt opp, de opplastede filene er tilgjengelige på objektet for innkommende forespørsel under params nøkkel. Dette er det samme som å bruke wrap-params mellomvare. Denne oppføringen er et kart som inneholder detaljene som trengs for å jobbe med filen, avhengig av butikken som brukes.

For eksempel returnerer standard midlertidig filbutikk verdier:

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

Hvor i : tempfile oppføringen er en java.io. fil objekt som direkte representerer filen i filsystemet.

4.4. Arbeide med informasjonskapsler

Informasjonskapsler er en mekanisme der serveren kan gi en liten mengde data som klienten vil fortsette å sende tilbake på etterfølgende forespørsler. Dette brukes vanligvis til økt-ID-er, tilgangstokener eller vedvarende brukerdata som de konfigurerte lokaliseringsinnstillingene.

Ring har mellomvare som gjør at vi enkelt kan jobbe med informasjonskapsler. Dette vil automatisk analysere informasjonskapsler på innkommende forespørsler, og vil også tillate oss å lage nye informasjonskapsler på utgående svar.

Konfigurering av denne mellomvaren følger de samme mønstrene som før:

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

På dette punktet, alle innkommende forespørsler får informasjonskapslene sine analysert og lagt i : informasjonskapsler tast inn forespørselen. Dette vil inneholde et kart over informasjonskapselens navn og verdi:

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

Vi kan deretter legge til informasjonskapsler i utgående svar ved å legge til : informasjonskapsler nøkkelen til det utgående svaret. Vi kan gjøre dette ved å opprette svaret direkte:

{: status 200: overskrifter {}: informasjonskapsler {"session_id" {: verdi "session-id-hash"}}: body "Angi en informasjonskapsel."}

Det er også en hjelperfunksjon som vi kan bruke til å legge til informasjonskapsler i svarene, på en lignende måte som hvor tidligere vi kunne angi statuskoder eller overskrifter:

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

Informasjonskapsler kan også ha flere alternativer, etter behov for HTTP-spesifikasjonen. Hvis vi bruker sett-informasjonskapsel så gir vi disse som en kartparameter etter nøkkel og verdi. Nøklene til dette kartet er:

  • :domene - Domenet å begrense informasjonskapselen til
  • :sti - Veien for å begrense informasjonskapselen til
  • :sikreekte å bare sende informasjonskapselen på HTTPS-tilkoblinger
  • : bare httpekte for å gjøre informasjonskapselen utilgjengelig for JavaScript
  • : maks alder - Antallet sekunder som nettleseren sletter informasjonskapselen etter
  • : utløper - En bestemt tidsstempel hvoretter nettleseren sletter informasjonskapselen
  • : samme sted - Hvis satt til :streng, da vil ikke nettleseren sende denne informasjonskapselen tilbake med forespørsler på tvers av nettsteder.
(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. Økter

Informasjonskapsler gir oss muligheten til å lagre biter av informasjon som klienten sender tilbake til serveren på hver forespørsel. En kraftigere måte å oppnå dette på er å bruke økter. Disse lagres helt på serveren, men klienten opprettholder identifikatoren som bestemmer hvilken økt som skal brukes.

Som med alt annet her, økter implementeres ved hjelp av en mellomvarefunksjon:

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

Som standard, dette lagrer øktdata i minnet. Vi kan endre dette om nødvendig, og Ring kommer med en alternativ butikk som bruker informasjonskapsler for å lagre alle sesjonsdataene.

Som med å laste opp filer, vi kan tilby lagringsfunksjonen vår om nødvendig.

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

Vi kan også justere detaljene i informasjonskapselen som brukes til å lagre øktnøkkelen.

For eksempel, for å gjøre det slik at øktkaken fortsetter i en time, kan vi gjøre:

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

Cookie-attributtene her er de samme som støttes av wrap-cookies mellomvare.

Økter kan ofte fungere som datalagre å jobbe med. Dette fungerer ikke alltid like bra i en funksjonell programmeringsmodell, så Ring implementerer dem litt annerledes.

I stedet, vi får tilgang til sesjonsdataene fra forespørselen, og vi returnerer et kart med data som skal lagres i den som en del av svaret. Dette er hele øktstatusen som skal lagres, ikke bare de endrede verdiene.

For eksempel holder følgende løpende opptelling av hvor mange ganger behandleren er blitt bedt om:

(defn handler [{session: session}] (let [count (: count session 0) session (assoc session: count (inc count)))] (-> (respons (str "Du åpnet denne siden" count "ganger." )) (assoc: session session))))

Arbeider på denne måten kan vi fjerne data fra økten ganske enkelt ved ikke å inkludere nøkkelen. Vi kan også slette hele økten ved å returnere null for det nye kartet.

(defn handler [forespørsel] (-> (svar "Økten er slettet.") (assoc: økt null)))

5. Leiningen Plugin

Ring gir et plugin for Leiningen byggeverktøy for å hjelpe både utvikling og produksjon.

Vi setter opp pluginet ved å legge til riktige plugin-detaljer i prosjekt.clj fil:

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

Det er viktig at versjonen av lein-ring er riktig for versjonen av Ring. Her har vi brukt Ring 1.7.1, noe som betyr at vi trenger lein-ring 0,12,5. Generelt er det tryggest å bare bruke den nyeste versjonen av begge, sett på Maven central eller med leinsøk kommando:

$ lein search ring-core Søker i clojars ... [ring / ring-core "1.7.1"] Ring core-biblioteker. $ leinsøk lein-ring Søker i klær ... [lein-ring "0.12.5"] Leiningen Ring-plugin

De : handler parameter til :ringe call er det fullt kvalifiserte navnet på handler som vi vil bruke. Dette kan omfatte hvilken som helst mellomvare som vi har definert.

Å bruke dette pluginet betyr at vi ikke lenger trenger en hovedfunksjon. Vi kan bruke Leiningen til å kjøre i utviklingsmodus, ellers kan vi bygge en produksjonsgjenstand for distribusjonsformål. Koden vår kommer nå nøyaktig til vår logikk og ikke noe mer.

5.1. Bygg en produksjonsgjenstand

Når dette er satt opp, Vi kan nå bygge en WAR-fil som vi kan distribuere til en hvilken som helst standard servletcontainer:

$ 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-frittstående.krig

Vi kan også bygge en frittstående JAR-fil som kjører handleren vår akkurat som forventet:

$ lein ring uberjar Kompilering av 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 Opprettet ./clojure/ring/target/uberjar/ring-0.1.0-SNAPSHOT-standalone.jar

Denne JAR-filen inkluderer en hovedklasse som starter behandleren i den innebygde beholderen som vi inkluderte. Dette vil også ære en miljøvariabel på HAVN slik at vi enkelt kan kjøre den i et produksjonsmiljø:

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 ADVARSEL: kan sees? refererer allerede til: # 'clojure.core / seqable? i navneområdet: clojure.core.incubator, blir erstattet av: # '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 [e-postbeskyttet] {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. Kjører i utviklingsmodus

For utviklingsformål, vi kan kjøre behandleren direkte fra Leiningen uten å måtte bygge og kjøre den manuelt. Dette gjør ting enklere for å teste applikasjonen vår i en ekte nettleser:

$ 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; 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: Started @ 1587ms

Dette hedrer også HAVN miljøvariabel hvis vi har satt det.

I tillegg det er et Ring Development-bibliotek som vi kan legge til i prosjektet vårt. Hvis dette er tilgjengelig, da utviklingsserveren vil prøve å laste inn eventuelle oppdagede kildeendringer automatisk. Dette kan gi oss en effektiv arbeidsflyt med å endre koden og se den live i nettleseren vår. Dette krever ringutvikling avhengighet legge til:

[ring / ring-utvikling "1.7.1"]

6. Konklusjon

I denne artikkelen ga vi en kort introduksjon til Ring-biblioteket som et middel til å skrive webapplikasjoner i Clojure. Hvorfor ikke prøve det på neste prosjekt?

Eksempler på noen av konseptene vi har dekket her, kan sees i GitHub.


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