快速摘要
在本文中, 我们将了解浏览旧的非 PWA 网站的用户的痛点以及 PWA 使网络变得更好的希望. 您将学习制作非常酷的 PWA 的大多数重要技术, 例如 service worker,web push notification 和 IndexedDB.
这是我父亲的生日, 我想给他订一块巧克力蛋糕和一件衬衫. 我前往谷歌搜索巧克力蛋糕并点击搜索结果中的第一个链接. 有一个屏幕空白几秒钟; 我不明白发生了什么. 耐心地盯着几秒钟后, 我的手机屏幕上装满了美味的蛋糕. 当我点击其中一个查看其详细信息时, 我得到了一个丑陋的弹出窗口, 要求我安装一个 Android 应用程序, 这样我就可以在订购蛋糕时获得丝般顺畅的体验.
那令人失望. 我的良心不允许我点击 "安装" 按钮. 我想做的就是点一块小蛋糕然后走开.
我点击了弹出窗口右侧的十字图标, 尽快摆脱它. 但随后安装弹出窗口位于屏幕底部, 占据了四分之一的空间. 随着片状 UI 的向下滚动是一个挑战. 我不知何故设法订购了荷兰蛋糕.
在经历了这种可怕的经历后, 我的下一个挑战是为我爸爸订购一件衬衫. 和以前一样, 我在谷歌搜索衬衫. 我点击了第一个链接, 眨眼间, 整个内容就在我面前. 滚动很顺利. 没有安装弹窗. 我觉得好像在浏览本机应用程序. 有一段时间我互联网断开了连接, 但我仍然能够看到内容而不是恐龙游戏. 即使有我的网络, 我还是为父亲订购了一件衬衫和牛仔裤. 最令人惊讶的是, 我收到了有关订单的通知.
我会称之为丝般顺畅的体验. 这些人做得对. 每个网站都应该为他们的用户做. 它被称为渐进式网络应用程序 PWA.
正如 Alex Russell 所说 one of his blog posts:
"It happens on the web from time to time that powerful technologies come to exist without the benefit of marketing departments or slick packaging. They linger and grow at the peripheries, becoming old-hat to a tiny group while remaining nearly invisible to everyone else. Until someone names them."
Web 上丝滑顺畅体验, PWA
渐进式 Web 应用程序 (PWA) 更像是一种涉及技术组合的方法, 可用于制作功能强大的 Web 应用程序. 随着用户体验的改善, 人们将花费更多时间在网站上并看到更多广告. 他们倾向于购买更多, 并且通知更新, 他们更有可能经常访问. 英国 "金融时报" 在 2011 年放弃了其原生应用程序, 并使用当时可用的最佳技术构建了一个 Web 应用程序. 现在, 该产品已发展成为一个成熟的 PWA.
但是, 毕竟这一次, 为什么当原生应用程序很好完成这项工作时, 你会构建一个 Web 应用程序吗?
我们来看看 Google IO 17 中分享的一些指标.
五十亿台设备连接到网络, 使网络成为计算历史上最大的平台. 在移动网络上, 每月有 1140 万独立访问者访问前 1000 个网站, 400 万访问前千名应用. 移动网络的用户数量是原生应用程序的四倍. 但是, 这个数字在交互方面急剧下降.
用户在原生应用程序中平均花费 188.6 分钟, 在移动网络上花费仅 9.3 分钟. 原生应用程序利用操作系统的强大功能发送推送通知, 为用户提供重要更新. 它们提供了比浏览器中的网站更好的用户体验和更快的启动. 用户只需点击主屏幕上的应用程序图标, 而不是在 Web 浏览器中键入 URL.
网络上的大多数访问者都不太可能回来, 因此开发人员提出了向他们展示弹窗以安装本机应用程序的解决方法, 以便让他们深入参与. 但是, 用户必须完成安装本机应用程序二进制文件的繁琐程序. 强制用户安装应用程序很烦人, 并且进一步降低了他们首先安装应用程序的可能性. 网络的机会很明显.
- (function main () {
- /* navigator is a Web API that allows scripts to register themselves and carry out their activities. */
- if ('serviceWorker' in navigator) {
- console.log('Service Worker is supported in your browser')
- /* register method takes in the path of service worker file and returns a promises, which returns the registration object */
- navigator.serviceWorker.register('./service-worker.js').then (registration => {
- console.log('Service Worker is registered!')
- })
- } else {
- console.log('Service Worker is not supported in your browser')
- }
- })()
- if ('serviceWorker' in navigator) {
- /* register method takes in an optional second parameter as an object. To restrict the scope of a service worker, the scope should be provided.
- scope: '/books' will intercept requests with '/books' in the url. */
- navigator.serviceWorker.register('./service-worker.js', { scope: '/books' }).then(registration => {
- console.log('Service Worker for scope /books is registered', registration)
- })
- }
- self.addEventListener('install', (event) => {
- let CACHE_NAME = 'xyz-cache'
- let urlsToCache = [
- '/',
- '/styles/main.CSS',
- '/scripts/bundle.js'
- ]
- event.waitUntil(
- /* open method available on caches, takes in the name of cache as the first parameter. It returns a promise that resolves to the instance of cache
- All the URLS above can be added to cache using the addAll method. */
- caches.open(CACHE_NAME)
- .then (cache => cache.addAll(urlsToCache))
- )
- })
- self.addEventListener('activate', (event) => {
- let cacheWhitelist = ['products-v2'] // products-v2 is the name of the new cache
- event.waitUntil(
- caches.keys().then (cacheNames => {
- return Promise.all(
- cacheNames.map( cacheName => {
- /* Deleting all the caches except the ones that are in cacheWhitelist array */
- if (cacheWhitelist.indexOf(cacheName) === -1) {
- return caches.delete(cacheName)
- }
- })
- )
- })
- )
- })
- self.addEventListener('activate', (event) => {
- self.skipWaiting()
- // The usual stuff
- })
- /* Fetch event handler for responding to GET requests with the cached assets */
- self.addEventListener('fetch', (event) => {
- event.respondWith(
- caches.open('products-v2')
- .then (cache => {
- /* Checking if the request is already present in the cache. If it is present, sending it directly to the client */
- return cache.match(event.request).then (response => {
- if (response) {
- console.log('Cache hit! Fetching response from cache', event.request.url)
- return response
- }
- /* If the request is not present in the cache, we fetch it from the server and then put it in cache for subsequent requests. */
- fetch(event.request).then (response => {
- cache.put(event.request, response.clone())
- return response
- })
- })
- })
- )
- })
- {
- "endpoint": "https://fcm.googleapis.com/fcm/send/c7Veb8VpyM0:APA91bGnMFx8GIxf__UVy6vJ-n9i728CUJSR1UHBPAKOCE_SrwgyP2N8jL4MBXf8NxIqW6NCCBg01u8c5fcY0kIZvxpDjSBA75sVz64OocQ-DisAWoW7PpTge3SwvQAx5zl_45aAXuvS",
- "expirationTime": null,
- "keys": {
- "p256dh": "BJsj63kz8RPZe8Lv1uu-6VSzT12RjxtWyWCzfa18RZ0-8sc5j80pmSF1YXAj0HnnrkyIimRgLo8ohhkzNA7lX4w",
- "auth": "TJXqKozSJxcWvtQasEUZpQ"
- }
- }
- /* Notification.permission can have one of these three values: default, granted or denied. */
- if (Notification.permission === 'default') {
- /* The Notification.requestPermission() method shows a notification permission prompt to the user. It returns a promise that resolves to the value of permission*/
- Notification.requestPermission().then (result => {
- if (result === 'denied') {
- console.log('Permission denied')
- return
- }
- if (result === 'granted') {
- console.log('Permission granted')
- /* This means the user has clicked the Allow button. We're to get the subscription token generated by the browser and store it in our database.
- The subscription token can be fetched using the getSubscription method available on pushManager of the serviceWorkerRegistration object. If subscription is not available, we subscribe using the subscribe method available on pushManager. The subscribe method takes in an object.
- */
- serviceWorkerRegistration.pushManager.getSubscription()
- .then (subscription => {
- if (!subscription) {
- const applicationServerKey = ''
- serviceWorkerRegistration.pushManager.subscribe({
- userVisibleOnly: true, // All push notifications from server should be displayed to the user
- applicationServerKey // VAPID Public key
- })
- } else {
- saveSubscriptionInDB(subscription, userId) // A method to save subscription token in the database
- }
- })
- }
- })
- }
- const webpush = require('web-push')
- const vapidKeys = webpush.generateVAPIDKeys()
- const options = {
- TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service
- vapidDetails: {
- subject: 'email@example.com',
- publicKey: '',
- privateKey: ''
- }
- }
- const data = {
- title: 'Update',
- body: 'Notification sent by the server'
- }
- webpush.sendNotification(subscription, data, options)
- self.addEventListener('push', (event) => {
- let options = {
- body: event.data.body,
- icon: 'images/example.png',
- }
- event.waitUntil(
- /* The showNotification method is available on the registration object of the service worker.
- The first parameter to showNotification method is the title of notification, and the second parameter is an object */
- self.registration.showNotification(event.data.title, options)
- )
- })
- let dbInstance
- openIdbRequest.onsuccess = (event) => {
- dbInstance = event.target.result
- console.log('booksdb is opened successfully')
- }
- openIdbRequest.onerror = (event) => {
- console.log('There was an error in opening booksdb database')
- }
- openIdbRequest.onupgradeneeded = (event) => {
- let db = event.target.result
- let objectstore = db.createObjectStore('books', { keyPath: 'id' })
- }
- let openIdbRequest = Windows.indexedDB.open('booksdb', 2) // New Version - 2
- /* Success and error event handlers remain the same.
- The onupgradeneeded method gets called when the version of the database changes. */
- openIdbRequest.onupgradeneeded = (event) => {
- let db = event.target.result
- if (!db.objectStoreNames.contains('books')) {
- let objectstore = db.createObjectStore('books', { keyPath: 'id' })
- }
- let oldVersion = event.oldVersion
- let newVersion = event.newVersion
- /* The users tables should be added for version 2. If the existing version is 1, it will be upgraded to 2, and the users object store will be created. */
- if (oldVersion === 1) {
- db.createObjectStore('users', { keyPath: 'id' })
- }
- }
- let transaction = dbInstance.transaction('books')
- let objectstore = dbInstance.objectstore('books')
- let bookRecord = {
- id: '1',
- name: 'The Alchemist',
- author: 'Paulo Coelho'
- }
- let addBookRequest = objectstore.add(bookRecord)
- addBookRequest.onsuccess = (event) => {
- console.log('Book record added successfully')
- }
- addBookRequest.onerror = (event) => {
- console.log('There was an error in adding book record')
- }
- let modifyBookRequest = objectstore.put(bookRecord) // put method takes in an object as the parameter
- modifyBookRequest.onsuccess = (event) => {
- console.log('Book record updated successfully')
- }
- let transaction = dbInstance.transaction('books')
- let objectstore = dbInstance.objectstore('books')
- /* get method takes in the id of the record */
- let getBookRequest = objectstore.get(1)
- getBookRequest.onsuccess = (event) => {
- /* event.target.result contains the matched record */
- console.log('Book record', event.target.result)
- }
- getBookRequest.onerror = (event) => {
- console.log('Error while retrieving the book record.')
- }
- {
- "name": "Demo PWA",
- "short_name": "Demo",
- "start_url": "/?standalone",
- "background_color": "#9F0C3F",
- "theme_color": "#fff1e0",
- "display": "standalone",
- "icons": [{
- "src": "/lib/img/icons/xxhdpi.png?v2",
- "sizes": "192x192"
- }]
- }
来源: https://juejin.im/post/5bff980f51882508851b72a5