MDN’s new design is in Beta! A sneak peek: https://blog.mozilla.org/opendesign/mdns-new-design-beta/

Menggunakan Service Workers

Terjemahan ini belum lengkap. Mohon bantu menerjemahkan artikel ini dari Bahasa Inggris.

This is an experimental technology
Because this technology's specification has not stabilized, check the compatibility table for usage in various browsers. Also note that the syntax and behavior of an experimental technology is subject to change in future versions of browsers as the specification changes.

Artikel ini memberikan informasi mengenai cara memulai dan menggunakan Service Workers, Termasuk arsitektur dasar, mendaftarkan service worker, penginstalan dan proses aktivasi service worker, memperbaharui service worker, kontrol cache dan custom response, Semuanya dalam konteks aplikasi sederhana dengan fungsi offline. 

Premis Service Workers

Salah satu masalah utama yang dialami pengguna web selama bertahun - tahun adalah hilangnya konektivitas. Bahkan aplikasi terbaik didunia sekalipun akan memberikan pengalaman pengguna yang buruk jika tidak bisa di muat atau di download. Berbagai upaya menciptakan teknologi telah dilakukan untuk memecahkan masalah tersebut, Agar halaman Offline dapat digunakan, dan beberapa masalah telah diselesaikan.Tapi masalah utamanya masih belum ada mekanisame kontrol yang terbaik untuk semua permintaan aset cache dan kostum jaringan.

Pada upaya sebelumnya — AppCache — dilihat sebagai ide yang bagus karena disana anda juga bisa menentukan aset untuk dicache dengan mudah. Namun, itu membuat asumsi tentang ketika anda mencoba dan kemudian pecah dengan mengerikan ketika aplikasi anda tidak mengikuti asumsi premis. Baca artikel Jake Archibald's Application Cache is a Douchebag untuk lebih detail.

Catatan: Pada firefox 44, ketika AppCache digunakan untuk menyediakan layanan offline untuk suatu halaman, pesan peringatan sekarang akan ditampilkan di console menyarankan pengembang untuk menggunakan Service workers  (bug 1204581.)

Service worker seharusnya telah memecahkan masalah ini. Syntax pada service worker lebih kompleks di bandingkan AppCache, tapi kesamaanya adalah anda bisa menggunkan javascript untuk mengontrol AppCache anda secara tersirat dengan derajat grunality yang lebih bagus, memungkinkan anda untuk menangani masalah ini dan banyak lagi. Dengan menggunakan Service worker anda bisa mengatur aplikasi untuk menggunakan aset utama, sehingga memberikan pengalaman utama bahkan saat offline, sebelum mendapatkan lebih banyak data dari jaringan (biasa disebut sebagai Offline First). Ini sudah tersedia dengan aplikasi asli, yang merupakan salah satu alasan utama aplikasi asli sering dipilih lebih dari aplikasi web.

Persiapan bermain dengan service worker

Banyak fitur service worker saat ini telah di aktifkan secara default pada browser versi terbaru yang mendukung. Tapi jika anda menemui kode demo tidak bekerja pada versi browser anda, mungkin anda perlu mengaktifkan preferensi:

  • Firefox Nightly: Buka about:config dan atur  dom.serviceWorkers.enabled ke true; kemudian restart browser.
  • Chrome Canary: Buka chrome://flags dan aktifkan experimental-web-platform-features; restart browser ( perlu dicatat bahwa beberapa fitur telah diaktifkan secara default di Chrome.)
  • Opera: Buka opera://flags dan aktifkan dukungan untuk ServiceWorker; restart browser.

Anda perlu menyajikan kode dengan menggunakan HTTPS — Service Workers dibatasi dengan hanya dapat di jalankan pada HTTPS untuk alasan keamanan. Github merupakan salah satu tempat yang di sarankan untuk bereksperimen, karena mendukung HTTPS.

Arsitektur dasar

