Özet
Web bileşenlerini, Polymer'i ve materyal tasarımı kullanarak nasıl tek sayfalık bir uygulama geliştirdiğimizi ve Google.com'da nasıl üretime aldığımızı öğrenin.
Sonuçlar
- Yerel uygulamaya göre daha fazla etkileşim (Android'in 2:40 dakikasına kıyasla 4:06 dakika mobil web).
- Hizmet çalışanı önbelleğe alma sayesinde geri gelen kullanıcılar için ilk boyama işlemi 450 ms daha hızlı
- Ziyaretçilerin %84'ü Service Worker'ı destekledi
- Ana ekrana ekle özelliğiyle kaydedilen içeriklerin sayısı 2015'e kıyasla %900 arttı.
- %3,8'lik bir kullanıcı kitlesi çevrimdışı olmasına rağmen 11.000 sayfa görüntüleme elde etmeyi başardı.
- Oturum açmış kullanıcıların %50'si bildirimleri etkinleştirdi.
- Kullanıcılara 536 bin bildirim gönderildi (%12'si uygulamayı geri yükledi).
- Kullanıcıların tarayıcılarının% 99'u, web bileşenlerinin çoklu dolgularını destekledi
Genel Bakış
Bu yıl, "IOWA" olarak adlandırılan Google I/O 2016 progresif web uygulamasında çalışma fırsatı buldum. Mobil öncelikli olan bu uygulama tamamen çevrimdışı çalışır ve materyal tasarımdan büyük ölçüde ilham alır.
IOWA, web bileşenleri, Polymer ve Firebase kullanılarak oluşturulmuş bir tek sayfalık uygulamadır (SPA). App Engine (Go)'da yazılmış kapsamlı bir arka ucu vardır. Bu hizmet, bir Service Worker kullanarak içeriği önbelleğe alır, yeni sayfaları dinamik olarak yükler, görünümler arasında sorunsuz geçiş yapar ve ilk yüklemeden sonra içeriği yeniden kullanır.
Bu örnek olayda, kullanıcı arayüzü için aldığımız daha ilginç mimari kararlardan bazılarını ele alacağım. Kaynak koduyla ilgileniyorsanız GitHub'da kaynağı inceleyebilirsiniz.
Web bileşenlerini kullanarak SPA oluşturma
Her sayfa bir bileşen olarak
Kullanıcı arayüzümüz, web bileşenlerine odaklandığı için temel özelliklerinden biridir. Aslında, SPA'mızdaki her sayfa bir web bileşenidir:
<io-home-page date="2016-05-18T17:00:00Z" app="[[app]]"></io-home-page>
<io-schedule-page date="2016-05-18T17:00:00Z" app="{ % templatetag openvariable % }app}}"></io-schedule-page>
<io-attend-page></io-attend-page>
<io-extended-page></io-extended-page>
<io-faq-page></io-faq-page>
Bunu neden yaptık? Bunun birinci nedeni, bu kodun okunabilir olmasıdır. İlk kez okuyan bir kullanıcı, uygulamamızdaki her sayfanın ne olduğunu kolayca anlayabilir. İkinci neden ise web bileşenlerinin SPA oluşturmak için bazı güzel özelliklere sahip olmasıdır. <template>
öğesinin, Özel Öğeler'in ve Gölge DOM'un doğal özellikleri sayesinde birçok yaygın sorun (durum yönetimi, görünüm etkinleştirme, stil kapsamı) ortadan kalkar. Bunlar, tarayıcıya yerleştirilmiş geliştirici araçlarıdır. Bu fırsatlardan yararlanmaya ne dersiniz?
Her sayfa için bir özel öğe oluşturarak ücretsiz olarak birçok avantaj elde ettik:
- Sayfa yaşam döngüsü yönetimi.
- Sayfaya özgü kapsamlı CSS/HTML.
- Bir sayfaya özgü tüm CSS/HTML/JS, gerektiğinde birlikte paketlenip yüklenir.
- Görünümler yeniden kullanılabilir. Sayfalar DOM düğümleri olduğundan, bunları eklemek veya kaldırmak görünümü değiştirir.
- Gelecekteki bakım ekibi üyeleri, uygulamamızı yalnızca işaretlemeyi anlayarak anlayabilir.
- Sunucu tarafından oluşturulan işaretleme, tarayıcı tarafından öğe tanımları kaydedilip yükseltildiğinde kademeli olarak iyileştirilebilir.
- Özel öğelerin bir devralma modeli vardır. DRY kod iyi koddur.
- ...daha pek çok şey daha var.
IOWA'da bu avantajlardan tam olarak yararlandık. Bazı ayrıntılara göz atalım.
Sayfaları dinamik olarak etkinleştirme
<template>
öğesi, tarayıcının yeniden kullanılabilir işaretleme oluşturmanın standart yoludur. <template>
, SPA'ların yararlanabileceği iki özelliğe sahiptir. Öncelikle, şablonun bir örneği oluşturulana kadar <template>
içindeki her şey etkin değildir. İkincisi, tarayıcı işaretlemeyi ayrıştırır ancak içeriklere ana sayfadan erişilemez. Gerçek, yeniden kullanılabilir bir işaretleme parçası. Örneğin:
<template id="t">
<div>This markup is inert and not part of the main page's DOM.</div>
<img src="profile.png"> <!-- not loaded by the browser -->
<video id="vid" src="vid.mp4"></video> <!-- doesn't load/start -->
<script>alert("Not run until the template is stamped");</script>
</template>
Polymer, <template>
öğelerini <template is="dom-if">
ve <template is="dom-repeat">
gibi birkaç tür uzantısı özel öğesiyle genişletir. Her ikisi de <template>
öğesini ek özelliklerle genişleten özel öğelerdir. Web bileşenlerinin açıklayıcı yapısı sayesinde her ikisi de tam olarak beklediğinizi yapar.
İlk bileşen, işaretlemeyi koşula göre damgalar. İkincisi, bir listedeki her öğe (veri modeli) için işaretlemeyi tekrarlar.
IOWA bu tür uzantı öğelerini nasıl kullanıyor?
IOWA'daki her sayfanın bir web bileşeni olduğunu hatırlıyor musunuz? Ancak ilk yüklemede her bileşeni tanımlamak aptalca olur. Bu, uygulama ilk yüklendiğinde her sayfanın bir örneğinin oluşturulması anlamına gelir. Özellikle bazı kullanıcıların yalnızca 1 veya 2 sayfaya gideceği için ilk yükleme performansımızı olumsuz yönde etkilemek istemedik.
Bizim çözümümüz hileydi. IOWA'da, her sayfanın öğesini içeriğinin ilk açılışta yüklenmemesi için bir <template is="dom-if">
içine sarmalıyoruz. Ardından, şablonun name
özelliği URL ile eşleştiğinde sayfaları etkinleştiririz. <lazy-pages>
web bileşeni tüm bu mantığı bizim için yönetir. İşaretleme şuna benzer:
<!-- Lazy pages manages the template stamping. It watches for route changes
and sets `template.if = true` on the appropriate template. -->
<lazy-pages>
<template is="dom-if" name="home">
<io-home-page date="2016-05-18T17:00:00Z"></io-home-page>
</template>
<template is="dom-if" name="schedule">
<io-schedule-page date="2016-05-18T17:00:00Z"></io-schedule-page>
</template>
<template is="dom-if" name="attend">
<io-attend-page></io-attend-page>
</template>
</lazy-pages>
Bu yöntemin avantajı, her sayfanın ayrıştırılması ve sayfa yüklendiğinde kullanıma hazır olmasıdır. Ancak CSS/HTML/JS'si yalnızca gerektiğinde (üst öğesi <template>
damgalandığında) yürütülür. Web bileşenlerini kullanan dinamik ve yavaş görünümler FTW.
Gelecekteki iyileştirmeler
Sayfa ilk yüklendiğinde, her sayfanın tüm HTML içe aktarma işlemlerini tek seferde yüklüyoruz. Öğe tanımlarını yalnızca gerektiğinde gecikmeli olarak yüklemek, bariz bir iyileştirme olacaktır. Polymer'da, HTML içe aktarma işlemlerini eşzamansız olarak yüklemek için kullanışlı bir yardımcı da vardır:
Polymer.Base.importHref('io-home-page.html', (e) => { ... });
IOWA bunu yapmaz çünkü a) tembel davrandığımız ve b) performansta ne kadar artış elde edeceğimiz belirsizdir. İlk boyamamız zaten yaklaşık 1 saniyeydi.
Sayfa yaşam döngüsü yönetimi
Özel Öğeler API'si, bir bileşenin durumunu yönetmek için "yaşam döngüsü geri çağırma işlevlerini" tanımlar. Bu yöntemleri uyguladığınızda, bileşenin yaşam döngüsüne ücretsiz kancalar eklersiniz:
createdCallback() {
// automatically called when an instance of the element is created.
}
attachedCallback() {
// automatically called when the element is attached to the DOM.
}
detachedCallback() {
// automatically called when the element is removed from the DOM.
}
attributeChangedCallback() {
// automatically called when an HTML attribute changes.
}
IOWA'da bu geri çağırmalardan yararlanmak kolaydı. Her sayfanın bağımsız bir DOM düğümü olduğunu unutmayın. SPA'mızda "yeni bir görünüme" gitmek, bir düğümü DOM'a ekleyip başka bir düğümü kaldırmak kadar basittir.
Kurulum işlemini (başlatma durumu, etkinlik işleyici ekleme) gerçekleştirmek için attachedCallback
yöntemini kullandık. Kullanıcılar farklı bir sayfaya gittiğinde detachedCallback
temizlik yapar (dinleyicileri kaldırır, paylaşılan durumu sıfırlar). Ayrıca, yerel yaşam döngüsü geri çağırmalarını kendi geri çağırmalarımızla genişlettik:
onPageTransitionDone() {
// page transition animations are complete.
},
onSubpageTransitionDone() {
// sub nav/tab page transitions are complete.
}
Bu özellikler, işleri ertelemek ve sayfa geçişleri arasındaki sarsıntıyı en aza indirmek için yararlı eklemeler oldu. Bu konuyla ilgili daha fazla bilgiyi aşağıda bulabilirsiniz.
Sayfalar arasında ortak işlevleri DRYing up
Devralma, Özel Öğeler'in güçlü bir özelliğidir. Web için standart bir devralma modeli sağlar.
Maalesef Polymer 1.0'da, bu makalenin yazıldığı sırada öğe devralma özelliği henüz uygulanmamıştır. Bu arada, Polymer'in Davranışlar özelliği de bir o kadar yararlıydı. Davranışlar, yalnızca mixin'lerdir.
Tüm sayfalarda aynı API yüzeyini oluşturmak yerine, paylaşılan mixin'ler oluşturarak kod tabanını DRY hale getirmek mantıklıydı. Örneğin, PageBehavior
, uygulamamızdaki tüm sayfaların ihtiyaç duyduğu ortak özellikleri/yöntemleri tanımlar:
PageBehavior.html
let PageBehavior = {
// Common properties all pages need.
properties: {
name: { type: String }, // Slug name of the page.
...
},
attached() {
// If the page defines a `onPageTransitionDone`, call it when the router
// fires 'page-transition-done'.
if (this.onPageTransitionDone) {
this.listen(document.body, 'page-transition-done', 'onPageTransitionDone');
}
// Update page meta data when new page is navigated to.
document.body.id = `page-${this.name}`;
document.title = this.title || 'Google I/O 2016';
// Scroll to top of new page.
if (IOWA.Elements.Scroller) {
IOWA.Elements.Scroller.scrollTop = 0;
}
this.setupSubnavEffects();
},
detached() {
this.unlisten(document.body, 'page-transition-done', 'onPageTransitionDone');
this.teardownSubnavEffects();
}
};
IOWA.IOBehaviors = IOWA.IOBehaviors || {PageBehavior: PageBehavior};
Gördüğünüz gibi PageBehavior
, yeni bir sayfa ziyaret edildiğinde çalışan sık kullanılan görevleri gerçekleştirir. document.title
öğesini güncelleme, kaydırma konumunu sıfırlama ve kaydırma ve alt gezinme efektleri için etkinlik işleyicileri ayarlama gibi işlemler.
Tek tek sayfalar, PageBehavior
'ü bağımlılık olarak yükleyerek ve behaviors
'ü kullanarak kullanır.
Ayrıca, gerekirse temel özelliklerini/yöntemlerini geçersiz kılmaları da serbesttir. Örneğin, ana sayfa "alt sınıfımız"ın geçersiz kıldığı öğeleri aşağıda bulabilirsiniz:
io-home-page.html
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="PageBehavior.html">
<!-- rest of the import dependencies used by the page. -->
<dom-module id="io-home-page">
<template>
<!-- PAGE'S MARKUP -->
</template>
<script>
Polymer({
is: 'io-home-page',
behaviors: [IOBehaviors.PageBehavior], // All pages have common functionality.
// Pages define their own title and slug for the router.
title: 'Schedule - Google I/O 2016',
name: 'home',
// The home page has custom setup work when it's added navigated to.
// Note: PageBehavior's attached also gets called.
attached() {
if (this.app.isPhoneSize) {
this.listen(IOWA.Elements.ScrollContainer, 'scroll', '_onPageScroll');
}
},
// The home page does its own cleanup when a new page is navigated to.
// Note: PageBehavior's detached also gets called.
detached() {
this.unlisten(IOWA.Elements.ScrollContainer, 'scroll', '_onPageScroll');
},
// The home page can define onPageTransitionDone to do extra work
// when page transitions are done, and thus preventing janky animations.
onPageTransitionDone() {
...
}
});
</script>
</dom-module>
Stilleri paylaşma
Uygulamamızın farklı bileşenleri arasında stilleri paylaşmak için Polymer'in paylaşılan stil modüllerini kullandık. Stil modülleri, bir CSS parçasını bir kez tanımlamanıza ve uygulama genelinde farklı yerlerde yeniden kullanmanıza olanak tanır. Bizim için "farklı yerler" farklı bileşenler anlamına geliyordu.
IOWA'da, sayfalarda ve oluşturduğumuz diğer bileşenlerde renkleri, tipografiyi ve düzen sınıflarını paylaşmak için shared-app-styles
oluşturduk.
shared-app-styles.html
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-flex-layout/iron-flex-layout.html">
<link rel="import" href="../bower_components/paper-styles/color.html">
<dom-module id="shared-app-styles">
<template>
<style>
[layout] {
@apply(--layout);
}
[layout][horizontal] {
@apply(--layout-horizontal);
}
.scrollable {
@apply(--layout-scroll);
}
.noscroll {
overflow: hidden;
}
/* Style radio buttons and tabs the same throughout the app */
paper-tabs {
--paper-tabs-selection-bar-color: currentcolor;
}
paper-radio-button {
--paper-radio-button-checked-color: var(--paper-cyan-600);
--paper-radio-button-checked-ink-color: var(--paper-cyan-600);
}
...
</style>
</template>
</dom-module>
io-home-page.html
<link rel="import" href="shared-app-styles.html">
<!-- Rest of import dependencies used by the page. -->
<dom-module id="io-home-page">
<template>
<style include="shared-app-styles">
:host { display: block} /* Other element styles can go here. */
</style>
<!-- PAGE'S MARKUP -->
</template>
<script>Polymer({...});</script>
</dom-module>
Burada <style include="shared-app-styles"></style>
, Polymer'in "stilleri "shared-app-styles" adlı modüle dahil et" ifadesini belirtmek için kullandığı söz dizimidir.
Paylaşım uygulaması durumu
Uygulamamızdaki her sayfanın Özel Öğe olduğunu artık biliyorsunuz. Bunu milyonlarca kez söyledim. Pekala, ancak her sayfa bağımsız web bileşeniyse, kendinize durumu uygulama genelinde nasıl paylaştığımızı soruyor olabilirsiniz.
IOWA, durumu paylaşmak için bağımlılık ekleme (Angular) veya redux (React) benzeri bir teknik kullanır. Dünya geneli bir app
mülkü oluşturduk ve bu mülkü paylaşılan alt mülklerle donattık. app
, verilerine ihtiyaç duyan her bir bileşene enjekte edilerek uygulamamızda aktarılır. Polymer'in veri bağlama özelliklerini kullanmak, kablo kurulumunu herhangi bir kod yazmadan yapabildiğimizden bu işlemi kolaylaştırır:
<lazy-pages>
<template is="dom-if" name="home">
<io-home-page date="2016-05-18T17:00:00Z" app="[[app]]"></io-home-page>
</template>
<template is="dom-if" name="schedule">
<io-schedule-page date="2016-05-18T17:00:00Z" app="{ % templatetag openvariable % }app}}"></io-schedule-page>
</template>
...
</lazy-pages>
<google-signin client-id="..." scopes="profile email"
user="{ % templatetag openvariable % }app.currentUser}}"></google-signin>
<iron-media-query query="(min-width:320px) and (max-width:768px)"
query-matches="{ % templatetag openvariable % }app.isPhoneSize}}"></iron-media-query>
Kullanıcılar uygulamamıza giriş yaptığında <google-signin>
öğesi user
mülkünü günceller. Bu mülk app.currentUser
'ye bağlı olduğundan, geçerli kullanıcıya erişmek isteyen tüm sayfaların app
'e bağlanması ve currentUser
alt mülkünü okuması yeterlidir. Bu teknik, tek başına uygulama genelinde durum paylaşmak için faydalıdır. Ancak tek bir oturum açma öğesi oluşturup sonuçlarını site genelinde yeniden kullanabilmemiz de bir avantajdı. Medya sorguları için de aynıdır. Her sayfanın oturum açmayı kopyalamasının veya kendi medya sorguları grubunu oluşturmasının boşa gidecek bir iş olması gerekirdi. Bunun yerine, uygulama genelindeki işlevlerden/verilerden sorumlu bileşenler uygulama düzeyinde bulunur.
Sayfa geçişleri
Google I/O web uygulamasında gezinirken uygulamadaki şık sayfa geçişlerini (à la materyal tasarım) fark edeceksiniz.
Kullanıcılar yeni bir sayfaya gittiğinde aşağıdaki işlemler gerçekleşir:
- Üst gezinme menüsünde, seçim çubuğu yeni bağlantıya kaydırılır.
- Sayfanın başlığı kaybolur.
- Sayfanın içeriği aşağı kayar ve kaybolur.
- Bu animasyonları tersine çevirdiğinizde yeni sayfanın başlığı ve içeriği görünür.
- (İsteğe bağlı) Yeni sayfa ek başlatma işlemi yapar.
Karşılaştığımız zorluklardan biri, performanstan ödün vermeden bu şık geçişi nasıl oluşturacağımızı bulmaktı. Çok dinamik bir çalışma ortamımız var ve gereksiz içeriklere yer vermiyoruz. Çözümümüz, Web Animasyonları API'si ve Promises'ın bir kombinasyonuydu. Bu ikisini birlikte kullanmak bize çok yönlülük, tak ve çalıştır animasyon sistemi ve das sarsıntısını en aza indirmek için ayrıntılı kontrol sağladı.
İşleyiş şekli
Kullanıcılar yeni bir sayfayı tıkladığında (veya geri/ileri tuşuna bastığında) yönlendiricimizin runPageTransition()
, bir dizi Promise'i çalıştırarak sihrini gösterir. Promise'leri kullanmak, animasyonları dikkatlice koordine etmemize ve CSS animasyonlarının "asynchronize" özelliğini ve dinamik olarak yüklenen içeriği mantıklı hale getirmemize yardımcı oldu.
class Router {
init() {
window.addEventListener('popstate', e => this.runPageTransition());
}
runPageTransition() {
let endPage = this.state.end.page;
this.fire('page-transition-start'); // 1. Let current page know it's starting.
IOWA.PageAnimation.runExitAnimation() // 2. Play exist animation sequence.
.then(() => {
IOWA.Elements.LazyPages.selected = endPage; // 3. Activate new page in <lazy-pages>.
this.state.current = this.parseUrl(this.state.end.href);
})
.then(() => IOWA.PageAnimation.runEnterAnimation()) // 4. Play entry animation sequence.
.then(() => this.fire('page-transition-done')) // 5. Tell new page transitions are done.
.catch(e => IOWA.Util.reportError(e));
}
}
"Her şeyi DRY tutma: Sayfalar arasında ortak işlevler" bölümünden hatırlayalım, sayfalar page-transition-start
ve page-transition-done
DOM etkinliklerini dinler. Artık bu etkinliklerin nerede tetiklendiğini görüyorsunuz.
runEnterAnimation
/runExitAnimation
yardımcıları yerine Web Animations API'yi kullandık. runExitAnimation
durumunda, birkaç DOM düğümü (başlıkta ve ana içerik alanında) alırız, her animasyonun başlangıç/bitiş noktasını belirtiriz ve ikisini paralel olarak çalıştırmak için bir GroupEffect
oluştururuz:
function runExitAnimation(section) {
let main = section.querySelector('.slide-up');
let masthead = section.querySelector('.masthead');
let start = {transform: 'translate(0,0)', opacity: 1};
let end = {transform: 'translate(0,-100px)', opacity: 0};
let opts = {duration: 400, easing: 'cubic-bezier(.4, 0, .2, 1)'};
let opts_delay = {duration: 400, delay: 200};
return new GroupEffect([
new KeyframeEffect(masthead, [start, end], opts),
new KeyframeEffect(main, [{opacity: 1}, {opacity: 0}], opts_delay)
]);
}
Görünüm geçişlerini daha ayrıntılı (veya daha az) hale getirmek için diziyi değiştirmeniz yeterlidir!
Kaydırma efektleri
Sayfayı kaydırdığınızda IOWA'nın birkaç ilginç etkisi vardır. Bunlardan ilki, kullanıcıları sayfanın üst kısmına geri götüren kayan işlem düğmemizdir (FAB):
<a href="#" tabindex="-1" aria-hidden="true" aria-label="back to top" onclick="backToTop">
<paper-fab icon="io:expand-less" noink tabindex="-1"></paper-fab>
</a>
Sorunsuz kaydırma, Polymer'in uygulama düzeni öğeleri kullanılarak uygulanır. Yapışkan/geri dönen üst gezinme menüleri, gölgeler, renk ve arka plan geçişleri, paralaks efektleri ve yumuşak kaydırma gibi hazır kaydırma efektleri sunarlar.
// Smooth scrolling the back to top FAB.
function backToTop(e) {
e.preventDefault();
Polymer.AppLayout.scroll({top: 0, behavior: 'smooth',
target: document.documentElement});
e.target.blur(); // Kick focus back to the page so user starts from the top of the doc.
}
<app-layout>
öğelerini, yapışkan gezinme menüsünde de kullandık. Videoda da görebileceğiniz gibi, kullanıcılar sayfayı aşağı kaydırdığında bu reklam kaybolur ve yukarı kaydırıldığında geri gelir.
<app-header>
öğesini neredeyse olduğu gibi kullandık. Uygulamaya ekleyip şık kaydırma efektleri elde etmek kolaydı. Elbette bunları kendimiz de uygulayabilirdik ancak ayrıntıların yeniden kullanılabilir bir bileşende kodlanmış olması çok zaman kazandırdı.
Öğeyi tanımlayın. Özellikleri kullanarak özelleştirin. Hepsi bu kadar!
<app-header reveals condenses effects="fade-background waterfall"></app-header>
Sonuç
I/O progresif web uygulaması için web bileşenleri ve Polymer'in önceden hazırlanmış materyal tasarım widget'ları sayesinde birkaç hafta içinde tüm ön uç oluşturmayı başardık. Yerel API'lerin (Özel Öğeler, Gölge DOM, <template>
) özellikleri, SPA'ların dinamizmine doğal olarak uygundur. Yeniden kullanılabilirlik çok fazla zaman kazandırır.
Kendi progresif web uygulamanızı oluşturmak istiyorsanız Uygulama Araç Kutusu'na göz atın. Polymer'in Uygulama Araç Kutusu, Polymer ile PWA oluşturmak için bileşenler, araçlar ve şablonlardan oluşan bir koleksiyondur. Bu, hızlıca çalışmaya başlamanın kolay bir yoludur.