Mimari mi?
Önemli ama yanlış anlaşılabilecek bir konuyu ele alacağım: Web uygulamanız için kullandığınız mimari, özellikle de progresif web uygulaması oluştururken mimari kararlarınızın nasıl devreye girdiği.
"Mimari" kulağa belirsiz gelebilir ve bunun neden önemli olduğu hemen anlaşılmayabilir. Mimariyi düşünmenin bir yolu kendinize şu soruları sormaktır: Bir kullanıcı sitemdeki bir sayfayı ziyaret ettiğinde hangi HTML yüklenir? Peki, kullanıcı başka bir sayfayı ziyaret ettiğinde ne yüklenir?
Bu soruların yanıtları her zaman basit değildir ve progresif web uygulamaları üzerine düşünmeye başladığınızda daha da karmaşık bir hal alabilir. Bu yüzden, etkili bulduğum olası bir mimari konusunda size yol göstermek istiyorum. Bu makale boyunca, aldığım kararları progresif web uygulaması geliştirme konusundaki "yaklaşımım" olarak etiketleyeceğim.
Kendi PWA'nızı oluştururken benim yaklaşımımı kullanabilirsiniz, ancak aynı zamanda başka geçerli alternatifler de her zaman vardır. Tüm parçaların nasıl bir araya geldiğini görmenin size ilham vereceğini ve bunu ihtiyaçlarınıza uygun şekilde özelleştirme konusunda kendinizi güvende hissedeceğinizi umuyorum.
Stack Overflow PWA (Yığın Taşması PWA)
Bu makaleyle birlikte bir Stack Overflow PWA oluşturdum. Stack Overflow'u okumak ve katkıda bulunmak için çok zaman harcıyorum. Ayrıca, belirli bir konuyla ilgili sık sorulan sorulara göz atmayı kolaylaştıracak bir web uygulaması geliştirmek istedim. Herkese açık Stack Exchange API'nin üzerine derlenmiştir. Açık kaynaklıdır ve GitHub projesini ziyaret ederek daha fazla bilgi edinebilirsiniz.
Çok Sayfalı Uygulamalar (MPA'lar)
Ayrıntılara girmeden önce bazı terimler tanımlayalım ve temel teknolojinin parçalarını açıklayalım. Öncelikle, "Çok Sayfalı Uygulamalar " veya"MPA'lar" adını verdiğim modeli ele alacağım.
MPA, web'in başlangıcından beri kullanılan geleneksel mimarinin gösterişli bir adıdır. Kullanıcı yeni bir URL'ye her gittiğinde, tarayıcı kademeli olarak o sayfaya özel HTML oluşturur. Gezinmeler arasında sayfanın durumunu veya içeriğini korumaya yönelik herhangi bir girişim yoktur. Yeni bir sayfayı her ziyaret edişinizde yeni bir başlangıç yaparsınız.
Bu, web uygulamaları oluşturmak için kullanılan tek sayfalı uygulama (SPA) modelinden zıtlıktır. Bu modelde, kullanıcı yeni bir bölümü ziyaret ettiğinde tarayıcı mevcut sayfayı güncellemek için JavaScript kodu çalıştırır. Hem SPA'lar hem de MPA'lar eşit ölçüde geçerli modellerdir, ancak bu yayında PWA kavramlarını çok sayfalı bir uygulama bağlamında keşfetmek istiyorum.
Güvenilir ve hızlı
Benim ve diğer pek çok kişinin "progresif web uygulaması" (PWA) ifadesini kullandığını duymuşsunuzdur. Bu sitede başka bir yerde, arka plan malzemelerinin bir kısmına zaten aşina olabilirsiniz.
PWA'yı, birinci sınıf kullanıcı deneyimi sağlayan ve kullanıcının ana ekranında gerçek bir yer kazandıran bir web uygulaması olarak düşünebilirsiniz. Hızlı, İentegre, Reliable ve Engaging anlamına gelen "FIRE" kısaltması, PWA oluştururken göz önünde bulundurulması gereken tüm özellikleri özetler.
Bu makalede, bu özelliklerin bir alt kümesine odaklanacağım: Hızlı ve Güvenilir.
Hızlı: "Hızlı" kavramı farklı bağlamlarda farklı anlamlara gelse de ağdan mümkün olduğunca az yüklemenin hız avantajlarını anlatacağım.
Güvenilirdir: Ancak ham hız yeterli değildir. Bir PWA gibi görünmesi için web uygulamanızın güvenilir olması gerekir. Bu ağ, ağın durumundan bağımsız olarak yalnızca özelleştirilmiş bir hata sayfası olsa bile bir şeyi her zaman yükleyecek kadar dirençli olmalıdır.
Güvenilir şekilde hızlı: Son olarak, PWA tanımını biraz farklı bir şekilde ifade edip güvenilir hızlı bir şey oluşturmanın ne anlama geldiğine bakacağım. Yalnızca düşük gecikmeli bir ağa bağlıyken hızlı ve güvenilir olmak yeterince iyi değildir. Güvenilir ve hızlı olmak, temel ağ koşullarından bağımsız olarak web uygulamanızın hızının tutarlı olduğu anlamına gelir.
Teknolojileri Etkinleştirme: Service Workers + Cache Storage API
PWA'lar hız ve esneklik için yüksek standartlar sunar. Neyse ki web platformu, bu tür bir performansı gerçeğe dönüştürecek bazı yapı taşları sunuyor. Service Worker ve Cache Storage API'den bahsediyorum.
Cache Storage API aracılığıyla gelen istekleri dinleyen, bazılarını ağa ileten ve gelecekte kullanılmak üzere yanıtın bir kopyasını saklayan bir hizmet çalışanı derleyebilirsiniz.
![Bir ağ yanıtının kopyasını kaydetmek için Cache Storage API kullanan bir hizmet çalışanı.](https://developer.chrome.com/static/blog/beyond-spa/image/a-service-worker-using-c-b29eb5eb773.png?authuser=5&hl=tr)
Web uygulaması aynı isteği tekrar gönderdiğinde, hizmet çalışanı önbelleklerini kontrol edebilir ve yalnızca daha önce önbelleğe alınmış yanıtı döndürebilir.
![Yanıt vermek için Cache Storage API'yi kullanan, ağı atlayan bir hizmet çalışanı.](https://developer.chrome.com/static/blog/beyond-spa/image/a-service-worker-using-c-5d450862f7ef4.png?authuser=5&hl=tr)
Mümkün olduğunda ağdan kaçınmak, güvenilir şekilde hızlı performans sunmanın önemli bir parçasıdır.
"İzomorfik" JavaScript
Bahsetmek istediğim diğer bir kavram da, bazen "izmorfik" veya "evrensel" JavaScript olarak adlandırılan şeydir. Basitçe ifade etmek gerekirse bu yöntem, aynı JavaScript kodunun farklı çalışma zamanı ortamları arasında paylaşılabilmesidir. PWA'mı oluştururken, JavaScript kodunu arka uç sunucum ve Service Worker arasında paylaşmak istedim.
Bu şekilde kod paylaşımına yönelik pek çok geçerli yaklaşım vardır ancak yaklaşımım, tanımlayıcı kaynak kodu olarak ES modüllerini kullanmaktı. Daha sonra bu modülleri, Babel ve Toplayıcı'yı kullanarak sunucu ve Service Worker için aktarabilirsiniz. Projemde, .mjs
dosya uzantısına sahip dosyalar ES modülünde bulunan bir kod.
Sunucu
Bu kavramları ve terminolojiyi aklınızda bulundurarak Stack Overflow PWA'mı nasıl oluşturduğuma yakından bakalım. Arka uç sunucumuzdan bahsederek başlayacağım ve bunun genel mimariye nasıl uyduğunu açıklayacağım.
Firebase platformunu kullanmak istedim. Dinamik bir arka ucun ve statik barındırmanın kombinasyonu arıyordum.
Firebase Cloud Functions, gelen bir istek olduğunda Düğüm tabanlı ortamı otomatik olarak başlatır ve zaten bildiğim popüler Express HTTP çerçevesiyle entegre olur. Ayrıca sitemin tüm statik kaynakları için kullanıma hazır barındırma olanağı da sunuyor. Sunucunun istekleri nasıl ele aldığına bakalım.
Bir tarayıcı sunucumuza yönelik bir gezinme isteğinde bulunduğunda aşağıdaki akıştan geçer:
![Sunucu tarafında gezinme yanıtı oluşturmaya genel bakış.](https://developer.chrome.com/static/blog/beyond-spa/image/an-overview-generating-ee8d81d8ba352.png?authuser=5&hl=tr)
Sunucu, isteği URL'ye göre yönlendirir ve eksiksiz bir HTML belgesi oluşturmak için şablon mantığını kullanır. Stack Exchange API'sinden gelen verilerin yanı sıra sunucunun yerel olarak depoladığı kısmi HTML parçalarının bir kombinasyonunu kullanıyorum. Hizmet çalışanınız nasıl yanıt vereceğini öğrendikten sonra, HTML'yi web uygulamamıza geri aktarmaya başlayabilir.
Bu resimde, daha ayrıntılı olarak incelenmeye değer iki şey var: yönlendirme ve şablon oluşturma.
Yönlendirme
Yönlendirme söz konusu olduğunda benim yaklaşımım Express çerçevesinin yerel yönlendirme söz dizimini kullanmak oldu. Basit URL öneklerini ve yolun parçası olarak parametreler içeren URL'leri eşleştirmek için yeterince esnektir. Burada, temeldeki Express kalıbının eşleştirileceği rota adları arasında bir eşleme oluşturuyorum.
const routes = new Map([
['about', '/about'],
['questions', '/questions/:questionId'],
['index', '/'],
]);
export default routes;
Böylece, bu eşlemeye doğrudan sunucunun kodundan başvurabilirim. Belirli bir Express kalıbı için eşleşme olduğunda uygun işleyici, eşleşen yola özel şablon mantığıyla yanıt verir.
import routes from './lib/routes.mjs';
app.get(routes.get('index'), async (req, res) => {
// Templating logic.
});
Sunucu tarafı şablon
Bu şablon oluşturma mantığı neye benziyor? Kısmi HTML parçalarını art arda birbiri ardına toplayan bir yaklaşımı benimsedim. Bu model akış için çok uygundur.
Sunucu hemen başlangıçtaki HTML ortak metnini geri gönderir ve tarayıcı bu kısmi sayfayı hemen oluşturabilir. Sunucu geri kalan veri kaynaklarını bir araya getirirken, belge tamamlanana kadar bunları tarayıcıya aktarır.
Ne demek istediğimi öğrenmek için rotalarımızdan birine ait Ekspres koda göz atın:
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();
});
response
nesnesinin write()
yöntemini kullanıp yerel olarak depolanan kısmi şablonlara başvurarak yanıt akışını herhangi bir harici veri kaynağını engellemeden hemen başlatabiliyorum. Tarayıcı bu ilk HTML'yi alır ve hemen anlamlı bir arayüz oluşturup mesajı yükler.
Sayfamızın bir sonraki bölümünde Stack Exchange API'sından alınan veriler kullanılmaktadır. Bu verileri almak, sunucumuzun bir ağ isteğinde bulunması gerektiği anlamına gelir. Web uygulaması, yanıt alıp işleyene kadar başka bir şey oluşturamaz, ancak en azından kullanıcılar beklerken boş bir ekrana bakmaz.
Web uygulaması Stack Exchange API'den yanıt aldıktan sonra, verileri API'den karşılık gelen HTML'ye çevirmek için özel bir şablon işlevi çağırır.
Şablon oluşturma dili
Şablon oluşturmak şaşırtıcı derecede ihtilaflı bir konu olabilir. Şimdi ele aldığım yaklaşım, bu birçok yaklaşımdan yalnızca biri. Özellikle mevcut bir şablon çerçevesiyle aranızda eski bağlantılar varsa, bunun yerine kendi çözümünüzü kullanmak istersiniz.
Benim kullanım alanım açısından mantıklı olan, JavaScript'in şablon değişmez değerlerine güvenmek ve bazı mantığı yardımcı işlevlere ayırmaktı. MPA oluşturmanın güzel yanlarından biri, durum güncellemelerini takip edip HTML'nizi yeniden oluşturmanızın gerekmemesidir. Bu sayede, statik HTML üreten temel yaklaşım işime yaradı.
Burada, web uygulamamın dizininin dinamik HTML kısmını nasıl oluşturduğuma dair bir örnek verilmiştir. Rotalarımda olduğu gibi şablon mantığı, hem sunucuya hem de Service Worker'a aktarılabilen bir ES modülünde depolanır.
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;
}
Bu şablon işlevleri sadece JavaScript'tir ve uygun olduğunda mantığı daha küçük, yardımcı işlevlere ayırmak faydalıdır. Burada, API yanıtında döndürülen öğelerin her birini bu tür bir işleve geçiriyorum. Bu işlev, tüm uygun özelliklere sahip standart bir HTML öğesi oluşturur.
function questionCard({id, title}) {
return `<a class="card"
href="/questions/${id}"
data-cache-url="${questionUrl(id)}">${title}</a>`;
}
Özellikle, her bağlantıya (data-cache-url
) eklediğim ve ilgili soruyu görüntülemek için ihtiyaç duyduğum Stack Exchange API URL'sine ayarlanmış bir veri özelliği. Bunu unutmayın. Daha sonra tekrar bakacağım.
Şablon oluşturma işlemi tamamlandıktan sonra yol işleyicime dönüp sayfamın HTML'sinin son kısmını tarayıcıya aktarıyorum ve akışı sonlandırıyorum. Bu, progresif oluşturmanın tamamlandığını belirten bir ipucudur.
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();
});
Bu, sunucu kurulumuma ilişkin kısa bir tur. Web uygulamamı ilk kez ziyaret eden kullanıcılar her zaman sunucudan bir yanıt alır ancak bir ziyaretçi web uygulamama geri döndüğünde hizmet çalışanım yanıt vermeye başlar. Şimdi biraz ayrıntılara girelim.
Hizmet çalışanı
![Service Worker'da gezinme yanıtı oluşturmaya genel bakış.](https://developer.chrome.com/static/blog/beyond-spa/image/an-overview-generating-f46e582b6bca7.png?authuser=5&hl=tr)
Bu diyagram size tanıdık gelecektir. Daha önce ele aldığımız parçaların çoğu, burada biraz farklı bir düzende bulunmaktadır. Service Worker'ı dikkate alarak istek akışını inceleyelim.
Hizmet çalışanımız belirli bir URL için gelen gezinme isteğini işler ve tıpkı sunucumun yaptığı gibi de nasıl yanıt vereceğini belirlemek için yönlendirme ve şablon mantığının bir kombinasyonunu kullanır.
Yaklaşım öncekiyle aynıdır ancak fetch()
ve Cache Storage API gibi farklı alt düzey temel öğeler içerir. Bu veri kaynaklarını, Service Worker'ın web uygulamasına geri gönderdiği HTML yanıtını oluşturmak için kullanıyorum.
Workbox
Alt düzey temel öğelerle sıfırdan başlamak yerine, Service Worker'ımı Workbox adlı bir üst düzey kitaplık grubu üzerinde oluşturacağım. Hizmet çalışanlarının önbelleğe alma, yönlendirme ve yanıt oluşturma mantığı için sağlam bir temel sağlar.
Yönlendirme
Sunucu tarafı kodumda olduğu gibi, hizmet çalışanımın da gelen bir isteği uygun yanıt mantığıyla nasıl eşleştireceğini bilmesi gerekiyor.
Benim yaklaşımım doğrultusunda, her Express rotasını karşılık gelen bir normal ifadeye dönüştürerek regexparam
adlı yardımcı bir kitaplıktan yararlandım. Bu çeviri tamamlandıktan sonra Workbox'ın normal ifade yönlendirme için sunduğu yerleşik desteğinden yararlanabilirim.
Normal ifadelerin bulunduğu modülü içe aktardıktan sonra her normal ifadeyi Workbox'ın yönlendiricisine kaydediyorum. Her rotada bir yanıt oluşturmak için özel şablon mantığı sunabiliyorum. Service Worker'da şablon oluşturmak, arka uç sunucumda olduğundan biraz daha karmaşık bir iş olsa da Workbox işin büyük bir kısmının üstesinden gelmeye yardımcı oluyor.
import regExpRoutes from './regexp-routes.mjs';
workbox.routing.registerRoute(
regExpRoutes.get('index')
// Templating logic.
);
Statik öğeyi önbelleğe alma
Şablon oluşturma hikayesinin önemli bir parçası, kısmi HTML şablonlarımın Cache Storage API aracılığıyla yerel olarak kullanılabilir olmasını ve web uygulamasında değişiklikleri dağıttığımda güncel olmalarını sağlamak. Önbellek bakımı elle yapıldığında hataya açık olabiliyor, bu nedenle derleme işlemimin bir parçası olarak önbelleğe alma işlemini ele almak için Workbox'a başvuruyorum.
Workbox'a bir yapılandırma dosyası kullanarak hangi URL'lerin önceden önbelleğe alınacağını bildiriyorum. Bunu yaparken, tüm yerel öğelerimi ve eşleşecek bir dizi kalıbı içeren dizini işaret ediyorum. Bu dosya, siteyi her yeniden derlediğimde run Workbox KSA tarafından otomatik olarak okunur.
module.exports = {
globDirectory: 'build',
globPatterns: ['**/*.{html,js,svg}'],
// Other options...
};
Workbox, her dosyanın içeriğinin anlık görüntüsünü alır ve bu URL ve düzeltme listesini otomatik olarak nihai hizmet çalışanı dosyama ekler. Workbox artık önceden önbelleğe alınan dosyaları her zaman kullanılabilir ve güncel tutmak için gereken her şeye sahip. Sonuç, aşağıdakine benzer bir öğe içeren bir service-worker.js
dosyası elde edilir:
workbox.precaching.precacheAndRoute([
{
url: 'partials/about.html',
revision: '518747aad9d7e',
},
{
url: 'partials/foot.html',
revision: '69bf746a9ecc6',
},
// etc.
]);
Daha karmaşık bir derleme işlemi kullanan kullanıcılar için Workbox'ta, komut satırı arayüzüne ek olarak hem webpack
eklentisi hem de genel düğüm modülü bulunur.
Canlı Yayın
Ardından Service Worker'ın, önbelleğe alınmış bu kısmi HTML'yi hemen web uygulamasına geri aktarmasını istiyorum. Bu, "güvenilir hızlı" olmanın çok önemli bir parçasıdır. Ekranda her zaman anlamlı bir şey karşıma çıkar. Neyse ki servis çalışanımızda Streams API'yi kullanmak bunu mümkün kılmaktadır.
Streams API'yi daha önce duymuş olabilirsiniz. Meslektaşım Jake Archibald yıllardır övgülerini söylüyor. 2016'nın web akışları yılı olacağına dair cesur bir tahminde bulundu. Streams API de iki yıl önce olduğu kadar bugün de mükemmel, ancak önemli bir farkı var.
O dönemde yalnızca Chrome tarafından sağlanan Akışlar'ı desteklese de Streams API'si artık daha yaygın şekilde desteklenmektedir. Genel hikaye olumlu ve uygun bir yedek kodla bugün Service Worker'ınızda akışları kullanmanızı engelleyen hiçbir şey yoktur.
Sizi engelleyen bir şey olabilir ve Streams API'nin işleyiş şekli konusunda akıllara durgunluk veren bir şey olabilir. Bu API, çok güçlü bir grup temel öğeyi açığa çıkarır ve bunu rahat bir şekilde kullanabilen geliştiriciler aşağıdakiler gibi karmaşık veri akışları oluşturabilir:
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);
}
});
},
});
Ancak bu kodun tüm etkilerini anlamak herkes için uygun olmayabilir. Bu mantığı kullanarak ayrıştırmak yerine Service Worker akışına yaklaşımımdan bahsedelim.
Yepyeni bir üst düzey sarmalayıcı olan workbox-streams
kullanıyorum.
Bununla birlikte, bunu hem önbelleklerden hem de ağdan gelebilecek çalışma zamanı verilerinden gelen akış kaynaklarını karışık olarak iletebiliyorum. Workbox, bağımsız kaynakları koordine eder ve bunları tek bir akış yanıtında bir araya getirir.
Buna ek olarak Workbox, Streams API'nin desteklenip desteklenmediğini otomatik olarak algılar. Desteklenmediğinde ise eşdeğer, akış olmayan bir yanıt oluşturur. Böylece, akışlar% 100 tarayıcı desteğine daha yakın olduğundan yedek yazma konusunda endişelenmenize gerek yoktur.
Çalışma zamanını önbelleğe alma
Service Worker'ımın çalışma zamanı verilerini Stack Exchange API'den nasıl işlediğine göz atalım. Workbox'ın, eski olan önbelleğe alma stratejisi için yerleşik destekten ve web uygulamasının depolama alanının sınırsız büyümesini önlemek için geçerlilik süresinin sona ermesiyle birlikte kullanıyorum.
Akış yanıtını oluşturacak farklı kaynakları işlemek için Workbox'ta iki strateji oluşturuyorum. Birkaç işlev çağrısı ve yapılandırmada Workbox, normalde yüzlerce satır elle yazılmış kod gerektirecek olanları yapmamıza olanak tanır.
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})],
});
İlk strateji, kısmi HTML şablonlarımız gibi önceden önbelleğe alınmış verileri okur.
Diğer strateji ise 50 girişe ulaştığımızda eski olan önbelleğe alma işlemini yeniden doğrulama mantığını ve en son kullanılan önbellek geçerlilik bitiş tarihini uygular.
Bu stratejileri uyguladığıma göre, geriye yalnızca Workbox'a eksiksiz bir akış yanıtı oluşturmak için bunların nasıl kullanılacağını söylemek kalıyor. Bir kaynak dizisini fonksiyon olarak iletiyorum ve bu işlevlerin her biri hemen yürütülecek. Workbox, her bir kaynaktan alınan sonucu sırayla sırayla web uygulamasına aktarır ve bu işlem, yalnızca dizideki bir sonraki işlev henüz tamamlanmamışsa geciktirilir.
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'}),
]);
İlk iki kaynak, doğrudan Cache Storage API'sinden okunan, önbelleğe alınmış kısmi şablonlardır. Böylece her zaman anında kullanılabilir olurlar. Bu sayede, sunucu tarafındaki kodumda olduğu gibi Service Worker uygulamamızın, isteklere güvenilir bir şekilde hızlı yanıt vermesini sağlayabilirsiniz.
Bir sonraki kaynak işlevimiz, Stack Exchange API'den veri getirir ve yanıtı web uygulamasının beklediği HTML ile işler.
Eski "yeniden doğrulama " stratejisi, bu API çağrısı için önceden önbelleğe alınan bir yanıtım varsa bunu sayfaya anında akış olarak uygulayabileceğim ve aynı zamanda, bir sonraki istekte bulunulduğunda önbellek girişini"arka planda" güncelleyebileceğim anlamına gelir.
Son olarak, yanıtı tamamlamak için altbilgimin önbelleğe alınmış bir kopyasını akış olarak alıyor ve son HTML etiketlerini kapatıyorum.
Paylaşılan kod her şeyin senkronize olmasını sağlar
Service Worker kodunun belirli parçalarının tanıdık geldiğini göreceksiniz. Service Worker'ım tarafından kullanılan kısmi HTML ve şablon mantığı, sunucu tarafı işleyicimin kullandığıyla aynı. Bu kod paylaşımı, kullanıcıların web uygulamamı ilk kez ziyaret etmeleri veya hizmet çalışanı tarafından oluşturulan bir sayfaya dönmeleri fark etmeksizin tutarlı bir deneyim yaşamalarını sağlar. İzomorfik JavaScript'in güzelliği de budur.
Dinamik, progresif geliştirmeler
PWA'm için hem sunucu hem de hizmet işçisini adım adım gösterdim, ancak ele alınması gereken son bir mantık daha var: Sayfalarımın her birinde, akış tamamen tamamlandıktan sonra küçük miktarda JavaScript çalıştırılıyor.
Bu kod, kullanıcı deneyimini kademeli olarak iyileştirir ancak çok önemli değildir; web uygulaması çalıştırılmadıysa çalışmaya devam eder.
Sayfa meta verisi
Uygulamam, API yanıtına göre bir sayfanın meta verilerini güncellemek için istemci tarafı JavaScript'i kullanıyor. Her sayfa için önbelleğe alınmış HTML'nin başlangıç bitini kullandığımdan, web uygulaması sonuçta dokümanımın kafasında genel etiketlere neden oluyor. Ancak, şablonum ve istemci tarafı kodum arasındaki koordinasyon sayesinde sayfaya özel meta verileri kullanarak pencerenin başlığını güncelleyebilirim.
Şablon kodunun bir parçası olarak, benim yaklaşımım doğru şekilde çıkış yapılmış dizeyi içeren bir komut dosyası etiketi eklemektir.
const metadataScript = `<script>
self._title = '${escape(item.title)}';
</script>`;
Ardından, sayfam yüklendiğinde, bu dizeyi okuyorum ve doküman başlığını güncelliyorum.
if (self._title) {
document.title = unescape(self._title);
}
Kendi web uygulamanızda güncellemek istediğiniz sayfaya özgü başka meta veri parçaları varsa aynı yaklaşımı uygulayabilirsiniz.
Çevrimdışı Kullanıcı Deneyimi
Eklediğim diğer aşamalı geliştirme, çevrimdışı özelliklerimize dikkat çekmek için kullanılıyor. Güvenilir bir PWA geliştirdim ve kullanıcıların, çevrimdışıyken de önceden ziyaret edilen sayfaları yükleyebildiklerini bilmelerini istiyorum.
İlk olarak, önceden önbelleğe alınan tüm API isteklerinin listesini almak için Cache Storage API'sini kullanıyorum ve bunu bir URL listesine dönüştürüyorum.
Daha önce bahsettiğim özel veri özelliklerini hatırlıyor musunuz? Bu özel özelliklerin her biri, bir soruyu göstermek için gereken API isteğinin URL'sini içerir. Bu veri özelliklerini önbelleğe alınmış URL'ler listesiyle çapraz karşılaştırabilir ve eşleşmeyen tüm soru bağlantılarından bir dizi oluşturabilirim.
Tarayıcı çevrimdışı duruma geçtiğinde, önbelleğe alınmamış bağlantılar listesinde dönüş yaparak çalışmayan bağlantıları karartıyorum. Bunun, kullanıcıya söz konusu sayfalardan ne beklemesi gerektiğine dair görsel bir ipucu olduğunu unutmayın. Aslında bağlantıları devre dışı bırakmıyorum veya kullanıcının gezinmesini engellemiyorum.
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);
Sık karşılaşılan tehlikeler
Şimdi, çok sayfalı PWA oluşturma yaklaşımımı gösteren bir tura çıktım. Kendi yaklaşımınızı belirlerken göz önünde bulundurmanız gereken pek çok etmen olabilir ve benden farklı seçimler yapmak zorunda kalabilirsiniz. Bu esneklik, web için site geliştirmenin en güzel yanlarından biridir.
Mimari kararları alırken karşılaşabileceğiniz bazı sıkıntılar var ve sizi bazı sıkıntılardan kurtarmak istiyorum.
Tam HTML'yi önbelleğe alma
Eksiksiz HTML dokümanlarını önbelleğinizde depolamamanızı öneririz. Birincisi, bu yer israfıdır. Web uygulamanız, sayfalarının her biri için aynı temel HTML yapısını kullanıyorsa, aynı işaretlemenin kopyalarını tekrar tekrar saklamak zorunda kalırsınız.
Daha da önemlisi, sitenizin paylaşılan HTML yapısında bir değişiklik yaparsanız, önceden önbelleğe alınan bu sayfaların her biri eski düzeninizde kalır. Geri gelen bir ziyaretçinin eski ve yeni sayfaları bir arada görmesinden rahatsız olduğunu hayal edin.
Sunucu / hizmet çalışanı kayması
Kaçınmanız gereken bir diğer sorun ise sunucunuzun ve hizmet çalışanınızın senkronizasyonunun bozulmasıdır. Yaklaşımım izomorfik JavaScript kullanılmasıydı, böylece her iki yerde de aynı kod çalıştırıldı. Mevcut sunucu mimarinize bağlı olarak bu her zaman mümkün değildir.
Mimari kararları ne olursa olsun, sunucunuzda ve hizmet çalışanınızda eşdeğer yönlendirme ve şablon kodu çalıştırmak için bir strateji belirlemeniz gerekir.
En kötü senaryolar
Tutarsız düzen / tasarım
Bu tehlikeleri göz ardı ettiğinizde ne olur? Her türlü hata mümkündür. Ancak en kötü durum senaryosu; geri gelen kullanıcının, önbelleğe alınan bir sayfayı çok eski bir düzene sahip (belki güncel olmayan başlık metni olabilir) veya artık geçerli olmayan CSS sınıfı adları kullanmasıdır.
En kötü senaryo: Bozuk yönlendirme
Alternatif olarak bir kullanıcı, sunucunuz tarafından işlenen bir URL'yle karşılaşır ancak hizmet çalışanınız tarafından işlenmez. Zombi düzenleri ve çıkmaz sokaklarla dolu bir site güvenilir bir PWA değildir.
Başarı için ipuçları
Ancak bu işte yalnız değilsiniz! Aşağıdaki ipuçları bu tür tehlikelerden kaçınmanıza yardımcı olabilir:
Birden fazla dilde uygulamalara sahip şablon oluşturma ve yönlendirme kitaplıklarını kullanma
JavaScript uygulamalarına sahip şablon oluşturma ve yönlendirme kitaplıklarını kullanmayı deneyin. Pazardaki her geliştiricinin mevcut web sunucunuzdan ve şablon dilinden geçiş yapma lüksüne sahip olmadığını biliyorum.
Ancak popüler şablon oluşturma ve yönlendirme çerçevelerinden bazıları birden çok dilde kullanıma sunulmuştur. Mevcut sunucunuzun dilinin yanı sıra JavaScript ile de çalışan bir sunucu bulabilirseniz hizmet çalışanınızı ve sunucunuzu senkronize etmeye bir adım daha yakınsınız demektir.
İç içe yerleştirilmiş şablonlar yerine sıralı şablonları tercih edin
Ardından, art arda yayınlanan bir dizi sıralı şablon kullanmanızı öneririm. HTML'nizin ilk kısmında mümkün olduğunca hızlı bir şekilde akış gerçekleştirebildiğiniz sürece, sayfanızın sonraki bölümlerinde daha karmaşık şablon mantığı kullanılması sorun yaratmaz.
Hizmet çalışanınızda hem statik hem de dinamik içeriği önbelleğe alın
En iyi performans için sitenizin tüm kritik statik kaynaklarını önceden önbelleğe almalısınız. API istekleri gibi dinamik içerikleri işlemek için çalışma zamanı önbelleğe alma mantığını da ayarlamanız gerekir. Workbox'ı kullanmak, tüm bunları sıfırdan uygulamak yerine, iyi test edilmiş, üretime hazır stratejileri temel alabileceğiniz anlamına gelir.
Yalnızca kesinlikle gerekli olduğunda ağda engelleme
Bununla ilgili olarak, yalnızca önbellekten yanıt akışı mümkün olmadığında ağda engelleme yapmalısınız. Önbelleğe alınmış bir API yanıtının anında gösterilmesi, genellikle yeni verileri beklemekten daha iyi bir kullanıcı deneyimi sunar.