Lebih dari sekadar SPA - arsitektur alternatif untuk PWA Anda

Mari kita bahas... arsitektur?

Saya akan membahas topik yang penting, tetapi berpotensi disalahpahami: Arsitektur yang Anda gunakan untuk aplikasi web, dan khususnya, bagaimana keputusan arsitektur Anda berperan saat Anda membangun progressive web app.

"Arsitektur" dapat terdengar samar, dan mungkin tidak langsung jelas mengapa hal ini penting. Salah satu cara untuk memikirkan arsitektur adalah dengan mengajukan pertanyaan berikut kepada diri sendiri: Saat pengguna mengunjungi halaman di situs saya, HTML apa yang dimuat? Lalu, apa yang dimuat saat mereka mengunjungi halaman lain?

Jawaban atas pertanyaan tersebut tidak selalu mudah, dan setelah Anda mulai memikirkan progressive web app, jawaban tersebut bisa menjadi lebih rumit. Jadi, tujuan saya adalah memandu Anda melalui satu kemungkinan arsitektur yang menurut saya efektif. Di sepanjang artikel ini, saya akan melabeli keputusan yang saya buat sebagai "pendekatan saya" untuk membangun progressive web app.

Anda bebas menggunakan pendekatan saya saat membangun PWA Anda sendiri, tetapi pada saat yang sama, selalu ada alternatif valid lainnya. Harapan saya, melihat bagaimana semua bagian saling melengkapi akan menginspirasi Anda, dan Anda akan merasa diberdayakan untuk menyesuaikannya sesuai kebutuhan Anda.

PWA Stack Overflow

Untuk melengkapi artikel ini, saya membuat PWA Stack Overflow. Saya menghabiskan banyak waktu untuk membaca dan berkontribusi di Stack Overflow, dan saya ingin membangun aplikasi web yang akan memudahkan penjelajahan pertanyaan umum (FAQ) untuk topik tertentu. Aplikasi ini dibuat di atas Stack Exchange API publik. Alat ini bersifat open source, dan Anda dapat mempelajari lebih lanjut dengan mengunjungi project GitHub.

Aplikasi Multi-Halaman (MPA)

Sebelum membahas hal-hal spesifik, mari kita definisikan beberapa istilah dan jelaskan bagian-bagian teknologi yang mendasarinya. Pertama, saya akan membahas apa yang saya sebut "Aplikasi Multi-Halaman", atau "MPA".

MPA adalah nama keren untuk arsitektur tradisional yang digunakan sejak awal web. Setiap kali pengguna membuka URL baru, browser akan merender HTML khusus untuk halaman tersebut secara progresif. Tidak ada upaya untuk mempertahankan status halaman atau konten di antara navigasi. Setiap kali Anda mengunjungi halaman baru, Anda memulai dari awal.

Hal ini berbeda dengan model aplikasi web satu halaman (SPA) untuk membuat aplikasi web, yang mengharuskan browser menjalankan kode JavaScript untuk memperbarui halaman yang ada saat pengguna mengunjungi bagian baru. SPA dan MPA adalah model yang sama-sama valid untuk digunakan, tetapi untuk postingan ini, saya ingin mengeksplorasi konsep PWA dalam konteks aplikasi multi-halaman.

Cepat dan andal

Anda telah mendengar saya (dan banyak orang lain) menggunakan frasa "progressive web app", atau PWA. Anda mungkin sudah memahami beberapa materi latar belakang, di tempat lain di situs ini.

Anda dapat menganggap PWA sebagai aplikasi web yang memberikan pengalaman pengguna terbaik, dan benar-benar layak mendapatkan tempat di layar utama pengguna. Akronim "FIRE", yang merupakan singkatan dari Fast (Cepat), Integrated (Terintegrasi), Reliable (Andal), dan Engaging (Menarik), merangkum semua atribut yang perlu dipikirkan saat membangun PWA.

