สรุป
มาดูว่าเราสร้างแอปหน้าเดียวโดยใช้คอมโพเนนต์เว็บ, Polymer และดีไซน์ Material แล้วเปิดตัวเป็นเวอร์ชันที่ใช้งานจริงใน Google.com ได้อย่างไร
ผลลัพธ์
- ได้รับการมีส่วนร่วมมากกว่าแอปที่มาพร้อมเครื่อง (ใช้เวลา 4:06 นาทีในเว็บบนอุปกรณ์เคลื่อนที่เทียบกับเวลา 2:40 นาทีของ Android)
- การแสดงผลครั้งแรกที่เร็วขึ้น 450 มิลลิวินาทีสำหรับผู้ใช้ที่กลับมาเนื่องจากการแคชของ Service Worker
- 84% ของผู้เข้าชมสนับสนุน Service Worker
- การบันทึกการเพิ่มลงในหน้าจอหลักเพิ่มขึ้น +900% เมื่อเทียบกับปี 2015
- ผู้ใช้ 3.8% ออฟไลน์ แต่ยังคงมีการดูหน้าเว็บถึง 1.1 หมื่นครั้ง
- 50% ของผู้ใช้ที่ลงชื่อเข้าใช้เปิดใช้การแจ้งเตือน
- มีการส่งการแจ้งเตือน 5.36 แสนครั้งไปยังผู้ใช้ (12% นำการแจ้งเตือนกลับมา)
- 99% ของผู้ใช้ เบราว์เซอร์ที่รองรับ polyfills คอมโพเนนต์เว็บ
ภาพรวม
ปีนี้ผมรู้สึกยินดีที่ได้ได้ทำงานใน Progressive Web App ของ Google I/O 2016 ซึ่งมีชื่อว่า "IOWA" โดยเน้นอุปกรณ์เคลื่อนที่เป็นอันดับแรก ทำงานแบบออฟไลน์ได้อย่างสมบูรณ์ และได้รับแรงบันดาลใจมาจากดีไซน์ Material
IOWA คือแอปพลิเคชันหน้าเว็บเดียว (SPA) ที่สร้างขึ้นโดยใช้คอมโพเนนต์เว็บ, Polymer และ Firebase และมีแบ็กเอนด์ที่ครอบคลุม ซึ่งเขียนใน App Engine (Go) โปรแกรมจะแคชเนื้อหาล่วงหน้าโดยใช้โปรแกรมทำงานของบริการ โหลดหน้าเว็บใหม่แบบไดนามิก เปลี่ยนระหว่างมุมมองอย่างสวยงาม และนำเนื้อหามาใช้ซ้ำหลังจากการโหลดครั้งแรก
ในกรณีศึกษานี้ ผมจะพูดถึงการตัดสินใจด้านสถาปัตยกรรมที่น่าสนใจกว่าเดิม ฟรอนท์เอนด์ หากคุณสนใจซอร์สโค้ด โปรดดูใน 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 เป็นโค้ดที่ดี
- ...อีกมากมาย
เราใช้ประโยชน์อย่างเต็มที่จากสิทธิประโยชน์เหล่านี้ใน IOWA มาเจาะลึกรายละเอียดกัน
การเปิดใช้งานหน้าเว็บแบบไดนามิก
องค์ประกอบ <template>
เป็นวิธีมาตรฐานของเบราว์เซอร์ในการสร้างมาร์กอัปที่ใช้ซ้ำได้ <template>
มี 2 เกม
ลักษณะเฉพาะที่ SPA ใช้ประโยชน์ได้ ก่อนอื่น ทุกอย่างภายใน
ของ <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>
พอลิเมอร์ ขยาย <template>
ด้วย องค์ประกอบที่กำหนดเองส่วนขยายประเภท 2-3 ประเภท
คือ <template is="dom-if">
และ <template is="dom-repeat">
ทั้ง 2 แบบเป็นแบบกำหนดเอง
องค์ประกอบที่ขยาย <template>
ด้วยความสามารถเพิ่มเติม และขอขอบคุณ
คอมโพเนนต์เว็บแบบประกาศ ทั้ง 2 อย่างนี้ทำงานตามที่คุณคาดหวังไว้จริงๆ
คอมโพเนนต์แรกจะประทับมาร์กอัปตามเงื่อนไข รายการที่ 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>
ระดับบนสุด) มุมมองแบบไดนามิก + แบบ Lazy ที่ใช้คอมโพเนนต์เว็บ FTW
การปรับปรุงในอนาคต
เมื่อโหลดหน้าเว็บเป็นครั้งแรก เราจะโหลดการนำเข้า HTML ทั้งหมดสำหรับแต่ละหน้าพร้อมกัน การปรับปรุงที่เห็นได้ชัดคือการโหลดการกำหนดองค์ประกอบแบบ Lazy Loading เมื่อจำเป็นเท่านั้น Polymer ยังมีตัวช่วยที่ดีสำหรับการนำเข้า HTML ที่โหลดแบบไม่พร้อมกัน:
Polymer.Base.importHref('io-home-page.html', (e) => { ... });
IOWA ไม่ทำแบบนี้เนื่องจาก ก) เราขี้เกียจ และ ข) เรายังไม่แน่ใจว่าควรเพิ่มประสิทธิภาพได้มากน้อยเพียงใด การลงสีครั้งแรกของเราไปแล้วประมาณ 1 วินาที
การจัดการวงจรของหน้าเว็บ
Custom Elements API กำหนด "lifecycle Callback" ในการจัดการสถานะของ คอมโพเนนต์ เมื่อใช้วิธีการเหล่านี้ คุณจะติดใจในชีวิต ของคอมโพเนนต์
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.
}
การใช้ประโยชน์จาก Callback เหล่านี้ใน IOWA นั้นทำได้ง่าย อย่าลืมว่าทุกหน้าเป็นโหนด DOM แบบในตัว การไปยัง "มุมมองใหม่" ใน SPA ของเราก็คือการแนบโหนดหนึ่งเข้ากับ DOM และนำอีกโหนดออก
เราใช้ attachedCallback
เพื่อทำการตั้งค่า (สถานะเริ่มต้น แนบ Listener เหตุการณ์) เมื่อผู้ใช้ไปยังหน้าอื่น 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
ส่งผ่านไปทั่วแอปพลิเคชันของเราโดยการแทรกโค้ดลงในคอมโพเนนต์ทั้งหมดที่
ต้องการข้อมูล การใช้ฟีเจอร์การเชื่อมโยงข้อมูลของ 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()
ของเราเตอร์จะทำงานได้อย่างมีประสิทธิภาพด้วยการเรียกใช้ผ่านชุดคำสัญญา การใช้ 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 จะมีเอฟเฟกต์ที่น่าสนใจบางอย่างเมื่อคุณเลื่อนหน้าเว็บ ปุ่มแรกคือปุ่มการทำงานแบบลอย (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 ซึ่งเป็นวิธีง่ายๆ ในการเริ่มต้นใช้งาน