Do you want to pick up from where you left of?
Take me there

Plug

Αν είστε εξοικειομένοι με τη Ruby μπορείτε να σκεφτείτε το Plug σαν το Rack με λίγο Sinatra. Παρέχει εναν προσδιορισμό για στοιχεία εφαρμογών web και αντάπτορες για εξυπηρετητές web. Παρόλο που δεν είναι μέρος του πυρήνα της Elixir, είναι ένα επίσημο Elixir project.

Σε αυτό το μάθημα θα δημιουργήσουμε ένα απλό HTTP σέρβερ από το μηδέν, χρησιμοποιώντας την βιβλιοθήκη της Elixir PlugCowboy. Το Cowboy είναι ένας απλός HTTP σέρβερ για Erlang και η Plug θα μας παρέχει με έναν προσαρμογέα σύνδεσης για αυτόν τον web server.

Αφού στήσουμε την μινιμαλιστική web εφαρμογή μας, θα μάθουμε για τον δρομολογητή της Plug και πως να χρησιμοποιήσουμε πολλαπλά plugs σε μια web εφαρμογή

Προαπαιτούμενα

Αυτός ο οδηγός υποθέτει ότι έχετε ήδη εγκαταστήσει την Elixir 1.5 ή υψηλότερη και το mix.

Θα ξεκινήσουμε δημιουργόντας ένα OTP project, με ένα δέντρο επίβλεψης.

mix new example --sup
cd example

Χρειαζόμαστε η Elixir εφαρμογή μας να συμπεριλαμβάνει ένα δέντρο επίβλεψης επειδή θα χρησιμοποιήσουμε εναν επιτηρητή για να εκκινήσουμε και να τρέξουμε τον Cowboy2 σέρβερ μας.

Εξαρτήσεις

Η προσθήκη εξαρτήσεων είναι πανεύκολη με το mix. Για να χρησιμοποιήσουμε το Plug σαν διεπαφή προσαρμογέα για τον webserver του Cowboy2, πρέπει να εγκαταστήσουμε το πακέτο PlugCowboy:

Προσθέστε τα ακόλουθα στο αρχείο mix.exs:

def deps do
  [
    {:plug_cowboy, "~> 2.0"},
  ]
end

Στη γραμμή εντολών, τρέξτε την ακόλουθη εργασία mix για να κατεβάσετε τις νέες αυτές εξαρτήσεις:

mix deps.get

Προσδιορισμός

Για να ξεκινήσουμε να φτιάχνουμε Plugs, θα πρέπει να ξέρουμε, και να εμμένουμε, στις προδιαγραφές Plug. Ευτυχώς για εμάς, υπάρχουν μόνο δύο συναρτήσεις που είναι απαραίτητες: οι init/1 και call/2.

Ορίστε ένα απλό Plug που επιστρέφει “Γειά σου κόσμε!”;

defmodule Example.HelloWorldPlug do
  import Plug.Conn

  def init(options), do: options

  def call(conn, _opts) do
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(200, "Hello World!\n")
  end
end

Αποθηκεύστε το αρχείο στο lib/example/hello_world_plug.ex.

Η συνάρτηση init/1 χρησιμοποιείται για να αρχικοποιήσει τις επιλογές του Plug μας. Καλείται από το δέντρο επιτήρησης μας, το οποίο εξηγείται στο επόμενο τμήμα. Για τώρα, θα είναι μια άδεια Λίστα η οποία αγνοείται.

Η τιμή που επιστρέφεται από την init/1 περνάει σαν το δεύτερο όρισμα στην συνάρτηση call/2 μας.

Η συνάρτηση call/2 καλείται για κάθε νέα αίτηση που έρχεται στον εξυπηρετητή μας, τον Cowboy. Δέχεται μία δομή σύνδεσης %Plug.Conn{} σαν το πρώτο όρισμα και αναμένεται να επιστρέψει μια δομή σύνδεσης %Plug.Conn{}.

Ρύθμιζοντας την Ενότητα Εφαρμογής του Project

