Lebih dari sekadar SPA - arsitektur alternatif untuk PWA Anda

Mari kita bicara tentang... arsitektur?

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

"Arsitektur" mungkin terdengar samar-samar, dan mungkin tidak segera jelas mengapa hal ini penting. Salah satu cara untuk membayangkan arsitektur adalah dengan bertanya pada diri sendiri pertanyaan berikut: Ketika 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 mempelajari satu kemungkinan arsitektur yang menurut saya efektif. Sepanjang artikel ini, saya akan melabeli keputusan yang saya buat sebagai "pendekatan saya" untuk membangun progressive web app.

Anda bebas menggunakan pendekatan saya saat membuat PWA Anda sendiri, tetapi pada saat yang sama, selalu ada alternatif valid lainnya. Dengan melihat bagaimana semua aspek ini disatukan, Anda akan terinspirasi, dan Anda akan merasa diberdayakan untuk menyesuaikannya dengan kebutuhan Anda.

PWA Stack Overflow

Untuk melengkapi artikel ini, saya membuat PWA Stack Overflow. Saya menghabiskan banyak waktu untuk membaca dan berkontribusi pada Stack Overflow, dan saya ingin membuat aplikasi web yang akan memudahkan penjelajahan pertanyaan umum untuk topik tertentu. API ini dibuat berdasarkan Stack Exchange API publik. Software ini bersifat open source, dan Anda dapat mempelajari lebih lanjut dengan membuka project GitHub.

Aplikasi Multi-halaman (MPA)

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

MPA adalah nama yang bagus untuk arsitektur tradisional yang digunakan sejak awal web. Setiap kali pengguna membuka URL baru, browser secara bertahap akan merender HTML khusus untuk halaman tersebut. 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 mem-build aplikasi web, yang memungkinkan browser menjalankan kode JavaScript untuk memperbarui halaman yang ada saat pengguna mengunjungi bagian baru. Baik SPA maupun MPA sama-sama valid untuk digunakan, tetapi untuk postingan ini, saya ingin mempelajari konsep PWA dalam konteks aplikasi multi-halaman.

Sangat cepat

Anda pernah mendengar saya (dan banyak lainnya) 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 kelas satu, dan yang benar-benar mendapatkan tempat di layar utama pengguna. Akronim "FIRE", adalah singkatan dari Fast, Integrated, Reliable, dan Engaging, merangkum semua atribut yang perlu dipertimbangkan saat membuat PWA.

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

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

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

Sangat cepat: Dan terakhir, saya akan sedikit mengubah definisi PWA dan melihat apa artinya membangun sesuatu yang sangat cepat dan andal. Tidak cukup baik untuk menjadi cepat dan andal jika Anda menggunakan jaringan latensi rendah. Kecepatan yang dapat diandalkan berarti kecepatan aplikasi web Anda konsisten, terlepas dari kondisi jaringan yang mendasarinya.

Teknologi yang Diaktifkan: Service Worker + Cache Storage API

PWA memperkenalkan standar kecepatan dan ketahanan yang tinggi. Untungnya, platform web menawarkan beberapa elemen penyusun untuk mewujudkan jenis performa tersebut. Yang saya maksud adalah pekerja layanan dan Cache Storage API.

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

Pekerja layanan yang menggunakan Cache Storage API untuk menyimpan salinan
          respons jaringan.

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

Pekerja layanan yang menggunakan Cache Storage API untuk merespons, melewati jaringan.

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

JavaScript "Isomorfik"

Satu konsep lagi yang ingin saya bahas adalah apa yang terkadang disebut sebagai JavaScript "isomorfik", atau "universal". Sederhananya, merupakan gagasan bahwa kode JavaScript yang sama dapat dibagikan di antara lingkungan runtime yang berbeda. Ketika saya membuat PWA, saya ingin berbagi kode JavaScript antara server back-end dan pekerja layanan.

Ada banyak pendekatan valid untuk berbagi 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 bagaimana saya sebenarnya membuat PWA Stack Overflow. Saya akan mulai dengan membahas server backend kami dan menjelaskan bagaimana hal itu sesuai dengan arsitektur 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 jika ada permintaan masuk, dan berintegrasi dengan framework Express HTTP populer yang sudah saya pahami. Alat ini juga menawarkan hosting siap pakai untuk semua resource statis situs saya. Mari kita lihat bagaimana server menangani permintaan.

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

