101 Elixir : Cukup dengan Plug untuk membangun website dinamis yang powerfull (part 2)

surya surakhman
6 min readJun 24, 2018

--

Setelah pada part sebelumnya kita telah membahas mengenai apa itu Plug, membuat server sederhana dengan Plug+Cowboy dan membuat router untuk Plug server, kali ini kita akan mencoba mengembangkan kembali server yang telah kita buat dengan beberapa fitur dari Plug framework dan menggunakan view engine untuk mengembalikan response berupa halaman html.

Sebelum membaca tulisan ini, perlu diperhatikan tulisan ini sangat tergantung dengan part pertama, jadi sebaiknya baca part pertama dulu jika belum.

Untuk code dari part sebelumnya bisa diclone di sini. Selanjutnya mari kita langsung ke pembahasan.

Mengenal Plug Pipeline

Seperti yang sudah kita bahas sebelumnya, Plug bertugas untuk menerima request, me-modifikasi request dan mengembalikan response atau bisa juga diartikan bahwa Plug bertugas sebagai middleware untuk menghandle request ke web server. Dan selayaknya sebuah middleware, sebuah Plug bisa meneruskan request ke Plug lainnya atau biasa kita sebut dengan Pipeline.

Plug sendiri mempunyai banyak module bawaan yang hampir semuanya berfungsi sebagai middleware. Dan untuk menambahkan module-module tersebut, kita hanya perlu menggunakan macro plug/1. Sebagai contoh, untuk menambahkan module Plug.Logger, kita perlu menggunakan macro plug/1 seperti ini di lib/platform.ex

defmodule Platform do
import Plug.Conn
plug Plug.Logger

Dengan menambahkan middleware Plug.Logger, setiap request yang diterima oleh web server akan terlebih dahulu masuk ke Logger, lalu kemudian request di teruskan kembali ke Plug yang lainnya. Ini contoh hasil dari logger jika kita mengakses http://localhost:4000

Yah, dari sini tentu sudah jelas bahwa Plug pipeline sama dengan pipe di code elixir |> hanya saja isi dari pipe tersebut adalah sebuah Plug connection.

Menggunakan Plug.Router

Di part pertama kita telah membahas bagaimana router di Plug bekerja. Namun sepertinya masih bisa kita refactor karena terlalu banyak function route yang harus di-match. Kabar baiknya, Plug sudah menyediakan sebuah module Plug.Router untuk meng-enkapsulasi proses itu semua. Jadi mari kita refactor lagi platform yang telah kita buat dengan menggunakan module tersebut sehingga bisa terlihat lebih simple. Berikut contoh penggunaan di lib/platform.ex

defmodule Platform do
use Plug.Router
plug Plug.Logger
plug :match
plug :dispatch
get “/” do
send_resp(conn, 200, “This is index page”)
end
match _ do
send_resp(conn, 404, “Not Found”)
end
def start do
{:ok, pid} = Plug.Adapters.Cowboy.http __MODULE__, []
pid
end
end

Kita menambahkan module dengan use Plug.Router Module inilah yang berisi beberapa macro seperti get, post, put dan delete untuk keperluan Restful. Namun di sini Plug.Router membutuhkan 2 pipeline sebagai dependency yaitu :match dan :dispatch . Match bertugas untuk melakukan pattern matching mirip seperti yang telah kita buat di module Platform.Macros.Router di functionroute(conn.method, conn.path_info, conn) .

Sedangkan Dispatch bertugas untuk meng-eksekusi code dari route yang sudah sesuai dengan return dari match.

Lalu bagaimana dengan module Platform.Router.User yang telah kita buat sebelumnya? Apakah bisa masih perlu menggunakan function route/3 secara manual? Jawabannya tentu tidak, kita juga bisa menggunakan Plug.Router di module tersebut kemudian menghubungkannnya dengan router yang ada di module Platform. Mari kita coba rubah code yang ada di file /lib/router/user.ex menjadi seperti ini

defmodule Platform.Router.User do
import Plug.Conn
use Plug.Router
plug :match
plug :dispatch
get “/” do
send_resp(conn, 200, “This is User page”)
end
get “/:user_id” do
send_resp(conn, 200, “You are accessing user with id # {user_id}”)
end
end

Setelah itu kita dapat meneruskan semua request yang mengarah ke url “/users” agar menuju ke Router yang ada di module Platform.Router.User dengan menambah macro forward/3 di Router Platform dengan cara seperti ini

defmodule Platform do
use Plug.Router
plug Plug.Logger
plug :match
plug :dispatch
forward "/users", to: Platform.Router.User get “/” do
send_resp(conn, 200, “This is index page”)
end
match _ do
send_resp(conn, 404, “Not Found”)
end
def start do
{:ok, pid} = Plug.Adapters.Cowboy.http __MODULE__, []
pid
end
end

Dengan begitu kita bisa mengakses kembali url http://localhost/users dan http://localhost/users/1. Yess, Mantapp!

Hmm,, sepertinya masih ada code yang kurang Dry. mari kita refactor lagi. Pindahkan code untuk mendefinisikan Router ke module macro yang pernah kit buat yaitu Platform.Macros.Router menjadi seperti ini

defmodule Platform.Macros.Router do
defmacro __using__(_options) do
quote do
use Plug.Router
plug Plug.Logger
plug :match
plug :dispatch
end
end
end

Kemudian kita kembalikan code untuk meng-include macro Router di module Platform dan juga di module Platform.Router.User

defmodule Platform do  use Platform.Macros.Router

dan

defmodule Platform.Router.User do  use Platform.Macros.Router

Dengan begini code kita menjadi terlihat lebih baik.

Wait,, kemana perginya function init/1 dan call/2 ? Bukankah setiap module Plug harus mempunyai ke-dua function tersebut? Nah, ketika kita menggunakan Plug.Router di module kita, sebenarnya di dalam Plug.Router tersebut sudah memanggil function init/1 dan call/2. Jadi kita sudah tidak perlu lagi membuatnya secara manual. Jadi lebih rapih bukan,,, :)

