Sayfa Yeniden Yüklemelerinde Kalıcı Veriler: Çerezler, IndexedDB ve Arasındaki Her Şey
Yayınlanan: 2022-03-11Diyelim ki bir web sitesini ziyaret ediyorum. Gezinme bağlantılarından birine sağ tıklıyorum ve bağlantıyı yeni bir pencerede açmayı seçiyorum. Ne olmalı? Çoğu kullanıcı gibiysem, yeni sayfanın, doğrudan bağlantıya tıklamış gibi aynı içeriğe sahip olmasını beklerim. Tek fark, sayfanın yeni bir pencerede görünmesi olmalıdır. Ancak web siteniz tek sayfalık bir uygulama (SPA) ise, bu durumu dikkatli bir şekilde planlamadığınız sürece garip sonuçlar görebilirsiniz.
Bir SPA'da tipik bir gezinme bağlantısının, genellikle bir kare işaretiyle (#) başlayan bir parça tanımlayıcı olduğunu hatırlayın. Bağlantıya doğrudan tıklamak sayfayı yeniden yüklemez, bu nedenle JavaScript değişkenlerinde depolanan tüm veriler korunur. Ancak bağlantıyı yeni bir sekmede veya pencerede açarsam, tarayıcı tüm JavaScript değişkenlerini yeniden başlatarak sayfayı yeniden yükler. Bu nedenle, bu verileri bir şekilde korumak için adımlar atmadığınız sürece, bu değişkenlere bağlı tüm HTML öğeleri farklı şekilde görüntülenecektir.
F5'e basmak gibi sayfayı açıkça yeniden yüklersem de benzer bir sorun var. F5'e basmama gerek olmadığını düşünebilirsiniz, çünkü değişiklikleri sunucudan otomatik olarak gönderecek bir mekanizma kurdunuz. Ama tipik bir kullanıcıysam, bahse girebilirsin ki sayfayı yeniden yükleyeceğim. Belki tarayıcım ekranı yanlış boyamış gibi görünüyor ya da en son hisse senedi fiyatlarına sahip olduğumdan emin olmak istiyorum.
API'ler Vatansız Olabilir, İnsan Etkileşimi Değil
Bir RESTful API aracılığıyla yapılan dahili bir isteğin aksine, bir insan kullanıcının bir web sitesiyle etkileşimi durumsuz değildir. Bir web kullanıcısı olarak, sitenize yaptığım ziyareti neredeyse bir telefon görüşmesi gibi bir oturum olarak düşünüyorum. Satış veya destek hattınızı aradığımda, temsilcinin görüşmede daha önce söylenenleri hatırlamasını beklediğim gibi, tarayıcının oturumumla ilgili verileri hatırlamasını bekliyorum.
Açık bir oturum verisi örneği, oturum açıp açmadığım ve eğer öyleyse hangi kullanıcı olarak oturum açtığımdır. Bir giriş ekranından geçtikten sonra, sitenin kullanıcıya özel sayfalarında özgürce gezinebilmeliyim. Yeni bir sekmede veya pencerede bir bağlantı açarsam ve başka bir giriş ekranıyla karşılaşırsam, bu pek kullanıcı dostu değildir.
Bir başka örnek de bir e-ticaret sitesinde alışveriş sepetinin içeriğidir. F5'e basmak alışveriş sepetini boşaltırsa, kullanıcıların üzülmesi muhtemeldir.
PHP ile yazılmış geleneksel çok sayfalı bir uygulamada, oturum verileri $_SESSION süper küresel dizisinde depolanır. Ancak bir SPA'da müşteri tarafında bir yerde olması gerekir. Bir SPA'da oturum verilerini depolamak için dört ana seçenek vardır:
- Kurabiye
- parça tanımlayıcı
- web depolama
- IndexedDB
Dört Kilobayt Çerez
Çerezler, tarayıcıdaki daha eski bir web depolama biçimidir. Başlangıçta sunucudan alınan verileri tek bir istekte depolamak ve sonraki isteklerde sunucuya geri göndermek için tasarlandılar. Ancak JavaScript'ten, çerez başına 4 KB'lık bir boyut sınırına kadar, hemen hemen her tür veriyi depolamak için çerezleri kullanabilirsiniz. AngularJS, çerezleri yönetmek için ngCookies modülünü sunar. Herhangi bir çerçevede benzer işlevsellik sağlayan bir js-cookies paketi de vardır.
Oluşturduğunuz herhangi bir tanımlama bilgisinin, ister sayfa yeniden yükleme isterse bir Ajax isteği olsun, her istekte sunucuya gönderileceğini unutmayın. Ancak, depolamanız gereken ana oturum verileri, oturum açmış kullanıcının erişim belirteciyse, bunun her istekte sunucuya gönderilmesini yine de istersiniz. Ajax istekleri için erişim belirtecini belirlemenin standart yolu olarak bu otomatik tanımlama bilgisi iletimini kullanmaya çalışmak doğaldır.
Tanımlama bilgilerini bu şekilde kullanmanın RESTful mimarisiyle uyumlu olmadığını iddia edebilirsiniz. Ancak bu durumda, API aracılığıyla yapılan her istek hala durumsuz olduğundan, bazı girdilere ve bazı çıktılara sahip olduğu için sorun yoktur. Sadece girdilerden biri bir çerez aracılığıyla komik bir şekilde gönderiliyor. Giriş API isteğinin erişim belirtecini bir çerezde geri göndermesini de ayarlayabilirseniz, istemci tarafı kodunuzun çerezlerle hiç ilgilenmesi gerekmez. Yine, alışılmadık bir şekilde döndürülen isteğin başka bir çıktısıdır.
Çerezler, web depolamaya göre bir avantaj sunar. Giriş formunda "oturumumu açık tut" onay kutusu sağlayabilirsiniz. Semantik ile, işaretlemeden bırakırsam, sayfayı yeniden yüklersem veya yeni bir sekmede veya pencerede bir bağlantı açarsam oturumum açık kalır, ancak tarayıcıyı kapattığımda oturumu kapatacağım garanti edilir. Paylaşılan bir bilgisayar kullanıyorsam bu önemli bir güvenlik özelliğidir. Daha sonra göreceğimiz gibi, web depolama bu davranışı desteklemiyor.
Peki bu yaklaşım pratikte nasıl çalışabilir? Sunucu tarafında LoopBack kullandığınızı varsayalım. Yerleşik Kullanıcı modelini genişleterek, her kullanıcı için korumak istediğiniz özellikleri ekleyerek bir Kişi modeli tanımladınız. Kişi modelini REST üzerinden gösterilecek şekilde yapılandırdınız. Şimdi, istenen tanımlama bilgisi davranışını elde etmek için server/server.js'de ince ayar yapmanız gerekiyor. Aşağıda server/server.js, slc geridönüşü tarafından oluşturulandan başlayarak, işaretli değişikliklerle birlikte verilmiştir:
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(); });İlk değişiklik, tanımlama bilgisi ayrıştırıcısını tanımlama bilgisi imzalama sırrı olarak 'gizli' kullanacak ve böylece imzalı tanımlama bilgilerini etkinleştirecek şekilde yapılandırır. Bunu yapmanız gerekir çünkü LoopBack, 'yetkilendirme' veya 'erişim_token' tanımlama bilgilerinin herhangi birinde bir erişim belirteci arasa da, böyle bir tanımlama bilgisinin imzalanmasını gerektirir. Aslında bu gereklilik anlamsızdır. Bir tanımlama bilgisinin imzalanması, tanımlama bilgisinin değiştirilmediğinden emin olmayı amaçlar. Ancak erişim belirtecini değiştirmeniz tehlikesi yoktur. Sonuçta, erişim belirtecini sıradan bir parametre olarak imzasız biçimde göndermiş olabilirsiniz. Bu nedenle, imzalı çerezleri başka bir şey için kullanmıyorsanız, çerez imzalama sırrının tahmin edilmesinin zor olması konusunda endişelenmenize gerek yoktur.
İkinci değişiklik, Person.login ve Person.logout yöntemleri için bazı son işlemler ayarlar. Person.login için, ortaya çıkan erişim belirtecini alıp müşteriye imzalı çerez 'yetkilendirmesi' olarak göndermek istiyorsunuz. İstemci, kimlik bilgileri parametresine, çerezin 2 hafta boyunca kalıcı olup olmayacağını belirten beni hatırla bir özellik daha ekleyebilir. Varsayılan doğrudur. Oturum açma yönteminin kendisi bu özelliği yok sayar, ancak son işlemci bunu kontrol eder.
Person.logout için bu tanımlama bilgisini temizlemek istiyorsunuz.
Bu değişikliklerin sonuçlarını hemen StrongLoop API Gezgini'nde görebilirsiniz. Normalde bir Person.login isteğinden sonra, erişim belirtecini kopyalamanız, sağ üstteki forma yapıştırmanız ve Erişim Simgesini Ayarla'yı tıklamanız gerekir. Ancak bu değişikliklerle bunların hiçbirini yapmanız gerekmez. Erişim belirteci otomatik olarak tanımlama bilgisi "yetkilendirmesi" olarak kaydedilir ve sonraki her istekte geri gönderilir. Gezgin, Person.login'den gelen yanıt başlıklarını görüntülerken, JavaScript'in Set-Cookie başlıklarını görmesine asla izin verilmediği için tanımlama bilgisini atlar. Ama içiniz rahat olsun, kurabiye orada.
İstemci tarafında, bir sayfanın yeniden yüklenmesinde çerez 'yetkilendirmesinin' olup olmadığını görürsünüz. Öyleyse, mevcut kullanıcı kimliği kaydınızı güncellemeniz gerekir. Muhtemelen bunu yapmanın en kolay yolu, başarılı oturum açma sırasında userId'yi ayrı bir çerezde depolamaktır, böylece onu bir sayfa yeniden yüklemesinde geri alabilirsiniz.
Parça Tanımlayıcı
SPA olarak uygulanmış bir web sitesini ziyaret ederken, tarayıcımın adres çubuğundaki URL “https://example.com/#/my-photos/37” gibi görünebilir. Bunun parça tanımlayıcı kısmı, “#/my-photos/37”, zaten oturum verisi olarak görülebilecek bir durum bilgisi koleksiyonudur. Bu durumda, muhtemelen fotoğraflarımdan birine bakıyorum, kimliği 37 olan.
Parça tanımlayıcı içine başka oturum verilerini gömmeye karar verebilirsiniz. Önceki bölümde, çerez 'yetkilendirmesinde' saklanan erişim belirteciyle, yine de bir şekilde userId'yi takip etmeniz gerektiğini hatırlayın. Bir seçenek, onu ayrı bir çerezde saklamaktır. Ancak başka bir yaklaşım, onu parça tanımlayıcısına gömmektir. Ben oturum açtığımda, ziyaret ettiğim tüm sayfaların "#/u/XXX" ile başlayan bir parça tanımlayıcıya sahip olacağına karar verebilirsiniz, burada XXX kullanıcı kimliğidir. Dolayısıyla önceki örnekte, kullanıcı kimliğim 59 ise parça tanımlayıcısı "#/u/59/my-photos/37" olabilir.
Teorik olarak, herhangi bir tanımlama bilgisi veya web depolama ihtiyacından kaçınarak erişim belirtecinin kendisini parça tanımlayıcısına gömebilirsiniz. Ama bu kötü bir fikir olurdu. Erişim anahtarım daha sonra adres çubuğunda görünür olacaktı. Omzumun üzerinden kamerayla bakan herkes ekranın fotoğrafını çekebilir, böylece hesabıma erişebilir.

