Data Bertahan Di Muat Ulang Halaman: Cookie, IndexedDB, dan Segalanya di Antaranya

Diterbitkan: 2022-03-11

Misalkan saya mengunjungi situs web. Saya mengklik kanan salah satu tautan navigasi dan memilih untuk membuka tautan di jendela baru. Apa yang harus terjadi? Jika saya seperti kebanyakan pengguna, saya berharap halaman baru memiliki konten yang sama seolah-olah saya telah mengklik tautan secara langsung. Satu-satunya perbedaan adalah halaman tersebut muncul di jendela baru. Tetapi jika situs web Anda adalah aplikasi satu halaman (SPA), Anda mungkin melihat hasil yang aneh kecuali jika Anda telah merencanakan dengan cermat untuk kasus ini.

Ingatlah bahwa dalam SPA, tautan navigasi tipikal sering kali merupakan pengidentifikasi fragmen, dimulai dengan tanda hash (#). Mengklik tautan secara langsung tidak memuat ulang halaman, sehingga semua data yang disimpan dalam variabel JavaScript dipertahankan. Tetapi jika saya membuka tautan di tab atau jendela baru, browser memuat ulang halaman, menginisialisasi ulang semua variabel JavaScript. Jadi setiap elemen HTML yang terikat pada variabel tersebut akan ditampilkan secara berbeda, kecuali jika Anda telah mengambil langkah-langkah untuk mempertahankan data tersebut.

Data Bertahan Di Muat Ulang Halaman: Cookie, IndexedDB, dan Segalanya di Antaranya

Data Bertahan Di Muat Ulang Halaman: Cookie, IndexedDB, dan Segalanya di Antaranya
Menciak

Ada masalah serupa jika saya memuat ulang halaman secara eksplisit, seperti dengan menekan F5. Anda mungkin berpikir saya tidak perlu menekan F5, karena Anda telah menyiapkan mekanisme untuk mendorong perubahan dari server secara otomatis. Tetapi jika saya adalah pengguna biasa, Anda dapat bertaruh bahwa saya masih akan memuat ulang halaman tersebut. Mungkin browser saya tampaknya salah mengecat ulang layar, atau saya hanya ingin memastikan bahwa saya memiliki harga saham terbaru.

API Mungkin Tanpa Kewarganegaraan, Interaksi Manusia Tidak

Tidak seperti permintaan internal melalui RESTful API, interaksi pengguna manusia dengan situs web bukanlah tanpa kewarganegaraan. Sebagai pengguna web, saya menganggap kunjungan saya ke situs Anda sebagai sesi, hampir seperti panggilan telepon. Saya berharap browser mengingat data tentang sesi saya, dengan cara yang sama ketika saya menelepon saluran penjualan atau dukungan Anda, saya mengharapkan perwakilan untuk mengingat apa yang dikatakan sebelumnya dalam panggilan tersebut.

Contoh nyata dari data sesi adalah apakah saya masuk, dan jika demikian, sebagai pengguna mana. Setelah saya melewati layar masuk, saya seharusnya dapat menavigasi dengan bebas melalui halaman khusus pengguna situs. Jika saya membuka tautan di tab atau jendela baru dan saya disajikan dengan layar masuk lain, itu sangat tidak ramah pengguna.

Contoh lain adalah isi keranjang belanja di situs e-commerce. Jika menekan F5 mengosongkan keranjang belanja, pengguna cenderung kesal.

Dalam aplikasi multi-halaman tradisional yang ditulis dalam PHP, data sesi akan disimpan dalam array superglobal $_SESSION. Tetapi dalam SPA, itu harus berada di suatu tempat di sisi klien. Ada empat opsi utama untuk menyimpan data sesi di SPA:

  • Kue
  • Pengidentifikasi fragmen
  • Penyimpanan web
  • DB terindeks

Empat Kilobyte Cookies

Cookie adalah bentuk penyimpanan web yang lebih lama di browser. Mereka awalnya dimaksudkan untuk menyimpan data yang diterima dari server dalam satu permintaan dan mengirimkannya kembali ke server dalam permintaan berikutnya. Tetapi dari JavaScript, Anda dapat menggunakan cookie untuk menyimpan hampir semua jenis data, hingga batas ukuran 4 KB per cookie. AngularJS menawarkan modul ngCookies untuk mengelola cookie. Ada juga paket js-cookies yang menyediakan fungsionalitas serupa dalam kerangka kerja apa pun.

Ingatlah bahwa cookie apa pun yang Anda buat akan dikirim ke server pada setiap permintaan, baik itu pemuatan ulang halaman atau permintaan Ajax. Tetapi jika data sesi utama yang perlu Anda simpan adalah token akses untuk pengguna yang masuk, Anda tetap ingin ini dikirim ke server pada setiap permintaan. Wajar untuk mencoba menggunakan transmisi cookie otomatis ini sebagai sarana standar untuk menentukan token akses untuk permintaan Ajax.

Anda mungkin berpendapat bahwa menggunakan cookie dengan cara ini tidak sesuai dengan arsitektur RESTful. Tetapi dalam hal ini baik-baik saja karena setiap permintaan melalui API masih stateless, memiliki beberapa input dan beberapa output. Hanya saja salah satu input dikirim dengan cara yang lucu, melalui cookie. Jika Anda dapat mengatur permintaan API masuk untuk mengirim token akses kembali dalam cookie juga, maka kode sisi klien Anda hampir tidak perlu berurusan dengan cookie sama sekali. Sekali lagi, ini hanyalah keluaran lain dari permintaan yang dikembalikan dengan cara yang tidak biasa.

Cookie menawarkan satu keuntungan dibandingkan penyimpanan web. Anda dapat memberikan kotak centang “biarkan saya tetap masuk” pada formulir masuk. Dengan semantik, saya berharap jika saya membiarkannya tidak dicentang maka saya akan tetap masuk jika saya memuat ulang halaman atau membuka tautan di tab atau jendela baru, tetapi saya dijamin akan keluar setelah saya menutup browser. Ini adalah fitur keamanan yang penting jika saya menggunakan komputer bersama. Seperti yang akan kita lihat nanti, penyimpanan web tidak mendukung perilaku ini.

Jadi bagaimana pendekatan ini dapat bekerja dalam praktik? Misalkan Anda menggunakan LoopBack di sisi server. Anda telah mendefinisikan model Person, memperluas model User bawaan, menambahkan properti yang ingin Anda pertahankan untuk setiap pengguna. Anda telah mengonfigurasi model Person untuk diekspos melalui REST. Sekarang Anda perlu mengubah server/server.js untuk mencapai perilaku cookie yang diinginkan. Di bawah ini adalah server/server.js, mulai dari apa yang dihasilkan oleh slc loopback, dengan perubahan yang ditandai:

 var loopback = require('loopback'); var boot = require('loopback-boot'); var app = module.exports = loopback(); app.start = function() { // start the web server return app.listen(function() { app.emit('started'); var baseUrl = app.get('url').replace(/\/$/, ''); console.log('Web server listening at: %s', baseUrl); if (app.get('loopback-component-explorer')) { var explorerPath = app.get('loopback-component-explorer').mountPath; console.log('Browse your REST API at %s%s', baseUrl, explorerPath); } }); }; // start of first change app.use(loopback.cookieParser('secret')); // end of first change // Bootstrap the application, configure models, datasources and middleware. // Sub-apps like REST API are mounted via boot scripts. boot(app, __dirname, function(err) { if (err) throw err; // start of second change app.remotes().after('Person.login', function (ctx, next) { if (ctx.result.id) { var opts = {signed: true}; if (ctx.req.body.rememberme !== false) { opts.maxAge = 1209600000; } ctx.res.cookie('authorization', ctx.result.id, opts); } next(); }); app.remotes().after('Person.logout', function (ctx, next) { ctx.res.cookie('authorization', ''); next(); }); // end of second change // start the server if `$ node server.js` if (require.main === module) app.start(); });

Perubahan pertama mengonfigurasi pengurai cookie untuk menggunakan 'rahasia' sebagai rahasia penandatanganan cookie, sehingga mengaktifkan cookie yang ditandatangani. Anda perlu melakukan ini karena meskipun LoopBack mencari token akses di salah satu cookie 'otorisasi' atau 'access_token', cookie semacam itu harus ditandatangani. Sebenarnya, persyaratan ini tidak ada gunanya. Menandatangani cookie dimaksudkan untuk memastikan bahwa cookie belum dimodifikasi. Tetapi tidak ada bahaya bagi Anda untuk mengubah token akses. Lagi pula, Anda bisa saja mengirim token akses dalam bentuk yang tidak ditandatangani, sebagai parameter biasa. Dengan demikian, Anda tidak perlu khawatir tentang rahasia penandatanganan cookie yang sulit ditebak, kecuali jika Anda menggunakan cookie yang ditandatangani untuk hal lain.

Perubahan kedua mengatur beberapa postprocessing untuk metode Person.login dan Person.logout. Untuk Person.login , Anda ingin mengambil token akses yang dihasilkan dan mengirimkannya ke klien sebagai 'otorisasi' cookie yang ditandatangani juga. Klien dapat menambahkan satu properti lagi ke parameter kredensial, ingat saya, yang menunjukkan apakah akan membuat cookie bertahan selama 2 minggu. Defaultnya benar. Metode login itu sendiri akan mengabaikan properti ini, tetapi postprocessor akan memeriksanya.

Untuk Person.logout , Anda ingin menghapus cookie ini.

Anda dapat langsung melihat hasil perubahan ini di StrongLoop API Explorer. Biasanya setelah permintaan Person.login, Anda harus menyalin token akses, menempelkannya ke formulir di kanan atas, dan klik Setel Token Akses. Tetapi dengan perubahan ini, Anda tidak perlu melakukan semua itu. Token akses secara otomatis disimpan sebagai 'otorisasi' cookie, dan dikirim kembali pada setiap permintaan berikutnya. Saat Explorer menampilkan header respons dari Person.login, itu menghilangkan cookie, karena JavaScript tidak pernah diizinkan untuk melihat header Set-Cookie. Tapi yakinlah, kuenya ada di sana.

Di sisi klien, pada halaman yang memuat ulang Anda akan melihat apakah 'otorisasi' cookie ada. Jika demikian, Anda perlu memperbarui catatan userId Anda saat ini. Mungkin cara termudah untuk melakukannya adalah dengan menyimpan userId dalam cookie terpisah pada login yang berhasil, sehingga Anda dapat mengambilnya pada halaman yang memuat ulang.

Pengidentifikasi Fragmen

Saat saya mengunjungi situs web yang telah diterapkan sebagai SPA, URL di bilah alamat browser saya mungkin terlihat seperti “https://example.com/#/my-photos/37”. Bagian pengenal fragmen ini, "#/my-photos/37", sudah merupakan kumpulan informasi status yang dapat dilihat sebagai data sesi. Dalam hal ini, saya mungkin melihat salah satu foto saya, yang ID-nya 37.

Anda dapat memutuskan untuk menyematkan data sesi lain dalam pengidentifikasi fragmen. Ingatlah bahwa di bagian sebelumnya, dengan token akses yang disimpan dalam 'otorisasi' cookie, Anda masih perlu melacak userId entah bagaimana. Salah satu opsi adalah menyimpannya di cookie terpisah. Tetapi pendekatan lain adalah dengan menyematkannya di pengidentifikasi fragmen. Anda dapat memutuskan bahwa saat saya masuk, semua halaman yang saya kunjungi akan memiliki pengenal fragmen yang dimulai dengan “#/u/XXX”, di mana XXX adalah userId. Jadi pada contoh sebelumnya, pengidentifikasi fragmen mungkin adalah “#/u/59/my-photos/37” jika userId saya adalah 59.

Secara teoritis, Anda dapat menyematkan token akses itu sendiri di pengidentifikasi fragmen, menghindari kebutuhan akan cookie atau penyimpanan web. Tapi itu akan menjadi ide yang buruk. Token akses saya kemudian akan terlihat di bilah alamat. Siapa pun yang melihat dari balik bahu saya dengan kamera dapat mengambil snapshot layar, sehingga mendapatkan akses ke akun saya.

Satu catatan terakhir: dimungkinkan untuk mengatur SPA sehingga tidak menggunakan pengidentifikasi fragmen sama sekali. Alih-alih menggunakan URL biasa seperti "http://example.com/app/dashboard" dan "http://example.com/app/my-photos/37", dengan server dikonfigurasi untuk mengembalikan HTML tingkat atas untuk Anda SPA sebagai tanggapan atas permintaan untuk salah satu URL ini. SPA Anda kemudian melakukan peruteannya berdasarkan jalur (misalnya “/app/dashboard” atau “/app/my-photos/37”) alih-alih pengenal fragmen. Ini memotong klik pada tautan navigasi, dan menggunakan History.pushState() untuk mendorong URL baru, kemudian melanjutkan dengan perutean seperti biasa. Itu juga mendengarkan acara popstate untuk mendeteksi pengguna mengklik tombol kembali, dan sekali lagi melanjutkan dengan perutean pada URL yang dipulihkan. Rincian lengkap tentang bagaimana menerapkan ini berada di luar cakupan artikel ini. Tetapi jika Anda menggunakan teknik ini, maka jelas Anda dapat menyimpan data sesi di jalur alih-alih pengidentifikasi fragmen.

Penyimpanan Web

Penyimpanan web adalah mekanisme JavaScript untuk menyimpan data di dalam browser. Seperti cookie, penyimpanan web terpisah untuk setiap asal. Setiap item yang disimpan memiliki nama dan nilai, keduanya berupa string. Tetapi penyimpanan web sama sekali tidak terlihat oleh server, dan menawarkan kapasitas penyimpanan yang jauh lebih besar daripada cookie. Ada dua jenis penyimpanan web: penyimpanan lokal dan penyimpanan sesi.

Item penyimpanan lokal terlihat di semua tab di semua jendela, dan tetap ada bahkan setelah browser ditutup. Dalam hal ini, ia berperilaku agak seperti cookie dengan tanggal kedaluwarsa yang sangat jauh di masa depan. Dengan demikian, sangat cocok untuk menyimpan token akses jika pengguna telah mencentang “biarkan saya tetap masuk” pada formulir masuk.

Item penyimpanan sesi hanya terlihat di dalam tab tempat item tersebut dibuat, dan item tersebut menghilang saat tab tersebut ditutup. Ini membuat masa pakainya sangat berbeda dari cookie mana pun. Ingatlah bahwa cookie sesi masih terlihat di semua tab di semua jendela.

Jika Anda menggunakan AngularJS SDK untuk LoopBack, sisi klien akan secara otomatis menggunakan penyimpanan web untuk menyimpan token akses dan userId. Ini terjadi di layanan LoopBackAuth di js/services/lb-services.js. Ini akan menggunakan penyimpanan lokal, kecuali jika parameter ingatMe salah (biasanya berarti kotak centang "biarkan saya tetap masuk" tidak dicentang), dalam hal ini akan menggunakan penyimpanan sesi.

Hasilnya adalah jika saya masuk dengan "biarkan saya tetap masuk" tidak dicentang, dan saya kemudian membuka tautan di tab atau jendela baru, saya tidak akan masuk ke sana. Kemungkinan besar saya akan melihat layar login. Anda dapat memutuskan sendiri apakah ini perilaku yang dapat diterima. Beberapa orang mungkin menganggapnya sebagai fitur yang bagus, di mana Anda dapat memiliki beberapa tab, masing-masing masuk sebagai pengguna yang berbeda. Atau Anda mungkin memutuskan bahwa hampir tidak ada orang yang menggunakan komputer bersama lagi, jadi Anda bisa menghilangkan kotak centang "biarkan saya tetap masuk" sama sekali.

Jadi bagaimana tampilan penanganan data sesi jika Anda memutuskan untuk menggunakan AngularJS SDK untuk LoopBack? Misalkan Anda memiliki situasi yang sama seperti sebelumnya di sisi server: Anda telah mendefinisikan model Person, memperluas model User, dan Anda telah mengekspos model Person melalui REST. Anda tidak akan menggunakan cookie, jadi Anda tidak memerlukan perubahan apa pun yang dijelaskan sebelumnya.

Di sisi klien, di suatu tempat di pengontrol terluar Anda, Anda mungkin memiliki variabel seperti $scope.currentUserId yang menyimpan userId dari pengguna yang saat ini masuk, atau null jika pengguna tidak masuk. Kemudian untuk menangani pemuatan ulang halaman dengan benar, Anda cukup sertakan pernyataan ini dalam fungsi konstruktor untuk pengontrol itu:

 $scope.currentUserId = Person.getCurrentId();

Semudah itu. Tambahkan 'Orang' sebagai dependensi pengontrol Anda, jika belum.

DB terindeks

IndexedDB adalah fasilitas yang lebih baru untuk menyimpan data dalam jumlah besar di browser. Anda dapat menggunakannya untuk menyimpan data jenis JavaScript apa pun, seperti objek atau larik, tanpa harus membuat serialisasi. Semua permintaan terhadap database tidak sinkron, jadi Anda mendapatkan panggilan balik saat permintaan selesai.

Anda mungkin menggunakan IndexedDB untuk menyimpan data terstruktur yang tidak terkait dengan data apa pun di server. Contohnya mungkin kalender, daftar tugas, atau permainan tersimpan yang dimainkan secara lokal. Dalam hal ini, aplikasi tersebut benar-benar aplikasi lokal, dan situs web Anda hanyalah sarana untuk mengirimkannya.

Saat ini, Internet Explorer dan Safari hanya memiliki sebagian dukungan untuk IndexedDB. Peramban utama lainnya mendukung sepenuhnya. Namun, satu batasan serius saat ini adalah Firefox menonaktifkan IndexedDB sepenuhnya dalam mode penjelajahan pribadi.

Sebagai contoh nyata penggunaan IndexedDB, mari kita ambil aplikasi puzzle geser oleh Pavol Daniš, dan ubah untuk menyimpan status puzzle pertama, puzzle geser Dasar 3x3 berdasarkan logo AngularJS, setelah setiap gerakan. Memuat ulang halaman kemudian akan mengembalikan keadaan teka-teki pertama ini.

Saya telah menyiapkan garpu repositori dengan perubahan ini, yang semuanya ada di app/js/puzzle/slidingPuzzle.js. Seperti yang Anda lihat, bahkan penggunaan IndexedDB yang belum sempurna pun cukup terlibat. Saya hanya akan menunjukkan highlight di bawah ini. Pertama, fungsi restore dipanggil selama pemuatan halaman, untuk membuka database IndexedDB:

 /* * Tries to restore game */ this.restore = function(scope, storekey) { this.storekey = storekey; if (this.db) { this.restore2(scope); } else if (!window.indexedDB) { console.log('SlidingPuzzle: browser does not support indexedDB'); this.shuffle(); } else { var self = this; var request = window.indexedDB.open('SlidingPuzzleDatabase'); request.onerror = function(event) { console.log('SlidingPuzzle: error opening database, ' + request.error.name); scope.$apply(function() { self.shuffle(); }); }; request.onupgradeneeded = function(event) { event.target.result.createObjectStore('SlidingPuzzleStore'); }; request.onsuccess = function(event) { self.db = event.target.result; self.restore2(scope); }; } };

Event request.onupgradeneeded menangani kasus dimana database belum ada. Dalam hal ini, kami membuat toko objek.

Setelah database terbuka, fungsi restore2 dipanggil, yang mencari catatan dengan kunci yang diberikan (yang sebenarnya akan menjadi konstanta 'Dasar' dalam kasus ini):

 /* * Tries to restore game, once database has been opened */ this.restore2 = function(scope) { var transaction = this.db.transaction('SlidingPuzzleStore'); var objectStore = transaction.objectStore('SlidingPuzzleStore'); var self = this; var request = objectStore.get(this.storekey); request.onerror = function(event) { console.log('SlidingPuzzle: error reading from database, ' + request.error.name); scope.$apply(function() { self.shuffle(); }); }; request.onsuccess = function(event) { if (!request.result) { console.log('SlidingPuzzle: no saved game for ' + self.storekey); scope.$apply(function() { self.shuffle(); }); } else { scope.$apply(function() { self.grid = request.result; }); } }; }

Jika catatan seperti itu ada, nilainya menggantikan susunan kisi teka-teki. Jika ada kesalahan dalam memulihkan permainan, kami hanya mengocok ubin seperti sebelumnya. Perhatikan bahwa grid adalah array 3x3 objek ubin, yang masing-masing cukup kompleks. Keuntungan besar dari IndexedDB adalah Anda dapat menyimpan dan mengambil nilai tersebut tanpa harus membuat serialisasi.

Kami menggunakan $apply untuk memberi tahu AngularJS bahwa modelnya telah diubah, sehingga tampilan akan diperbarui dengan tepat. Ini karena pembaruan terjadi di dalam event handler DOM, jadi AngularJS tidak akan dapat mendeteksi perubahan tersebut. Aplikasi AngularJS apa pun yang menggunakan IndexedDB mungkin perlu menggunakan $apply untuk alasan ini.

Setelah tindakan apa pun yang akan mengubah larik kisi, seperti pemindahan oleh pengguna, penyimpanan fungsi dipanggil yang menambahkan atau memperbarui catatan dengan kunci yang sesuai, berdasarkan nilai kisi yang diperbarui:

 /* * Tries to save game */ this.save = function() { if (!this.db) { return; } var transaction = this.db.transaction('SlidingPuzzleStore', 'readwrite'); var objectStore = transaction.objectStore('SlidingPuzzleStore'); var request = objectStore.put(this.grid, this.storekey); request.onerror = function(event) { console.log('SlidingPuzzle: error writing to database, ' + request.error.name); }; request.onsuccess = function(event) { // successful, no further action needed }; }

Perubahan yang tersisa adalah memanggil fungsi-fungsi di atas pada waktu yang tepat. Anda dapat meninjau komit yang menunjukkan semua perubahan. Perhatikan bahwa kami memanggil pemulihan hanya untuk teka-teki dasar, bukan untuk tiga teka-teki lanjutan. Kami mengeksploitasi fakta bahwa tiga teka-teki lanjutan memiliki atribut api, jadi untuk itu kami hanya melakukan pengocokan normal.

Bagaimana jika kita ingin menyimpan dan memulihkan teka-teki tingkat lanjut juga? Itu akan membutuhkan beberapa restrukturisasi. Di setiap teka-teki lanjutan, pengguna dapat menyesuaikan file sumber gambar dan dimensi teka-teki. Jadi kita harus meningkatkan nilai yang disimpan di IndexedDB untuk memasukkan informasi ini. Lebih penting lagi, kami membutuhkan cara untuk memperbaruinya dari pemulihan. Itu sedikit banyak untuk contoh yang sudah panjang ini.

Kesimpulan

Dalam kebanyakan kasus, penyimpanan web adalah pilihan terbaik Anda untuk menyimpan data sesi. Ini didukung penuh oleh semua browser utama, dan menawarkan kapasitas penyimpanan yang jauh lebih besar daripada cookie.

Anda akan menggunakan cookie jika server Anda sudah diatur untuk menggunakannya, atau jika Anda memerlukan data agar dapat diakses di semua tab di semua jendela, tetapi Anda juga ingin memastikannya akan dihapus saat browser ditutup.

Anda sudah menggunakan pengidentifikasi fragmen untuk menyimpan data sesi yang khusus untuk halaman itu, seperti ID foto yang dilihat pengguna. Meskipun Anda dapat menyematkan data sesi lain di pengidentifikasi fragmen, ini tidak benar-benar menawarkan keuntungan apa pun dibandingkan penyimpanan web atau cookie.

Menggunakan IndexedDB kemungkinan akan membutuhkan lebih banyak pengkodean daripada teknik lainnya. Tetapi jika nilai yang Anda simpan adalah objek JavaScript kompleks yang akan sulit untuk diserialisasi, atau jika Anda memerlukan model transaksional, maka itu mungkin bermanfaat.