Πρέπει να πούμε στην εφαρμογή μας να ξεκινήσει και να επιτηρεί τον εξυπηρετητή Cowboy όταν εκκινεί η εφαρμογή.

Θα το κάνουμε με την συνάρτηση Plug.Cowboy.child_spec/1.

Αυτή η συνάρτηση αναμένει τρεις επιλογές:

Το αρχείο lib/example/application.ex θα πρέπει να υλοποιεί τις προδιαγραφές παιδιού στην συνάρτηση του start/2:

defmodule Example.Application do
  use Application
  require Logger

  def start(_type, _args) do
    children = [
      {Plug.Cowboy, scheme: :http, plug: Example.HelloWorldPlug, options: [port: 8080]}
    ]
    opts = [strategy: :one_for_one, name: Example.Supervisor]

    Logger.info("Starting application...")

    Supervisor.start_link(children, opts)
  end
end

Σημείωση: Δεν χρειάζεται να καλέσουμε το child_spec εδώ, αυτή η συνάρτηση θα κληθεί από τον επιτηρητή που εκκινεί αυτήν την διεργασία. Εμείς απλά δίνουμε μια τούπλα με την ενότητα για την οποία θέλουμε να δημιουργήσουμε τις προδιαγραφές παιδιού και έπειτα τις τρεις επιλογές που απαιτούνται.

Αυτό εκκινεί έναν εξυπηρετητή Cowboy2 κάτω από το δέντρο επιτήρησης της εφαρμογής μας. Αρχίζει να τρέχει το Cowboy κάτω από το σχήμα HTTP (μπορείτε επίσης να προσδιορίσετε το HTTPS), στην δοθείσα πόρτα, 8080, προσδιορίζοντας το plug, Example.HelloWorldPlug, ως την διεπαφή για εισερχόμενα αιτήματα ιστού.

Πλέον είμαστε σε θέση να τρέξουμε την εφαρμογή μας και να της στείλουμε μερικά αιτήματα ιστού! Παρατηρείτε οτι, επειδή δημιουργήσαμε μια OTP εφαρμογή με την σημαία --sup, η Example εφαρμογή μας θα ξεκινήσει αυτόματα εξ’ αιτίας της συνάρτησης application.

Στο mix.exs θα πρέπει να βλέπετε τα ακόλουθα:

def application do
  [
    extra_applications: [:logger],
    mod: {Example.Application, []}
  ]
end

Είμαστε έτοιμοι να δοκιμάσουμε αυτόν τον μινιμαλιστικό, βασισμένο στο plug, web server. Στη γραμμή εντολών τρέξτε:

mix run --no-halt

Όταν ολοκληρωθεί η σύνταξη, και εμφανιστεί το [info] Starting application..., ανοίξτε έναν φυλλομετρητή στη σελίδα http://127.0.0.1:8080. Θα πρέπει να εμφανίζει:

Hello World!

Plug.Router

Για τις περισσότερες εφαρμογές, όπως μια σελίδα web ή ένα REST API, θα θέλετε ένα δρομολογητή να δρομολογεί τις αιτήσεις για διαφορετικές διαδρομές και ρήματα HTTP σε διαφορετικούς χειριστές. Το Plug παρέχει ένα δρομολογητή για αυτό. Όπως θα δούμε, δεν χρειαζόμαστε ένα σκελετό εφαρμογής σαν το Sinatra στην Elixir από τη στιγμή που το έχουμε με το Plug.

Για αρχή ας δημιουργήσουμε το αρχείο lib/example/router.ex και ας αντιγράψουμε τα ακόλουθα σε αυτό:

defmodule Example.Router do
  use Plug.Router

  plug :match
  plug :dispatch

  get "/" do
    send_resp(conn, 200, "Welcome")
  end

  match _ do
    send_resp(conn, 404, "Oops!")
  end
end

Αυτός είναι ο πλέον μινιμαλιστικός δρομολογητής αλλά ο κώδικας θα πρέπει να είναι αρκετά αυτονόητος. Έχουμε συμπεριλάβει μερικές μακροεντολές μέσω της use Plug.Router και μετά ορίσαμε δύο από τα προυπάρχοντα Plugs: τα :match και :dispatch. Υπάρχουν δύο ορισμένες διαδρομές, μία για το χειρισμό αιτήσεων GET στην πηγαία διαδρομή (root) και η δεύτερη για το ταίριασμα όλων των άλλων αιτήσεων ώστε να επιστρέψουμε ένα μήνυμα σφάλματος 404.

