
สรุป
ดูวิธีที่เราสร้างแอปแบบหน้าเดียวโดยใช้คอมโพเนนต์เว็บ, Polymer และการออกแบบมาเทเรียล รวมถึงเปิดตัวแอปดังกล่าวในเวอร์ชันที่ใช้งานจริงบน Google.com
ผลลัพธ์
- การมีส่วนร่วมมากกว่าแอปเนทีฟ (เว็บบนอุปกรณ์เคลื่อนที่ 4:06 นาที เทียบกับ Android 2:40 นาที)
- First Paint เร็วขึ้น 450 มิลลิวินาทีสําหรับผู้ใช้ที่กลับมาเนื่องจากการแคช Service Worker
- ผู้เข้าชม 84% รองรับ Service Worker
- การบันทึกเพิ่มลงในหน้าจอหลักเพิ่มขึ้น 900% เมื่อเทียบกับปี 2015
- ผู้ใช้ 3.8% ออฟไลน์ไปแล้วแต่ยังคงสร้างการดูหน้าเว็บ 11,000 ครั้ง
- ผู้ใช้ที่ลงชื่อเข้าใช้ 50% เปิดใช้การแจ้งเตือน
- มีการส่งการแจ้งเตือน 536,000 รายการไปยังผู้ใช้ (12% กลับมาใช้งานอีกครั้ง)
- เบราว์เซอร์ของผู้ใช้ 99% รองรับโพลีฟิลล์คอมโพเนนต์เว็บ
ภาพรวม
ปีนี้เรามีโอกาสได้ทํางานกับ Progressive Web App ของ Google I/O 2016 ที่มีชื่อเล่นว่า "IOWA" โดยออกแบบมาเพื่ออุปกรณ์เคลื่อนที่เป็นหลัก ทำงานแบบออฟไลน์ได้เต็มรูปแบบ และได้รับแรงบันดาลใจอย่างมากจากการออกแบบเชิงวัสดุ
IOWA คือแอปพลิเคชันหน้าเว็บเดียว (SPA) ที่สร้างขึ้นโดยใช้ Web Component, Polymer และ Firebase และมีแบ็กเอนด์ที่ครอบคลุมซึ่งเขียนขึ้นใน App Engine (Go) โดยจะแคชเนื้อหาไว้ล่วงหน้าโดยใช้ Service Worker, โหลดหน้าใหม่แบบไดนามิก, เปลี่ยนมุมมองอย่างราบรื่น และนำเนื้อหามาใช้ซ้ำหลังจากโหลดครั้งแรก
ในกรณีศึกษานี้ เราจะพูดถึงการตัดสินใจด้านสถาปัตยกรรมที่น่าสนใจบางอย่างที่เราทำสำหรับหน้าเว็บ หากสนใจซอร์สโค้ด โปรดดูใน GitHub
การสร้าง SPA โดยใช้คอมโพเนนต์เว็บ
ทุกหน้าเป็นคอมโพเนนต์
หนึ่งในแง่มุมหลักเกี่ยวกับหน้าเว็บของเราคือหน้าเว็บนั้นมุ่งเน้นที่คอมโพเนนต์เว็บ อันที่จริงแล้ว หน้าเว็บทุกหน้าใน SPA ของเราคือคอมโพเนนต์เว็บ
<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>
เหตุผลที่เราทำเช่นนี้ เหตุผลแรกคือโค้ดนี้อ่านง่าย ในฐานะผู้อ่านครั้งแรก หน้าเว็บทุกหน้าในแอปของเรานั้นชัดเจนมาก เหตุผลที่ 2 คือคอมโพเนนต์เว็บมีคุณสมบัติที่ยอดเยี่ยมในการสร้าง SPA ปัญหาที่พบได้ทั่วไปหลายอย่าง (การจัดการสถานะ การเปิดใช้งานมุมมอง การกําหนดขอบเขตสไตล์) จะหมดไปด้วยฟีเจอร์ที่มีอยู่แล้วขององค์ประกอบ <template>
, องค์ประกอบที่กําหนดเอง และ Shadow DOM เครื่องมือเหล่านี้เป็นเครื่องมือสําหรับนักพัฒนาซอฟต์แวร์ที่ฝังอยู่ในเบราว์เซอร์ เหตุใดจึงไม่ใช้ประโยชน์จากฟีเจอร์เหล่านี้
การสร้างองค์ประกอบที่กำหนดเองสำหรับแต่ละหน้าทำให้เราได้รับสิ่งต่างๆ มากมายโดยไม่มีค่าใช้จ่าย
- การจัดการวงจรของหน้าเว็บ
- CSS/HTML ที่กําหนดขอบเขตเฉพาะหน้านั้นๆ
- ระบบจะรวม CSS/HTML/JS ทั้งหมดสำหรับหน้าเว็บหนึ่งๆ และโหลดพร้อมกันตามต้องการ
- มุมมองนํามาใช้ซ้ำได้ เนื่องจากหน้าเว็บเป็นโหนด DOM การเพิ่มหรือนําหน้าเว็บออกจึงเปลี่ยนมุมมอง
- ผู้ดูแลระบบในอนาคตจะเข้าใจแอปของเราได้ง่ายๆ เพียงทำความเข้าใจมาร์กอัป
- มาร์กที่แสดงผลจากเซิร์ฟเวอร์สามารถปรับปรุงได้อย่างต่อเนื่องเมื่อเบราว์เซอร์ลงทะเบียนและอัปเกรดคําจํากัดความขององค์ประกอบ
- องค์ประกอบที่กําหนดเองมีรูปแบบการสืบทอด โค้ด DRY คือโค้ดที่ดี
- …และอีกมากมาย
เราใช้ประโยชน์จากสิทธิประโยชน์เหล่านี้อย่างเต็มที่ในไอโอวา มาดูรายละเอียดกัน
การเปิดใช้งานหน้าเว็บแบบไดนามิก
องค์ประกอบ <template>
เป็นวิธีที่เบราว์เซอร์ใช้สร้างมาร์กอัปที่นํากลับมาใช้ซ้ำได้ <template>
มีลักษณะ 2 อย่างที่สปาใช้ประโยชน์ได้ ขั้นแรก ทุกอย่างภายใน <template>
จะหยุดทำงานจนกว่าจะมีการสร้างอินสแตนซ์ของเทมเพลต ประการที่ 2 เบราว์เซอร์จะแยกวิเคราะห์มาร์กอัป แต่เข้าถึงเนื้อหาจากหน้าหลักไม่ได้ นั่นคือกลุ่มมาร์กอัปที่นำมาใช้ซ้ำได้จริง เช่น
<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>
ด้วยองค์ประกอบที่กำหนดเองสำหรับส่วนขยายประเภท 2-3 รายการ ได้แก่ <template is="dom-if">
และ <template is="dom-repeat">
ทั้ง 2 รายการเป็นองค์ประกอบที่กําหนดเองซึ่งขยาย <template>
ด้วยความสามารถเพิ่มเติม และด้วยลักษณะการประกาศของคอมโพเนนต์เว็บ ทั้งสองอย่างจึงทํางานตามที่คาดไว้
คอมโพเนนต์แรกประทับมาร์กอัปตามเงื่อนไข ส่วนรายการที่ 2 จะทำซ้ำมาร์กอัปสำหรับทุกรายการในรายการ (โมเดลข้อมูล)
IOWA ใช้องค์ประกอบส่วนขยายประเภทเหล่านี้อย่างไร
โปรดทราบว่าทุกหน้าใน IOWA เป็นคอมโพเนนต์เว็บ อย่างไรก็ตาม การประกาศคอมโพเนนต์ทุกรายการในการโหลดครั้งแรกนั้นไม่ฉลาดนัก ซึ่งหมายความว่าจะต้องสร้างอินสแตนซ์ของทุกหน้าเมื่อแอปโหลดเป็นครั้งแรก เราไม่อยากลดประสิทธิภาพการโหลดครั้งแรก โดยเฉพาะเมื่อผู้ใช้บางรายจะไปยังหน้าเว็บเพียง 1-2 หน้าเท่านั้น
ทางออกของเราคือการโกง ใน IOWA เราจะตัดองค์ประกอบของหน้าแต่ละหน้าไว้ใน <template is="dom-if">
เพื่อไม่ให้เนื้อหาของหน้านั้นโหลดในการบูตครั้งแรก จากนั้นเราจะเปิดใช้งานหน้าเว็บเมื่อแอตทริบิวต์ name
ของเทมเพลตตรงกับ URL คอมโพเนนต์เว็บ <lazy-pages>
จะจัดการตรรกะทั้งหมดนี้ให้เรา มาร์กอัปจะมีลักษณะดังนี้
<!-- 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>
สิ่งที่ฉันชอบเกี่ยวกับเรื่องนี้คือทุกหน้าได้รับการแยกวิเคราะห์และพร้อมใช้งานเมื่อโหลดหน้าเว็บ แต่ CSS/HTML/JS จะทำงานเฉพาะเมื่อมีการเรียกใช้ (เมื่อมีการประทับ <template>
ของหน้าหลัก) มุมมองแบบไดนามิก + การโหลดแบบเลื่อนดูทีละหน้าโดยใช้คอมโพเนนต์ของเว็บ
การปรับปรุงในอนาคต
เมื่อหน้าเว็บโหลดเป็นครั้งแรก เราจะโหลดการนําเข้า HTML ทั้งหมดสําหรับแต่ละหน้าพร้อมกัน การปรับปรุงที่เห็นได้ชัดคือการโหลดคําจํากัดความขององค์ประกอบแบบ Lazy Load เมื่อจําเป็นเท่านั้น นอกจากนี้ Polymer ยังมีตัวช่วยที่ยอดเยี่ยมสำหรับการโหลดการนําเข้า HTML แบบแอซิงค์ด้วย
Polymer.Base.importHref('io-home-page.html', (e) => { ... });
IOWA ไม่ได้ทําเช่นนี้เนื่องจาก 1) เราขี้เกียจ และ 2) เราไม่แน่ใจว่าประสิทธิภาพจะเพิ่มขึ้นมากน้อยเพียงใด First Paint ของเราอยู่ที่ประมาณ 1 วินาทีอยู่แล้ว
การจัดการวงจรหน้าเว็บ
Custom Elements API จะกำหนด "การเรียกกลับเกี่ยวกับวงจร" เพื่อจัดการสถานะของคอมโพเนนต์ เมื่อใช้เมธอดเหล่านี้ คุณจะได้รับฮุกสำหรับวงจรชีวิตของคอมโพเนนต์โดยไม่มีค่าใช้จ่าย
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 ได้อย่างง่ายดาย โปรดทราบว่าหน้าเว็บแต่ละหน้าคือโหนด DOM ที่แยกต่างหาก การไปยัง "มุมมองใหม่" ใน SPA ของเราคือการแนบโหนดหนึ่งกับ DOM และนำโหนดอื่นออก
เราใช้ attachedCallback
เพื่อทํางานตั้งค่า (สถานะเริ่มต้น แนบโปรแกรมรับฟังเหตุการณ์) เมื่อผู้ใช้ไปยังหน้าอื่น detachedCallback
จะล้างข้อมูล (นํา Listener ออก รีเซ็ตสถานะการแชร์) นอกจากนี้ เรายังขยายการเรียกกลับวงจรชีวิตของเนทีฟด้วยฟีเจอร์ของเราเองอีกหลายรายการ ดังนี้
onPageTransitionDone() {
// page transition animations are complete.
},
onSubpageTransitionDone() {
// sub nav/tab page transitions are complete.
}
การดำเนินการเหล่านี้มีประโยชน์ในการเลื่อนเวลาทำงานและลดการกระตุกระหว่างการเปลี่ยนหน้า เราจะแจ้งข้อมูลเพิ่มเติมในภายหลัง
การทำให้ฟังก์ชันการทำงานทั่วไปในหน้าเว็บต่างๆ ซ้ำกันน้อยลง
การสืบทอดเป็นฟีเจอร์ที่มีประสิทธิภาพขององค์ประกอบที่กําหนดเอง ซึ่งให้บริการรูปแบบการสืบทอดมาตรฐานสําหรับเว็บ
ขออภัย Polymer 1.0 ยังไม่ได้ใช้งานการสืบทอดองค์ประกอบในขณะที่เขียนบทความนี้ ในระหว่างนี้ ฟีเจอร์ลักษณะการทำงานของ Polymer ก็มีประโยชน์ไม่แพ้กัน ลักษณะการทํางานเป็นเพียงมิกซ์อิน
แทนที่จะสร้างแพลตฟอร์ม API เดียวกันในทุกหน้า เราจึงควรทำให้โค้ดฐานซ้ำกันน้อยที่สุดด้วยการสร้างมิกซ์อินที่แชร์ ตัวอย่างเช่น PageBehavior
จะกำหนดพร็อพเพอร์ตี้/เมธอดทั่วไปที่หน้าเว็บทั้งหมดในแอปของเราต้องการ
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};
ดังที่คุณเห็น PageBehavior
ดําเนินการงานทั่วไปที่ทํางานเมื่อมีการเข้าชมหน้าใหม่ เช่น การอัปเดต document.title
, รีเซ็ตตําแหน่งการเลื่อน และการตั้งค่า Listener เหตุการณ์สําหรับเอฟเฟกต์การเลื่อนและการนำทางย่อย
หน้าเว็บแต่ละหน้าใช้ PageBehavior
โดยการโหลดเป็น Dependency และใช้ behaviors
นอกจากนี้ ผู้ใช้ยังสามารถลบล้างพร็อพเพอร์ตี้/เมธอดพื้นฐานได้หากจําเป็น ตัวอย่างเช่น ต่อไปนี้คือสิ่งที่ "คลาสย่อย" ของหน้าแรกลบล้าง
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>
รูปแบบการแชร์
เราใช้โมดูลสไตล์ที่แชร์ของ Polymer เพื่อแชร์สไตล์ในคอมโพเนนต์ต่างๆ ในแอป โมดูลสไตล์ช่วยให้คุณกำหนด CSS ส่วนหนึ่งเพียงครั้งเดียวแล้วนำไปใช้ซ้ำในตำแหน่งต่างๆ ทั่วทั้งแอปได้ "ตำแหน่งต่างๆ" ในที่นี้หมายถึงคอมโพเนนต์ต่างๆ
ใน IOWA เราได้สร้าง shared-app-styles
เพื่อแชร์คลาสสี การจัดวางตัวอักษร และเลย์เอาต์ในหน้าเว็บและคอมโพเนนต์อื่นๆ ที่เราสร้างขึ้น
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>
ในที่นี้ <style include="shared-app-styles"></style>
คือไวยากรณ์ของ Polymer สำหรับ "รวมสไตล์ในโมดูลชื่อ "shared-app-styles"
การแชร์สถานะแอปพลิเคชัน
ตอนนี้คุณทราบแล้วว่าหน้าทุกหน้าในแอปของเราเป็นองค์ประกอบที่กำหนดเอง เราพูดเรื่องนี้ไปหลายล้านครั้งแล้ว โอเค แต่หากหน้าเว็บทุกหน้าเป็นคอมโพเนนต์เว็บแบบสแตนด์อโลน คุณอาจสงสัยว่าเราแชร์สถานะในแอปได้อย่างไร
IOWA ใช้เทคนิคที่คล้ายกับการฉีดข้อมูล Dependency (Angular) หรือ Redux (React) ในการแชร์สถานะ เราสร้างพร็อพเพอร์ตี้ app
ทั่วโลกและแขวนพร็อพเพอร์ตี้ย่อยที่แชร์ไว้กับพร็อพเพอร์ตี้หลัก app
ระบบจะส่ง app
ไปทั่วแอปพลิเคชันโดยการแทรกลงในคอมโพเนนต์ทุกรายการที่ต้องการข้อมูล การใช้ฟีเจอร์การเชื่อมโยงข้อมูลของ Polymer ช่วยให้ทําสิ่งต่างๆ เหล่านี้ได้ง่ายขึ้น เนื่องจากเราเดินสายไฟได้โดยไม่ต้องเขียนโค้ด
<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>
องค์ประกอบ <google-signin>
จะอัปเดตพร็อพเพอร์ตี้ user
เมื่อผู้ใช้เข้าสู่ระบบแอปของเรา เนื่องจากพร็อพเพอร์ตี้ดังกล่าวเชื่อมโยงกับ app.currentUser
หน้าเว็บที่ต้องการเข้าถึงผู้ใช้ปัจจุบันจึงต้องเชื่อมโยงกับ app
และอ่านพร็อพเพอร์ตี้ย่อย currentUser
เทคนิคนี้มีประโยชน์สำหรับการแชร์สถานะในแอปอยู่แล้ว แต่ประโยชน์อีกอย่างหนึ่งคือเราสร้างองค์ประกอบการลงชื่อเพียงครั้งเดียวและนําผลลัพธ์ไปใช้ซ้ำในเว็บไซต์ เช่นเดียวกับคิวรีสื่อ การที่ทุกหน้าต้องลงชื่อเข้าใช้ซ้ำหรือสร้างชุดการค้นหาสื่อของตัวเองนั้นเป็นเรื่องที่สิ้นเปลือง แต่คอมโพเนนต์ที่รับผิดชอบฟังก์ชันการทำงาน/ข้อมูลทั่วทั้งแอปจะอยู่ที่ระดับแอป
ทรานซิชันหน้าเว็บ
เมื่อไปยังส่วนต่างๆ ของเว็บแอป Google I/O คุณจะเห็นการเปลี่ยนหน้าเว็บที่ลื่นไหล (ตามแบบ Material Design)