Dalam artikel ini, saya akan berfokus pada sebagian atribut tersebut: Cepat dan Andal.

Cepat: Meskipun "cepat" memiliki arti yang berbeda dalam konteks yang berbeda, saya akan membahas manfaat kecepatan pemuatan sesedikit mungkin dari jaringan.

Andal: Namun, kecepatan mentah saja tidak cukup. Agar terasa seperti PWA, aplikasi web Anda harus andal. Service worker harus cukup tangguh untuk selalu memuat sesuatu, meskipun hanya halaman error yang disesuaikan, terlepas dari status jaringan.

Cepat dan andal: Terakhir, saya akan sedikit mengubah definisi PWA dan melihat apa artinya membangun sesuatu yang cepat dan andal. Tidak cukup hanya menjadi cepat dan andal saat Anda berada di jaringan berlatensi rendah. Cepat dan andal berarti kecepatan aplikasi web Anda konsisten, terlepas dari kondisi jaringan yang mendasarinya.

Teknologi Pendukung: Service Worker + Cache Storage API

PWA menetapkan standar tinggi untuk kecepatan dan ketahanan. Untungnya, platform web menawarkan beberapa elemen penyusun untuk mewujudkan jenis performa tersebut. Yang kami maksud adalah service worker dan Cache Storage API.

Anda dapat membangun pekerja layanan yang memproses permintaan masuk, meneruskan beberapa permintaan ke jaringan, dan menyimpan salinan respons untuk digunakan di masa mendatang, melalui Cache Storage API.

Service worker yang menggunakan Cache Storage API untuk menyimpan salinan respons jaringan.

Saat aplikasi web membuat permintaan yang sama, pekerja layanannya dapat memeriksa cache-nya dan hanya menampilkan respons yang di-cache sebelumnya.

Service worker yang menggunakan Cache Storage API untuk merespons, melewati jaringan.

Menghindari jaringan jika memungkinkan adalah bagian penting dalam menawarkan performa yang cepat dan andal.

JavaScript "Isomorfik"

Satu lagi konsep yang ingin saya bahas adalah apa yang terkadang disebut sebagai JavaScript "isomorfik" atau "universal". Sederhananya, ini adalah ide bahwa kode JavaScript yang sama dapat dibagikan di antara berbagai lingkungan runtime. Saat membuat PWA, saya ingin membagikan kode JavaScript antara server backend dan pekerja layanan.

Ada banyak pendekatan yang valid untuk membagikan kode dengan cara ini, tetapi pendekatan saya adalah menggunakan modul ES sebagai kode sumber definitif. Kemudian, saya mentranspilasi dan memaketkan modul tersebut untuk server dan pekerja layanan menggunakan kombinasi Babel dan Rollup. Dalam project saya, file dengan ekstensi file .mjs adalah kode yang berada dalam modul ES.

Server

Dengan mengingat konsep dan terminologi tersebut, mari kita pelajari cara saya membangun PWA Stack Overflow. Saya akan mulai dengan membahas server backend kami, dan menjelaskan bagaimana server tersebut cocok dengan arsitektur secara keseluruhan.

Saya mencari kombinasi backend dinamis dan hosting statis, dan pendekatan saya adalah menggunakan platform Firebase.

Firebase Cloud Functions akan otomatis menjalankan lingkungan berbasis Node saat ada permintaan masuk, dan berintegrasi dengan framework HTTP Express yang populer, yang sudah saya kenal. Firebase juga menawarkan hosting siap pakai untuk semua resource statis situs saya. Mari kita lihat cara server menangani permintaan.

Saat browser membuat permintaan navigasi ke server kita, browser akan melalui alur berikut:

Ringkasan pembuatan respons navigasi, di sisi server.

Server merutekan permintaan berdasarkan URL, dan menggunakan logika template untuk membuat dokumen HTML lengkap. Saya menggunakan kombinasi data dari Stack Exchange API, serta fragmen HTML parsial yang disimpan server secara lokal. Setelah mengetahui cara merespons, pekerja layanan kita dapat mulai men-streaming HTML kembali ke aplikasi web kita.

