Tóm tắt
Tìm hiểu cách chúng tôi xây dựng một ứng dụng một trang bằng các thành phần web, Polymer và Material Design, đồng thời phát hành ứng dụng đó trên Google.com.
Kết quả
- Mức độ tương tác cao hơn so với ứng dụng gốc (4:06 phút web dành cho thiết bị di động so với 2:40 phút của Android).
- Tăng tốc độ vẽ lần đầu tiên cho người dùng cũ thêm 450 mili giây nhờ tính năng lưu vào bộ nhớ đệm của worker dịch vụ
- 84% khách truy cập đã hỗ trợ Service Worker
- Số lượt lưu vào màn hình chính bằng tính năng Thêm vào màn hình chính đã tăng hơn 900% so với năm 2015.
- 3,8% người dùng không có kết nối mạng nhưng vẫn tiếp tục tạo ra 11.000 lượt xem trang!
- 50% người dùng đã đăng nhập đã bật thông báo.
- 536 nghìn thông báo đã được gửi đến người dùng (12% người dùng đã quay lại).
- 99% trình duyệt của người dùng hỗ trợ các thành phần web polyfill
Tổng quan
Năm nay, tôi rất vui khi được tác động đến ứng dụng web tiến bộ của Google I/O 2016, được đặt tên là "IOWA". Ứng dụng này ưu tiên thiết bị di động, hoạt động hoàn toàn khi không có mạng và lấy cảm hứng rất nhiều từ Material Design.
IOWA là một ứng dụng một trang (SPA) được tạo bằng các thành phần web, Polymer và Firebase, đồng thời có một phần phụ trợ rộng rãi được viết bằng App Engine (Go). Công cụ này lưu nội dung vào bộ nhớ đệm trước bằng cách sử dụng trình chạy dịch vụ, tải động các trang mới, chuyển đổi linh hoạt giữa các chế độ xem và sử dụng lại nội dung sau lần tải đầu tiên.
Trong nghiên cứu điển hình này, tôi sẽ trình bày một số quyết định thú vị hơn về kiến trúc mà chúng tôi đưa ra cho giao diện người dùng. Nếu bạn quan tâm đến mã nguồn, hãy xem qua trên GitHub.
Tạo SPA bằng các thành phần web
Mỗi trang là một thành phần
Một trong những khía cạnh cốt lõi về giao diện người dùng là giao diện này tập trung vào các thành phần web. Trên thực tế, mỗi trang trong SPA đều là một thành phần web:
<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>
Tại sao chúng tôi làm việc này? Lý do đầu tiên là mã này dễ đọc. Đối với người đọc lần đầu, bạn sẽ thấy rõ mọi trang trong ứng dụng của chúng ta. Lý do thứ hai là các thành phần web có một số thuộc tính hữu ích để tạo SPA. Nhiều vấn đề thường gặp (quản lý trạng thái, kích hoạt chế độ xem, phạm vi kiểu) sẽ biến mất nhờ các tính năng vốn có của phần tử <template>
, Phần tử tuỳ chỉnh và Shadow DOM. Đây là các công cụ dành cho nhà phát triển được tích hợp vào trình duyệt. Tại sao bạn không tận dụng những lợi ích này?
Bằng cách tạo Phần tử tùy chỉnh cho mỗi trang, chúng tôi nhận được rất nhiều lợi ích miễn phí:
- Quản lý vòng đời trang.
- CSS/HTML trong phạm vi cụ thể cho trang.
- Tất cả CSS/HTML/JS dành riêng cho một trang được đóng gói và tải cùng nhau khi cần.
- Bạn có thể sử dụng lại thành phần hiển thị. Vì các trang là nút DOM, nên chỉ cần thêm hoặc xoá các trang đó là bạn có thể thay đổi chế độ xem.
- Những người duy trì sau này có thể hiểu ứng dụng của chúng ta chỉ bằng cách tìm hiểu mã đánh dấu.
- Bạn có thể cải thiện dần các mã đánh dấu do máy chủ hiển thị khi trình duyệt đăng ký và nâng cấp các định nghĩa phần tử.
- Phần tử tuỳ chỉnh có mô hình kế thừa. Mã DRY là mã tốt.
- …và nhiều tính năng khác.
Chúng tôi đã tận dụng tối đa những lợi ích này trong IOWA. Hãy cùng tìm hiểu một số thông tin chi tiết.
Kích hoạt trang một cách linh động
Phần tử <template>
là cách thức tiêu chuẩn của trình duyệt để tạo mã đánh dấu có thể sử dụng lại. <template>
có hai đặc điểm mà SPA có thể tận dụng. Trước tiên, mọi nội dung bên trong <template>
đều không hoạt động cho đến khi một thực thể của mẫu được tạo. Thứ hai, trình duyệt phân tích cú pháp mã đánh dấu nhưng không thể truy cập nội dung từ trang chính. Đây là một đoạn mã đánh dấu thực sự, có thể sử dụng lại. Ví dụ:
<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 mở rộng <template>
bằng một số phần tử tuỳ chỉnh của phần mở rộng loại, cụ thể là <template is="dom-if">
và <template is="dom-repeat">
. Cả hai đều là phần tử tuỳ chỉnh mở rộng <template>
với các tính năng bổ sung. Và nhờ bản chất khai báo của các thành phần web, cả hai đều thực hiện chính xác những gì bạn mong đợi.
Thành phần đầu tiên đánh dấu con dấu dựa trên một điều kiện. Phần tử thứ hai lặp lại
thẻ đánh dấu cho mọi mục trong danh sách (mô hình dữ liệu).
IOWA sử dụng các phần tử mở rộng loại này như thế nào?
Nếu bạn nhớ thì mọi trang trong IOWA đều là một thành phần web. Tuy nhiên, sẽ thật ngớ ngẩn khi khai báo mọi thành phần trong lần tải đầu tiên. Tức là bạn phải tạo một bản sao của mỗi trang khi ứng dụng tải lần đầu tiên. Chúng tôi không muốn làm giảm hiệu suất tải ban đầu, đặc biệt là khi một số người dùng sẽ chỉ điều hướng đến 1 hoặc 2 trang.
Giải pháp của chúng tôi là gian lận. Trong IOWA, chúng ta gói từng phần tử của trang trong một <template is="dom-if">
để nội dung của phần tử đó không tải trong lần khởi động đầu tiên. Sau đó, chúng ta kích hoạt các trang khi thuộc tính name
của mẫu khớp với URL. Thành phần web <lazy-pages>
sẽ xử lý tất cả logic này cho chúng ta. Mã đánh dấu sẽ có dạng như sau:
<!-- 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>
Điều tôi thích về tính năng này là mọi trang đều được phân tích cú pháp và sẵn sàng hoạt động khi trang tải, nhưng CSS/HTML/JS của trang chỉ được thực thi theo yêu cầu (khi <template>
mẹ được đóng dấu). Khung hiển thị động và khung hiển thị từng phần sử dụng thành phần web FTW.
Các điểm cải tiến trong tương lai
Khi trang tải lần đầu tiên, chúng tôi sẽ tải tất cả lệnh Nhập HTML cho mỗi trang cùng một lúc. Một điểm cải tiến rõ ràng là chỉ tải từng phần định nghĩa phần tử khi cần. Polymer cũng có một trình trợ giúp hữu ích để tải không đồng bộ các tệp nhập HTML:
Polymer.Base.importHref('io-home-page.html', (e) => { ... });
IOWA không làm điều này vì a) chúng tôi có lazy và b) chúng tôi không rõ hiệu suất sẽ tăng đến mức nào. Lần vẽ đầu tiên của chúng ta đã mất khoảng 1 giây.
Quản lý vòng đời trang
API Phần tử tuỳ chỉnh xác định "lệnh gọi lại trong vòng đời" để quản lý trạng thái của một thành phần. Khi triển khai các phương thức này, bạn sẽ có các hook miễn phí trong vòng đời của một thành phần:
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.
}
Bạn có thể dễ dàng tận dụng các lệnh gọi lại này trong IOWA. Hãy nhớ rằng mỗi trang là một nút DOM độc lập. Việc điều hướng đến "chế độ xem mới" trong SPA của chúng tôi là vấn đề đính kèm một nút vào DOM và xóa một nút khác.
Chúng ta đã sử dụng attachedCallback
để thực hiện công việc thiết lập (trạng thái khởi tạo, đính kèm trình nghe sự kiện). Khi người dùng chuyển đến một trang khác, detachedCallback
sẽ dọn dẹp (xoá trình nghe, đặt lại trạng thái chia sẻ). Chúng tôi cũng đã mở rộng các phương thức gọi lại trong vòng đời gốc bằng một số phương thức gọi lại của riêng mình:
onPageTransitionDone() {
// page transition animations are complete.
},
onSubpageTransitionDone() {
// sub nav/tab page transitions are complete.
}
Đây là những tính năng bổ sung hữu ích để trì hoãn công việc và giảm thiểu hiện tượng giật giữa các lượt chuyển đổi trang. Chúng ta sẽ nói thêm về điều này ở phần sau.
Giảm thiểu chức năng chung trên các trang
Tính kế thừa là một tính năng mạnh mẽ của Phần tử tuỳ chỉnh. Lớp này cung cấp một mô hình kế thừa tiêu chuẩn cho web.
Rất tiếc, Polymer 1.0 chưa triển khai tính kế thừa của phần tử tại thời điểm viết bài. Trong thời gian chờ đợi, tính năng Hành vi của Polymer cũng hữu ích không kém. Hành vi chỉ là sự pha trộn.
Thay vì tạo cùng một nền tảng API trên tất cả các trang, bạn nên KHÔI cơ sở mã bằng cách tạo các hỗn hợp dùng chung. Ví dụ: PageBehavior
xác định các thuộc tính/phương thức phổ biến mà tất cả các trang trong ứng dụng của chúng ta cần:
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};
Như bạn có thể thấy, PageBehavior
thực hiện các tác vụ phổ biến chạy khi một trang mới được truy cập. Các thao tác như cập nhật document.title
, đặt lại vị trí cuộn và thiết lập trình nghe sự kiện cho hiệu ứng cuộn và điều hướng phụ.
Các trang riêng lẻ sử dụng PageBehavior
bằng cách tải nó dưới dạng phần phụ thuộc và sử dụng behaviors
.
Các lớp con cũng có thể ghi đè các thuộc tính/phương thức cơ sở của lớp mẹ nếu cần. Ví dụ: sau đây là nội dung mà "lớp con" trên trang chủ của chúng tôi ghi đè:
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>
Kiểu chia sẻ
Để chia sẻ kiểu trên các thành phần khác nhau trong ứng dụng, chúng tôi đã sử dụng mô-đun kiểu dùng chung của Polymer. Mô-đun kiểu cho phép bạn xác định một đoạn CSS một lần và sử dụng lại đoạn CSS đó ở nhiều vị trí trong ứng dụng. Đối với chúng tôi, "nhiều vị trí" có nghĩa là nhiều thành phần.
Trong IOWA, chúng ta đã tạo shared-app-styles
để chia sẻ màu sắc, kiểu chữ và lớp bố cục trên các trang và thành phần khác mà chúng tôi đã tạo.
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>
Ở đây, <style include="shared-app-styles"></style>
là cú pháp của Polymer để nói "bao gồm các kiểu trong mô-đun có tên "shared-app-styles".
Chia sẻ trạng thái của ứng dụng
Bây giờ, bạn đã biết rằng mỗi trang trong ứng dụng của chúng ta đều là một Phần tử tuỳ chỉnh. Tôi đã nói điều này hàng triệu lần. Ok, nhưng nếu mỗi trang đều là một thành phần web độc lập, bạn có thể tự hỏi làm cách nào để chia sẻ trạng thái trên ứng dụng.
IOWA sử dụng một kỹ thuật tương tự như chèn phần phụ thuộc (Angular) hoặc giảm thiểu (React) để chia sẻ trạng thái. Chúng ta đã tạo một thuộc tính app
toàn cục và treo các thuộc tính phụ dùng chung trên đó. app
được truyền xung quanh ứng dụng của chúng ta bằng cách chèn vào mọi thành phần cần dữ liệu của app
. Việc sử dụng các tính năng liên kết dữ liệu của Polymer giúp việc này trở nên dễ dàng vì chúng ta có thể kết nối mà không cần viết mã:
<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>
Phần tử <google-signin>
sẽ cập nhật thuộc tính user
khi người dùng đăng nhập vào ứng dụng của chúng ta. Vì thuộc tính đó được liên kết với app.currentUser
, nên mọi trang muốn truy cập vào người dùng hiện tại chỉ cần liên kết với app
và đọc thuộc tính phụ currentUser
. Bản thân kỹ thuật này rất hữu ích để chia sẻ trạng thái trên ứng dụng. Tuy nhiên, một lợi ích khác là chúng ta đã tạo ra một thành phần đăng nhập duy nhất và sử dụng lại kết quả của thành phần đó trên trang web. Điều tương tự cũng áp dụng cho truy vấn nội dung nghe nhìn. Sẽ là lãng phí nếu mỗi trang phải đăng nhập sao chép hoặc tạo một nhóm truy vấn nội dung nghe nhìn riêng. Thay vào đó, các thành phần chịu trách nhiệm về chức năng/dữ liệu trên toàn ứng dụng tồn tại ở cấp ứng dụng.
Chuyển đổi trang
Khi di chuyển trong ứng dụng web Google I/O, bạn sẽ nhận thấy các hiệu ứng chuyển đổi trang mượt mà (theo phong cách material design).
Khi người dùng chuyển đến một trang mới, một chuỗi những việc sẽ xảy ra:
- Thanh điều hướng trên cùng sẽ trượt thanh lựa chọn đến đường liên kết mới.
- Tiêu đề của trang sẽ mờ dần.
- Nội dung của trang trượt xuống rồi mờ dần.
- Bằng cách đảo ngược các hoạt ảnh đó, tiêu đề và nội dung của trang mới sẽ xuất hiện.
- (Không bắt buộc) Trang mới thực hiện thêm công việc khởi chạy.
Một trong những thách thức của chúng tôi là tìm ra cách tạo ra hiệu ứng chuyển cảnh mượt mà mà không làm giảm hiệu suất. Vì có rất nhiều công việc cần xử lý linh hoạt nên bữa tiệc của chúng ta không được chấp nhận hiện tượng giật. Giải pháp của chúng tôi là kết hợp Web Animations API và Promises. Việc sử dụng cả hai công cụ này mang lại cho chúng tôi tính linh hoạt, hệ thống ảnh động dạng cắm và chạy cũng như khả năng kiểm soát chi tiết để giảm thiểu hiện tượng giật das.
Cách hoạt động
Khi người dùng nhấp vào một trang mới (hoặc nhấn nút quay lại/tiến), runPageTransition()
của bộ định tuyến sẽ thực hiện phép thuật của nó bằng cách chạy qua một loạt Lời hứa. Việc sử dụng Lời hứa cho phép chúng tôi điều phối cẩn thận ảnh động và giúp hợp lý hoá tính "không đồng bộ" của Ảnh động CSS cũng như tải nội dung một cách linh động.
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));
}
}
Gọi lại từ phần "Giữ cho mọi thứ ở trạng thái DRY: chức năng phổ biến trên các trang", các trang sẽ theo dõi các sự kiện DOM page-transition-start
và page-transition-done
. Bây giờ, bạn sẽ thấy vị trí các sự kiện đó được kích hoạt.
Chúng tôi đã sử dụng API Ảnh động trên web thay vì trình trợ giúp runEnterAnimation
/runExitAnimation
. Trong trường hợp runExitAnimation
, chúng ta sẽ lấy một vài nút DOM (quảng cáo trên đầu trang chủ và vùng nội dung chính), khai báo thời điểm bắt đầu/kết thúc từng ảnh động và tạo một GroupEffect
để chạy hai nút này song song:
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)
]);
}
Bạn chỉ cần sửa đổi mảng này để các hiệu ứng chuyển đổi khung hiển thị trở nên chi tiết hơn (hoặc kém hơn)!
Hiệu ứng cuộn
IOWA có một số hiệu ứng thú vị khi bạn cuộn trang. Đầu tiên là nút hành động nổi (FAB) đưa người dùng quay lại đầu trang:
<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>
Thao tác cuộn mượt mà được triển khai bằng cách sử dụng phần tử bố cục ứng dụng của Polymer. Các hiệu ứng này cung cấp các hiệu ứng cuộn ngay từ đầu như thanh điều hướng trên cùng cố định/trả về, bóng đổ, chuyển đổi màu và nền, hiệu ứng thị sai và cuộn mượt.
// 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.
}
Một nơi khác chúng tôi sử dụng các phần tử <app-layout>
là cho thanh điều hướng cố định. Như bạn có thể thấy trong video, nhãn này sẽ biến mất khi người dùng cuộn xuống phía dưới trang và quay lại khi cuộn lên lại.
Chúng tôi đã sử dụng phần tử <app-header>
gần như nguyên gốc. Bạn có thể dễ dàng truy cập và nhận được các hiệu ứng cuộn thú vị trong ứng dụng. Chắc chắn là chúng tôi đã có thể tự triển khai chúng, nhưng việc có các chi tiết đã được mã hoá trong một thành phần có thể sử dụng lại giúp tiết kiệm rất nhiều thời gian.
Khai báo phần tử. Tuỳ chỉnh bằng các thuộc tính. Bạn đã hoàn tất!
<app-header reveals condenses effects="fade-background waterfall"></app-header>
Kết luận
Đối với ứng dụng web tiến bộ I/O, chúng tôi đã có thể xây dựng toàn bộ giao diện người dùng trong vài tuần nhờ các thành phần web và tiện ích thiết kế Material Design được thiết kế sẵn của Polymer. Các tính năng của API gốc (Thành phần tuỳ chỉnh, Shadow DOM, <template>
) tự nhiên phù hợp với tính linh động của SPA. Tính năng tái sử dụng giúp tiết kiệm rất nhiều thời gian.
Nếu bạn muốn tạo ứng dụng web tiến bộ của riêng mình, hãy tham khảo Bộ công cụ ứng dụng. Hộp công cụ ứng dụng của Polymer là một tập hợp các thành phần, công cụ và mẫu để tạo PWA bằng Polymer. Đây là cách dễ dàng để bắt đầu và chạy.