Ringkasan menghasilkan respons navigasi, sisi server.

Server mengarahkan 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 secara lokal oleh server. Setelah mengetahui cara merespons, pekerja layanan dapat memulai streaming HTML kembali ke aplikasi web.

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

Pemilihan rute

Terkait perutean, pendekatan saya adalah menggunakan sintaksis perutean native framework Express. Cukup fleksibel untuk mencocokkan awalan URL sederhana serta URL yang menyertakan parameter sebagai bagian dari jalur. Di sini, saya membuat pemetaan antara nama rute yang akan dicocokkan dengan pola Express dasar.

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 khusus untuk rute yang cocok.

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

Template sisi server

Dan seperti apa logika template tersebut? Saya menggunakan pendekatan yang menggabungkan fragmen HTML parsial secara berurutan. Model ini sangat cocok untuk streaming.

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

Untuk memahami maksud saya, lihat Kode ekspres 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 serta memuat pesan.

Bagian berikutnya dari halaman kami menggunakan data dari Stack Exchange API. Mendapatkan data itu berarti bahwa server perlu membuat permintaan jaringan. Aplikasi web tidak dapat merender apa pun hingga mendapatkan respons kembali dan memprosesnya, tetapi setidaknya pengguna tidak menatap pada layar kosong saat mereka menunggu.

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

Membuat template bahasa

Membuat template bisa menjadi topik yang menimbulkan perdebatan, dan yang saya lakukan hanyalah salah satu pendekatan di antara banyak pendekatan lainnya. Sebaiknya Anda mengganti solusi Anda sendiri, terutama jika Anda memiliki ikatan lama dengan framework template yang sudah ada.

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

Jadi, inilah contoh bagaimana saya membuat template bagian HTML dinamis dari indeks aplikasi web saya. Seperti rute saya, logika pembuatan 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 akan berguna untuk membagi logika menjadi fungsi bantuan yang lebih kecil jika sesuai. Di sini, saya meneruskan setiap item yang ditampilkan dalam respons API ke dalam satu fungsi tersebut, yang membuat elemen HTML standar dengan semua atribut yang sesuai.

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

Catatan tertentu 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 meninjaunya kembali nanti.

Kembali ke pengendali rute saya, setelah template selesai, saya mengalirkan bagian terakhir HTML halaman saya ke browser, dan mengakhiri streaming. Hal ini menunjukkan kepada browser bahwa rendering progresif 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 itu adalah tur singkat tentang pengaturan 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, pekerja layanan saya akan mulai merespons. Ayo kita mulai.

Pekerja layanan

Ringkasan menghasilkan respons navigasi, dalam pekerja layanan.

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

Pekerja layanan kami menangani permintaan navigasi masuk untuk URL tertentu, dan seperti yang dilakukan server saya, layanan ini menggunakan kombinasi perutean dan logika 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 level rendah, saya akan mem-build pekerja layanan di atas kumpulan library tingkat tinggi yang disebut Workbox. Layanan ini memberikan fondasi yang kokoh untuk logika caching, pemilihan rute, dan pembuatan respons pekerja layanan.

Pemilihan rute

Sama seperti kode sisi server, pekerja layanan saya perlu tahu 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 berguna yang disebut 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 {i>template<i} khusus untuk menghasilkan respons. Pemberian template di pekerja layanan sedikit lebih rumit dibandingkan dengan yang ada di server backend, tetapi Workbox membantu banyak pekerjaan berat.

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

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

Penyimpanan cache aset statis

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

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

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

Workbox mengambil snapshot setiap konten file, dan secara 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-cache selalu tersedia, dan terus 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.
]);

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

Streaming

Selanjutnya, saya ingin pekerja layanan melakukan streaming HTML sebagian yang di-precache kembali ke aplikasi web dengan segera. Ini adalah bagian penting untuk menjadi "sangat cepat"—Saya selalu mendapatkan sesuatu yang berarti di layar langsung. Untungnya, penggunaan Streams API dalam pekerja layanan dapat memungkinkan hal tersebut.