Ada dua bagian dari gambar ini yang perlu dipelajari lebih detail: pemilihan rute, dan pembuatan template.

Pemilihan rute

Terkait pemilihan rute, pendekatan saya adalah menggunakan sintaksis pemilihan rute native framework Express. Pola ini cukup fleksibel untuk mencocokkan awalan URL sederhana, serta URL yang menyertakan parameter sebagai bagian dari jalur. Di sini, saya membuat pemetaan antara nama rute dan pola Express yang mendasarinya untuk dicocokkan.

const routes = new Map([
  ['about', '/about'],
  ['questions', '/questions/:questionId'],
  ['index', '/'],
]);

export default routes;

Kemudian, saya dapat mereferensikan pemetaan ini langsung dari kode server. Jika ada kecocokan untuk pola Express tertentu, pengendali yang sesuai akan merespons dengan logika template yang spesifik untuk rute yang cocok.

import routes from './lib/routes.mjs';
app.get(routes.get('index'), async (req, res) => {
  // Templating logic.
});

Template sisi server

Seperti apa logika pembuatan template tersebut? Saya menggunakan pendekatan yang menyatukan fragmen HTML parsial secara berurutan, satu per satu. Model ini sangat cocok untuk streaming.

Server segera mengirimkan kembali beberapa boilerplate HTML awal, dan browser dapat langsung merender halaman parsial tersebut. Saat server menyatukan sumber data lainnya, server akan mengalirkannya ke browser hingga dokumen selesai.

Untuk melihat maksudnya, lihat kode Express untuk salah satu rute kami:

app.get(routes.get('index'), async (req, res) => {
  res.write(headPartial + navbarPartial);
  const tag = req.query.tag || DEFAULT_TAG;
  const data = await requestData(...);
  res.write(templates.index(tag, data.items));
  res.write(footPartial);
  res.end();
});

Dengan menggunakan metode write() objek response, dan mereferensikan template parsial yang disimpan secara lokal, saya dapat segera memulai streaming respons, tanpa memblokir sumber data eksternal apa pun. Browser mengambil HTML awal ini dan langsung merender antarmuka yang bermakna dan pesan pemuatan.

Bagian berikutnya dari halaman kita menggunakan data dari Stack Exchange API. Untuk mendapatkan data tersebut, server kita perlu membuat permintaan jaringan. Aplikasi web tidak dapat merender hal lain hingga mendapatkan respons kembali dan memprosesnya, tetapi setidaknya pengguna tidak menatap layar kosong saat menunggu.

Setelah aplikasi web menerima respons dari Stack Exchange API, aplikasi web akan memanggil fungsi template kustom untuk menerjemahkan data dari API ke dalam HTML yang sesuai.

Bahasa template

Pembuatan template bisa menjadi topik yang sangat kontroversial, dan apa yang saya lakukan hanyalah salah satu pendekatan di antara banyak pendekatan. Anda mungkin ingin mengganti solusi Anda sendiri, terutama jika Anda memiliki keterikatan lama dengan framework template yang ada.

Yang masuk akal untuk kasus penggunaan saya adalah mengandalkan literal template JavaScript, dengan beberapa logika yang dipecah menjadi fungsi bantuan. Salah satu hal yang menyenangkan dari membangun MPA adalah Anda tidak perlu melacak pembaruan status dan merender ulang HTML, sehingga pendekatan dasar yang menghasilkan HTML statis berhasil bagi saya.

Berikut contoh cara membuat template bagian HTML dinamis dari indeks aplikasi web saya. Seperti rute saya, logika template disimpan dalam modul ES yang dapat diimpor ke server dan pekerja layanan.