Berikutnya mari kita beralih ke bagian view/template engine.

Menggunakan Template Engine

Sama seperti Ruby on Rails yang punya template engine seperti Erb, Elixir juga punya template engine sendiri yaitu Eex. Penggunaannya pun relatif sama karena eex memang di adaptasi dari erb. Namun bagi kamu yang belum familiar dengan keduanya, tenang saja.. ini akan sangat mudah. Trust me :).

Pada dasarnya, Eex bertugas untuk merubah string menjadi sebuah template yang dinamis sehingga bisa dikombinasikan dengan kode elixir. Contohnya seperti ini

iex> EEx.eval_string “foo <%= bar %>”, [bar: “baz”]
"foo baz"

Pada contoh diatas kata foo adalah string dan baz adalah nilai dari variable bar yang mana variable bar didefinisikan dengan code elixir. Bisa dilihat bahwa semua yang ada di antara tanda <%= dan %> adalah code elixir.

Lalu bagaimana menambahkannya ke dalam module Plug? Mudah saja, mari kita coba membuat halaman index untuk module Platform. Buat sebuah file di /lib/Templates/index.eex dengan code berikut

<!DOCTYPE html>
<html>
<body>
<h1>Hai, welcome to Index page</h1>
<p>Plug is awesome</p>
</body>
</html>

Setelah itu, mari kita rubah route index kita di module Platform seperti ini

get “/” do
page_content = EEx.eval_file(“lib/Templates/index.eex”, [])
conn
|> put_resp_content_type(“text/html”)
|> send_resp(200, page_content)
end

Pada code diatas kita menggunakan function EEx.eval_file/2 untuk mengambil content dari file index.eex. Parameter pertama berisi string source file dan parameter kedua berisi list variable yang akan di extend ke template. Setelah content untuk template kita dapatkan, selanjutnya kita tambahkan response content type menjadi html dengan put_resp_content_type/2 untuk kemudian dikirim dikirim ke client side

Sebagai contoh, jika kita ingin membuat template untuk module Platform.Router.User dengan url /users/:user_id, maka untuk menampilkan user_id tersebut ke template cukup dengan menggunakan parameter kedua

page_contents = EEx.eval_file("lib/Templates/show_user.eex", [user_id: user_id])

dan untuk contoh menampilkan data ke template, dilakukan seperti ini

<!DOCTYPE html>
<html>
<body>
<h1>User Information Page</h1>
<p>You looking for user with ID <%= user_id %>.</p>
<p>You can also execute elixir code, 1 + 1 = <%= 1 + 1 %></p>
<%= if user_id == "1" do %>
<%# this is not rendered %>
User with ID 1 is Surya
<% end %>
</body>
</html>

Yup, Sangat mudah bukan? dengan ini code menjadi lebih bersahabat dengan mata. :)

Precompiling templates

Kita sudah tahu bahwa template engine seperti EEx membaca file ber-extensi .eex berisi syntax html. Tapi bayangkan jika isi dari file tersebut adalah code html yang sangat rumit berisi 100 baris code, apakah EEx harus selalu melakukan proses read dari file tersebut setiap kali ada request masuk? tentu hal itu kurang efisien dan membuat load time nya jadi lambat.

Untuk mengatasi masalah tersebut EEx menyediakan macro khusus yaitu EEx.function_from_file/4 . macro itu bekerja ketika proses precompiling, jadi hanya dijalankan satu kali saja sehingga kita hanya perlu memanggil sebuah function untuk mendapatkan template setiap kali ada request masuk.

Mari kita kembali ke code router yang menerima url users/:id di module Platform.Router.User lalu kita tambahkan function untuk precompiling.

defmodule Platform.Router.User do
import Plug.Conn
use Platform.Macros.Router
require EEx
EEx.function_from_file :def, :show_user, “lib/Templates/show_user.eex”, [:user_id] get “/” do
send_resp(conn, 200, “This is User page”)
end
get “/:user_id” do
page_content = show_user(user_id)
conn
|> put_resp_content_type(“text/html”)
|> send_resp(200, page_content)
end
end

Macro EEx.function_from_file/4 memindahkan template yang sudah di read dari file ke dalam sebuah function :show_user (didefinisikan di parameter ke-2 sebagai atom) sehingga proses rendering html menjadi lebih cepat.

Next

Berikutnya adalah sesi terakhir dari pembahasan Plug microframework dimana kita akan membuat migration database, melakukan proses transaksi dengan database menggunakan ORM dan membuat Testing.

Semoga tulisan ini bermanfaat.

--

--