Πίσω στο lib/example/application.ex, πρέπει να προσθέσουμε τον Example.Router μας στο δέντρο επιτήρησης του εξυπηρετητή web. Αλλάξτε το plug Example.HelloWorldPlug με το νέο μας δρομολογητή:

def start(_type, _args) do
  children = [
    {Plug.Cowboy, scheme: :http, plug: Example.Router, options: [port: 8080]}
  ]
  opts = [strategy: :one_for_one, name: Example.Supervisor]

  Logger.info("Starting application...")

  Supervisor.start_link(children, opts)
end

Εκκινήστε τον εξυπηρετητή πάλι, αφού πρώτα σταματήσετε τον προηγούμενο αν ακόμα τρέχει (πατήστε Ctrl+C δύο φορές).

Τώρα στο web browser, πηγαίνετε στη διαδρομή http://127.0.0.1:8080. Θα πρέπει να εμφανίσει το Welcome. Έπειτα, πηγαίνετε στη τοποθεσία http://127.0.0.1:8080/waldo, ή οποιοδήποτε άλλη διαδρομή. Θα πρέπει να εμφανίζει Ουπς! με μια απάντηση 404.

Προσθήκη ενός άλλου Plug

Είναι σύνηθες να χρησιμοποιούμε περισσότερα από ένα plug σε μια εφαρμογή ιστού, κάθε ένα από τα οποία έχει δική του ευθύνη. Για παράδειγμα, μπορεί να έχουμε ένα plug που χειρίζεται την δρομολόγηση, ένα plug που επικυρώνει εισερχόμενα αιτήματα ιστού, ένα plug που πιστοποιεί εισερχόμενα αιτήματα, κλπ. Σε αυτό το τμήμα, θα ορίσουμε ενα plug το οποίο θα πιστοποιεί παραμέτρους εισερχόμενων αιτημάτων και δώσουμε στην εφαρμογή μας την δυνατότητα να χρησιμοποιεί και τα δύο plug μας–τον δρομολογητή και το plug επικύρωσης.

Θελουμε να δημιουργήσουμε ένα Plug για να επιβεβαιώσουμε αν η αίτηση έχει ένα σετ απαιτούμενων παραμέτρων. Με την υλοποίηση της επικύρωσης σε ένα Plug μπορούμε να βεβαιωθούμε ότι μόνο έγκυρες αιτήσεις θα καταφέρουν να περάσουν στην εφαρμογή μας. Θα περιμένουμε το Plug μας να αρχικοποιηθεί με δύο επιλογές: την :paths και την :fields. Αυτές θα αναπαριστούν τις διαδρομές που εφαρμόζουμε τη λογική μας και ποιά πεδία να απαιτήσουμε.

Σημείωση: Τα Plugs εφαρμόζονται σε όλες τις αιτήσεις, γι’αυτό και θα φιλτράρουμε τις αιτήσεις και θα εφαρμόσουμε τη λογική μας μόνο σε ένα υποσύνολό τους. Για να αγνοήσουμε μια αίτηση απλά θα μεταβιβάσουμε τη σύνδεση.

Θα ξεκινήσουμε υλοποιώντας το Plug μας και μετά θα συζητήσουμε πως λειτουργεί. Θα το δημιουργήσουμε στο lib/example/plug/verify_request.ex:

defmodule Example.Plug.VerifyRequest do
  defmodule IncompleteRequestError do
    @moduledoc """
    Error raised when a required field is missing.
    """

    defexception message: ""
  end

  def init(options), do: options

  def call(%Plug.Conn{request_path: path} = conn, opts) do
    if path in opts[:paths], do: verify_request!(conn.params, opts[:fields])
    conn
  end

  defp verify_request!(params, fields) do
    verified =
      params
      |> Map.keys()
      |> contains_fields?(fields)

    unless verified, do: raise(IncompleteRequestError)
  end

  defp contains_fields?(keys, fields), do: Enum.all?(fields, &(&1 in keys))
