Üretimde Hizmet Çalışanları

Dikey ekran görüntüsü

Özet

Google I/O 2015 web uygulamasını hızlı ve öncelikli olarak çevrimdışı hale getirmek için hizmet çalışanı kitaplıklarını nasıl kullandığımızı öğrenin.

Genel Bakış

Bu yılki Google I/O 2015 web uygulaması, Google'ın Geliştirici İlişkileri Ekibi tarafından, Instrument'taki arkadaşlarımız tarafından tasarlanan ve sesli/görsel denemeyi yazan tasarımlara göre yazılmıştır. Ekibimizin misyonu, I/O web uygulamasının (kod adı IOWA olarak anılacaktır) modern web'in yapabileceği her şeyi sergilemesini sağlamaktı. O halde, olması gereken özellikler listemizin başında tam olarak çevrimdışı öncelikli bir deneyim yer alıyordu.

Son zamanlarda bu sitedeki diğer makalelerden herhangi birini okuduysanız hizmet işçileriyle kesinlikle karşılaşmışsınızdır. IOWA'nın çevrimdışı desteğinin büyük ölçüde bunlara bağlı olduğunu duymak sizi şaşırtmayacaktır. IOWA'nın gerçek ihtiyaçlarından yola çıkarak iki farklı çevrimdışı kullanım alanını ele alacak iki kitaplık geliştirdik: Statik kaynakların ön önbelleğe alınmasını otomatikleştirmek için sw-precache ve çalışma zamanında önbelleğe alma ve yedek stratejileri yönetmek için sw-toolbox.

Kitaplıklar birbirini iyi bir şekilde tamamlıyor ve IOWA'nın statik içerik "kabuk"unun her zaman doğrudan önbellekten, dinamik veya uzak kaynakların ise ağdan sunulduğu, gerektiğinde önbelleğe alınmış veya statik yanıtlara yedekleme olanağı sunan performanslı bir strateji uygulamamıza olanak tanıdı.

sw-precache ile önbelleğe alma

IOWA'nın statik kaynakları (HTML, JavaScript, CSS ve resimleri) web uygulamasının temel kabuğunu sağlar. Bu kaynakları önbelleğe alma konusunda dikkate almamız gereken iki önemli şart vardı: Çoğu statik kaynağın önbelleğe alındığından ve güncel tutulduğundan emin olmak istedik. sw-precache, bu gereksinimler göz önünde bulundurularak tasarlanmıştır.

Derleme zamanı entegrasyonu

sw-precache ile IOWA'nın gulp tabanlı derleme sürecini kullanırız ve IOWA'nın kullandığı tüm statik kaynakların eksiksiz bir listesini oluşturmak için bir dizi glob kalıbından yararlanırız.

staticFileGlobs: [
    rootDir + '/bower_components/**/*.{html,js,css}',
    rootDir + '/elements/**',
    rootDir + '/fonts/**',
    rootDir + '/images/**',
    rootDir + '/scripts/**',
    rootDir + '/styles/**/*.css',
    rootDir + '/data-worker-scripts.js'
]

Dosya adlarının listesini bir dizeye sabit kodlama ve bu dosyalardan herhangi biri değiştiğinde önbelleğe ait sürüm numarasını artırmayı unutmamak gibi alternatif yaklaşımlar, özellikle de kodu birden fazla ekip üyesinin kontrol etmesi nedeniyle çok fazla hataya yol açıyordu. Manuel olarak yönetilen bir dizeye yeni bir dosya ekleyerek çevrimdışı desteğin çalışmasını kimse istemez. Derleme zamanında entegrasyon, bu endişeleri yaşamadan mevcut dosyalarda değişiklik yapabilmemizi ve yeni dosya ekleyebilmemizi sağladı.

Önbelleğe Alınan Kaynakları Güncelleme

