原文地址: An Extensive Guide To Progressive Web Applications
快速摘要
在本文中,我們將瞭解瀏覽舊的非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。
網路上的大多數訪問者都不太可能回來,因此開發人員提出了向他們展示彈窗以安裝本機應用程式的解決方法,以便讓他們深入參與。但是,使用者必須完成安裝本機應用程式二進位制檔案的繁瑣程式。強制使用者安裝應用程式很煩人,並且進一步降低了他們首先安裝應用程式的可能性。網路的機會很明顯。
推薦閱讀:Native And PWA: Choices, Not Challengers!
如果Web應用程式具有豐富的使用者體驗,推送通知,離線支援和即時載入,它們可以征服世界。 這是漸進式Web應用程式的功能。
PWA提供豐富的使用者體驗,因為它具有以下幾個優勢:
-
快速
使用者介面不古里古怪;滾動平滑,應用快速響應使用者互動
-
可靠
當使用者連線伺服器繁忙的時候,一個普通的網站迫使使用者等待,什麼都不做。但是,PWA從快取中即時載入資料。即使在2G連線上,PWA也可以無縫工作。每個獲取資源或資料的網路請求都通過service worker(稍後會詳細介紹),該service worker首先驗證特定請求的響應是否已經在快取中。當使用者幾乎立即獲得真實內容時,即使連線不良,他們也會更加信任應用並將其視為更可靠。
-
參與度
PWA可以在使用者的主螢幕上獲得一個位置。 它通過提供全屏工作區提供原生應用程式般的體驗。 它利用推送通知來保持使用者參與。
現在我們知道PWA帶來了什麼,讓我們深入瞭解什麼使PWA優於原生應用程式。PWA使用service worker,Web app manifests,web push notification和用於快取的IndexedDB /本地資料結構等技術構建。 讓我們詳細研究一下。
Service Workers
service worker是一個在後臺執行的JavaScript檔案,不會干擾使用者的互動。 對伺服器的所有GET請求都通過service worker進行。它就像一個客戶端代理。 通過攔截網路請求,它可以完全控制傳送回客戶端的響應。PWA立即載入,因為service worker通過響應來自快取的資料來消除對網路的依賴性。
service worker只能攔截其範圍內的網路請求。 例如,根範圍的service worker可以攔截來自網頁的所有提取請求。 service worker作為事件驅動系統執行。 它在不需要時進入休眠狀態,從而節省了記憶體。 要在Web應用程式中使用service worker,我們首先必須使用JavaScript在頁面上註冊它。
(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')
}
})()
複製程式碼
我們首先檢查瀏覽器是否支援service worker。要在Web應用程式中註冊service worker,我們將其URL作為註冊函式的引數提供,可在navigator.serviceWorker中找到(navigator是一個允許指令碼自行註冊並執行其活動的Web API)。 service worker只註冊一次。 每次載入頁面時都不會進行註冊。僅當現有啟用的service worker與較新的service worker之間存在位元組差異或者其URL已更改時,瀏覽器才會下載service worker檔案(./service-worker.js)。
上述service worker將攔截來自根(/)的所有請求。 為了限制service worker的範圍,我們將傳遞一個可選引數,其中一個鍵作為範圍。
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)
})
}
複製程式碼
上面的service worker將攔截在URL中具有/ books的請求。 例如,它不會攔截/ products的請求,但它可以很好地攔截/ books / products的請求。
如上所述,service worker作為事件驅動系統執行。 它偵聽事件(安裝,啟用,獲取,推送),並相應地呼叫相應的事件處理程式。 其中一些事件是service worker生命週期的一部分,它按順序通過這些事件來啟用。
INSTALLATION
成功註冊服務工作程式後,將觸發安裝事件。 這是進行初始化工作的好地方,比如在IndexedDB中設定快取或建立物件儲存。(一旦我們瞭解了它的細節,IndexedDB會對你更有意義。現在,我們可以說它是一個鍵值對結構。)
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指的是service worker例項。event.waitUntil使service worker等待,直到其中的所有程式碼都完成執行。
ACTIVATION
一旦安裝了service worker,它就無法監聽獲取請求。 相反,會觸發一個activate事件。如果沒有啟用的service worker在同一範圍內的網站上執行,則會立即啟用已安裝的service worker。但是,如果網站已有啟用的service worker,則會延遲啟用新service worker,直到關閉在舊service worker程式上執行的所有選項卡。 這是有道理的,因為舊的service worker可能正在使用現在在較新的伺服器中修改的快取例項。 因此,啟用步驟是擺脫舊快取的好地方。
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)
}
})
)
})
)
})
複製程式碼
在上面的程式碼中,我們刪除舊的快取。 如果快取的名稱與cacheWhitelist不匹配,則將其刪除。要跳過等待階段並立即啟用service worker,我們使用skip.waiting()。
self.addEventListener('activate', (event) => {
self.skipWaiting()
// The usual stuff
})
複製程式碼
一旦啟用了service worker,它就可以監聽獲取請求和推送事件。
FETCH EVENT HANDLER
只要網頁通過網路觸發對資源的獲取請求,就會呼叫來自service worker的fetch事件。 fetch事件處理程式首先在快取中查詢請求的資源。如果它存在於快取中,則它返回具有快取資源的響應。 否則,它會向伺服器發起一個獲取請求,當伺服器發回帶有請求資源的響應時,它會將其放入快取中以供後續請求使用。
/* 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
})
})
})
)
})
複製程式碼
event.respondWith讓service worker向客戶端傳送一個自定義響應。
離線優先現在是一件事。 對於任何非關鍵請求,我們必須提供來自快取的響應,而不是去請求伺服器。如果快取中沒有任何資源,我們從伺服器獲取它,然後將其快取以用於後續請求。
service worker只能在HTTPS網站上工作,因為他們有權操縱任何獲取請求的響應。 有惡意的人可能會篡改HTTP網站上的請求響應。因此,在HTTPS上託管PWA是強制性的。 service worker不會中斷DOM的正常執行。 他們無法直接與網頁通訊。 要將任何訊息傳送到網頁,它會使用釋出訊息。
Web Push Notifications
假設您正在忙著在手機上玩遊戲,並會彈出一條通知,告訴您自己喜歡的品牌可享受30%的折扣。沒有任何進一步的麻煩,你點選通知,然後屏住呼吸。 在使用者使用產品時,獲取板球或足球比賽的實時更新或將重要的電子郵件和提醒作為通知是一件大事。此功能僅在原生應用程式中可用,直到PWA出現。 PWA利用Web推送通知來競爭原生應用程式提供的強大功能。即使在任何瀏覽器選項卡中未開啟PWA,即使瀏覽器未開啟,使用者仍會收到Web推送通知。
Web應用程式必須要求使用者允許向其傳送推送通知。
一旦使用者通過單擊“允許”按鈕確認,瀏覽器將生成唯一的訂閱token。 此令牌對於此裝置是唯一的。 Chrome生成的訂閱token格式如下:{
"endpoint": "https://fcm.googleapis.com/fcm/send/c7Veb8VpyM0:APA91bGnMFx8GIxf__UVy6vJ-n9i728CUJSR1UHBPAKOCE_SrwgyP2N8jL4MBXf8NxIqW6NCCBg01u8c5fcY0kIZvxpDjSBA75sVz64OocQ-DisAWoW7PpTge3SwvQAx5zl_45aAXuvS",
"expirationTime": null,
"keys": {
"p256dh": "BJsj63kz8RPZe8Lv1uu-6VSzT12RjxtWyWCzfa18RZ0-8sc5j80pmSF1YXAj0HnnrkyIimRgLo8ohhkzNA7lX4w",
"auth": "TJXqKozSJxcWvtQasEUZpQ"
}
}
複製程式碼
上述token中包含的endpoint對於每個訂閱都是唯一的。在一般的網站上,成千上萬的使用者會同意接收推送通知,對於每個使用者,這個endpoint都是唯一的。因此,在此endpoint的幫助下,應用程式可以通過向其傳送推送通知來定位這些使用者。expirationTime是訂閱對特定裝置有效的時間量。如果expirationTime是20天,則意味著使用者的推送訂閱將在20天后過期,並且使用者將無法接收舊訂閱的推送通知。在這種情況下,瀏覽器將為該裝置生成新的訂閱token。 auth和p256dh金鑰用於加密。
現在,要在將來向這些成千上萬的使用者傳送推送通知,我們首先必須儲存他們各自的訂閱token。應用程式伺服器(後端伺服器,可能是Node.js指令碼)的工作是向這些使用者傳送推送通知。這可能聽起來像使用請求有效負載中的通知資料向端點URL發出POST請求一樣簡單。但是,應該注意的是,如果使用者在伺服器觸發了針對他們的推送通知時不線上,則他們應該在他們重新聯機後仍然會收到該通知。伺服器必須處理這些場景,同時向使用者傳送數千個請求。跟蹤使用者連線的伺服器聽起來很複雜。因此,中間的某些東西將負責將Web推送通知從伺服器路由到客戶端。這稱為推送服務,每個瀏覽器都有自己的推送服務實現。瀏覽器必須告知推送服務以下資訊才能傳送任何通知:
-
生命週期
這是訊息應排隊的時間長度,以防它未傳遞給使用者。 一旦這段時間過去,訊息將從佇列中刪除。
-
訊息緊迫性
這樣推送服務通過僅傳送高優先順序訊息來保留使用者的電量。
推送服務將訊息路由到客戶端。 因為即使其各自的Web應用程式未在瀏覽器中開啟,客戶端也必須接收推送,因此必須通過在後臺持續監視的內容來監聽推送事件。 你猜對了:這是service worker的工作。 service worker偵聽推送事件並執行向使用者顯示通知的工作。
因此,現在我們知道瀏覽器,推送服務,service worker和應用伺服器協同工作以向使用者傳送推送通知。 我們來看看實現細節。
WEB PUSH CLIENT
詢問使用者的許可是一次性的事情。 如果使用者已經授予接收推送通知的許可權,我們不應再訊問。 許可權值儲存在Notification.permission中。
/* 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
}
})
}
})
}
複製程式碼
在上面的訂閱方法中,我們傳遞userVisibleOnly和applicationServerKey來生成訂閱token。userVisibleOnly屬性應始終為true,因為它告訴瀏覽器伺服器傳送的任何推送通知都將顯示給客戶端。 要了解applicationServerKey的用途,讓我們考慮一個場景。
如果有人獲得了數千個訂閱token,他們可以很好地向這些訂閱中包含的端點傳送通知。 端點無法連結到您的唯一標識。為了向Web應用程式上生成的訂閱token提供唯一標識,我們使用VAPID協議。使用VAPID,應用程式伺服器在傳送推送通知時自動向推送服務標識自己。 我們生成兩個鍵,如下所示:
const webpush = require('web-push')
const vapidKeys = webpush.generateVAPIDKeys()
複製程式碼
web-push是一個npm模組,vapidKeys將擁有一個公鑰和一個私鑰。 上面使用的應用程式伺服器金鑰是公鑰。
Web Push Server
Web推送伺服器(應用程式伺服器)的工作非常簡單。 它向訂閱令牌傳送通知有效負載。
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)
複製程式碼
它使用Web推送庫中的sendNotification方法。
Service Workers
service worker向使用者顯示通知:
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)
)
})
複製程式碼
到目前為止,我們已經看到service worker如何利用快取來儲存請求並使PWA快速可靠,我們已經看到了Web推送通知如何讓使用者參與其中。
要在客戶端儲存大量資料以供離線支援,我們需要一個巨大的資料結構。 讓我們來看看金融時報的PWA。你必須親眼目睹這種資料結構的強大功能。 在瀏覽器中載入URL,然後關閉Internet連線。重新載入頁面。爾加! 它還在運作嗎? 它是。 (就像我說的,離線是新的黑色。)資料不是來自網路。 它正在從快取裡供應。 轉到Chrome開發者工具的“Application”標籤。 在“Storage”下,你將找到“IndexedDB”。
檢視“文章”物件庫,並展開任何專案以檢視自己的魔力。 英國“金融時報”已儲存此資料以供離線支援。這種允許我們儲存大量資料的資料結構稱為IndexedDB。 IndexedDB是一個基於JavaScript的物件導向的資料庫,用於儲存結構化資料。 我們可以在此資料庫中建立不同的物件儲存用於各種目的。 例如,正如我們在上圖中看到的那樣,“Resources”,“ArticleImages”和“Articles”被稱為物件儲存。物件儲存中的每個記錄都使用金鑰進行唯一標識。 IndexedDB甚至可以用於儲存檔案和blob。
讓我們嘗試通過建立用於儲存書籍的資料庫來理解IndexedDB。
let openIdbRequest = window.indexedDB.open('booksdb', 1)
如果資料庫booksdb尚不存在,則上面的程式碼將建立booksdb資料庫。 open方法的第二個引數是資料庫的版本。指定版本會處理將來可能發生的與架構相關的更改。例如,booksdb現在只有一個表,但是當應用程式增長時,我們打算再新增兩個表。 為了確保我們的資料庫與更新的模式同步,我們將指定比前一個更高的版本。
呼叫open方法不會立即開啟資料庫。 這是一個返回IDBOpenDBRequest物件的非同步請求。 該物件具有成功和錯誤屬性;我們必須為這些屬性編寫適當的處理程式來管理連線的狀態。
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' })
}
複製程式碼
要管理物件儲存的建立或修改(物件儲存類似於基於SQL的表 -它們具有鍵值結構),則在openIdbRequest物件上呼叫onupgradeneeded方法。 只要版本更改,就會呼叫onupgradeneeded方法。 在上面的程式碼片段中,我們建立了一個使用唯一鍵作為ID的書籍物件庫。
讓我們說,在部署這段程式碼之後,我們必須再建立一個物件儲存,users
。 所以,現在我們的資料庫版本將是2。
let openIdbRequest = window.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' })
}
}
複製程式碼
我們在開啟請求的成功事件處理程式中快取了dbInstance。 要在IndexedDB中檢索或新增資料,我們將使用dbInstance。讓我們在圖書物件商店中新增一些圖書記錄。
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')
}
複製程式碼
我們使用transactions
,特別是在物件儲存上寫入記錄時。 事務只是確保資料完整性的操作的包裝器。如果事務中的任何操作失敗,則不對資料庫執行任何操作。
讓我們用put方法修改一本書記錄:
let modifyBookRequest = objectstore.put(bookRecord) // put method takes in an object as the parameter
modifyBookRequest.onsuccess = (event) => {
console.log('Book record updated successfully')
}
複製程式碼
讓我們用get方法檢索一本書記錄:
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.')
}
複製程式碼
新增ICON到主螢幕
既然PWA和原生應用程式之間幾乎沒有任何區別,那麼為PWA提供一個主要位置是有意義的。如果您的網站符合PWA的基本標準(託管在HTTPS上,與service worker整合並具有manifest.json),並且在使用者花了一些時間在網頁上之後,瀏覽器將在底部呼叫提示,詢問 使用者將應用程式新增到其主螢幕,如下所示:
當使用者點選“將FT新增到主螢幕”時,PWA可以將其設定在主螢幕以及應用程式抽屜中。當使用者在其手機上搜尋任何應用程式時,將列出與搜尋查詢匹配的任何PWA。 系統設定中也會顯示它們,這使使用者可以輕鬆管理它們。 從這個意義上講,PWA的行為類似於原生應用程式。PWA使用manifest.json來提供此功能。 讓我們看一個簡單的manifest.json檔案。
{
"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"
}]
}
複製程式碼
short_name顯示在使用者的主螢幕和系統設定中。該名稱將顯示在Chrome提示符和啟動螢幕上。啟動畫面是使用者在應用程式準備啟動時看到的內容。 start_url是您應用的主螢幕。這是使用者點選主螢幕上的圖示時獲得的內容。 background_color用於初始螢幕。 theme_color設定工具欄的顏色。顯示模式的獨立值表示應用程式將以全屏模式執行(隱藏瀏覽器的工具欄)。當使用者安裝PWA時,其大小僅為千位元組,而不是兆位元組的原生應用程式。
service worker,Web推送通知,IndexedDB和主螢幕位置彌補了離線支援,可靠性和參與度。應該注意的是,service worker沒有啟用並且在第一次載入時開始工作。在快取所有靜態資產和其他資源之前,第一個載入仍然很慢。我們可以實施一些策略來優化第一次載入。
打包資源
所有資源,包括HTML,樣式表,影像和JavaScript,都將從伺服器中獲取。 檔案越多,獲取它們所需的HTTPS請求就越多。 我們可以使用像WebPack這樣的打包工具打包我們的靜態資源,從而減少對伺服器的HTTP請求數量。WebPack通過使用諸如程式碼分割之類的技術(即僅打包當前頁面載入所需的那些檔案,而不是將所有這些檔案打包在一起)和treeShaking(即刪除重複的依賴項或 匯入但未在程式碼中使用的依賴項)。
減少迴圈呼叫
網路延遲的主要原因之一是網路延遲。一個位元組從A行進到B所需的時間因網路連線而異。例如,通過Wi-Fi進行的特定往返行程在3G連線上需要50毫秒和500毫秒,而在2G連線上需要2500毫秒。這些請求是使用HTTP協議傳送的,這意味著當某個特定連線用於請求時,在提供前一個請求的響應之前,它不能用於任何其他請求。一個網站一次可以發出六個非同步HTTP請求,因為網站可以使用六個連線來發出HTTP請求。一般的網站提出大約100個請求;因此,如果最多有六個連線可用,使用者最終可能會在一次往返中花費大約833毫秒。 (計算是833毫秒 - 100/6 = 1666.我們必須將1666除以2,因為我們正在計算往返時間。)在HTTP2到位的情況下,週轉時間大大縮短。HTTP2不會阻止連線頭,因此可以同時傳送多個請求。
大多數HTTP響應包含last-modified
和Etag
標頭。last-modified
的標頭是上次修改檔案的日期,Etag
是基於檔案內容的唯一值。只有在更改檔案內容時才會更改它。如果快取版本已在本地可用,則可以使用這兩個標頭來避免再次下載檔案。如果瀏覽器在本地提供此檔案的版本,則可以在請求中新增以下兩個標頭中的任何一個:
現在的情況是快速響應,但我們的工作還沒完,我們任然需要去解析HTML,載入css樣式,使頁面可互動。顯示一些空元素盒子是有必要的,而不是一個blank螢幕。當HTML文件開始解析,當遇到<script src='asset.js'></script>
,他將發起一個非同步請求到伺服器獲取asset.js
,這時,整個渲染程式將會被阻塞直到返回資料。想象一下如果有很多非同步獲取靜態資源請求,在script
標籤中使用async
將會是個好的選擇,例如:<script src='asset.js' async></script>.
通過在此處引入async關鍵字,瀏覽器將發出非同步請求以獲取asset.js,而不會妨礙HTML的解析。如果稍後需要指令碼檔案,我們可以推遲下載該檔案,直到整個HTML被解析為止。 可以使用defer關鍵字來延遲指令碼檔案,例如<script src ='asset.js'defer> </ script>
。
結論
我們已經學到了很多新東西,這些東西都是很酷的Web應用程式。 以下是我們在本文中探討的所有內容的摘要:
service worker
充分利用快取來加速資源的載入。Web
推送通知在引擎蓋下工作。- 我們使用
IndexedDB
來儲存大量資料。 - 一些即時首次載入的優化,如使用
HTTP2
和新增headers標籤,如Etag
,last-modified
和If-None-Match
,可防止下載有效的快取資源。