export function index(tag, items) {
  const title = `<h3>Top "${escape(tag)}" Questions</h3>`;
  const form = `<form method="GET">...</form>`;
  const questionCards = items
    .map(item =>
      questionCard({
        id: item.question_id,
        title: item.title,
      })
    )
    .join('');
  const questions = `<div id="questions">${questionCards}</div>`;
  return title + form + questions;
}

Fungsi template ini adalah JavaScript murni, dan sebaiknya pisahkan logika menjadi fungsi bantuan yang lebih kecil jika sesuai. Di sini, saya meneruskan setiap item yang ditampilkan dalam respons API ke salah satu fungsi tersebut, yang membuat elemen HTML standar dengan semua atribut yang sesuai ditetapkan.

function questionCard({id, title}) {
  return `<a class="card"
             href="/questions/${id}"
             data-cache-url="${questionUrl(id)}">${title}</a>`;
}

Yang perlu diperhatikan adalah atribut data yang saya tambahkan ke setiap link, data-cache-url, yang ditetapkan ke URL Stack Exchange API yang saya perlukan untuk menampilkan pertanyaan yang sesuai. Ingatlah hal itu. Saya akan memeriksanya lagi nanti.

Kembali ke pengendali rute, setelah pembuatan template selesai, saya mengalirkan bagian akhir HTML halaman ke browser, dan mengakhiri streaming. Ini adalah petunjuk bagi browser bahwa rendering progresif telah selesai.

app.get(routes.get('index'), async (req, res) => {
  res.write(headPartial + navbarPartial);
  const tag = req.query.tag || DEFAULT_TAG;
  const data = await requestData(...);
  res.write(templates.index(tag, data.items));
  res.write(footPartial);
  res.end();
});

Jadi, itulah tur singkat penyiapan server saya. Pengguna yang mengunjungi aplikasi web saya untuk pertama kalinya akan selalu mendapatkan respons dari server, tetapi saat pengunjung kembali ke aplikasi web saya, service worker saya akan mulai merespons. Mari kita bahas lebih lanjut.

Service worker

Ringkasan pembuatan respons navigasi, di pekerja layanan.

Diagram ini seharusnya terlihat familier—banyak bagian yang sama dengan yang telah saya bahas sebelumnya ada di sini dalam susunan yang sedikit berbeda. Mari kita pelajari alur permintaan, dengan mempertimbangkan pekerja layanan.

Service worker kami menangani permintaan navigasi yang masuk untuk URL tertentu, dan seperti yang dilakukan server saya, service worker menggunakan kombinasi logika pemilihan rute dan template untuk mencari tahu cara merespons.

Pendekatannya sama seperti sebelumnya, tetapi dengan primitif level rendah yang berbeda, seperti fetch() dan Cache Storage API. Saya menggunakan sumber data tersebut untuk membuat respons HTML, yang diteruskan kembali oleh pekerja layanan ke aplikasi web.

Workbox

Daripada memulai dari awal dengan primitif tingkat rendah, saya akan membangun pekerja layanan saya di atas serangkaian library tingkat tinggi yang disebut Workbox. Workbox menyediakan fondasi yang kuat untuk logika caching, pemilihan rute, dan pembuatan respons service worker.

Pemilihan rute

Sama seperti kode sisi server, pekerja layanan saya perlu mengetahui cara mencocokkan permintaan masuk dengan logika respons yang sesuai.

Pendekatan saya adalah menerjemahkan setiap rute Express ke dalam ekspresi reguler yang sesuai, dengan memanfaatkan library yang berguna bernama regexparam. Setelah terjemahan tersebut dilakukan, saya dapat memanfaatkan dukungan bawaan Workbox untuk perutean ekspresi reguler.

Setelah mengimpor modul yang memiliki ekspresi reguler, saya mendaftarkan setiap ekspresi reguler dengan router Workbox. Di dalam setiap rute, saya dapat memberikan logika template kustom untuk menghasilkan respons. Pembuatan template di pekerja layanan sedikit lebih rumit daripada di server backend saya, tetapi Workbox membantu banyak pekerjaan berat.