Dengan service workers, langkah - langkah umum berikut perlu diamati untuk konfigurasi dasar:

  1. URL service worker diambil di daftarkan melalui serviceWorkerContainer.register().
  2. Jika sukses, service worker dijalankan di ServiceWorkerGlobalScope; pada dasarnya ini merupakan hal yang sepesial dari konteks worker, menjalankan tread script utama untuk pengeksekusian, tanpa dukungan akses DOM.
  3. Sekarang service worker telah siap untuk memproses event.
  4. Instalasai service worker dicoba ketika service worker mengontrol halaman yang diakses setelah dan sebelumnya. Even Install akan selalu di kirim pertama kali ke service workerr ( ini bisa digunakan untuk memulai proses untuk mengumpulkan IndexedDB, dan mencache aset situs). Sama halnya seperti prosedur penginstalan asli atau aplikasi firefox OS — memungkinkan semuanya tersedia untuk digunakan secara offline.
  5. Ketika handler oninstall selesai, service worker ditetapkan untuk diinstall.
  6. Selanjutnya adalah aktivasi. Ketika service worker terinstall, selanjutnya akan menerima event activate. Penggunaan utama dari onactivate adalah untuk membersihkan sumberdaya yang digunakan pada versi sebelumnya.
  7. Service Worker sekarang dapat mengontrol halaman, tapi hanya yang di buka setelah register() telah sukses. misal dokumen mulai aktif dengan atau tanpa Service Worker dan menjaganya selama masih digunakan. Jadi dokumen harus di muat ulang agar benar - benar terkontrol.

Grafik dibawah menunjukan ringkasan dari event yang tersedia pada service worker:

install, activate, message, fetch, sync, push

Promis

Promis adalah mekanisme yang sebagian besar digunakan untuk menjalankan operasi secara asinkron, dengan bergantung pada pencapaian satu dan lainnya. Ini merupakan pusat dari cara kerja service workers.

Promis bisa digunakan untuk melakukan banyak hal, tapi untuk saat ini, yang perlu anda tahu adalah jika sesuatu yang mengembalikan promise, anda bisa menambahkan.then() di akhir dan menambahkan callback di dalamnya untuk mengetahui apakah berhasil, gagal da lainnya., atau anda bisa menambahkan .catch() di akhir jika anda ingin menambahkan calback status gagal.

Mari bandingkan kesamaan struktur callback tradisional synchronous dan promise asynchronous.

sync

try {
  var value = myFunction();
  console.log(value);
} catch(err) {
  console.log(err);
}

async

myFunction().then(function(value) {
  console.log(value);
  }).catch(function(err) {
  console.log(err);
});

Pada contoh pertama, kita perlu mengunggu myFunction() untuk dijalankan dan mengembalikan nilai sebelum kode lain bisa dijalankan. Pada contoh kedua, myFunction() mengembalikan promise untuk nilai, kemudian semua kode dapat di bawa untuk dapat dijalankan. Ketika promise diselesaikan, kode didalamnya bisa dijalankan secara asynchronously.

Sebagai contoh nyata — jika kita ingin memuat gambar secara dinamic, tapi kita ingin memastikan bahwa gambar tersebut di muat terlebih dahulu sebelum di tampilkan? Hal standar ini yang ingin dilakukan, tapi ini akan sedikit sulit. Kita bisa mengunakan .onload untuk hanya menampilkan gambar setelah di muat, tapi bagaimana dengan event yang memulai sebelum kita memulai melacaknya ? kita bisa mencoba melakukannya dengan menggunakan.complete, tapi ini tetap tidak foolproof, dan bagaimana dengan lebih dari satu gambar? Dan, ummm, ini tetap synchronous, Jadi di blok di tread utama.

Gantinya, kita bisa membuat promis kita sendiri untuk mengatasi kasus seperi ini. (Lihat contoh Promises test untuk kode sumber, atau Lihat cara kerja secara live.)

Note: Implementasi service worker akan mengutamakan menggunakan caching dan onfetch daripada XMLHttpRequest API. Fitur tersebut tidak di gunakan disini jadi anda hanya fokus pada penggunaan Promises.

function imgLoad(url) {
  return new Promise(function(resolve, reject) {      
    var request = new XMLHttpRequest();
    request.open('GET', url);
    request.responseType = 'blob';

    request.onload = function() {
      if (request.status == 200) {
        resolve(request.response);
      } else {
        reject(Error('Image didn\'t load successfully; error code:' + request.statusText));
      }
    };

    request.onerror = function() {
      reject(Error('There was a network error.'));
    };

    request.send();
  });
}