sw-precache, önceden önbelleğe alınan her kaynak için benzersiz bir MD5 karması içeren temel bir hizmet çalışanı komut dosyası oluşturur. Mevcut bir kaynak her değiştiğinde veya yeni bir kaynak eklendiğinde hizmet çalışanı komut dosyası yeniden oluşturulur. Bu işlem, yeni kaynakların önbelleğe alındığı ve güncel olmayan kaynakların temizlendiği hizmet çalışanı güncelleme akışını otomatik olarak tetikler. Aynı MD5 karma oluşturma değerine sahip mevcut kaynaklar olduğu gibi bırakılır. Yani siteyi daha önce ziyaret etmiş kullanıcılar yalnızca değiştirilen kaynakların minimum kümesini indirir. Bu da, önbelleğin tamamının toplu olarak süresi dolduğunda elde edilenden çok daha verimli bir deneyim sağlar.

Glob kalıplarından biriyle eşleşen her dosya, kullanıcı IOWA'yı ilk kez ziyaret ettiğinde indirilir ve önbelleğe alınır. Yalnızca sayfayı oluşturmak için gereken kritik kaynakların önbelleğe alındığından emin olduk. İşitsel/görsel denemede kullanılan medya veya oturumların konuşmacılarının profil resimleri gibi ikincil içerikler kasıtlı olarak önceden önbelleğe alınmadı. Bunun yerine, bu kaynaklara yönelik çevrimdışı istekleri işlemek için sw-toolbox kitaplığını kullandık.

sw-toolbox, for All Our Dynamic Needs

Daha önce de belirtildiği gibi, bir sitenin çevrimdışı çalışması için ihtiyaç duyduğu her kaynağı önceden önbelleğe almak mümkün değildir. Bazı kaynaklar, bu işlemin faydalı olması için çok büyük veya sık kullanılmaz. Diğer kaynaklar ise uzak bir API'den veya hizmetten gelen yanıtlar gibi dinamiktir. Ancak bir istek önceden önbelleğe alınmamış olsa bile NetworkError ile sonuçlanmalıdır. sw-toolbox, bazı kaynaklar için çalışma zamanında önbelleğe alma ve diğerleri için özel yedek çözümler işleyen istem işleyicileri uygulama esnekliği sağladı. Ayrıca, daha önce önbelleğe alınmış kaynaklarımızı push bildirimlerine yanıt olarak güncellemek için de kullandık.

Aşağıda, sw-toolbox'un üzerine inşa ettiğimiz özel istek işleyicilere dair birkaç örnek verilmiştir. Bağımsız JavaScript dosyalarını hizmet çalışanının kapsamına alan sw-precache'ın importScripts parameter özelliği sayesinde bu dosyaları temel hizmet çalışanı komut dosyasıyla entegre etmek kolaydı.

Ses/Görüntü Denemesi

Ses/görsel denemesi için sw-toolbox'nin networkFirst önbelleğe alma stratejisini kullandık. Denemenin URL kalıbıyla eşleşen tüm HTTP istekleri önce ağa gönderilir ve başarılı bir yanıt döndürülürse bu yanıt Cache Storage API kullanılarak saklanır. Ağ kullanılamadığında sonraki bir istek yapılırsa daha önce önbelleğe alınan yanıt kullanılır.

Başarılı bir ağ yanıtı her geldiğinde önbellek otomatik olarak güncellendiğinden, kaynakların sürümünü belirtmemiz veya girişlerin süresini sonlandırmamız gerekmiyordu.

toolbox.router.get('/experiment/(.+)', toolbox.networkFirst);

Hoparlör profil resimleri

Konuşmacı profil resimleri için amacımız, mevcutsa belirli bir konuşmacının resminin daha önce önbelleğe alınmış bir sürümünü göstermek, yoksa resmi almak için ağa geri dönmekti. Bu ağ isteği başarısız olursa son yedek olarak, önceden önbelleğe alınmış (ve bu nedenle her zaman kullanılabilecek) genel bir yer tutucu resim kullandık. Bu, genel bir yer tutucu ile değiştirilebilecek resimlerle çalışırken kullanılan yaygın bir stratejidir ve sw-toolbox'ın cacheFirst ve cacheOnly işleyicilerini zincirleyerek uygulanması kolaydır.