Sekarang, Anda mungkin pernah mendengar tentang Streams API sebelumnya. Kolega saya, Jake Archhibald, telah menyanyikan pujian selama bertahun-tahun. Dia membuat prediksi tebal bahwa 2016 akan menjadi tahun streaming web. Dan Streams API saat ini sama hebatnya dengan dua tahun lalu, tetapi dengan perbedaan penting.

Meskipun saat itu hanya Chrome yang mendukung Streams, Streams API kini didukung secara lebih luas. Kisah keseluruhannya positif, dan dengan kode penggantian yang sesuai, tidak ada yang menghentikan Anda menggunakan streaming di pekerja layanan saat ini.

Mungkin ada satu hal yang menghentikan Anda, yaitu memikirkan cara kerja Streams API. Dependensi ini mengekspos sekumpulan primitif yang sangat canggih, dan developer yang merasa 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 bagi semua orang. Daripada menguraikan logika ini, mari kita bahas pendekatan saya terhadap streaming pekerja layanan.

Saya menggunakan wrapper tingkat tinggi yang baru, workbox-streams. Dengannya, 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 didukung, akan membuat respons non-streaming yang setara. Artinya, Anda tidak perlu mengkhawatirkan penggantian, karena streaming semakin mendekati 100% dukungan browser.

Penyimpanan cache runtime

Mari kita lihat bagaimana pekerja layanan saya menangani data runtime, dari Stack Exchange API. Saya memanfaatkan dukungan bawaan Workbox untuk strategi caching yang tidak berlaku saat validasi ulang, beserta habisnya masa berlaku untuk memastikan penyimpanan aplikasi web tidak bertambah tanpa batas.

Saya menyiapkan dua strategi di Workbox untuk menangani sumber berbeda yang akan membentuk respons streaming. Dalam beberapa panggilan fungsi dan konfigurasi, Workbox memungkinkan kita melakukan apa yang seharusnya mengambil ratusan baris kode tulisan tangan.

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-cache, seperti template HTML parsial kita.

Strategi lainnya menerapkan logika caching yang sudah usang saat validasi ulang, beserta masa berlaku cache yang paling jarang digunakan setelah mencapai 50 entri.

Setelah saya menerapkan strategi tersebut, Anda hanya perlu memberi tahu Workbox cara menggunakannya untuk membuat respons streaming yang lengkap. Saya meneruskan array sumber sebagai fungsi, dan setiap fungsi tersebut akan segera 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 keduanya akan selalu tersedia. Hal ini memastikan bahwa implementasi pekerja layanan kami akan sangat cepat dalam merespons permintaan, sama seperti kode sisi server saya.

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

Strategi usang saat validasi ulang berarti bahwa jika saya memiliki respons yang sebelumnya di-cache untuk panggilan API ini, saya akan dapat langsung mengalirkannya ke halaman, sambil memperbarui entri cache "di latar belakang" untuk saat diminta lagi.

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

Berbagi kode membuat semuanya tetap sinkron

Anda akan melihat bahwa bit tertentu dari kode pekerja layanan terlihat familier. Logika pembuatan template dan HTML sebagian yang digunakan oleh pekerja layanan identik dengan yang digunakan oleh pengendali sisi server. Berbagi kode ini memastikan pengguna mendapatkan pengalaman yang konsisten, baik ketika mereka mengunjungi aplikasi web saya untuk pertama kalinya maupun kembali ke halaman yang dirender oleh pekerja layanan. Itulah keunggulan JavaScript isomorfik.

Peningkatan yang dinamis dan progresif

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

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

Metadata halaman

Aplikasi saya menggunakan JavaScipt sisi klien untuk memperbarui metadata halaman berdasarkan respons API. Karena saya menggunakan bit awal yang sama dari HTML yang di-cache untuk setiap halaman, aplikasi web berakhir dengan tag umum di kepala dokumen saya. Namun, melalui koordinasi antara kode template dan kode sisi klien, saya dapat memperbarui judul jendela menggunakan metadata khusus 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 khusus halaman yang ingin Anda perbarui di aplikasi web Anda sendiri, Anda dapat mengikuti pendekatan yang sama.

UX Offline