Kita mengembalikan promis baru menggunakan konstruktor Promise(), di mana menggunakan argumen sebagai fungsi callback dengan parameter resolve dan reject. DImanapung di fungsi, kita perlu mendefinisikan apa yang terjadi untuk promise yang diselesaikan secara sukses atau ditolak — pada kasus ini mengembalikan status 200 OK atau tidak — dan kemudian memanggil  resolve ketika sukses, atau reject jika gagal. Semua konten dari fungsi ini adalah perangkat dasar XHR, jadi kita tidak perlu khawatir untuk saat ini.

Ketika memanggil fungsi imgLoad(), kita memanggilnya dengan menggunakan url gambar yang akan dimuat, seperti yang kita harapkan, namun kodenya sedikit berbeda:

var body = document.querySelector('body');
var myImage = new Image();

imgLoad('myLittleVader.jpg').then(function(response) {
  var imageURL = window.URL.createObjectURL(response);
  myImage.src = imageURL;
  body.appendChild(myImage);
}, function(Error) {
  console.log(Error);
});

Pada akhir pemanggilan fungsi, kita menggabungkan promise method then(), dimana terdiri dari dua fungsi — yang pertama akan di eksekusi ketika promise berhasil di selesaikan, dan yang kedua akan dipanggil ketika promise di tolak. Pada penyelesaian kasus, kita menampilkan gambar di dalam myImage dan menambahkannya ke body (dengan argumen request.response yang berada di dalam method resolve); pada kasus ditolak kita mengembalikan error di console.

Semua terjadi secara asynchronously.

Catatan: Anda juga bisa menggabungkan panggilan promise secara bersamaan, Misal:
myPromise().then(success, failure).then(success).catch(failure);

Catatan: Anda bisa mendapatkan lebih banyak informasi tentang promise dengan membaca tulisan Jake Archibald’s excellent JavaScript Promises: there and back again.

Demo service workers

Sebagai demonstrasi hanya sekedar dasar pendaftaran dan penginstalan service worker, kita membuat sebuah demo sederhana dengan nama sw-test, berupa galeri gambar Star wars Lego. Mengunakan fungsi dengan promise untuk membaca data gambar dari sebuah objek JSON dan memuat gambar menggunakan Ajax, Sebelum menamilkan gambar pada halaman. Kita akan semuanya tetap simple dan statis untuk saat ini. Di sini pendaftaran, instalasi, dan akativasi sebuah service worker, dan ketika semua spec di dukung oleh browsers maka semua file yang diperukan akan dicache untuk di muat secara offline!




Anda bisa melihat source code di GitHub, dan Melihat contoh secara live. The one bit we’ll call out here is the promise (see app.js lines 17-42), which is a modified version of what you read about above, in the Promises test demo. It is different in the following ways:

  1. In the original, we only passed in a URL to an image we wanted to load. In this version, we pass in a JSON fragment containing all the data for a single image (see what they look like in image-list.js). This is because all the data for each promise resolve has to be passed in with the promise, as it is asynchronous. If you just passed in the url, and then tried to access the other items in the JSON separately when the for() loop is being iterated through later on, it wouldn’t work, as the promise wouldn’t resolve at the same time as the iterations are being done (that is a synchronous process.)
  2. We actually resolve the promise with an array, as we want to make the loaded image blob available to the resolving function later on in the code, but also the image name, credits and alt text (see app.js lines 26-29). Promises will only resolve with a single argument, so if you want to resolve with multiple values, you need to use an array/object.
  3. To access the resolved promise values, we then access this function as you’d then expect (see app.js lines 55-59.) This may seem a bit odd at first, but this is the way promises work.

Enter Service workers

Now let’s get on to service workers!

Mendaftarkan worker