var DEFAULT_PROFILE_IMAGE = 'images/touch/homescreen96.png';

function profileImageRequest(request) {
    return toolbox.cacheFirst(request).catch(function() {
    return toolbox.cacheOnly(new Request(DEFAULT_PROFILE_IMAGE));
    });
}

toolbox.precache([DEFAULT_PROFILE_IMAGE]);
toolbox.router.get('/(.+)/images/speakers/(.*)',
                    profileImageRequest,
                    {origin: /.*\.googleapis\.com/});
Oturum sayfasından profil resimleri
Oturum sayfasından alınan profil resimleri.

Kullanıcıların programlarında yapılan güncellemeler

IOWA'nın önemli özelliklerinden biri, oturum açmış kullanıcıların katılmayı planladıkları oturumların programını oluşturmasına ve sürdürmesine olanak tanımasıydı. Beklediğiniz gibi, oturum güncellemeleri bir arka uç sunucuya gönderilen HTTP POST istekleri aracılığıyla yapıldı ve kullanıcı çevrimdışıyken bu durum değiştiren istekleri işlemenin en iyi yolunu bulmak için biraz zaman harcadık. Başarısız isteklerin IndexedDB'de sıraya alınmasını sağlayan bir çözümle birlikte, ana web sayfasındaki mantıkla IndexedDB'de sıraya alınmış istekleri kontrol edip bulduğu tüm istekleri yeniden deneyen bir çözüm geliştirdik.

var DB_NAME = 'shed-offline-session-updates';

function queueFailedSessionUpdateRequest(request) {
    simpleDB.open(DB_NAME).then(function(db) {
    db.set(request.url, request.method);
    });
}

function handleSessionUpdateRequest(request) {
    return global.fetch(request).then(function(response) {
    if (response.status >= 500) {
        return Response.error();
    }
    return response;
    }).catch(function() {
    queueFailedSessionUpdateRequest(request);
    });
}

toolbox.router.put('/(.+)api/v1/user/schedule/(.+)',
                    handleSessionUpdateRequest);
toolbox.router.delete('/(.+)api/v1/user/schedule/(.+)',
                        handleSessionUpdateRequest);

Yeniden denemeler ana sayfa bağlamında yapıldığından, yeni bir kullanıcı kimlik bilgisi grubu içerdiklerinden emin olabiliriz. Yeniden denemeler başarılı olduğunda, kullanıcıya daha önce sıraya alınmış güncellemelerinin uygulandığını bildiren bir mesaj gösterdik.

simpleDB.open(QUEUED_SESSION_UPDATES_DB_NAME).then(function(db) {
    var replayPromises = [];
    return db.forEach(function(url, method) {
    var promise = IOWA.Request.xhrPromise(method, url, true).then(function() {
        return db.delete(url).then(function() {
        return true;
        });
    });
    replayPromises.push(promise);
    }).then(function() {
    if (replayPromises.length) {
        return Promise.all(replayPromises).then(function() {
        IOWA.Elements.Toast.showMessage(
            'My Schedule was updated with offline changes.');
        });
    }
    });
}).catch(function() {
    IOWA.Elements.Toast.showMessage(
    'Offline changes could not be applied to My Schedule.');
});

Çevrimdışı Google Analytics

Benzer şekilde, başarısız olan Google Analytics isteklerini sıraya ekleyip ağ kullanılabilir olduğunda bunları daha sonra yeniden oynatmaya çalışan bir işleyici uyguladık. Bu yaklaşımla, çevrimdışı olmak Google Analytics'in sunduğu analizlerden ödün vermek anlamına gelmez. Google Analytics arka ucuna doğru bir etkinlik ilişkilendirme süresinin ulaşmasını sağlamak için, sıraya eklenen her isteğe qt parametresini ekledik. Bu parametre, istek ilk kez denendikten sonra geçen süreye ayarlanır. Google Analytics, qt için yalnızca 4 saate kadar olan değerleri resmi olarak destekler. Bu nedenle, hizmet çalışanı her başlatıldığında bu istekleri en kısa sürede yeniden oynatmak için elimizden geleni yaptık.