import regExpRoutes from './regexp-routes.mjs';

workbox.routing.registerRoute(
  regExpRoutes.get('index')
  // Templating logic.
);

Caching aset statis

Salah satu bagian penting dari pembuatan template adalah memastikan bahwa template HTML parsial saya tersedia secara lokal melalui Cache Storage API, dan terus diperbarui saat saya men-deploy perubahan ke aplikasi web. Pemeliharaan cache dapat rentan terhadap error jika dilakukan secara manual, jadi saya beralih ke Workbox untuk menangani pra-cache sebagai bagian dari proses build saya.

Saya memberi tahu Workbox URL mana yang harus di-precache menggunakan file konfigurasi, yang mengarah ke direktori yang berisi semua aset lokal saya beserta serangkaian pola yang harus dicocokkan. File ini otomatis dibaca oleh CLI Workbox, yang dijalankan setiap kali saya membangun ulang situs.

module.exports = {
  globDirectory: 'build',
  globPatterns: ['**/*.{html,js,svg}'],
  // Other options...
};

Workbox mengambil snapshot konten setiap file, dan otomatis memasukkan daftar URL dan revisi tersebut ke dalam file pekerja layanan akhir saya. Workbox kini memiliki semua yang diperlukan untuk membuat file yang di-precache selalu tersedia, dan selalu diperbarui. Hasilnya adalah file service-worker.js yang berisi sesuatu yang mirip dengan berikut ini:

workbox.precaching.precacheAndRoute([
  {
    url: 'partials/about.html',
    revision: '518747aad9d7e',
  },
  {
    url: 'partials/foot.html',
    revision: '69bf746a9ecc6',
  },
  // etc.
]);

Bagi pengguna yang menggunakan proses build yang lebih kompleks, Workbox memiliki plugin webpack dan modul node generik, selain antarmuka command line.

Streaming

Selanjutnya, saya ingin service worker segera menstreaming HTML parsial yang telah di-precache tersebut kembali ke aplikasi web. Ini adalah bagian penting dari "kecepatan yang andal"—saya selalu mendapatkan sesuatu yang bermakna di layar dengan segera. Untungnya, penggunaan Streams API dalam service worker kami memungkinkan hal tersebut.

Sekarang, Anda mungkin pernah mendengar tentang Streams API sebelumnya. Rekan saya, Jake Archibald, telah memujinya selama bertahun-tahun. Dia membuat prediksi berani bahwa 2016 akan menjadi tahun streaming web. Streams API saat ini sama hebatnya dengan dua tahun lalu, tetapi dengan perbedaan yang penting.

Meskipun hanya Chrome yang mendukung Streams pada saat itu, Streams API kini didukung secara lebih luas. Secara keseluruhan, ceritanya positif, dan dengan kode penggantian yang sesuai, tidak ada yang menghalangi Anda untuk menggunakan streaming di pekerja layanan Anda saat ini.

Mungkin ada satu hal yang menghalangi Anda, yaitu memahami cara kerja Streams API yang sebenarnya. Alat ini mengekspos serangkaian primitif yang sangat canggih, dan developer yang nyaman menggunakannya dapat membuat aliran data yang kompleks, seperti berikut:

const stream = new ReadableStream({
  pull(controller) {
    return sources[0]
      .then(r => r.read())
      .then(result => {
        if (result.done) {
          sources.shift();
          if (sources.length === 0) return controller.close();
          return this.pull(controller);
        } else {
          controller.enqueue(result.value);
        }
      });
  },
});

Namun, memahami implikasi penuh dari kode ini mungkin tidak cocok untuk semua orang. Daripada mengurai logika ini, mari kita bahas pendekatan saya terhadap streaming pekerja layanan.

Saya menggunakan wrapper tingkat tinggi yang baru, workbox-streams. Dengan begitu, saya dapat meneruskannya dalam campuran sumber streaming, baik dari cache maupun data runtime yang mungkin berasal dari jaringan. Workbox menangani koordinasi setiap sumber dan menggabungkannya menjadi satu respons streaming.