end

Το πρώτο πράγμα που πρέπει να σημειώσουμε είναι ότι ορίσαμε μια νέα εξαίρεση, την IncompleteRequestError η οποία θα ενεργοποιείται στην περίπτωση ενός μη έγκυρου αιτήματος.

Το δεύτερο μέρος του Plug μας είναι η συνάρτηση call/2. Εδώ είναι που αποφασίζουμε αν θα εφαρμόσουμε ή όχι την λογική επικύρωσης. Μόνο όταν η διαδρομή της αίτησης περιλαμβάνεται στην επιλογή μας :paths, τότε θα καλέσουμε την verify_request!/2.

Το τελευταίο μέρος του plug μας είναι η ιδιωτική συνάρτηση verify_request!/2 η οποία επικυρώνει αν τα απαιτούμενα :fields είναι όλα παρόντα. Σε περίπτωση που κάποια λείπουν, σηκώνουμε το σφάλμα IncompleteRequestError.

Ορίσαμε το Plug μας να επικυρώνει ότι όλες οι αιτήσεις στη διαδρομή /upload συμπεριλαμβάνει τα "content" και "mimetype". Μόνο τότε θα εκτελεστεί ο κώδικας της διαδρομής.

Στη συνέχεια, πρέπει να ενημερώσουμε το δρομολογητή για το νέο Plug. Επεξεργαστείτε το lib/example/router.ex και κάντε τις κάτωθι αλλαγές:

defmodule Example.Router do
  use Plug.Router

  alias Example.Plug.VerifyRequest

  plug Plug.Parsers, parsers: [:urlencoded, :multipart]
  plug VerifyRequest, fields: ["content", "mimetype"], paths: ["/upload"]
  plug :match
  plug :dispatch

  get "/" do
    send_resp(conn, 200, "Welcome")
  end

  get "/upload" do
    send_resp(conn, 201, "Uploaded")
  end

  match _ do
    send_resp(conn, 404, "Oops!")
  end
end

Με αυτόν τον κώδικα, λέμε στην εφαρμογή μας να στείλει τα εισερχόμενα αιτήματα από το plug VerifyRequest πριν τα στείλει στον κώδικα, στον δρομολογητή. Μέσω της κλήσης της συνάρτησης:

plug VerifyRequest, fields: ["content", "mimetype"], paths: ["/upload"]

Αυτόματα επικαλούμαστε την VerifyRequest.init(fields: ["content", "mimetype"], paths: ["/upload"]). Αυτό με τη σειρά του περνάει τις δοθείσες επιλογές στην συνάρτηση VerifyRequest.call(conn, opts)

Ας δούμε αυτό το plug σε λειτουργία! Κλείστε τον τοπικό σας εξυπηρετητή (θυμηθείτε, αυτό γίνεται πατώντας δύο φορές ctrl + c). Μετά κάντε επανεκκίνηση του εξυπηρετητή (mix run --no-halt). Τώρα πηγαίντε στην τοποθεσία http://127.0.0.1:8080/upload στον φυλλομετρητή σας και θα δείτε οτι η σελίδα απλά δεν δουλεύει. Θα δείτε απλά μια προκαθορισμένη σελίδα σφάλματος που παρέχεται από τον φυλλομετρητή σας.

Τώρα ας προσθέσουμε τις απαιτούμενες παραμέτρους πηγαίνοντας στην σελίδα http://127.0.0.1:8080/upload?content=thing1&mimetype=thing2. Θα πρέπει τώρα να μπορούμε να βλέπουμε το ‘Ανεβασμένο’ μήνυμά μας.

Κάνοντας τη θύρα HTTP παραμετροποιήσημη

Όταν ορίσαμε την ενότητα Example και την εφαρμογή, η θύρα HTTP γράφτηκε απευθείας στον κώδικά μας μέσα στην ενότητα. Θεωρείται καλή πρακτική το να κάνουμε τη θύρα παραμετροποιήσημη αποθηκεύοντάς την σε ένα αρχείο παραμετροποίησης.

