101 Elixir : Cukup dengan Plug untuk membangun website dinamis yang powerfull (part 2)
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
endend
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}”)
endend
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
endend
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)
endend
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.