{i>Progressive enhancement <i}yang saya tambahkan digunakan untuk menarik perhatian pada kemampuan offline. Saya telah membuat PWA yang andal, dan saya ingin pengguna tahu bahwa ketika mereka offline, mereka masih dapat memuat halaman yang telah dikunjungi sebelumnya.

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

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

Ketika browser memasuki keadaan offline, I loop melalui daftar link yang tidak disimpan dalam cache, dan meredupkan link yang tidak akan berfungsi. Perlu diingat bahwa ini hanyalah petunjuk visual kepada pengguna tentang hal yang akan mereka dapatkan dari halaman tersebut. Saya tidak benar-benar menonaktifkan link, atau mencegah pengguna untuk membuka halaman.

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

Sekarang saya telah melakukan tur tentang pendekatan saya untuk membangun PWA multi-halaman. Ada banyak faktor yang harus dipertimbangkan saat membuat pendekatan sendiri, dan Anda mungkin akhirnya membuat pilihan yang berbeda dari yang saya pilih. Fleksibilitas tersebut adalah salah satu hal penting dalam membangun web.

Ada beberapa kesalahan umum yang mungkin Anda temui saat membuat keputusan arsitektural, dan kami ingin membantu Anda dari kesulitan.

Jangan simpan HTML lengkap di cache

Sebaiknya jangan menyimpan dokumen HTML lengkap dalam cache. Salah satunya, hal ini membuang ruang. Jika aplikasi web Anda menggunakan struktur HTML dasar yang sama untuk setiap halamannya, Anda akan terus menyimpan salinan markup yang sama.

Lebih penting lagi, jika Anda menerapkan perubahan pada struktur HTML bersama di situs, setiap halaman yang sebelumnya di-cache tersebut akan tetap menggunakan tata letak lama. Bayangkan kekesalan pengunjung yang kembali karena melihat perpaduan halaman lama dan baru.

Pergeseran server / pekerja layanan

Kesalahan lain yang harus dihindari adalah server dan pekerja layanan tidak menyinkronkan. Pendekatan saya adalah menggunakan JavaScript isomorfik, sehingga kode yang sama dijalankan di kedua tempat tersebut. Hal ini tidak selalu dapat dilakukan, tergantung pada arsitektur server yang ada.

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

Skenario kasus terburuk

Tata letak / desain tidak konsisten

Apa yang terjadi ketika Anda mengabaikan perangkap itu? Ya, segala jenis kegagalan mungkin terjadi, tetapi skenario terburuknya adalah pengguna yang kembali mengunjungi halaman yang disimpan dalam cache dengan tata letak yang sangat tidak berlaku—mungkin halaman tanpa teks header yang sudah tidak berlaku, atau yang menggunakan nama class CSS yang sudah tidak valid.

Skenario kasus 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 dapat diandalkan.

Kiat untuk sukses

Tapi Anda tidak sendirian dalam situasi ini! Tips berikut dapat membantu Anda menghindari kesalahan tersebut:

Menggunakan library template dan perutean yang memiliki implementasi multibahasa

Coba gunakan library template dan perutean yang memiliki implementasi JavaScript. Sekarang, saya tahu bahwa tidak semua pengembang di luar sana memiliki kemewahan untuk bermigrasi dari server web saat ini dan membuat bahasa template.

Namun, sejumlah framework template dan perutean populer memiliki implementasi dalam berbagai bahasa. Jika Anda dapat menemukan solusi yang berfungsi dengan JavaScript serta bahasa server Anda saat ini, berarti Anda selangkah lebih dekat untuk menjaga agar pekerja layanan dan server Anda tetap sinkron.

Memilih template berurutan, bukan template bertingkat

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

Meng-cache konten statis dan dinamis di pekerja layanan

Untuk mendapatkan performa terbaik, sebaiknya Anda melakukan precache untuk 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 membuat berdasarkan strategi yang sudah teruji dengan baik dan siap produksi, bukan menerapkan semuanya dari awal.

Hanya blokir di jaringan saat benar-benar diperlukan

Terkait dengan itu, sebaiknya hanya blokir di jaringan jika tidak mungkin melakukan streaming respons dari cache. Menampilkan respons API yang di-cache sering kali dapat memberikan pengalaman pengguna yang lebih baik daripada menunggu data baru.

Referensi