สรุป
มาดูว่าเราสร้างแอปหน้าเดียวโดยใช้คอมโพเนนต์ของเว็บ, Polymer และดีไซน์ Material แล้วเปิดตัวเป็นเวอร์ชันที่ใช้งานจริงใน Google.com ได้อย่างไร
ผลลัพธ์
- ได้รับการมีส่วนร่วมมากกว่าแอปที่มาพร้อมเครื่อง (ใช้เวลา 4:06 นาทีในเว็บบนอุปกรณ์เคลื่อนที่เทียบกับเวลา 2:40 นาทีของ Android)
- การแสดงผลครั้งแรกที่เร็วขึ้น 450 มิลลิวินาทีสำหรับผู้ใช้ที่กลับมาเนื่องจากการแคชของ Service Worker
- 84% ของผู้เข้าชมสนับสนุน Service Worker
- การบันทึกการเพิ่มลงในหน้าจอหลักเพิ่มขึ้น +900% เมื่อเทียบกับปี 2015
- ผู้ใช้ 3.8% ออฟไลน์ไปแล้วแต่ยังคงสร้างการดูหน้าเว็บ 11,000 ครั้ง
- 50% ของผู้ใช้ที่ลงชื่อเข้าใช้เปิดใช้การแจ้งเตือน
- มีการส่งการแจ้งเตือน 536,000 รายการไปยังผู้ใช้ (12% กลับมาใช้งานอีกครั้ง)
- 99% ของเบราว์เซอร์ของผู้ใช้รองรับ Polyfill คอมโพเนนต์ของเว็บ
ภาพรวม
ปีนี้เรามีโอกาสได้ทํางานกับ 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 Loading เมื่อจำเป็นเท่านั้น นอกจากนี้ Polymer ยังมีตัวช่วยที่ยอดเยี่ยมสำหรับการโหลดการนําเข้า HTML แบบแอซิงค์ด้วย
Polymer.Base.importHref('io-home-page.html', (e) => { ... });
IOWA ไม่ทำแบบนี้เนื่องจาก ก) เราขี้เกียจ และ ข) เรายังไม่แน่ใจว่าควรเพิ่มประสิทธิภาพได้มากน้อยเพียงใด การลงสีครั้งแรกของเราไปแล้วประมาณ 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 คุณจะสังเกตเห็นการเปลี่ยนหน้าอย่างราบรื่น (à la ดีไซน์ Material)
เมื่อผู้ใช้ไปยังหน้าใหม่ เหตุการณ์ต่างๆ จะเกิดขึ้นตามลําดับดังนี้
- แถบนำทางด้านบนจะเลื่อนแถบการเลือกไปยังลิงก์ใหม่
- บรรทัดแรกของหน้าจะค่อยๆ เลือนหายไป
- เนื้อหาของหน้าจะเลื่อนลงแล้วค่อยๆ เลือนหายไป
- เมื่อเล่นภาพเคลื่อนไหวเหล่านั้นย้อนกลับ หัวเรื่องและเนื้อหาของหน้าใหม่จะปรากฏขึ้น
- (ไม่บังคับ) หน้าใหม่จะทํางานเริ่มต้นเพิ่มเติม
หนึ่งในความท้าทายของเราคือการหาวิธีสร้างการเปลี่ยนภาพอย่างราบรื่นนี้โดยไม่ลดประสิทธิภาพ เรามีงานจำนวนมากที่ต้องทำอย่างต่อเนื่อง และข้อบกพร่องไม่อยู่ในงานของเรา โซลูชันของเราคือ 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 เราสามารถสร้างฟรอนท์เอนด์ทั้งหมดได้ในไม่กี่สัปดาห์ด้วยคอมโพเนนต์ของเว็บและวิดเจ็ตดีไซน์ Material ของ Polymer ที่สร้างไว้ล่วงหน้า ฟีเจอร์ของ API เดิม (องค์ประกอบที่กําหนดเอง, Shadow DOM, <template>
) เหมาะสําหรับความยืดหยุ่นของ SPA อย่างเป็นธรรมชาติ ความสามารถในการนํากลับมาใช้ใหม่ช่วยประหยัดเวลาได้เป็นอย่างมาก
หากสนใจสร้าง Progressive Web App ของคุณเอง โปรดดูกล่องเครื่องมือสำหรับแอป กล่องเครื่องมือแอปของ Polymer คือคอลเล็กชันคอมโพเนนต์ เครื่องมือ และเทมเพลตสําหรับสร้าง PWA ด้วย Polymer ซึ่งช่วยให้เริ่มต้นใช้งานได้ง่าย