var DB_NAME = 'offline-analytics';
var EXPIRATION_TIME_DELTA = 86400000;
var ORIGIN = /https?:\/\/((www|ssl)\.)?google-analytics\.com/;

function replayQueuedAnalyticsRequests() {
    simpleDB.open(DB_NAME).then(function(db) {
    db.forEach(function(url, originalTimestamp) {
        var timeDelta = Date.now() - originalTimestamp;
        var replayUrl = url + '&qt=' + timeDelta;
        fetch(replayUrl).then(function(response) {
        if (response.status >= 500) {
            return Response.error();
        }
        db.delete(url);
        }).catch(function(error) {
        if (timeDelta > EXPIRATION_TIME_DELTA) {
            db.delete(url);
        }
        });
    });
    });
}

function queueFailedAnalyticsRequest(request) {
    simpleDB.open(DB_NAME).then(function(db) {
    db.set(request.url, Date.now());
    });
}

function handleAnalyticsCollectionRequest(request) {
    return global.fetch(request).then(function(response) {
    if (response.status >= 500) {
        return Response.error();
    }
    return response;
    }).catch(function() {
    queueFailedAnalyticsRequest(request);
    });
}

toolbox.router.get('/collect',
                    handleAnalyticsCollectionRequest,
                    {origin: ORIGIN});
toolbox.router.get('/analytics.js',
                    toolbox.networkFirst,
                    {origin: ORIGIN});

replayQueuedAnalyticsRequests();

Push Bildirimi Açılış Sayfaları

Hizmet çalışanları, IOWA'nın çevrimdışı işlevini yönetmenin yanı sıra kullanıcıları yer işareti eklenmiş oturumlarındaki güncellemeler hakkında bilgilendirmek için kullandığımız push bildirimlerini de destekledi. Bu bildirimlerle ilişkili açılış sayfasında güncellenen oturum ayrıntıları gösteriliyordu. Bu açılış sayfaları, genel sitenin bir parçası olarak zaten önbelleğe alınıyordu. Bu nedenle, çevrimdışı olarak görüntülendiğinde bile bu sayfadaki oturum ayrıntılarının güncel olduğundan emin olmamız gerekiyordu. Bunu yapmak için daha önce önbelleğe alınmış oturum meta verilerini, push bildirimini tetikleyen güncellemelerle değiştirdik ve sonucu önbellekte sakladık. Bu güncel bilgiler, oturum ayrıntıları sayfası bir sonraki kez açıldığında (online veya çevrimdışı) kullanılır.

caches.open(toolbox.options.cacheName).then(function(cache) {
    cache.match('api/v1/schedule').then(function(response) {
    if (response) {
        parseResponseJSON(response).then(function(schedule) {
        sessions.forEach(function(session) {
            schedule.sessions[session.id] = session;
        });
        cache.put('api/v1/schedule',
                    new Response(JSON.stringify(schedule)));
        });
    } else {
        toolbox.cache('api/v1/schedule');
    }
    });
});

Dikkat edilmesi gereken noktalar

Elbette, IOWA ölçeğindeki bir projede çalışan hiç kimse birkaç sorunla karşılaşmadan çalışmaz. Karşılaştığımız bazı sorunlar ve bu sorunları nasıl çözdüğümüzü aşağıda bulabilirsiniz.

Eski İçerik

Bir önbelleğe alma stratejisi planlarken, hizmet işçileri aracılığıyla veya standart tarayıcı önbelleğiyle uygulanmış olsun, kaynakları mümkün olduğunca hızlı bir şekilde sunma ile en güncel kaynakları sunma arasında bir denge vardır. sw-precache aracılığıyla, uygulamamızın kabuğu için agresif bir önbellek öncelikli strateji uyguladık. Bu, hizmet işleyicimizin sayfadaki HTML, JavaScript ve CSS'yi döndürmeden önce ağda güncelleme olup olmadığını kontrol etmeyeceği anlamına geliyor.