Selain itu, Workbox otomatis mendeteksi apakah Streams API didukung, dan jika tidak, Workbox akan membuat respons non-streaming yang setara. Artinya, Anda tidak perlu khawatir untuk menulis penggantian, karena streaming semakin mendekati dukungan browser 100%.

Caching runtime

Mari kita lihat cara service worker saya menangani data runtime, dari Stack Exchange API. Saya memanfaatkan dukungan bawaan Workbox untuk strategi caching stale-while-revalidate, bersama dengan masa berlaku untuk memastikan bahwa penyimpanan aplikasi web tidak bertambah tanpa batas.

Saya menyiapkan dua strategi di Workbox untuk menangani berbagai sumber yang akan membentuk respons streaming. Dalam beberapa panggilan fungsi dan konfigurasi, Workbox memungkinkan kita melakukan hal yang seharusnya memerlukan ratusan baris kode yang ditulis secara manual.

const cacheStrategy = workbox.strategies.cacheFirst({
  cacheName: workbox.core.cacheNames.precache,
});

const apiStrategy = workbox.strategies.staleWhileRevalidate({
  cacheName: API_CACHE_NAME,
  plugins: [new workbox.expiration.Plugin({maxEntries: 50})],
});

Strategi pertama membaca data yang telah di-precaching, seperti template HTML parsial kami.

Strategi lainnya menerapkan logika caching stale-while-revalidate, bersama dengan masa berlaku cache yang paling jarang digunakan setelah kita mencapai 50 entri.

Setelah strategi tersebut diterapkan, yang tersisa hanyalah memberi tahu Workbox cara menggunakannya untuk membuat respons streaming yang lengkap. Saya meneruskan array sumber sebagai fungsi, dan setiap fungsi tersebut akan langsung dieksekusi. Workbox mengambil hasil dari setiap sumber dan mengalirkannya ke aplikasi web, secara berurutan, hanya menunda jika fungsi berikutnya dalam array belum selesai.

workbox.streams.strategy([
  () => cacheStrategy.makeRequest({request: '/head.html'}),
  () => cacheStrategy.makeRequest({request: '/navbar.html'}),
  async ({event, url}) => {
    const tag = url.searchParams.get('tag') || DEFAULT_TAG;
    const listResponse = await apiStrategy.makeRequest(...);
    const data = await listResponse.json();
    return templates.index(tag, data.items);
  },
  () => cacheStrategy.makeRequest({request: '/foot.html'}),
]);

Dua sumber pertama adalah template parsial yang di-precache dan dibaca langsung dari Cache Storage API, sehingga akan selalu tersedia dengan segera. Hal ini memastikan bahwa implementasi pekerja layanan kami akan cepat dan andal dalam merespons permintaan, sama seperti kode sisi server saya.

Fungsi sumber berikutnya mengambil data dari Stack Exchange API, dan memproses respons menjadi HTML yang diharapkan oleh aplikasi web.

Strategi stale-while-revalidate berarti bahwa jika saya memiliki respons yang di-cache sebelumnya untuk panggilan API ini, saya akan dapat langsung men-streaming-nya ke halaman, sekaligus mengupdate entri cache "di latar belakang" untuk permintaan berikutnya.

Terakhir, saya men-streaming salinan footer yang di-cache dan menutup tag HTML akhir, untuk menyelesaikan respons.

Berbagi kode membuat semuanya tetap sinkron

Anda akan melihat bahwa beberapa bagian kode pekerja layanan terlihat familier. HTML parsial dan logika template yang digunakan oleh pekerja layanan saya sama dengan yang digunakan oleh pengendali sisi server saya. Berbagi kode ini memastikan bahwa pengguna mendapatkan pengalaman yang konsisten, baik saat mereka mengunjungi aplikasi web saya untuk pertama kalinya atau kembali ke halaman yang dirender oleh pekerja layanan. Itulah keunggulan JavaScript isomorfik.

