Javascript Routing

Pada bagian ini kita akan membahas penggunaan History API untuk membangun sebuah routing library sederhana. Tujuan utama dari pustaka yang ingin kita kembangkan adalah untuk memetakan URL yang diakses user dengan fungsi atau objek tertentu dalam kode kita. Misalkan ketika pengguna mengakses http://contoh.com/profile/bertzzie maka fungsi Contoh.Profile("bertzzie") di dalam kode kita akan dipanggil. Karena menggunakan History API, tentunya pengguna tidak perlu benar-benar berpindah halaman (data bisa diambil menggunakan AJAX / WebSocket) dan sejarah penelusuran dalam browser akan tetap diperbaharui.

Contoh penggunaan

Agar pembaca dapat lebih mudah memahami apa yang akan kita kembangkan, kita terlebih dahulu akan melihat contoh penggunaan routing library yang akan dikembangkan. Berikut adalah contoh penggunaan routing library yang akan kita kembangkan:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var root      = "/routing/",        // route relatif dari http://contoh.com/routing/*
    route     = MyLib.Routes(root); // pembuatan objek routing

route.add(/profile\/(.*)/, function (pid) {
    // kode dijalankan ketika pengguna mengakses
    // http://contoh.com/routing/profile/*
});

route.add(/options\/(.*)/, function (type) {
    // kode dijalankan ketika pengguna mengakses
    // http://contoh.com/routing/options/*
});

route.init(); // inisialisasi semua route

Beberapa hal penting yang perlu diperhatikan dari kode di atas:

  1. Pembuatan objek dilakukan melalui pemanggilan fungsi biasa, yaitu MyLib.Routes.
  2. Kita dapat memilih beberapa titik asal, yang disimpan pada variabel root. Jika root berisi "/master/" maka seluruh route yang dibuat akan dianggap relatif terhadap http://contoh.com/master/.
  3. Penambahan route dilakukan dengan memanggil method Routes.add.
  4. Pencocokan URL dilakukan menggunakan regular expression (dari parameter pertama pada Routes.add).
  5. Setelah semua route yang ada ditambahkan, kita harus tetap melakukan inisialisasi melalui Routes.init.

Pada akhirnya, kode yang kita buat di atas cukup sederhana, dan terlihat tidak jauh berbeda dengan kode framework aplikasi pada umumnya.

Dengan menggunakan routing library seperti pada kode di atas, secara otomatis seluruh request yang datang dari pengguna akan hanya boleh datang dari satu titik saja. Seluruh request harus masuk ke satu halaman yang sama, dan kemudian request tersebut ditangani oleh kode router kita di atas. Karena seluruh request masuk ke satu halaman (biasanya index.html), maka aplikasi yang dibangun dengan cara seperti ini dikenal dengan nama “Single Page Application (SPA)” [1].

Persiapan Awal

Sebelum mulai mengembangkan routing library ini, terdapat satu hal yang perlu kita persiapkan terlebih dahulu, yaitu memastikan semua request pengguna masuk ke satu titik pada kode kita saja. Jika kita ingin menggunakan routing library dengan optimal, kita harus dapat memastikan request pengguna, dalam bentuk apapun, kembali ke index.html. Hal ini berarti ketika pengguna mengakses http://contoh.com/profile/bertzzie (atau URL lainnya) sebenarnya ia akan dibawa ke http://contoh.com/index.html di balik layar.

Untuk dapat mencapai hal ini, kita perlu melakukan konfigurasi melalui web server yang digunakan. Silahkan baca dokumentasi pada web server yang anda gunakan untuk melakukan hal ini [2]. Berikut adalah contoh konfigurasi yang dapat digunakan jika anda menggunakan web server Apache httpd (yang biasa dipaketkan dalam XAMPP [3]):

  1. Pastikan ekstensi mod_rewrite telah aktif pada server anda (tutorial linux, tuorial windows).
  2. Buat sebuah file bernama .htaccess pada direktori utama aplikasi web anda.
  3. Isikan .htaccess dengan kode berikut:
1
2
3
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.html

Kemudian coba akses halaman secara acak pada URL relatif terhadap aplikasi web anda. Jika anda menyimpan kode di htdocs/routing/ maka akses ke http://localhost/routing/url/acak/asdflak akan tetap menampilkan isi index.html sekarang.

Objek Pendukung

Hal lain yang perlu kita persiapkan sebelum mulai membangun routing library kita adalah objek pendukung yang akan membantu kita dalam memproses string URL yang akan kita olah. Kita akan mulai dengan membuat sebuah objek sederhana. Buat sebuah file dengan nama URL.js dan isikan dengan kode berikut:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var MyLib = MyLib || {};

