Tóm tắt
Tìm hiểu cách chúng tôi tạo ứng dụng trang đơn bằng các thành phần web, Polymer và Material Design, đồng thời triển khai ứng dụng chính thức 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).
- Thời gian vẽ đầu tiên cho người dùng cũ nhanh hơn 450 mili giây nhờ tính năng lưu vào bộ nhớ đệm của trình chạy dịch vụ
- 84% khách truy cập đã hỗ trợ Service Worker
- Số lượt 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% số 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% đưa họ trở lại).
- 99% người dùng các trình duyệt có hỗ trợ thành phần web polyfill
Tổng quan
Năm nay, tôi rất vui khi được làm việc với ứng dụng web tiến bộ Google I/O 2016, có tên một cách trìu mến là "IOWA". Đây là phiên bản ưu tiên cho thiết bị di động, hoạt động hoàn toàn không cần mạng và lấy cảm hứng từ thiết kế Material Design.
IOWA là một ứng dụng trang đơn (SPA) được xây dựng bằng thành phần web, Polymer và Firebase, đồng thời có phần phụ trợ mở rộng được viết trong App Engine (Go). Dịch vụ này lưu trước nội dung trong bộ nhớ đệm bằng cách sử dụng một service worker, tự động tải các trang mới, chuyển đổi linh hoạt giữa các khung hiển thị 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 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.
Xây dựng SPA bằng các thành phần web
Mỗi trang như 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 của chúng tôi là giao diện người dùng tập trung vào các thành phần web. Trên thực tế, mỗi trang trong SPA của chúng tôi đề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 có thể đọc được. Là người mới đọc, có thể thấy rõ nội dung của mỗi trang trong ứng dụng của chúng tôi. Lý do thứ hai là các thành phần web có một số thuộc tính hữu ích để xây dựng SPA. Rất 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ẽ không còn do các tính năng vốn có của phần tử <template>
, Phần tử tuỳ chỉnh và DOM tối. Đây là những 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 đều được nhóm và tải cùng nhau khi cần thiết.
- Các thành phần hiển thị có thể sử dụng lại được. Vì trang là các nút DOM, chỉ cần thêm hoặc xoá chúng cũng sẽ thay đổi chế độ xem.
- Những nhà bảo trì trong tương lai chỉ cần tìm hiểu mã đánh dấu là có thể hiểu được ứng dụng của chúng ta.
- Mã đánh dấu do máy chủ hiển thị có thể được nâng cao dần 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.
- ...còn rất nhiều nội dung 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 đi sâu vào một số 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ó 2
đặc điểm mà các SPA có thể tận dụng. Trước tiên, mọi thứ bên trong
của <template>
là trơ 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 đánh dấu nhưng không thể truy cập vào nội dung từ trang chính. Đây là một đoạn mã đánh dấu thực và 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>
với một số phần tử tuỳ chỉnh tiện ích loại,
cụ thể là <template is="dom-if">
và <template is="dom-repeat">
. Cả hai đều là tuỳ chỉnh
các phần tử mở rộng <template>
cùng với các chức năng bổ sung. Và nhờ có
bản chất khai báo của các thành phần web, cả hai đều hoạt động 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. Lần lặp lại thứ hai
đá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ử phần mở rộng loại này như thế nào?
Nếu bạn nhớ lại, mỗi trang trong IOWA là một thành phần web. Tuy nhiên, sẽ khá ngớ ngẩn nếu khai báo mọi thành phần trong lần tải đầu tiên. Điều đó có nghĩa là bạn phải tạo một phiên bản 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 ảnh hưởng đến hiệu suất tải ban đầu, đặc biệt là khi một số người dùng chỉ chuyển đế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 phần tử của mỗi trang trong một <template is="dom-if">
để nội dung của tệp không tải trong lần khởi động đầu tiên. Sau đó, chúng tôi sẽ 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>
xử lý toàn bộ 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 ở đâ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 chỉ được thực thi theo yêu cầu (khi <template>
mẹ của trang đượ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 các định nghĩa phần tử khi cần thiết. Polymer cũng có một trình trợ giúp hữu ích để nhập HTML tải không đồng bộ:
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 là khoảng 1 giây.
Quản lý vòng đời trang
Custom Elements API định nghĩa "các phương thức 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 pháp này, bạn sẽ có cơ hội làm quen với cuộc sống mà không mất phí 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. Chuyển đế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à xoá 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 nội dung bổ sung hữu ích trong việc trì hoãn công việc và giảm thiểu hiện tượng giật giữa các lần chuyển đổi trang. Chúng ta sẽ nói thêm về điều này ở phần sau.
Làm khô 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ô hình kế thừa 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 DẨY khô
cơ sở mã bằng cách tạo các trình trộn dùng chung. Chẳng hạn, PageBehavior
xác định phổ biến
thuộc tính/phương thức 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 sẽ chạy khi một trang mới
đã truy cập. Những việ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
.
Bạn cũng có thể tuỳ ý ghi đè các thuộc tính/phương thức cơ sở nếu cần. Ví dụ:
đây là nội dung trên trang chủ "lớp học phụ" 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 nhiều thành phần trong ứng dụng, chúng ta đã 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 ở nhiều vị trí trong toàn bộ ứng dụng. Đối với chúng tôi, "những địa điểm khác nhau" có nghĩa là các thành phần khác nhau.
Trong IOWA, chúng ta đã tạo shared-app-styles
để chia sẻ màu, kiểu chữ và bố cục
trên các trang và các 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 "thêm các kiểu vào mô-đun có tên là "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 đó cả triệu lần. Được rồi, nhưng nếu mỗi trang đều là thành phần web độc lập, thì có thể bạn sẽ tự hỏi về cách chúng ta chia sẻ trạng thái trên ứng dụng.
IOWA sử dụng kỹ thuật tương tự như chèn phần phụ thuộc (Angular) hoặc Redux (React) để chia sẻ trạng thái. Chúng tôi đã tạo một tài sản app
toàn cầu và treo các tài sản phụ dùng chung bên ngoài tài sản đó. app
được truyền qua ứng dụng của chúng ta bằng cách chèn ứng dụng đó vào mọi thành phần
cần dữ liệu của nó. Việc sử dụng các tính năng liên kết dữ liệu của Polymer sẽ dễ dàng hơn vì chúng ta có thể thực hiện việc đấu dây 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>
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 trong việc chia sẻ trạng thái trên toàn ứng dụng. Tuy nhiên, một lợi ích khác là cuối cùng chúng tôi đã tạo một phần tử đăng nhập duy nhất và sử dụng lại kết quả của phần tử đó trên trang web. Tương tự như vậy đối với các truy vấn nội dung nghe nhìn. Sẽ là lãng phí nếu mỗi trang cần đăng nhập sao chép hoặc tạo 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 điều hướng trong ứng dụng web Google I/O, bạn sẽ thấy ứng dụng này hiệu ứng chuyển đổi trang (à la Material Design).
Khi người dùng chuyển đến một trang mới, một chuỗi các hoạt động 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 đề trang mờ dần.
- Nội dung trang trượt xuống rồi biến mất.
- 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 tác vụ 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 tiến/lùi), runPageTransition()
của bộ định tuyến sẽ thực hiện điều kỳ diệu 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 cẩn thận sắp xếp các ảnh động và giúp hợp lý hoá "tính không đồng bộ" về Hoạt ảnh CSS và nội dung tải độ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));
}
}
Ôn lại từ phần "Giữ cho nội dung ở trạng thái KHÔ: chức năng phổ biến trên các trang",
các trang theo dõi các sự kiện DOM page-transition-start
và page-transition-done
. Bây giờ, bạn đã biết vị trí kích hoạt những sự kiện đó.
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. Chúng cung cấp các hiệu ứng cuộn ngay lập tức như thanh điều hướng cố định/trả về trên cùng, bóng đổ, chuyển tiếp màu sắc và nền, hiệu ứng thị sai và cuộn mượt mà.
// 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 vị trí khác chúng tôi sử dụng các phần tử <app-layout>
là cho đ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 ta đã sử dụng phần tử <app-header>
gần như nguyên bản. Họ rất dễ dàng truy cập để thu hút
hiệu ứng cuộn ưa thích trong ứng dụng. Chắc chắn là chúng ta có thể tự triển khai chúng, nhưng việc có các chi tiết đã được mã hoá trong 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 trang tổng quan 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 các API gốc (Phần tử tuỳ chỉnh, Shadow DOM, <template>
) bổ sung cho sự linh hoạt của SPA một cách tự nhiên. Việc tái sử dụng giúp tiết kiệm được rất nhiều thời gian.
Nếu bạn muốn tạo một ứng dụng web tiến bộ của riêng mình, hãy tham khảo App Toolbox. 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à một cách dễ dàng để thiết lập và sử dụng.