Peningkatan dinamis dan progresif

Saya telah membahas server dan pekerja layanan untuk PWA saya, tetapi ada satu logika terakhir yang harus dibahas: ada sejumlah kecil JavaScript yang berjalan di setiap halaman saya, setelah di-streaming sepenuhnya.

Kode ini secara progresif meningkatkan pengalaman pengguna, tetapi tidak terlalu penting—aplikasi web akan tetap berfungsi jika tidak dijalankan.

Metadata halaman

Aplikasi saya menggunakan JavaScript sisi klien untuk memperbarui metadata halaman berdasarkan respons API. Karena saya menggunakan bit awal HTML yang sama yang di-cache untuk setiap halaman, aplikasi web akan memiliki tag generik di bagian head dokumen saya. Namun, melalui koordinasi antara kode template dan kode sisi klien, saya dapat memperbarui judul jendela menggunakan metadata spesifik per halaman.

Sebagai bagian dari kode template, pendekatan saya adalah menyertakan tag skrip yang berisi string yang di-escape dengan benar.

const metadataScript = `<script>
  self._title = '${escape(item.title)}';
</script>`;

Kemudian, setelah halaman dimuat, saya membaca string tersebut dan memperbarui judul dokumen.

if (self._title) {
  document.title = unescape(self._title);
}

Jika ada bagian lain dari metadata spesifik per halaman yang ingin Anda perbarui di aplikasi web Anda sendiri, Anda dapat mengikuti pendekatan yang sama.

UX Offline

Progressive enhancement lain yang telah saya tambahkan digunakan untuk menarik perhatian pada kemampuan offline kami. Saya telah membuat PWA yang andal, dan saya ingin pengguna tahu bahwa saat offline, mereka masih dapat memuat halaman yang dikunjungi sebelumnya.

Pertama, saya menggunakan Cache Storage API untuk mendapatkan daftar semua permintaan API yang di-cache sebelumnya, dan saya menerjemahkannya menjadi daftar URL.

Ingat atribut data khusus yang saya bahas, yang masing-masing berisi URL untuk permintaan API yang diperlukan untuk menampilkan pertanyaan? Saya dapat mereferensikan silang atribut data tersebut dengan daftar URL yang di-cache, dan membuat array dari semua link pertanyaan yang tidak cocok.

Saat browser memasuki status offline, saya akan mengulang daftar link yang tidak di-cache, dan meredupkan link yang tidak akan berfungsi. Perlu diingat bahwa ini hanyalah petunjuk visual bagi pengguna tentang apa yang dapat mereka harapkan dari halaman tersebut—Saya tidak benar-benar menonaktifkan link, atau mencegah pengguna melakukan navigasi.

const apiCache = await caches.open(API_CACHE_NAME);
const cachedRequests = await apiCache.keys();
const cachedUrls = cachedRequests.map(request => request.url);

const cards = document.querySelectorAll('.card');
const uncachedCards = [...cards].filter(card => {
  return !cachedUrls.includes(card.dataset.cacheUrl);
});

const offlineHandler = () => {
  for (const uncachedCard of uncachedCards) {
    uncachedCard.style.opacity = '0.3';
  }
};

const onlineHandler = () => {
  for (const uncachedCard of uncachedCards) {
    uncachedCard.style.opacity = '1.0';
  }
};

window.addEventListener('online', onlineHandler);
window.addEventListener('offline', offlineHandler);

Kesalahan umum

Saya telah membahas pendekatan saya dalam membangun PWA multi-halaman. Ada banyak faktor yang harus Anda pertimbangkan saat membuat pendekatan sendiri, dan Anda mungkin akan membuat pilihan yang berbeda dari yang saya lakukan. Fleksibilitas tersebut adalah salah satu hal hebat dalam membangun web.

Ada beberapa kesalahan umum yang mungkin Anda temui saat membuat keputusan arsitektur sendiri, dan saya ingin membantu Anda menghindari masalah tersebut.