The first block of code in our app’s JavaScript file — app.js — is as follows. This is our entry point into using service workers.

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw-test/sw.js', { scope: '/sw-test/' }).then(function(reg) {
    // registration worked
    console.log('Registration succeeded. Scope is ' + reg.scope);
  }).catch(function(error) {
    // registration failed
    console.log('Registration failed with ' + error);
  });
};
  1. The outer block performs a feature detection test to make sure service workers are supported before trying to register one.
  2. Next, we use the ServiceWorkerContainer.register() function to register the service worker for this site, which is just a JavaScript file residing inside our app (note this is the file's URL relative to the origin, not the JS file that references it.)
  3. The scope parameter is optional, and can be used to specify the subset of your content that you want the service worker to control. In this case, we have specified '/sw-test/', which means all content under the app's origin. If you leave it out, it will default to this value anyway, but we specified it here for illustration purposes.
  4. The .then() promise function is used to chain a success case onto our promise structure.  When the promise resolves successfully, the code inside it executes.
  5. Finally, we chain a .catch() function onto the end that will run if the promise is rejected.

This registers a service worker, which runs in a worker context, and therefore has no DOM access. You then run code in the service worker outside of your normal pages to control their loading.

A single service worker can control many pages. Each time a page within your scope is loaded, the service worker is installed against that page and operates on it. Bear in mind therefore that you need to be careful with global variables in the service worker script: each page doesn’t get its own unique worker.

Note: Your service worker functions like a proxy server, allowing you to modify requests and responses, replace them with items from its own cache, and more.

Note: One great thing about service workers is that if you use feature detection like we’ve shown above, browsers that don’t support service workers can just use your app online in the normal expected fashion. Furthermore, if you use AppCache and SW on a page, browsers that don’t support SW but do support AppCache will use that, and browsers that support both will ignore the AppCache and let SW take over.

Kenapa service worker saya gagal didaftarkan?

Hal tersebut bisa terjadi karena alasan berikut:

  1. Anda tidak menjalankan aplikasi dari HTTPS.
  2. Path dari file service worker anda tidak ditulis dengan benar — seharusnya di tulis relatif terhadap origin, bukan root dari direktori aplikasi anda. Pada contoh, worker berada di https://mdn.github.io/sw-test/sw.js, dan root aplikasi di https://mdn.github.io/sw-test/. Namun path harus ditulis /sw-test/sw.js, bukan /sw.js.
  3. The service worker being pointed to is on a different origin to that of your app. This is also not allowed.

Also note:

  • The service worker will only catch requests from clients under the service worker's scope.
  • The max scope for a service worker is the location of the worker.
  • If your server worker is active on a client being served with the Service-Worker-Allowed header, you can specify a list of max scopes for that worker.
  • In Firefox, Service Worker APIs are hidden and cannot be used when the user is in private browsing mode.

Install dan activasi: mengumpulkan cache

Setelah service worker anda terdaftar, browser akan menginstall dan mengaktifkan service worker untuk halaman/situs anda.

Event instal dijalankan ketika install selesai dengan sukses. Event install biasanya digunakan oleh browser untuk mengumpulkan cache offline dari aset yang akan digunakan untuk menjalankan aplikasi anda secara offline. Untuk melakukannya, kita menggunakan Storage API dari Service Worker — cache —  global pada service worker yang memungkinkan kita menympan aset yang diterima dari response, dan di sandikan berdasarkan request. API ini sama halnya dengan cache pada browser umumnya, tapi lebih spesifik ke domain. Ini persis hinga anda tidak menginginkan — anda punya kontrol penuh.

Note: The Cache API is not supported in every browser. (See the Browser support section for more information.) If you want to use this now, you could consider using a polyfill like the one available in Google's Topeka demo, or perhaps store your assets in IndexedDB.

Let’s start this section by looking at a code sample — this is the first block you’ll find in our service worker:

this.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('v1').then(function(cache) {
      return cache.addAll([
        '/sw-test/',
        '/sw-test/index.html',
        '/sw-test/style.css',
        '/sw-test/app.js',
        '/sw-test/image-list.js',
        '/sw-test/star-wars-logo.jpg',
        '/sw-test/gallery/',
        '/sw-test/gallery/bountyHunters.jpg',
        '/sw-test/gallery/myLittleVader.jpg',
        '/sw-test/gallery/snowTroopers.jpg'
      ]);
    })
  );
});
  1. Here we add an install event listener to the service worker (hence this), and then chain a ExtendableEvent.waitUntil() method onto the event — this ensures that the Service Worker will not install until the code inside waitUntil() has successfully occurred.
  2. Inside waitUntil() we use the caches.open() method to create a new cache called v1, which will be version 1 of our site resources cache. This returns a promise for a created cache; once resolved, we then call a function that calls addAll() on the created cache, which for its parameter takes an array of origin-relative URLs to all the resources you want to cache.
  3. If the promise is rejected, the install fails, and the worker won’t do anything. This is ok, as you can fix your code and then try again the next time registration occurs.
  4. After a successful installation, the service worker activates. This doesn’t have much of a distinct use the first time your service worker is installed/activated, but it means more when the service worker is updated (see the Updating your service worker section later on.)