Son bir not: Parça tanımlayıcılarını hiç kullanmayacak şekilde bir SPA kurmak mümkündür. Bunun yerine "http://example.com/app/dashboard" ve "http://example.com/app/my-photos/37" gibi sıradan URL'ler kullanır ve sunucu, sizin için en üst düzey HTML'yi döndürecek şekilde yapılandırılmıştır. SPA, bu URL'lerden herhangi birine yönelik bir isteğe yanıt olarak. SPA'nız daha sonra yönlendirmesini parça tanımlayıcısı yerine yola (örneğin “/app/dashboard” veya “/app/my-photos/37”) göre yapar. Gezinme bağlantılarındaki tıklamaları yakalar ve yeni URL'yi göndermek için History.pushState() öğesini kullanır, ardından her zamanki gibi yönlendirmeye devam eder. Ayrıca, kullanıcının geri düğmesini tıkladığını algılamak için popstate olaylarını da dinler ve yeniden, geri yüklenen URL'de yönlendirmeye devam eder. Bunun nasıl uygulanacağına ilişkin tüm ayrıntılar bu makalenin kapsamı dışındadır. Ancak bu tekniği kullanırsanız, açıkçası oturum verilerini parça tanımlayıcısı yerine yolda depolayabilirsiniz.
Web depolama
Web depolama, JavaScript'in tarayıcı içinde veri depolaması için bir mekanizmadır. Çerezler gibi, web depolama her kaynak için ayrıdır. Depolanan her öğenin, her ikisi de dize olan bir adı ve değeri vardır. Ancak web depolama, sunucu tarafından tamamen görünmezdir ve çerezlerden çok daha fazla depolama kapasitesi sunar. İki tür web depolaması vardır: yerel depolama ve oturum depolama.
Bir yerel depolama öğesi, tüm pencerelerin tüm sekmelerinde görünür ve tarayıcı kapatıldıktan sonra bile devam eder. Bu bakımdan, son kullanma tarihi çok ileride olan bir çerez gibi davranır. Bu nedenle, kullanıcının oturum açma formunda "oturumumu açık tut" seçeneğini işaretlemesi durumunda bir erişim belirtecinin saklanması uygundur.
Bir oturum depolama öğesi yalnızca oluşturulduğu sekmede görünür ve o sekme kapatıldığında kaybolur. Bu, ömrünü herhangi bir çerezinkinden çok farklı kılar. Bir oturum tanımlama bilgisinin tüm pencerelerin tüm sekmelerinde hala görünür olduğunu hatırlayın.
LoopBack için AngularJS SDK kullanıyorsanız, istemci tarafı hem erişim belirtecini hem de userId'yi kaydetmek için otomatik olarak web depolama alanını kullanır. Bu, js/services/lb-services.js içindeki LoopBackAuth hizmetinde gerçekleşir. RememberMe parametresi false (normalde "oturumumu açık tut" onay kutusunun işaretlenmemiş olduğu anlamına gelir) olmadığı sürece yerel depolamayı kullanır, bu durumda oturum depolamayı kullanır.
Sonuç olarak, "oturumumu açık tut" işareti kaldırılmış olarak oturum açarsam ve ardından yeni bir sekmede veya pencerede bir bağlantı açarsam, orada oturum açmayacağım. Büyük ihtimalle giriş ekranını göreceğim. Bunun kabul edilebilir bir davranış olup olmadığına kendiniz karar verebilirsiniz. Bazıları, her biri farklı bir kullanıcı olarak oturum açmış birkaç sekmeye sahip olabileceğiniz güzel bir özellik olarak düşünebilir. Veya artık neredeyse hiç kimsenin paylaşılan bilgisayarları kullanmadığına karar verebilirsiniz, böylece "oturumumu açık tut" onay kutusunu tamamen atlayabilirsiniz.
LoopBack için AngularJS SDK'yı kullanmaya karar verirseniz, oturum verilerinin işlenmesi nasıl görünür? Sunucu tarafında daha önce olduğu gibi aynı duruma sahip olduğunuzu varsayalım: Kullanıcı modelini genişleterek bir Kişi modeli tanımladınız ve REST üzerinden Kişi modelini kullanıma sundunuz. Tanımlama bilgilerini kullanmayacaksınız, bu nedenle daha önce açıklanan değişikliklerin hiçbirine ihtiyacınız olmayacak.
İstemci tarafında, en dıştaki denetleyicinizde bir yerde, muhtemelen şu anda oturum açmış olan kullanıcının userId'sini tutan $scope.currentUserId gibi bir değişkeniniz var veya kullanıcı oturum açmadıysa null. Ardından, sayfa yeniden yüklemelerini düzgün bir şekilde işlemek için, sadece bu ifadeyi o kontrolörün yapıcı işlevine ekleyin:
$scope.currentUserId = Person.getCurrentId();Bu kadar kolay. Henüz değilse, denetleyicinizin bir bağımlılığı olarak 'Kişi' ekleyin.
IndexedDB
IndexedDB, tarayıcıda büyük miktarda veri depolamak için daha yeni bir tesistir. Bir nesne veya dizi gibi herhangi bir JavaScript türündeki verileri seri hale getirmek zorunda kalmadan depolamak için kullanabilirsiniz. Veritabanına yönelik tüm istekler eşzamansızdır, bu nedenle istek tamamlandığında bir geri arama alırsınız.
Sunucudaki herhangi bir veriyle ilgisi olmayan yapılandırılmış verileri depolamak için IndexedDB kullanabilirsiniz. Örnek olarak bir takvim, yapılacaklar listesi veya yerel olarak oynanan kayıtlı oyunlar verilebilir. Bu durumda, uygulama gerçekten yerel bir uygulamadır ve web siteniz onu teslim etmek için sadece bir araçtır.
Şu anda, Internet Explorer ve Safari, IndexedDB için yalnızca kısmi desteğe sahiptir. Diğer büyük tarayıcılar bunu tam olarak desteklemektedir. Ancak şu anda ciddi bir sınırlama, Firefox'un IndexedDB'yi tamamen özel tarama modunda devre dışı bırakmasıdır.
IndexedDB kullanımına somut bir örnek olarak, Pavol Daniš'in kayan bulmaca uygulamasını alalım ve her hareketten sonra ilk bulmacanın, AngularJS logosuna dayalı Temel 3x3 kayan bulmacanın durumunu kaydetmek için ince ayar yapalım. Sayfayı yeniden yüklemek, bu ilk bulmacanın durumunu geri yükleyecektir.
Bu değişikliklerle birlikte, tümü app/js/puzzle/slidingPuzzle.js içinde olan bir depo çatalı kurdum. Gördüğünüz gibi, IndexedDB'nin ilkel bir kullanımı bile oldukça ilgili. Aşağıda sadece öne çıkanları göstereceğim. İlk olarak, IndexedDB veritabanını açmak için sayfa yükleme sırasında geri yükleme işlevi çağrılır:
/* * 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); }; } };request.onupgradeneeded olayı, veritabanının henüz mevcut olmadığı durumu ele alır. Bu durumda, nesne deposunu oluşturuyoruz.
Veritabanı açıldığında, belirli bir anahtarla (bu durumda aslında sabit 'Temel' olacaktır) bir kayıt arayan restore2 işlevi çağrılır:
/* * 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; }); } }; }Böyle bir kayıt varsa, değeri bulmacanın ızgara dizisinin yerini alır. Oyunu geri yüklemede herhangi bir hata varsa, taşları eskisi gibi karıştırıyoruz. Izgaranın, her biri oldukça karmaşık olan 3x3'lük bir döşeme nesnesi dizisi olduğunu unutmayın. IndexedDB'nin en büyük avantajı, bu değerleri seri hale getirmek zorunda kalmadan depolayıp alabilmenizdir.
Modelin değiştirildiğini AngularJS'e bildirmek için $apply kullanıyoruz, böylece görünüm uygun şekilde güncellenecektir. Bunun nedeni, güncellemenin bir DOM olay işleyicisi içinde gerçekleşmesidir, dolayısıyla AngularJS aksi takdirde değişikliği tespit edemez. IndexedDB kullanan herhangi bir AngularJS uygulamasının bu nedenle muhtemelen $apply kullanması gerekecektir.
Kullanıcı tarafından bir hareket gibi ızgara dizisini değiştirecek herhangi bir eylemden sonra, güncellenmiş ızgara değerine göre kaydı uygun anahtarla ekleyen veya güncelleyen kaydetme işlevi çağrılır:
/* * 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 }; }Kalan değişiklikler, yukarıdaki işlevleri uygun zamanlarda çağırmak içindir. Tüm değişiklikleri gösteren taahhüdü inceleyebilirsiniz. Üç gelişmiş bulmaca için değil, yalnızca temel bulmaca için geri yükleme dediğimizi unutmayın. Üç gelişmiş bulmacanın bir api özniteliğine sahip olduğu gerçeğinden yararlanıyoruz, bu yüzden onlar için sadece normal karıştırmayı yapıyoruz.
Ya gelişmiş bulmacaları da kaydedip geri yüklemek istersek? Bu biraz yeniden yapılanmayı gerektirecek. Gelişmiş bulmacaların her birinde, kullanıcı görüntü kaynak dosyasını ve bulmaca boyutlarını ayarlayabilir. Bu nedenle, bu bilgiyi dahil etmek için IndexedDB'de depolanan değeri geliştirmemiz gerekir. Daha da önemlisi, onları bir geri yüklemeden güncellemenin bir yoluna ihtiyacımız olacak. Zaten uzun olan bu örnek için bu biraz fazla.
Çözüm
Çoğu durumda, web depolama, oturum verilerini depolamak için en iyi seçeneğinizdir. Tüm büyük tarayıcılar tarafından tam olarak desteklenir ve çerezlerden çok daha fazla depolama kapasitesi sunar.
Sunucunuz bunları kullanmak üzere ayarlanmışsa veya verilere tüm pencerelerin tüm sekmelerinden erişilmesi gerekiyorsa, ancak tarayıcı kapatıldığında bunların silinmesini de istiyorsanız, çerezleri kullanırsınız.
Parça tanımlayıcıyı, kullanıcının baktığı fotoğrafın kimliği gibi o sayfaya özel oturum verilerini depolamak için zaten kullanıyorsunuz. Parça tanımlayıcıya diğer oturum verilerini gömebilseniz de, bu, web depolama veya tanımlama bilgilerine göre herhangi bir avantaj sağlamaz.
IndexedDB'yi kullanmak, muhtemelen diğer tekniklerin herhangi birinden çok daha fazla kodlama gerektirir. Ancak, depoladığınız değerler, seri hale getirilmesi zor olan karmaşık JavaScript nesneleriyse veya bir işlem modeline ihtiyacınız varsa, buna değebilir.