Jangan meng-cache HTML lengkap

Sebaiknya jangan menyimpan dokumen HTML lengkap di cache Anda. Pertama, hal ini membuang-buang ruang. Jika aplikasi web Anda menggunakan struktur HTML dasar yang sama untuk setiap halamannya, Anda akan menyimpan salinan markup yang sama berulang kali.

Yang lebih penting, jika Anda men-deploy perubahan pada struktur HTML bersama situs, setiap halaman yang di-cache sebelumnya masih akan menggunakan tata letak lama. Bayangkan rasa frustrasi pengunjung yang kembali melihat campuran halaman lama dan baru.

Penyimpangan server/service worker

Kesalahan lain yang harus dihindari adalah server dan pekerja layanan Anda tidak sinkron. Pendekatan saya adalah menggunakan JavaScript isomorfik, sehingga kode yang sama dijalankan di kedua tempat. Bergantung pada arsitektur server yang ada, hal itu tidak selalu memungkinkan.

Apa pun keputusan arsitektur yang Anda buat, Anda harus memiliki beberapa strategi untuk menjalankan kode pemilihan rute dan template yang setara di server dan pekerja layanan Anda.

Skenario terburuk

Tata letak/desain yang tidak konsisten

Apa yang terjadi jika Anda mengabaikan kesalahan tersebut? Berbagai kegagalan dapat terjadi, tetapi skenario terburuknya adalah pengguna yang kembali mengunjungi halaman yang di-cache dengan tata letak yang sangat usang—mungkin dengan teks header yang sudah tidak berlaku, atau yang menggunakan nama class CSS yang tidak lagi valid.

Skenario terburuk: Pemilihan rute rusak

Atau, pengguna mungkin menemukan URL yang ditangani oleh server Anda, tetapi bukan pekerja layanan Anda. Situs yang penuh dengan tata letak zombie dan jalan buntu bukanlah PWA yang andal.

Tips untuk meraih sukses

Namun, Anda tidak sendirian. Tips berikut dapat membantu Anda menghindari kesalahan tersebut:

Menggunakan library template dan pemilihan rute yang memiliki penerapan multibahasa

Coba gunakan library template dan pemilihan rute yang memiliki implementasi JavaScript. Sekarang, kami tahu bahwa tidak semua developer memiliki kemudahan untuk bermigrasi dari server web dan bahasa template saat ini.

Namun, sejumlah framework pembuatan template dan pemilihan rute yang populer memiliki implementasi dalam beberapa bahasa. Jika Anda dapat menemukan library yang berfungsi dengan JavaScript serta bahasa server Anda saat ini, Anda selangkah lebih dekat untuk menjaga service worker dan server tetap sinkron.

Lebih memilih template berurutan, bukan bertingkat

Selanjutnya, sebaiknya gunakan serangkaian template berurutan yang dapat di-streaming satu per satu. Tidak masalah jika bagian selanjutnya dari halaman Anda menggunakan logika template yang lebih rumit, asalkan Anda dapat melakukan streaming di bagian awal HTML secepat mungkin.

Menyimpan konten statis dan dinamis ke dalam cache di pekerja layanan Anda

Untuk performa terbaik, Anda harus melakukan precache pada semua resource statis penting situs Anda. Anda juga harus menyiapkan logika caching runtime untuk menangani konten dinamis, seperti permintaan API. Dengan menggunakan Workbox, Anda dapat membangun strategi yang telah diuji dengan baik dan siap produksi, bukan menerapkan semuanya dari awal.

Hanya blokir di jaringan jika benar-benar diperlukan

Dan terkait hal itu, Anda hanya boleh memblokir di jaringan jika tidak memungkinkan untuk melakukan streaming respons dari cache. Menampilkan respons API yang di-cache dengan segera sering kali dapat menghasilkan pengalaman pengguna yang lebih baik daripada menunggu data baru.

Resource