Θα ορίσουμε μια μεταβλητή περιβάλλοντος εφαρμογής στο config/config.exs

Για έκδοση Elixir μικρότερη από την 1.9:

use Mix.Config

config :example, cowboy_port: 8080

Για έκδοση Elixir μεγαλύτερη από 1.9: (το Mix.Config έγινε deprecate στην έκδοση Elixir 1.9):

import Config

config :example,
  cowboy_port: 8080

Στη συνέχεια πρέπει να ενημερώσουμε το lib/example/application.ex, να διαβάσουμε την διαμορφομένη τιμή της θύρας, και να την περάσουμε στο Cowboy. Θα ορίσουμε μια ιδιοτική συνάρτηση για να περιέχει αυτήν την ευθύνη.

defmodule Example.Application do
  use Application
  require Logger

  def start(_type, _args) do
    children = [
      {Plug.Cowboy, scheme: :http, plug: Example.Router, options: [port: cowboy_port()]}
    ]
    opts = [strategy: :one_for_one, name: Example.Supervisor]

    Logger.info("Starting application...")

    Supervisor.start_link(children, opts)
  end

  defp cowboy_port, do: Application.get_env(:example, :cowboy_port, 8080)

end

Η τρίτη παράμετρος της Application.get_env είναι η προκαθορισμένη τιμή, για όταν η οδηγία ρύθμισης δεν έχει οριστεί.

Τώρα για να τρέξουμε την εφαρμογή μας μπορούμε να χρησιμοποιήσουμε:

mix run --no-halt

Δοκιμή ενός Plug

Η δοκιμή των Plugs είναι αρκετά προφανής χάρη στο Plug.Test. Περιλαμβάνει έναν αριθμό βοηθητικών συναρτήσεων για να κάνει εύκολες τις δοκιμές.

Γράψτε το ακόλουθο τέστ στο test/example/router_test.exs:

defmodule Example.RouterTest do
  use ExUnit.Case
  use Plug.Test

  alias Example.Router

  @content "<html><body>Hi!</body></html>"
  @mimetype "text/html"

  @opts Router.init([])

  test "returns welcome" do
    conn =
      :get
      |> conn("/", "")
      |> Router.call(@opts)

    assert conn.state == :sent
    assert conn.status == 200
  end

  test "returns uploaded" do
    conn =
      :get
      |> conn("/upload?content=#{@content}&mimetype=#{@mimetype}")
      |> Router.call(@opts)

    assert conn.state == :sent
    assert conn.status == 201
  end

  test "returns 404" do
    conn =
      :get
      |> conn("/missing", "")
      |> Router.call(@opts)

    assert conn.state == :sent
    assert conn.status == 404
  end
end

Τρέξτε το με αυτή την εντολή:

mix test test/example/router_test.exs

Plug.ErrorHandler

Παρατηρήσαμε προηγουμένως πως όταν πήγαμε στην σελίδα http://127.0.0.1:8080/upload χωρίς τις αναμενόμενες παραμέτρους, δεν είδαμε μια φιλική σελίδα σφάλματος ή ένα λογικό στάτους HTTP - απλά την προκαθορισμένη σελίδα σφάλματος του φυλλομετρητή μας με ένα 500 Internal Server Error.

Ας το φτιάξουμε αυτό τώρα προσθέτοντας στο Plug.ErrorHandler.

Αρχικά, ανοίξτε το lib/example/router.ex και προσθέστε τα ακόλουθα σε αυτό το αρχείο.

defmodule Example.Router do
  use Plug.Router
  use Plug.ErrorHandler

  alias Example.Plug.VerifyRequest

  plug Plug.Parsers, parsers: [:urlencoded, :multipart]
  plug VerifyRequest, fields: ["content", "mimetype"], paths: ["/upload"]
  plug :match
  plug :dispatch

  get "/" do
    send_resp(conn, 200, "Welcome")
  end

  get "/upload" do
    send_resp(conn, 201, "Uploaded")
  end

  match _ do
    send_resp(conn, 404, "Oops!")
  end

  defp handle_errors(conn, %{kind: kind, reason: reason, stack: stack}) do
    IO.inspect(kind, label: :kind)
    IO.inspect(reason, label: :reason)
    IO.inspect(stack, label: :stack)
    send_resp(conn, conn.status, "Something went wrong")
  end