Note: localStorage works in a similar way to service worker cache, but it is synchronous, so not allowed in service workers.

Note: IndexedDB can be used inside a service worker for data storage if you require it.

Custom responses to requests

Now you’ve got your site assets cached, you need to tell service workers to do something with the cached content. This is easily done with the fetch event.

A fetch event fires every time any resource controlled by a service worker is fetched, which includes the documents inside the specified scope, and any resources referenced in those documents (for example if index.html makes a cross origin request to embed an image, that still goes through its service worker.)

You can attach a fetch event listener to the service worker, then call the respondWith() method on the event to hijack our HTTP responses and update them with your own magic.

this.addEventListener('fetch', function(event) {
  event.respondWith(
    // magic goes here
  );
});

We could start by simply responding with the resource whose url matches that of the network request, in each case:

this.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request);
  );
});

caches.match(event.request) allows us to match each resource requested from the network with the equivalent resource available in the cache, if there is a matching one available. The matching is done via url and vary headers, just like with normal HTTP requests.

Let’s look at a few other options we have when defining our magic (see our Fetch API documentation for more information about Request and Response objects.)

  1. The Response() constructor allows you to create a custom response. In this case, we are just returning a simple text string:

    new Response('Hello from your friendly neighbourhood service worker!');
  2. This more complex Response below shows that you can optionally pass a set of headers in with your response, emulating standard HTTP response headers. Here we are just telling the browser what the content type of our synthetic response is:

    new Response('

    Hello from your friendly neighbourhood service worker!

    ', { headers: { 'Content-Type': 'text/html' } })
  3. If a match wasn’t found in the cache, you could tell the browser to simply fetch the default network request for that resource, to get the new resource from the network if it is available:

    fetch(event.request)
  4. If a match wasn’t found in the cache, and the network isn’t available, you could just match the request with some kind of default fallback page as a response using match(), like this:

    caches.match('/fallback.html');
  5. You can retrieve a lot of information about each request by calling parameters of the Request object returned by the FetchEvent:

    event.request.url
    event.request.method
    event.request.headers
    event.request.body

Recovering failed requests

So caches.match(event.request) is great when there is a match in the service worker cache, but what about cases when there isn’t a match? If we didn’t provide any kind of failure handling, our promise would reject and we would just come up against a network error when a match isn’t found.

Fortunately service workers’ promise-based structure makes it trivial to provide further options towards success. We could do this:

this.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).catch(function() {
      return fetch(event.request);
    })
  );
});

If the promise rejects, the catch() function returns the default network request for the resource instead, meaning that those who have network available can just load the resource from the server.

If we were being really clever, we would not only request the resource from the network; we would also save it into the cache so that later requests for that resource could be retrieved offline too! This would mean that if extra images were added to the Star Wars gallery, our app could automatically grab them and cache them. The following would do the trick:

this.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).catch(function() {
      return fetch(event.request).then(function(response) {
        return caches.open('v1').then(function(cache) {
          cache.put(event.request, response.clone());
          return response;
        });  
      });
    })
  );
});

Here we return the default network request with return fetch(event.request), which returns a promise. When this promise is resolved, we respond by running a function that grabs our cache using caches.open('v1'); this also returns a promise. When that promise resolves, cache.put() is used to add the resource to the cache. The resource is grabbed from event.request, and the response is then cloned with response.clone() and added to the cache. The clone is put in the cache, and the original response is returned to the browser to be given to the page that called it.

Why? This is because request and response streams can only be read once.  In order to return the response to the browser and put it in the cache we have to clone it. So the original gets returned to the browser and the clone gets sent to the cache.  They are each read once.