(function () {
    "use strict";

    MyLib.URL = {
        trimSlash: function (path) {
            return path.toString().replace(/\/$/, '').replace(/^\//, '');
        },
        getPathName: function (url) {
            var a  = document.createElement("a");
            a.href = url;

            return a.pathname;
        }
    };
})(MyLib);

MyLib.URL merupakan sebuah objek sederhana yang hanya memiliki dua buah method, yaitu:

  1. trimSlash, sebuah method yang membantu kita dalam membuang garis miring (slash; /) pada awal maupun akhir URL. Kita membuat method ini agar rute seperti /profile/update/ dan /profile/update yang pada dasarnya adalah sama, dapat dianggap sama oleh sistem kita nantinya. Selain memotong garis miring pada akhir, kita juga memotong garis miring pada awal, sehingga /route/ dan route dianggap sama.
  2. getPathName, method yang mengambil pathname dari sebuah URL. Pathname merupakan bagian URL yang muncul setelah URL dan sebelum query data. Contohnya, pathname dari http://www.google.com/search?q=testing adalah /search. Kita ingin mengambil pathname karena biasanya bagian inilah yang paling penting untuk routing (yang secara definisi menampilkan halaman yang tepat untuk sebuah path).

Setelah membuat objek pendukung yang dibutuhkan, mari kita kembangkan objek utama dari routing library kita.

Objek Router Utama

Langkah pertama untuk membuat routing library pastinya adalah membuat objek router yang akan kita gunakan nantinya. Kode pembuatan objek sangat sederhana, sesuai dengan prinsip awal yang kita pelajari pada Javascript: Sebuah Pembedahan. Langsung saja, buat sebuah file dengan nama Routes.js, dengan isi seperti berikut:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var MyLib = MyLib || {};

(function () {
    "use strict";

    MyLibs.Routes = function (r) {
        var root   = r? MyLib.URL.trimSlash(r) : "/",
            routes = [],
            fin    = {};

        return fin;
    };
})();

Constructor objek utama kita menerima sebuah parameter opsional, yaitu r. r merupakan “akar” atau rute awal yang kita inginkan untuk aplikasi kita. Jika kita memasukkan nilai "/routing/" ke dalam r, maka semua rute yang ditangani oleh library kita akan berawal dari /routing/ (http://contoh.com/routing/profile ditangani tetapi http://contoh.com/about/profile tidak). Opsi ini tentunya sangat berguna, memberikan fleksibilitas kepada kita untuk membangun apliksai dengan beberapa titik awal rute berbeda. Jika dikosongkan, maka rute akar akan otomatis menjadi /, yang artinya semua rute diproses.

Selanjutnya kita membuat sebuah array, routes, yang akan menyimpan seluruh objek rute yang nantinya kita gunakan untuk memanggil fungsi yang dikaitkan dengan rute. Rute dan fungsi yang akan dipanggil nantinya akan diisikan oleh pengguna routing library kita. Setiap kali pengguna menambahkan rute baru beserta dengan fungsi yang harus dipanggil ketika rute dipanggil, kita akan membuat objek baru dan menyimpannya ke dalam array routes ini.

Terakhir, kita membuat objek fin, yang akan dikirimkan ke pengguna nanti. Kita akan menambahkan banyak method ke dalam objek ini pada bagian berikutnya.

Penambahan dan Penghapusan Rute

Fungsi paling pertama yang akan kita kembangkan ialah penambahan dan penghapusan rute, karena fungsi inilah yang pertama kali akan digunakan oleh pengguna. Langsung saja, tambahkan kode berikut sebelum return fin;:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fin.add = function (pattern, handler) {
    routes.push({'pattern': pattern, 'handler': handler});
    return this;
};

fin.remove = function (pattern) {
    var i   = 0,
        len = routes.length,
        r;

    for (i = 0; i < len; i += 1) {
        r = routes[i].pattern.toString();
        if (r === pattern.toString()) {
            routes.splice(i, 1);
            return this;
        }
    }

    return this;
};

fin.reset = function () {
    routes = [];
    root   = ''
};

Penambahan rute baru, melalui fin.add cukup sederhana. Method ini menerima dua argumen: argumen pertama berupa regular expression yang digunakan untuk mencocokkan pola rute, sementara argumen kedua adalah fungsi yang dipanggil ketika rute cocok dengan URL. Kita hanya menambahkan objek baru ke dalam array routes, dan kemudian mengembalikan this agar method dapat dipanggil secara berantai jika diperlukan.

Penghapusan rute pada fin.remove sedikit lebih panjang, karena kita terlebih dahulu harus menelusuri seluruh rute yang ada. Setiap rute dicek apakah pattern-nya sama dengan rute yang akan dihapus, dan jika sama rute tersebut kita buang dari array routes. Pembuangan array dilakukan melalui Array.prototype.splice (dokumentasi splice) untuk menyederhanakan fungsi.

Sebagai tambahan, kita juga membuat fungsi fin.reset yang hanya menghapus semua rute, beserta dengan rute asal.

Masuk ke Rute Baru

Setelah bisa menambahkan rute baru, tentunya kita ingin memberikan fasilitas kepada pengguna library untuk berpindah rute. Misalkan, pengguna web dapat dibawa ke rute baru ketika melakukan klik sebuah tombol. Berikut adalah kode untuk mencapai perpindahan rute ini:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var Run = function (url) {
    var path = url || fin.getCurrentPath(),
        i    = 0,
        len  = routes.length,
        match;

    path = MyLib.URL.trimSlash(path);

    for (i = 0; i < len; i += 1) {
        match = path.match(routes[i].pattern);
        if (match) {
            match.shift();
            routes[i].handler.apply({}, match);
        }
    }
};

fin.getCurrentPath = function () {
    var path = '';

    path = MyLib.URL.trimSlash(location.pathname);
    path = path.replace(/\?(.*)$/, '');
    path = root != '/' ? path.replace(root, ''): path;

    return MyLib.URL.trimSlash(path);
};

fin.navigate = function (path, title, state) {
    path = MyLib.URL.trimSlash(MyLib.URL.getPathName(path));
    Run(path);

    history.pushState(state, title, "/" + root + "/" + path);
};

Run merupakan sebuah fungsi privat yang melakukan pencocokan pola URL terhadap regular expression dari rute yang kita berikan pada fin.add. Jika kita tidak memberikan nilai url kepada Run, maka eksekusi akan dilakukan terhadap URL yang sedang dibuka pengguna pada saat itu (sehingga pengguna tidak berpindah halaman). Pada fungsi Run, pada dasarnya kita menelusuri satu per satu rute, dan mencocokkan polanya dengan menggunakan method String.prototype.match (dokumentasi match. Karena String.prototype.match mengembalikan sebuah array sesuai dengan pola yang kita berikan, kita kemudian dapat menggunakan fungsi apply [4] untuk memanggil fungsi handler yang kita simpan ketika memanggil fin.add. Tentu saja pemanggilan apply dilakukan setelah kita membuang elemen pertama dari array hasil, karena elemen pertama merupakan pola dasar URL yang diberikan (contohnya "/profile/bertzzie" dengan pola "/profile\/(.*)/" akan mengembalikan ["profile/bertzzie", "bertzzie"], yang mana kita hanya memerlukan "bertzzie").

Pengambilan URL pengguna sekarang dilakukan melalui fin.getCurrentPath. Fungsi ini cukup gamblang, di mana kita hanya mengambil informasi URL sekarang melalui location.pathname, dan kemudian melakukan pembersihan terhadap data yang diberikan. Pembersihan yang dimaksud adalah dengan menghapus garis miring di depan maupun belakang, serta penambahan root. Fungsi ini dapat juga diletakkan pada MyLib.URL jika diinginkan. Kita meletakkan fungsi ini pada MyLib.Routes dengan pertimbangan bahwa currentPath atau pathname yang kita gunakan memiliki aturan spesifik dari routing library kita (garis miring pembuka dan penutup harus dihapus, rute asal, dst). Jika diletakkan pada MyLib.URL, kita pada dasarnya dapat langsung mengembalikan location.pathname.

Terakhir, pengguna library dapat melakukan pemindahan rute melalui fungsi publik fin.navigate, yang hanya melakukan pemanggilan Run dan kemudian menyimpan perpindahan ini ke dalam sejarah penelusuran browser melalui history.pushState. fin.navigate perlu dipisahkan dengan Run terutama karena penyimpanan sejarah ini. Terkadang kita ingin memanggil Run tanpa memindahkan sejarah, misalnya ketika pengguna menekan tombol “Back”. Jika aksi pengguna menekan “Back” disimpan dalam sejarah, kita akan memiliki perulangan sejarah sampai tidak terbatas.

Penutup

Pada akhir tulisan ini kita telah melihat bagaimana menggunakan History API dengan praktis dan maksimal. Walaupun masih terdapat banyak kekurangan dari routing library yang kita kembangkan, kita telah memanfaatkan History API dengan efektif. Adapun beberapa kekurangan yang belum terimpelemntasi dengan baik yaitu:

  1. HrefClickHandler belum dapat menangani link eksternal. Jika tujuan link yang diberikan oleh a adalah ke website lain, HrefClickHandler seharusnya tahu dan tidak menanganinya dengan MyLib.Routes.
  2. Belum terdapat mekanisme atau method yang dapat digunakan ketika kita ingin mengubah root secara dinamis.
  3. MyLib.Routes belum menangani HTTP dengan baik. Misalnya, pengiriman data form melalui POST atau PUT tidak akan dapat ditangani.

Implementasi perbaikan dari beberapa kekurangan di atas (dan pastinya masih terdapat banyak kekurangan lain lagi) akan diserahkan sebagai latihan untuk pembaca.

Kode lengkap dari routing library yang kita kembangkan, beserta contoh penggunaannya dapat diambil pada halaman Github untuk artikel ini.

Catatan Kaki

[1]Dari sudut pandang Rekayasa Perangkat Lunak (Software Engineering) pola aplikasi seperti ini dikenal dengan nama Front Controller Pattern.
[2]Kata kunci yang umumnya digunakan adalah “Rewrite all URL to index.html/index.php”
[3]Pengguna nginx dapat mencoba mengakses tutorial berikut.
[4]Baca tentang Indirect Invocation Pattern jika tidak mengerti fungsi apply.
comments powered by Disqus
Kembali ke bertzzie.com