Neyse ki sayfa yüklendikten sonra yeni içeriğin ne zaman kullanılabileceğini tespit etmek için hizmet çalışanı yaşam döngüsü etkinliklerinden yararlanabildik. Güncellenen bir hizmet çalışanı algılandığında, kullanıcıya en yeni içeriği görmek için sayfasını yeniden yüklemesi gerektiğini bildiren bir pop-up mesaj gösterilir.

if (navigator.serviceWorker && navigator.serviceWorker.controller) {
    navigator.serviceWorker.controller.onstatechange = function(e) {
    if (e.target.state === 'redundant') {
        var tapHandler = function() {
        window.location.reload();
        };
        IOWA.Elements.Toast.showMessage(
        'Tap here or refresh the page for the latest content.',
        tapHandler);
    }
    };
}
Son içerik bildirimi
"Son içerik" bildirimi.

Statik İçeriğin Statik Olmasına Dikkat Edin

sw-precache, yerel dosyaların içeriğinin MD5 karma değerini kullanır ve yalnızca karma değeri değişen kaynakları getirir. Bu, kaynakların sayfaya neredeyse hemen erişilebilir olduğu anlamına gelir ancak bir öğe önbelleğe alındıktan sonra, güncellenmiş bir hizmet çalışanı komut dosyasında yeni bir karma oluşturma işlemi uygulanana kadar önbelleğe alınmış olarak kalır.

Arka uç sistemimizin, konferansın her günü için canlı yayın YouTube video kimliklerini dinamik olarak güncellemesi gerektiğinden, I/O sırasında bu davranışla ilgili bir sorunla karşılaştık. Temel şablon dosyası statik olduğu ve değişmediği için hizmet çalışanı güncelleme akışımız tetiklenmedi ve YouTube videolarının güncellendiği sunucudan dinamik bir yanıt gelmesi gerekirken, bazı kullanıcılar için önbelleğe alınmış yanıt döndürüldü.

Web uygulamanızın, kabuğun her zaman statik ve güvenli bir şekilde önceden önbelleğe alınabileceği, kabuğunu değiştiren dinamik kaynakların ise bağımsız olarak yükleneceği şekilde yapılandırıldığından emin olarak bu tür sorunları önleyebilirsiniz.

Önbelleğe alma isteklerinizi önbellekten kaldırma

sw-precache, önbelleğe alınacak kaynaklar için istek gönderdiğinde, dosyanın MD5 karmasının değişmediğine karar verdiği sürece bu yanıtları süresiz olarak kullanır. Bu nedenle, önbelleğe alma isteğinin yanıtının yeni olduğundan ve tarayıcının HTTP önbelleğinden döndürülmediğinden emin olmak özellikle önemlidir. (Evet, bir hizmet çalışanında yapılan fetch() istekleri, tarayıcının HTTP önbelleğindeki verilerle yanıt verebilir.)

Önceden önbelleğe aldığımız yanıtların tarayıcının HTTP önbelleğinden değil, doğrudan ağdan gelmesini sağlamak için sw-precache, istek gönderdiği her URL'ye otomatik olarak önbelleği bozan bir sorgu parametresi ekler. sw-precache kullanmıyorsanız ve önbelleğe öncelikli yanıt stratejisi kullanıyorsanız kendi kodunuzda benzer bir şey yaptığınızdan emin olun.

Ön önbelleğe alma için kullanılan her Request öğesinin önbelleğe alma modunu reload olarak ayarlamak, önbelleği bozma sorununa daha temiz bir çözümdür. Bu ayar, yanıtın ağdan gelmesini sağlar. Ancak bu yazı hazırlandığı sırada, önbelleğe alma modu seçeneği Chrome'da desteklenmiyor.

Oturum açma ve kapama desteği

IOWA, kullanıcıların Google Hesaplarını kullanarak giriş yapmalarına ve özelleştirilmiş etkinlik programlarını güncellemelerine olanak tanısa da kullanıcıların daha sonra oturumlarını kapatabilecekleri anlamına da geliyordu. Kişiselleştirilmiş yanıt verilerini önbelleğe almak, hassas bir konudur ve her zaman tek bir doğru yaklaşım yoktur.