เมื่อผู้ใช้ไปยังหน้าใหม่ เหตุการณ์ต่างๆ จะเกิดขึ้นตามลําดับดังนี้
- การนําทางด้านบนจะเลื่อนแถบการเลือกไปยังลิงก์ใหม่
- บรรทัดแรกของหน้าจะค่อยๆ เลือนหายไป
- เนื้อหาของหน้าจะเลื่อนลงแล้วค่อยๆ เลือนหายไป
- เมื่อเล่นภาพเคลื่อนไหวเหล่านั้นย้อนกลับ หัวเรื่องและเนื้อหาของหน้าใหม่จะปรากฏขึ้น
- (ไม่บังคับ) หน้าใหม่จะทํางานเริ่มต้นเพิ่มเติม
หนึ่งในความท้าทายของเราคือการหาวิธีสร้างการเปลี่ยนภาพอย่างราบรื่นนี้โดยไม่ลดประสิทธิภาพ เรามีงานจำนวนมากที่ต้องทำอย่างต่อเนื่อง และเราไม่อนุญาตให้มีข้อบกพร่องในงานของเรา โซลูชันของเราคือ Web Animations API และ Promises รวมกัน การใช้ทั้ง 2 อย่างนี้ร่วมกันทำให้เรามีความหลากหลาย ระบบภาพเคลื่อนไหวแบบปลั๊กแอนด์เพลย์ และการควบคุมแบบละเอียดเพื่อลดอาการกระตุกของ das
วิธีการทำงาน
เมื่อผู้ใช้คลิกไปยังหน้าใหม่ (หรือกดย้อนกลับ/ไปข้างหน้า) runPageTransition()
ของเราจะทํางานอย่างน่าอัศจรรย์โดยเรียกใช้ชุด Promise การใช้ Promises ช่วยให้เราจัดระเบียบภาพเคลื่อนไหวอย่างละเอียดรอบคอบ และช่วยอธิบายเหตุผลของ "การทำงานแบบอะซิงโครนัส" ของภาพเคลื่อนไหว CSS และเนื้อหาที่โหลดแบบไดนามิก
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));
}
}
โปรดจำจากส่วน"การเขียนโค้ดซ้ำกันน้อยที่สุด: ฟังก์ชันการทำงานทั่วไปในหน้าเว็บต่างๆ" หน้าเว็บจะคอยฟังเหตุการณ์ DOM ของ page-transition-start
และ page-transition-done
ตอนนี้คุณจะเห็นตําแหน่งที่เหตุการณ์เหล่านั้นเริ่มทํางาน
เราใช้ Web Animations API แทนตัวช่วย runEnterAnimation
/runExitAnimation
ในกรณีของ runExitAnimation
เราจะดึงโหนด DOM 2 โหนด (ส่วนโฆษณา Masthead และพื้นที่เนื้อหาหลัก) ประกาศจุดเริ่มต้น/สิ้นสุดของภาพเคลื่อนไหวแต่ละรายการ และสร้าง GroupEffect
เพื่อเรียกใช้ทั้ง 2 รายการพร้อมกัน ดังนี้
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)
]);
}
เพียงแก้ไขอาร์เรย์เพื่อทำให้การเปลี่ยนมุมมองซับซ้อนขึ้น (หรือน้อยลง)
เอฟเฟกต์การเลื่อน
IOWA มีเอฟเฟกต์ที่น่าสนใจ 2-3 อย่างเมื่อคุณเลื่อนหน้าเว็บ รายการแรกคือปุ่มการทำงานแบบลอย (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>
การใช้การเลื่อนที่ราบรื่นทำได้โดยใช้องค์ประกอบเลย์เอาต์แอปของ Polymer ซึ่งจะมีเอฟเฟกต์การเลื่อนที่พร้อมใช้งานทันที เช่น การนําทางด้านบนแบบติดหนึบ/กลับมาแสดงอีกครั้ง เงาตกกระทบ การเปลี่ยนสีและพื้นหลัง เอฟเฟกต์ภาพพาโนรามา และการเลื่อนอย่างราบรื่น
// 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>
คือการนำทางแบบติดหนึบ ดังที่เห็นในวิดีโอ โฆษณาจะหายไปเมื่อผู้ใช้เลื่อนหน้าลงและกลับมาเมื่อเลื่อนกลับขึ้น

เราใช้องค์ประกอบ <app-header>
เกือบจะตรงตามที่เป็นอยู่ การวางองค์ประกอบนี้ลงในแอปและรับเอฟเฟกต์การเลื่อนที่ดูดีนั้นทำได้ง่ายมาก แน่นอนว่าเราติดตั้งใช้งานเองได้ แต่การที่มีรายละเอียดที่เขียนโค้ดไว้แล้วในคอมโพเนนต์ที่นำกลับมาใช้ซ้ำได้ช่วยประหยัดเวลาได้มาก
ประกาศองค์ประกอบ ปรับแต่งด้วยแอตทริบิวต์ เท่านี้ก็เรียบร้อย
<app-header reveals condenses effects="fade-background waterfall"></app-header>
บทสรุป
สําหรับ Progressive Web App ของ I/O เราสามารถสร้างส่วนหน้าทั้งหมดได้ภายในไม่กี่สัปดาห์ด้วยคอมโพเนนต์เว็บและวิดเจ็ตการออกแบบมาเทเรียลที่สร้างไว้ล่วงหน้าของ Polymer ฟีเจอร์ของ API เดิม (องค์ประกอบที่กําหนดเอง, Shadow DOM, <template>
) เหมาะสําหรับความยืดหยุ่นของ SPA อย่างเป็นธรรมชาติ ความสามารถในการนํากลับมาใช้ใหม่ช่วยประหยัดเวลาได้เป็นอย่างมาก
หากสนใจสร้าง Progressive Web App ของคุณเอง โปรดดูกล่องเครื่องมือสำหรับแอป กล่องเครื่องมือแอปของ Polymer คือคอลเล็กชันคอมโพเนนต์ เครื่องมือ และเทมเพลตสําหรับสร้าง PWA ด้วย Polymer ซึ่งช่วยให้เริ่มต้นใช้งานได้ง่าย