The only trouble we have now is that if the request doesn’t match anything in the cache, and the network is not available, our request will still fail. Let’s provide a default fallback so that whatever happens, the user will at least get something:

this.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).catch(function() {
      return fetch(event.request).then(function(response) {
        return caches.open('v1').then(function(cache) {
          cache.put(event.request, response.clone());
          return response;
        });  
      });
    }).catch(function() {
      return caches.match('/sw-test/gallery/myLittleVader.jpg');
    })
  );
});

We have opted for this fallback image because the only updates that are likely to fail are new images, as everything else is depended on for installation in the install event listener we saw earlier.

Updated code pattern suggestion

This uses more standard promise chaining and returns the response to the document without having to wait for caches.open() to resolve:

this.addEventListener('fetch', function(event) {
  var response;
  event.respondWith(caches.match(event.request).catch(function() {
    return fetch(event.request);
  }).then(function(r) {
    response = r;
    caches.open('v1').then(function(cache) {
      cache.put(event.request, response);
    });
    return response.clone();
  }).catch(function() {
    return caches.match('/sw-test/gallery/myLittleVader.jpg');
  }));
});

Membaharui service worker

If your service worker has previously been installed, but then a new version of the worker is available on refresh or page load, the new version is installed in the background, but not yet activated. It is only activated when there are no longer any pages loaded that are still using the old service worker. As soon as there are no more such pages still loaded, the new service worker activates.

You’ll want to update your install event listener in the new service worker to something like this (notice the new version number):

this.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('v2').then(function(cache) {
      return cache.addAll([
        '/sw-test/',
        '/sw-test/index.html',
        '/sw-test/style.css',
        '/sw-test/app.js',
        '/sw-test/image-list.js',
        
             …

              // include other new resources for the new version...
      ]);
    });
  );
});

While this happens, the previous version is still responsible for fetches. The new version is installing in the background. We are calling the new cache v2, so the previous v1 cache isn't disturbed.

When no pages are using the current version, the new worker activates and becomes responsible for fetches.

Deleting old caches

You also get an activate event. This is a generally used to do stuff that would have broken the previous version while it was still running, for example getting rid of old caches. This is also useful for removing data that is no longer needed to avoid filling up too much disk space — each browser has a hard limit on the amount of cache storage that a given service worker can use. The browser does its best to manage disk space, but it may delete the Cache storage for an origin.  The browser will generally delete all of the data for an origin or none of the data for an origin.

Promises passed into waitUntil() will block other events until completion, so you can rest assured that your clean-up operation will have completed by the time you get your first fetch event on the new cache.

this.addEventListener('activate', function(event) {
  var cacheWhitelist = ['v2'];

  event.waitUntil(
    caches.keys().then(function(keyList) {
      return Promise.all(keyList.map(function(key) {
        if (cacheWhitelist.indexOf(key) === -1) {
          return caches.delete(key);
        }
      }));
    })
  );
});

Dev tools

Chrome has chrome://inspect/#service-workers, which shows current service worker activity and storage on a device, and chrome://serviceworker-internals, which shows more detail and allows you to start/stop/debug the worker process. In the future they will have throttling/offline modes to simulate bad or non-existent connections, which will be a really good thing.

Firefox has also started to implement some useful tools related to service workers:

  • You can navigate to about:serviceworkers to see what SWs are registered and update/remove them.
  • When testing you can get around the HTTPS restriction by checking the "Enable Service Workers over HTTP (when toolbox is open)" option in the Firefox Devtools options (gear menu.)

Spesifikasi

Spesifikasi Status Comment
Service Workers Working Draft Initial definition.

Browser compatibility

Fitur Chrome Firefox (Gecko) Internet Explorer Opera Safari (WebKit)
Dukungan dasar 40.0 33.0 (33.0)[1] No support 24 No support
Fitur Android Chrome for Android Firefox Mobile (Gecko) Firefox OS IE Phone Opera Mobile Safari Mobile
Dukungan dasar No support 40.0 (Yes) (Yes) No support (Yes) No support

[1] Service workers (and Push) have been disabled in the Firefox 45 Extended Support Release (ESR.)

Lihat juga

Tag Dokumen dan Kontributor

 Kontributor untuk laman ini: rmsubekti
 Terakhir diperbarui oleh: rmsubekti,