IOWA deneyiminin temelinde, çevrimdışıyken bile kişisel programınızı görüntüleme özelliği olduğu için önbelleğe alınmış verileri kullanmanın uygun olduğuna karar verdik. Kullanıcı oturumunu kapattığında, daha önce önbelleğe alınmış oturum verilerini temizlediğimizden emin oluruz.

    self.addEventListener('message', function(event) {
      if (event.data === 'clear-cached-user-data') {
        caches.open(toolbox.options.cacheName).then(function(cache) {
          cache.keys().then(function(requests) {
            return requests.filter(function(request) {
              return request.url.indexOf('api/v1/user/') !== -1;
            });
          }).then(function(userDataRequests) {
            userDataRequests.forEach(function(userDataRequest) {
              cache.delete(userDataRequest);
            });
          });
        });
      }
    });

Ek sorgu parametrelerine dikkat edin.

Bir hizmet çalışanı, önbelleğe alınmış bir yanıt olup olmadığını kontrol ederken anahtar olarak bir istek URL'si kullanır. Varsayılan olarak istek URL'si, URL'nin arama bölümündeki tüm sorgu parametreleri dahil olmak üzere önbelleğe alınan yanıtı depolamak için kullanılan URL ile tam olarak eşleşmelidir.

Bu durum, trafiğimizin nereden geldiğini takip etmek için URL parametrelerini kullanmaya başladığımızda geliştirme sırasında sorun yaşamamıza neden oldu. Örneğin, bildirimlerimizden birini tıklandığında açılan URL'lere utm_source=notification parametresini ekledik ve web uygulaması manifestimiz için start_url içinde utm_source=web_app_manifest kullandık. Daha önce önbelleğe alınan yanıtlarla eşleşen URL'ler, bu parametreler eklendiğinde eksik olarak görünmekteydi.

Bu sorun, Cache.match() çağrısı yapılırken kullanılabilen ignoreSearch seçeneğiyle kısmen giderilebilir. Maalesef Chrome ignoreSearch'yi henüz desteklemiyor. Desteklemiş olsa bile bu davranış ya tamamen etkin ya da tamamen devre dışıdır. Anlamlı olanları dikkate alırken bazı URL sorgu parametrelerini yoksaymanın bir yolunu bulmamız gerekiyordu.

sw-precache'ü, önbelleğe eşleşme olup olmadığını kontrol etmeden önce bazı sorgu parametrelerini çıkarmak ve geliştiricilerin ignoreUrlParametersMatching seçeneği aracılığıyla hangi parametrelerin yoksayıldığını özelleştirmelerine olanak tanımak için genişlettik. Temel uygulama şu şekildedir:

function stripIgnoredUrlParameters(originalUrl, ignoredRegexes) {
    var url = new URL(originalUrl);

    url.search = url.search.slice(1)
    .split('&')
    .map(function(kv) {
        return kv.split('=');
    })
    .filter(function(kv) {
        return ignoredRegexes.every(function(ignoredRegex) {
        return !ignoredRegex.test(kv[0]);
        });
    })
    .map(function(kv) {
        return kv.join('=');
    })
    .join('&');

    return url.toString();
}

Bu durum sizin için ne anlama geliyor?

Google I/O web uygulamasındaki hizmet çalışanı entegrasyonu, muhtemelen bugüne kadar kullanıma sunulan en karmaşık gerçek kullanım alanıdır. Web geliştirici topluluğunun, kendi web uygulamalarınızı güçlendirmek için sw-precache ve sw-toolbox araçlarımızı ve açıkladığımız teknikleri kullanmasını heyecanla bekliyoruz. Hizmet çalışanları, hemen kullanmaya başlayabileceğiniz progresif bir geliştirmedir. Düzgün bir şekilde yapılandırılmış bir web uygulamasının parçası olarak kullanıldığında, hız ve çevrimdışı avantajları kullanıcılarınız için önemli olur.