end

Θα παρατηρήσετε πως τώρα στην αρχή προσθέτουμε το use Plug.ErrorHandler.

Αυτό το plug πιάνει τα σφάλματα, και μετά ψάχνει την συνάρτηση handle_errors/2 ώστε να την καλέσει και να διαχειρηστεί τα σφάλματα.

Η handle_errors/2 απλά πρέπει να δεχθεί το conn ως το πρώτο όρισμα και κατόπιν έναν χάρτη με τρια κλειδιά (:kind, :reason, and :stack) ως δεύτερο όρισμα.

Μπορείτε να δείτε ότι ορίσαμε μια πολύ απλή συνάρτηση handle_errors/2 για να δούμε τι συμβαίνει. Ας σταματήσουμε και ας επανεκκινήσουμε την εφαρμογή μας για να δούμε πως δουλεύει!

Τώρα, όταν πάμε στην σελίδα http://127.0.0.1:8080/upload, θα δούμε ένα φιλικό μήνυμα σφάλματος.

Αν δείτε το τερματικό σας, θα δείτε περίπου τα ακόλουθα:

kind: :error
reason: %Example.Plug.VerifyRequest.IncompleteRequestError{message: ""}
stack: [
  {Example.Plug.VerifyRequest, :verify_request!, 2,
   [file: 'lib/example/plug/verify_request.ex', line: 23]},
  {Example.Plug.VerifyRequest, :call, 2,
   [file: 'lib/example/plug/verify_request.ex', line: 13]},
  {Example.Router, :plug_builder_call, 2,
   [file: 'lib/example/router.ex', line: 1]},
  {Example.Router, :call, 2, [file: 'lib/plug/error_handler.ex', line: 64]},
  {Plug.Cowboy.Handler, :init, 2,
   [file: 'lib/plug/cowboy/handler.ex', line: 12]},
  {:cowboy_handler, :execute, 2,
   [
     file: '/path/to/project/example/deps/cowboy/src/cowboy_handler.erl',
     line: 41
   ]},
  {:cowboy_stream_h, :execute, 3,
   [
     file: '/path/to/project/example/deps/cowboy/src/cowboy_stream_h.erl',
     line: 293
   ]},
  {:cowboy_stream_h, :request_process, 3,
   [
     file: '/path/to/project/example/deps/cowboy/src/cowboy_stream_h.erl',
     line: 271
   ]}
]

Αυτή τη στιγμή, ακόμα επιστρέφουμε ένα 500 Internal Server Error. Μπορούμε να προσαρμόσουμε τον κώδικα κατάστασης προσθέτοντας ένα πεδίο :plug_status στην εξαίρεσή μας. Ανοίξτε το lib/example/plug/verify_request.ex και προσθέστε τα ακόλουθα:

defmodule IncompleteRequestError do
  defexception message: "", plug_status: 400
end

Επανεκκινήστε τον εξυπηρετητή σας και κάντε ανανέωση, τώρα θα σας επιστρέψει 400 Bad Request.

Αυτό το plug το κάνει πραγματικά εύκολο ώστε να πίασετε τις χρήσιμες πληροφορίες που χρειάζεται ένας προγραμματιστής για να διορθώσει προβλήματα, ενώ παράλληλα επιστρέφει στον τελικό χρήστη μια καλή σελίδα ώστε να μην φαίνεται ότι η εφαρμογή μας “τα τίναξε” εντελώς!

Διαθέσιμα Plugs

Υπάρχει ένας αριθμός από Plugs διαθέσιμα από την αρχή. Η πλήρης λίστα μπορεί να βρεθεί στα έγγραφα Plug εδώ.

Έπιασες λάθος ή θέλεις να συνεισφέρεις στο μάθημα; Επεξεργαστείτε αυτό το μάθημα στο GitHub!