2015年,國際電信聯盟預估到15年年末全球上網人口將到達32億,也就是說全球將近一半的人口都在上網。 想象一下每秒鐘的上網人數,32億。大約32000個足球場才裝得下這麼多人!這幾乎是一個大到沒法理解的數字。當這些人上網時,他們使用的設備不盡相同、他們的網速也各不相同、甚至同一個的網速也會變化。做爲 Web 開發者,試圖知足全部這些不一樣的場景彷佛使人望而生畏!這正是 PWA 出現的契機。它們賦予了開發者能夠構建速度更快、富有彈性而且更吸引人的網站的能力,這些網站可以被全球數十億人訪問。在本書的第1部分中,咱們將直接深刻地定義到底什麼纔是 PWA 。javascript
在第1章中,你將瞭解漸進式網絡應用 ( Progressive Web Apps ),即咱們所熟知的 PWA,以及 PWA 帶來的好處。而後,咱們一塊兒來看看已經利用 PWA 的能力來改善用戶瀏覽體驗的企業。咱們還會詳細分析一個真實世界中 PWA ,並瞭解下像 Twitter 和 Flipkart 這樣的公司是如何創建本身的 PWA 的。PWA 的關鍵組成部分是 Service Worker, 咱們將深刻介紹此主題,以及瞭解它在 Web 瀏覽器中加載時所經歷的生命週期。css
在第2章中,首先介紹了構建 PWA 時能夠使用的不一樣架構方法,以及如何最佳地組織你的代碼。咱們將研究兩種不一樣的方法,「汲取功能」或「應用外殼架構」 - 這兩種方法均可以知足項目的須要。PWA 最棒的一點就是你無需重寫已存在的 Web 應用便能開始使用 PWA 的功能,只要你以爲這些功能會使用戶受益並提高他們的體驗, 就能夠添加它們。最後,本章會以剖析一個現有的 PWA 來結尾,該 PWA 是 Twitter 團隊開發的 Twitter Lite ( 精簡版 Twitter ) 。html
在第1部分結束之際,你應該對 PWA 是什麼,以及它們能帶給用戶的好處有一個清晰的認知。第1部分將爲本書的下一部分奠基基礎,在下一部分中咱們將直接進入編碼環節,從頭開始構建一個 PWA 。前端
在咱們開始探索爲何 PWA 對於當今 Web 世界是個重大飛躍以前,值得回憶下自 Web 問世以來的歷程。這要追溯到1990年的聖誕節,Tim Berners-Lee 爵士和他在 CERN 的團隊建立了工做網絡所需的全部工具,他們建立了 HTTP、HTML 和 WorldWideWeb (全世界第一個網頁瀏覽器)。WorldWideWeb 只能運行由超連接的簡單純文本組成的網頁。事實上,這些第一代的網頁仍然在線,而且能夠瀏覽!java
回到如今,咱們所瀏覽的網頁與最初的網頁並無太大的不一樣。固然,如今咱們有了像 CSS 和 Javacript 這樣的功能,但網頁的核心依舊是使用 HTML、HTTP 以及一些其餘構建模塊來構建的,這些都是 Tim Berners-Lee 及他的團隊在多年前所建立的。這些輝煌的構建模塊意味着 Web 已經可以以驚人的速度增加。然而,咱們用來訪問網頁的設備數量也在不斷增加。不管你的用戶是在旅途中仍是坐在書桌前,他們都無時無刻不在獲取信息。咱們對於 Web 的指望從未如此之高。git
雖然咱們的移動設備變得愈發強大,但咱們的移動網絡並非總能知足需求。若是你使用智能手機,你就會知道移動鏈接是有多麼的脆弱。2G、3G 或 4G 這些鏈接自己都很不錯,可是它們時常會失去鏈接,或者網速變得不好。若是你的業務是跟網絡相關的,那這就是你須要去解決的問題。github
從歷史上來講,原生應用 (下載到手機的) 已經可以提供更好的總體用戶體驗,你只要下載好原生應用,它便會當即加載。即便沒有網絡鏈接,也並不是是徹底不可用的: 你的設備上已經存儲了供用戶使用的絕大部分資源。原生應用具有提供有彈性、吸引人的體驗的能力,同時也意味它的數量已經呈爆炸式增加。目前在蘋果和 Google 的應用商店中,已經有超過400萬的原生應用!web
從歷史上來講,Web 沒法提供原生應用所具有的這些強大功能,好比離線能力,瞬時加載和更高的可靠性。這也正是 PWA 成爲 Web 顛覆者的契機。主要的瀏覽器廠商一直在努力改進構建 Web 的方式, 並建立了一組新功能以使 Web 開發者可以建立快速、可靠和吸引人的網站。PWA 應該具有如下特色:編程
做爲 Web 開發者,這是咱們傳統構建網站方式的一種轉變。這意味着咱們能夠開始構建能夠應對不斷變化的網絡條件或無網絡鏈接的網站。這還意味着咱們能夠創建更吸引人的網站來爲咱們的用戶提供一流的瀏覽體驗。json
讀到這,你可能會想,這太瘋狂了!那些不支持這些新功能的老瀏覽器怎麼辦? PWA 最棒的一點就是它們真的是「漸進式」的。若是你構建一個 PWA,即便在一個不支持的老舊瀏覽器上運行,它仍然能夠做爲一個普通的網站來運行。驅動 PWA 的技術就是這樣設計的,只有在支持這些新功能的瀏覽器中才會加強體驗。若是用戶的設備支持,那麼他們將得到全部額外的好處和更多的改進功能。不管怎樣,這對你和你的用戶來講都是共贏!
那麼 PWA 究竟是由什麼組成的呢?咱們一直將它們做爲一組功能和原理來討論,但真正使某個網站成爲 「PWA」 的究竟是什麼呢?最最簡單的 PWA 其實只是普通的網站。它們是由咱們這些 Web 開發者所熟悉和喜歡的技術所建立的,即 HTML、CSS 和 JavaScript 。然而, PWA 卻更進一步,它爲用戶提供了加強的體驗。我很是喜歡 Google Chrome 團隊的開發人員 Alex Russell 的描述方式:
「這些應用沒有經過應用商店進行打包和部署,它們只是汲取了所須要的原生功能的網站而已。」
PWA 會指向一個清單 (manifest) 文件,其中包含網站相關的信息,包括圖標,背景屏幕,顏色和默認方向。(在第5章中,你將學習到如何使用清單文件來使你的網站更加吸引人)
PWA 使用了叫作 Service Workers 的重要新功能,它能夠令你深刻網絡請求並構建更好的 Web 體驗。隨着本章的深刻,咱們將進一步瞭解它們以及它們帶給瀏覽器的改進。PWA 還容許你將其「添加」到設備的主屏幕上。它會像原生應用那樣,經過點擊圖標即可讓你輕鬆訪問一個 Web 應用。(咱們將在第5章中深刻討論)
PWA 還能夠離線工做。使用 Service Workers,你能夠選擇性地緩存部分網站以提供離線體驗。若是你如今在沒有網絡鏈接的狀況下瀏覽網站,那麼對於絕大多數網站,你看到的應該是相似於下面圖1.1所示的樣子。
圖1.1 做爲用戶,離線頁面可能會很是使人沮喪,尤爲是迫切須要獲取這些信息時!
有了 Service Workers,咱們的用戶無需再面對恐怖的「無網絡鏈接」屏幕了。使用 Service Workers,你能夠攔截並緩存任何來自你網站的網絡請求。不管你是爲移動設備,桌面設備仍是平板設備構建網站, 均可以在有網絡鏈接或沒有網絡鏈接的狀況下控制如何響應請求。(咱們將在第3章中深刻了解緩存,並在第8章中構建一個離線網頁。)
簡而言之,PWA 不只僅是一組很是棒的新功能,它們其實是咱們構建更好的網站的一種方式。PWA 正在迅速成爲一套最佳實踐。構建 PWA 所採起的步驟將有利於訪問你網站的任何人,不管他們選擇使用何種設備。
一旦你解鎖了開始構建 PWA 所需的基本構建塊,你會很快發現,比較高級的例子並無看上去那麼高級。對於不知情的外行人來講,這本書可能看起來無足輕重,可是一旦你進入構建 PWA 的節奏後,你會發現一切都是如此的簡單!
做爲一名開發者,我固然知道當一項新技術或一系列功能出現時,是有多麼的使人興奮。但爲你的網站發掘並引進最新最好的庫或框架的強烈慾望每每會掩蓋其爲企業帶來的價值。不管你是否相信,PWA 能實際上爲咱們的用戶帶來真正的價值,並使網站更具吸引力,更有彈性,甚至更快。
PWA 最棒的一點是能夠一步步地來加強現有的 Web 應用。咱們在本書中學習的技術集合能夠應用於任何現有的網站,甚至是你正在構建的新的 Web 應用。不管你選擇何種技術棧來開發網站,PWA 都將與你的解決方案緊密結合在一塊兒,由於它只是簡單地基於 HTML、CSS 和 JavaScript 。這簡直太棒了!
如今你對 PWA 已經有了基本的瞭解,讓咱們先暫時停下腳步,想象一下用 PWA 來構建的各類可能性。假設你的在線業務是報紙,人們經過它來了解更多關於當地的新聞。若是你知道有人常常訪問你的網站並瀏覽多個頁面,爲何不提早緩存這些頁面,這樣他們就能夠徹底離線地瀏覽新聞?或者想象下,你的 Web 應用服務於一個慈善機構,志願者們在這個網絡鏈接不穩定或壓根無網絡鏈接的區域進行工做。PWA 的功能將容許你構建一個離線應用,使他們在沒有網絡鏈接的現場也能收集信息。一旦他們回到辦公室或有網絡鏈接的區域,數據就能夠同步到服務器。對於 Web 開發者來講,PWA 是個完全的顛覆者,而且我我的對它們將帶給 Web 的功能感到興奮不已。
在本章的前面,我提到了你能夠將 PWA 「添加」 到設備的主屏幕上。一旦添加後,它便會出如今你的主屏幕上並能夠經過點擊圖標來訪問你的網站。能夠把它當作臺式機的快捷方式,以使你輕鬆訪問網站。
2015年,印度最大的電商網站 Flipkart 開始構建 Flipkart Lite,它是 Web 和 Flipkart 原生應用完美結合的 PWA 。若是你在瀏覽器中打開 flipkart.com,你會明白爲何這個網站是如此成功。就用戶體驗來講是使人印象深入的,網站的速度很快,能夠離線工做,而且用起來令人愉悅。經過將它的網站構建成 PWA,Flipkart 可以顯示「添加到主屏幕」 操做欄。
不管你是否相信,經過「添加到主屏幕」圖標到達的用戶實際上在網站上購買的可能性高達70%!! (參見圖 1.2)
圖1.2 添加到主屏幕功能是從新與用戶接觸的好方法。
任何進入蘋果或 Google 應用商店的新原生應用可能看起來就像沙灘上的一粒沙。截至2016年6月,在這些商店中始終保持將近200萬個應用。若是你開發了一個原生應用,那麼它很容易就被應用商店中的海量應用所掩蓋。然而,因爲 PWA 只是汲取了豐富功能的網站,所以能夠經過搜索引擎輕鬆發現。人們能夠很天然地經過社交媒體連接或瀏覽網頁發現 PWA。構建 PWA 可讓你接觸比單獨使用原生應用更多的人,由於它們是爲任何可以運行瀏覽器的平臺而構建的!
PWA 另外一個很棒的點是它們是用 Web 開發者所熟悉和喜好的技術所構建的。CSS、JavaScript 和 HTML 都是構建 PWA 的基石。我我的在一家小型創業公司工做,我知道編寫一個能夠在多個平臺 (iOS、Android 和網站) 上運行的應用是多麼的昂貴。有了 PWA,你只須要一個瞭解 Web 語言的開發團隊便可。它使得招聘更容易,並且確定便宜得多!這並非說你不該該構建原生應用,由於不一樣的用戶會有不一樣的需求,但只要你想的話,你能夠專一於爲網絡上的用戶營造一個至關好的體驗並使他們留下來。
當涉及到 Web 的構建時,用戶能夠輕鬆訪問你網站的一部分,而無需先下載龐大的文件。使用正確的緩存技術的 PWA 能夠保存用戶數據並當即爲用戶提供功能。隨着世界各地愈來愈多的用戶開始上網,爲下一個十億人構建網站從未如此重要。PWA 經過構建快速、精簡的 Web 應用來幫助你實現此目標。
若是你在當今的網絡上閱讀過一些軟件開發文章的話,經常會有圍繞「原生 vs. Web」的爭論。哪一個更好?各自的優點與劣勢是什麼? 原生應用自己是很是好的,但事實是 PWA 不只僅是將原生的功能引入 Web 。它們解決了企業面臨的真正問題,旨在爲用戶創造一個名副其實的可發現、快速和有吸引力的體驗。
正如我以前所提到的,釋放 PWA 力量的關鍵在於 Service Workers 。就其核心來講,Service Workers 只是後臺運行的 worker 腳本。它們是用 JavaScript 編寫的,只需短短几行代碼,它們即可使開發者可以攔截網絡請求,處理推送消息並執行許多其餘任務。
最棒的一點是,若是用戶的瀏覽器不支持 Service Workers 的話,它們只是簡單地回退,你的網站還做爲普通的網站。正是因爲這一點,它們被描述爲「完美的漸進加強」。漸進加強術語是指你能夠先建立能在任何地方運行的體驗,而後爲支持更高級功能的設備加強體驗。
Service Worker 是如何...工做的呢?那麼爲了儘量地簡單易懂,我真的很想解釋下 Google 的 Jeff Posnick 是如何描述他們的:
「將你的網絡請求想象成飛機起飛。Service Worker 是路由請求的空中交通管制員。它能夠經過網絡加載,或甚至經過緩存加載。」
做爲「空中交通管制員」,Service Workers 可讓你全權控制網站發起的每個請求,這爲許多不一樣的使用場景開闢了可能性。空中交通管制員可能將飛機重定向到另外一個機場,甚至延遲降落,Service Worker 的行爲方式也是如此,它能夠重定向你的請求,甚至完全中止。
雖然 Service Workers 是用 JavaScript 編寫的,但須要明白它們與你的標準 JavaScript 文件略有不一樣,這一點很重要。Service Worker:
你無需成爲 JavaSript 專家後才能夠開始嘗試 Service Workers 。它們是事件驅動的,你能夠簡單地選擇想要進入的事件。當你對這些不一樣的事件有了基本的瞭解後,開始使用 Service Workers 要比你想象中簡單!
爲了更好地解釋 Service Workers,咱們來看看下面的圖1.3。
圖1.3 Service Workers 可以攔截進出的 HTTP 請求,從而徹底控制你的網站。
Service Worker 運行在 worker 上下文中,這意味着它沒法訪問 DOM,它與應用的主要 JavaScript 運行在不一樣的線程上,因此它不會被阻塞。它們被設計成是徹底異步的,所以你沒法使用諸如同步 XHR 和 localStorage 之類的功能。在上面的圖1.3中,你能夠看到 Service Worker 處於不一樣的線程,而且能夠攔截網絡請求。記住,Service Worker 就像是「空中交通管制員」,它可讓你全權控制網站中全部進出的網絡請求。這種能力使它們極其強大,並容許你來決定如何響應請求。
在深刻代碼示例以前,理解 Service Worker 在其生命週期中經歷的不一樣階段很重要。爲了更好的進行解釋,讓咱們想象一下一個已經建好的基礎網站,而且該網站使用了 Service Worker 。該網站是一個流行的博客平臺,數以百萬計的做家天天都在使用它來分享內容。
簡單點說,該網站不停地在接收包括圖像甚至視頻在內的內容請求。爲了理解 Service Worker 生命週期是如何工做的,咱們從網站每一天數百萬次交互請求中挑選出一個。
圖1.4展現了 Service Worker 生命週期,它會在用戶訪問該網站的博客頁面時發生。
圖1.4 Service Worker 生命週期
讓咱們慢慢來理解上面的圖1.4,一步步地瞭解 Service Worker 生命週期是如何工做的。
當用戶首次導航至 URL 時,服務器會返回響應的網頁。在圖1.4中,你能夠看到在第1步中,當你調用 register() 函數時, Service Worker 開始下載。在註冊過程當中,瀏覽器會下載、解析並執行 Service Worker (第2步)。若是在此步驟中出現任何錯誤,register() 返回的 promise 都會執行 reject 操做,而且 Service Worker 會被廢棄。
一旦 Service Worker 成功執行了,install 事件就會激活 (第3步)。Service Workers 很棒的一點就是它們是基於事件的,這意味着你能夠進入這些事件中的任意一個。咱們將在本書的第3章中使用這些不一樣的事件來實現超快速緩存技術。
一旦安裝這步完成,Service Worker 便會激活 (第4步) 並控制在其範圍內的一切。若是生命週期中的全部事件都成功了,Service Worker 便已準備就緒,隨時能夠使用了!
圖1.5 Service Worker 生命週期經歷了不一樣階段,這有點像交通燈系統
對我我的而言,我以爲記住 Service Worker 生命週期最簡單的方法就是把它當成一組交通訊號燈。在註冊過程當中,Service Worker 處於紅燈狀態,由於它還須要下載和解析。接下來,它處於黃燈狀態,由於它正在執行,尚未徹底準備好。若是上述全部步驟都成功了,你的 Service Worker 在將處於綠燈狀態,隨時能夠使用。
須要注意的是,當第一次加載頁面時,Service Worker 尚未激活,因此它不會處理任何請求。只有當它安裝和激活後,才能控制在其範圍內的一切。這意味着,只有你刷新頁面或者導航到另外一個頁面,Service Worker 內的邏輯纔會啓動。
我很確定,到目前爲止你一直迫切地想看看代碼應該是怎麼樣的,因此咱們開始吧。
由於 Service Worker 只是運行在後臺線程的 JavaScript 文件,因此在 HTML 頁面中你能夠像引用任何 JavaScript 文件同樣來引用它。假設咱們建立了一個 Service Worker 文件,並將其命名爲 sw.js 。要註冊它,須要在 HTML 頁面中使用以下代碼。(參見代碼清單 1.1)
<html>
<head>The best web page ever</head> <body> <script> // 註冊 service worker if ('serviceWorker' in navigator) { ❶ navigator.serviceWorker.register('/sw.js').then(function(registration) { ❷ // 註冊成功 console.log('ServiceWorker registration successful with scope: ', registration.scope); ❸ }).catch(function(err) { ❹ // 註冊失敗 :( console.log('ServiceWorker registration failed: ', err); }); } </script> </body> </html>
在 script 標籤內,咱們首先檢查瀏覽器其實是否支持 Service Workers 。若是支持,咱們就使用 navigator.serviceWorker.register('/sw.js')
函數註冊,該函數又會通知瀏覽器下載 Service Worker 文件。若是註冊成功,它會開始 Service Worker 生命週期的剩餘階段。
在上面的代碼示例中,你或許會注意到 JavaScript 代碼並無使用回調函數。那是由於 Service Workers 使用 JavaScript 中的 Promises,Promises 以一種十分整潔、可讀的方式來處理回調函數。Promise 表示一個操做還未完成,可是期待它未來會完成。這使得異步方法返回的值如同同步方法那樣,並使編寫的 JavaScript 更整潔,也更容易閱讀。Promises 能夠作不少事情,但就目前而言,你所須要知道的就是,若是某些函數返回 Promise,你能夠在後面附加 .then(),then 裏面包含成功的回調、失敗的回調,等等。咱們將在後面的章節中更密切地關注 JavaScript 中的 Promises 。
navigator.serviceWorker.register()
函數返回 promise,若是註冊成功的話,咱們能夠決定如何繼續進行。
以前我提到過 Service Workers 是事件驅動的,並且 Service Workers 最強大的功能之一就是容許你經過進入 fetch 事件來監放任何網絡請求。當一個資源發起 fetch 事件時,你能夠決定如何繼續進行。你能夠將發出的 HTTP 請求或接收的 HTTP 響應更改爲任何內容。這至關簡單,但同時卻很是強大!
假設你的 Service Worker 文件中的代碼片斷以下。(參見代碼清單1.2)
self.addEventListener('fetch', function(event) { ❶ if (/\.jpg$/.test(event.request.url)) { ❷ event.respondWith(fetch('/images/unicorn.jpg')); ❸ } });
在上面的代碼中,咱們監聽了 fetch 事件,若是 HTTP 請求的是 JPEG 文件,咱們就攔截請求並強制返回一張獨角獸圖片,而不是原始 URL 請求的圖片。上面的代碼會爲該網站上的每一個 JPEG 圖片請求執行一樣的操做。雖然獨角獸的圖片棒極了,可是你可能不想在現實世界的網站上這樣作,由於你的用戶可能不滿意這樣的結果!上面的代碼示例可讓你瞭解 Service Workers 的能力。只是短短几行代碼,咱們在瀏覽器中建立了一個強大的代理。
爲了讓 Service Worker 能在網站上運行,須要經過 HTTPS 來提供服務。雖然這讓使用它們變得有些困難,但這樣作有一個很重要的緣由。還記得將 Service Worker 比喻成空中交通管制員嗎?能力越大,責任越大,對於 Service Worker 而言,它們實際上也可能用於惡意的用途。若是有人可以在你的網頁上註冊一個狡詐的 Service Worker,他們將可以劫持鏈接並將其重定向到惡意端點。事實上,壞蛋們可能會利用你的 HTTP 請求作任何他們想要的事情!爲了不這種狀況發送,Service Worker 只能在經過 HTTPS 提供服務的網頁上註冊。這確保了網頁在經過網絡的過程當中沒有被篡改。
若是你是一個想要構建 PWA 的網站開發者,讀到這你可能會有點沮喪,千萬別!傳統上,爲你的網站獲取 SSL 證書可能會花費了至關多的錢,但無論你是否相信,實際上如今有許多免費的解決方案能夠供 Web 開發者使用。
首先,若是你想要在本身的電腦上測試 Service Workers,你能夠經過 localhost 提供的頁面來執行此操做。它們已經建立了這個功能,以使開發者在部署應用以前輕鬆進行本地調試。
若是你準備好將你的 PWA 發佈到網上,那麼你能夠使用一些免費的服務。Let’s Encrypt https://letsencrypt.org/ 是一個新的證書受權,它是免費的、自動的和開放的。你能夠使用 Let’s Encrypt 來快速開始讓的你網站經過 HTTPS 提供服務。若是你想了解更多關於 Let’s Encrypt 的信息,去它們的「新手入門」頁面 https://letsencrypt.org/getting-started/。
若是你像我同樣,使用 GitHub 進行源代碼控制的話,那麼你能夠使用 GitHub Pages 來測試 Service Worker 。(參見圖1.6) 經過 GitHub Pages,你能夠直接在你的 GitHub 倉庫中託管基礎網站,而無需後端。
圖1.6 GitHub Pages 容許你經過 SSL 直接在 GitHub 倉庫中託管網站。
使用 GitHub Pages 的優勢是,默認狀況下,你的網頁是經過 HTTPS 來提供的。當我第一次開始試用 Service Workers 時,GitHub Pages 容許我快速地啓動一個網站,並隨時驗證個人想法。
在本章的前面,咱們介紹過一個名爲 Flipkart 的電子商務公司的案例,Flipkart 決定將它的網站構建成 PWA 。Flipkart 是印度最大的電子商務網站,一個快速,有吸引力的網站對於他們業務的成功相當重要。還值得注意的是,在像印度這樣的新興市場,移動數據包的成本可能至關高,而且移動網絡多是不穩定的。出於這些緣由,許多新興市場中的電子商務公司都須要構建輕便、精簡的網頁,以知足任何網絡上的用戶需求。
2015年,Flipkart 採用了僅使用原生應用的策略,並決定暫時關閉它的移動網站。後來公司發現,在原生應用中提供快速和有吸引力的用戶體驗變得愈發困難。因此 Flipkart 決定從新思考它們的開發方式。經過引入可當即運行的移動 Web 應用,離線工做和從新吸引用戶的功能,使其開發人員又回到移動 Web 開發工做之中,這些引入的功能都是 PWA 所提供的。
當他們實現了本身的新的 PWA 後,便看到了立竿見影的效果。不只網站幾乎是瞬時加載的,並且當他們離線時還可以繼續瀏覽分類頁面,查看之前的搜索結果和產品頁面。數據使用量是 Flipkart 的關鍵指標,最重要的是將 Flipkart Lite 與原生應用進行比較時,Flipkart Lite 的數據量減小了3倍。
構建 PWA 給予了他們更多的好處,由於網站速度很快而且吸引人,結果是他們的用戶在網站上的使用時間增長了3倍,以及高達40%的參與度。這些都是想當使人印象深入的改進!若是你想親自見證效果,請訪問 flipkart.com 盡情享受體驗。
第1章中 Alex Russell 的引言 (關於汲取了所須要的全部原生功能的網站) 完美地總結了 PWA 的特性,並且我首次開始嘗試 Service Workers 時也是這種感受。當我真正理解了它們工做的基本概念後,我慢慢地意識到,它們的強大其實遠遠超乎個人想象,甚至讓我腦洞大開。隨着我愈來愈多地瞭解 PWA,並開始嘗試每次學習使用一個新功能或「元素」。一般學習新技術就像是在登山,若是你以一步一趨的思惟方式來學習 PWA 相關的知識的話,你將很快掌握 PWA 的藝術。
我相信,不少讀這本書的人都會在他們目前的項目中花費大量的時間和精力。幸運的是,構建一個 PWA 並不須要你從頭開始把項目再重作一遍。當我嘗試改善現有的應用時,每當我以爲一個功能對用戶有益並能爲他們提供加強的體驗時,我就會添加這個新「功能」。我喜歡把每一個 PWA 的新功能都看做是能夠升級超級馬里奧的新蘑菇!
若是你認爲你現有的 Web 應用能夠從 PWA 的功能中受益,我推薦你一個叫作 Lighthouse (https://github.com/GoogleChrome/lighthouse) 的便利工具。它提供 Web 應用相關的有用的性能信息和審覈信息。(參見圖2.1)
圖2.1 Lighthouse 工具很是適用於衡量 PWA 的審覈和生產性能。
你也能夠把它當作命令行界面來使用,或者若是你使用 Google Chrome 瀏覽器的話,還有方便的 Chrome 插件能夠使用。若是你在打開網站時運行它,它會生成與上面圖片相似的內容。該工具針對你的網站進行審覈,並生成一個有用的功能和性能指標清單,可用於改進你的網站。若是你想使用這個方便的工具並但願將其運行到你現有的一個網站上,請移步至 github.com/GoogleChrome/lighthouse 以瞭解更多信息。
有了 Lighthouse 工具的反饋,你能夠每次添加一個新功能,慢慢地提高你網站的總體體驗。
此刻,你可能想了解有哪些功能是能夠添加到你現有網站上的! Service Workers 開闢了一個充滿可能性的世界,因此,決定從哪裏入手是相當重要的。在本書的其他部分,每章都會重點介紹 PWA 的一個新功能,不管你是爲了優化現有網站,仍是爲了構建一個全新的網站,均可以即學即用。
在開發者之中,一般會討論是構建原生應用仍是 Web 應用,到底哪一個更好。就我我的而言,我認爲你應該根據用戶的須要來構建應用。不該該出現 PWA 和原生應用相爭的情況,做爲開發者,咱們應該不斷探索提高用戶體驗的方法。就像你想的那樣,我會對構建 Web 應用有本身的偏好,但不管你是否喜歡,若是你將 PWA 視爲一套「最佳」實踐的話,你都會構建出更好的網站。假設說,你喜歡用 React 或 Angular 進行開發,你徹底能夠繼續使用它們,由於構建 PWA 只會加強 Web 應用,並使其速度更快,更具吸引力和更有彈性。
原生應用開發者長期以來一直可以給他們的用戶提供 Web 開發者求之不得的功能,例如離線操做和不管網絡鏈接如何均可以響應的功能。然而,要感謝 PWA 帶給 Web 的新功能,咱們能夠努力構建更好的網站。許多原生應用都有着良好的架構,做爲 Web 開發者,咱們能夠從他們的架構方法中進行學習。在下一節中,咱們會來看看構建 PWA 時,在前端代碼中能夠使用的不一樣架構方式。
當今有不少很是棒的原生應用。就我的而言,我以爲 Facebook 的原生應用爲用戶提供了很是棒的體驗。當你離線時它會給你提示,它會緩存你的時間軸,以便你能更快地訪問,它還能作到瞬間加載。若是你有一段時間沒有訪問 Facebook 的原生應用,你仍會在任何動態內容加載以前,當即看到一個空的「UI 外殼」,包括頭部和導航條。
藉助 Service Workers 的力量,咱們沒有任何理由不爲 Web 上的用戶提供一樣的體驗。使用智能的 Service Worker 緩存,你實際上能夠緩存你網站的 UI 外殼,以便用戶重複訪問。這些新功能使咱們可以以不一樣的方式來思考和構建網站。
此刻你可能想知道什麼是 「UI 外殼」:它只是用戶界面所必需的最小化的 HTML、CSS 和 JavaScript 。它可能會是相似網站頭部,底部和導航這樣沒有任何動態內容的部分。若是咱們能加載並緩存 UI 外殼,咱們就能夠在稍後的階段將動態內容加載到頁面中。Google 的 Inbox 就是一個很好的現成例子。咱們來看看下面的圖片,以得到更好的理解。
圖2.2 Google 的 Inbox 利用 Service Workers 來緩存 UI 外殼。
你可能對 Google 的 Inbox 已經很熟悉了。(參見圖2.2) 它是一個便利的 Web 應用,它容許你組織和發送郵件。在底層它使用 Service Workers 來緩存,併爲用戶提供超級快的體驗。如你在上圖中所見,當你訪問該網站的時候,首先它的 UI 外殼會當即呈如今你眼前。這是很是棒的,由於用戶得到了即時反饋,這會讓他們感到網站速度很是快,即便咱們仍在等待其他部分的動態內容加載。該應用給用戶一種感觀上的速度快,即便它用來獲取內容的時間並不比以前短。用戶還會注意到 「loading」 指示符,它表示網站正在發生一些事情,此刻正忙。這比等待一個空白頁面加載好久好多了!
一旦外殼加載完,網站的動態內容就會使用 JavaScript 來獲取並加載。
圖2.3 一旦 UI 外殼加載完,就能夠獲取網站的動態內容,而後添加到頁面的剩餘部分。
上面的圖2.3展現了 Google 的 Inbox 網站在動態內容加載完後就會將其填充到 Web 應用之中。使用一樣的技術,你能夠爲網站的重複訪問提供瞬時加載,還能夠緩存應用的 UI 外殼,這樣它就能離線工做。這樣即便用戶當前沒有鏈接,他們也能夠看到應用的 UI 外殼。
在第3章中,你將學習如何利用 Service Workers 來緩存你的內容而且爲你的用戶提供離線體驗。在本書中,咱們會構建一個 PWA,它會使用應用外殼架構,你能夠下載並遵循此代碼,而後使用此方法來構建你本身的應用。
使用了應用外殼架構的 Web 應用「瞬時」加載提及來很容易,但這對用戶到底意味着什麼呢?多快纔是「瞬時」?爲了更好的從視覺上感覺出使用應用外殼架構的 PWA 加載有多塊,我使用了叫作 webpagetest.org 的工具來生成下面的幻燈片。(參見圖2.4)
圖2.4 即便在動態內容加載完成以前,應用外殼架構也能夠在屏幕上爲用戶提供有意義的內容。上圖顯示了使用 Service Workers 進行緩存先後的加載時間。
我對我構建的名爲 Progressive Beer 的 PWA 運行該工具。上圖顯示了一段時間內 PWA 加載的幻燈片視圖。在圖像的頂部,你能夠看到以秒爲單位的時間及其對應的屏幕上的頁面顯示狀況。
對於首次訪問的用戶,該網站須要更長時間進行下載,由於是首次獲取這些資源。一旦全部的資源下載完成,首次訪問的用戶大約在4秒後可以在與網站進行充分的互動。
對於再次訪問的用戶,激活的 Service Worker 便會進行安裝,用戶大概會在0.5秒 (500毫秒) 後看到網站的 UI 外殼。儘管動態內容尚未從服務器返回,但 UI 外殼會首先加載。此後,剩餘的動態內容會被加載並填充到屏幕之中。此方法最棒的是,即便沒有網絡鏈接,用戶仍然能夠在大約500毫秒後看到網站的 UI 外殼。此刻,咱們能夠向他們展現一些有意義的東西,要麼通知他們處於離線狀態,要麼爲他們提供緩存的內容。
記住,每次用戶從新訪問網站,他們都會體驗到這種快速、可靠和有吸引力的加強體驗。若是你即將開發一個新的 Web 應用的話,使用應用外殼架構會是一種利用 Service Workers 的高效方式。
在第1章中,咱們經歷了 Service Worker 生命週期的各個階段。起初,它可能沒有太多的意義,但隨着咱們深刻了解應用外殼架構是如何工做的,它便開始有意義了。記住,使用 Service Worker 你便可以進入它生命週期的各個不一樣事件。
爲了更好的理解如何進入這些事件,咱們來看看下面的圖2.5。
圖2.5 在 Service Worker 安裝過程當中,咱們能夠獲取資源併爲下次訪問作好預緩存。
當用戶首次訪問網站時,Service Worker 會開始下載並安裝自身。在安裝階段,咱們能夠進入這個事件並準備緩存 UI 外殼所需的全部資源。也就是說,基礎的 HTML 頁面和任何可能須要的 CSS 或 JavaScript 。
此時,咱們能夠當即提供網站的「外殼」,由於它已經添加到 Service Worker 緩存之中。這些資源的 HTTP 請求不再須要轉到服務器了。一旦用戶導航到另外一個頁面,他們將當即看到外殼。(參見圖2.6)
圖2.6 對於發起的任意 HTTP 請求,咱們能夠檢查資源是否存在於緩存之中,若是不存在的話,咱們再經過網絡來獲取它們。
動態內容被加載到網站中,網站會正常運行。由於你可以進入這些請求的 fetch 事件,因此你能夠決定是否要緩存它們。你可能有常常更新的動態內容,所以緩存它們沒有意義。可是,你的用戶仍然會獲得更快、更好的瀏覽體驗。在下章中,咱們會深刻 Service Worker 緩存,在那裏你將瞭解到更多關於這個強大功能的具體內容。
儘管 PWA 仍是個比較新的概念,但已有一些了不得的 PWA 已經在網絡上供天天數百萬用戶使用。
在下章中,咱們會深刻代碼並向你展現如何開始構建本身的 PWA 。在咱們更進一步以前,爲了更好的理解 PWA 的這些功能是如何工做的,剖析現有的 PWA 仍是有必要的。
在本節中,咱們來看一個我我的很是喜歡的 PWA 。Twitter 的手機網站就是 PWA,它爲使用移動設備的用戶提供了加強體驗。若是你使用 Twitter,那這是在旅途中查看推文的好方法。(參見圖2.7)
圖2.7 Twitter 的手機網站就是 PWA,它採用了應用外殼架構
若是在移動設備上導航到 twitter.com , 會重定向到 mobile.twitter.com 並展示一個不一樣的網站。Twitter 將它們的 PWA 命名爲 「Twitter Lite」,由於它佔用的存儲空間不到1MB,並聲稱能夠節省多達70%的數據,同時加快30%的速度。
就我的而言,我以爲它很是棒,它應該還能夠 PC 端使用!相對於原生版本,我實際上更喜歡 PWA 版本。你仍然能夠在 PC 端上經過直接導航到mobile.twitter.com 來訪問 Web 應用。
在底層,Twitter Lite 是使用應用外殼架構構建的。這意味着它使用簡單的 HTML 頁面做爲網站的 UI 外殼,而頁面的主要內容是使用 JavaScript 動態注入的。若是用戶的瀏覽器支持 Service Workers,那麼 UI 外殼所需的全部資源都會在 Service Worker 安裝階段被緩存。
圖2.8 應用外殼架構當即賦予屏幕有意義的內容。左邊的圖片是用戶首先看到的,一旦數據加載完成,就像右邊圖片那樣呈現出數據。
對於重複訪客,這意味着外殼會瞬間加載並可以在沒有任何延遲的狀況下賦予屏幕有意義的內容。(參見圖2.8) 對於不支持 Service Workers 的瀏覽器,此方法仍將以相同的方式工做,他們只是沒有緩存 UI 外殼的資源, 而且會失去超快性能的附加獎勵。Web 應用還針對使用響應式網頁設計的不一樣屏幕尺寸進行了優化。
Service Worker 緩存是一個強大的功能,它賦予咱們這些 Web 開發者使用編程方式來緩存所需資源的能力。你可以攔截 HTTP 請求和響應,並根據你的須要調整它們。這個強大的功能是解鎖更好的 Web 應用的關鍵。使用 Service Worker 容許你進入任何網絡請求並徹底由你來決定想要如何響應。
使用 Service Worker 緩存能夠輕鬆完成快速、有彈性的 PWA 。Twitter Lite 很快,真的很是快。若是你在頁面之間進行瀏覽,你會以爲這個 Web 應用很是好用,已緩存的頁面幾乎都是瞬時加載。做爲用戶,這種體驗是我對於每個網站的指望!
在底層,Twitter Lite 使用了一個叫作 Service Worker Toolbox 的庫。這個庫很方便,它包含一些使用 Service Workers 進行嘗試並驗證過的緩存技術。該工具箱爲你提供了一些基本輔助方法,以便你開始建立本身的 Service Workers,並使你避免編寫重複代碼。在第3章中,咱們將深刻緩存,但目前咱們仍是先來看下使用 Service Worker Toolbox 的緩存示例。
toolbox.router.get("/emoji/v2/svg/:icon", function(event) { ❶ return caches.open('twemoji').then(function(response) { ❷ return response.match(event.request).then(function(response) { ❸ return response || fetch(event.request) ❹ }) }).catch(function() { return fetch(event.request) ❺ }) }, { origin: /abs.*\.twimg\.com$/ ❻ })
在上面的代碼清單2.1中,Service Worker Toolbox 尋找 URL 匹配'/emoji/v2/svg/' 而且來自 *.twimg.com 站點的任何請求。一旦它攔截了匹配此路由的任意 HTTP 請求,它會將它們存儲在名爲 'twemoji' 的緩存之中。等下次用戶再次發起匹配此路由的請求時,呈現給用戶的將是緩存的結果。
這段代碼是很是強大的,它賦予咱們這些開發者一種能力,使咱們能夠精準控制如何以及什麼時候在網站上緩存資源。若是起初這段代碼會讓你有些困惑,也不要擔憂。在下章中,咱們會深刻 Service Worker 緩存並使用這個強大功能來構建頁面。
我天天上下班的途中都是在火車上度過的。很幸運,旅途不算太長,但不幸的是在某些區域網絡信號很弱,甚至是掉線。這意味着若是我正在手機上瀏覽網頁,有時我可能會失去鏈接,或者鏈接至關不穩定。這是至關使人沮喪的!
幸運的是,Service Worker 緩存是個強大的功能,它其實是將網站資源保存到用戶的設備上。這意味着使用 Service Workers 就可讓你攔截任何 HTTP 請求並直接用設備上緩存的資源進行響應。你甚至不須要訪問網絡就能夠獲取緩存的資源。
考慮到這一點,咱們能夠使用這些功能來構建離線頁面。使用 Service Worker 緩存,你能夠緩存個別的資源,甚至是整個網頁,這徹底取決於你。若是用戶沒有網絡鏈接,Twitter Lite 會爲用戶展示一個自定義的離線頁面。
圖2.9 若是用戶沒有網絡鏈接,Twitter PWA 會爲用戶顯示一個自定義的錯誤頁面。
用戶如今看到的是一個有幫助的自定義離線頁面,而不是可怕的錯誤: 「沒法訪問此網站」 (參見圖2.9)。他們還能夠經過點擊提供的按鈕來檢查鏈接是否恢復。對於用戶來講,這種 Web 體驗更好。在第8章中,你將掌握開始構建本身的離線頁面所需的必要技能,併爲你的用戶提供富有彈性的瀏覽體驗。
Twitter Lite 很快,並且針對小屏幕進行了優化,還能離線工做。還有什麼?好吧,它須要如同原生應用同樣的外觀感覺!若是你仔細查看過 Web 應用主頁的 HTML 的話,可能會注意到下面這行代碼:
<link rel="manifest" href="/manifest.json">
這個連接指向一個被稱爲「清單文件」的文件。這個文件只是簡單的 JSON 文件,它遵循 W3C 的 Web App Manifest 規範,並使開發者可以控制應用中不一樣元素的外觀感受。它提供 Web 應用的信息,好比名稱,做者,圖標和描述。
它帶來了一些好處。首先,它使瀏覽器可以將 Web 應用安裝到設備的主屏幕,以便爲用戶提供更快捷的訪問和更豐富的體驗。其次,經過在清單文件中設置品牌顏色,你能夠自定義瀏覽器自動顯示的啓動畫面。它還容許你自定義瀏覽器的地址欄以匹配你的品牌顏色。
使用清單文件真正地使 Web 應用的外觀感受更加完美,併爲你的用戶提供了更豐富的體驗。Twitter Lite 使用清單文件以利用瀏覽器中的許多內置功能。
在第5章中,咱們會探索如何使用清單文件來加強 PWA 的外觀感覺,併爲用戶提供有吸引力的瀏覽體驗。
Twitter Lite 是一個全面的例子,它很好地詮釋了 PWA 應該是怎樣的。它涵蓋了貫穿本書的絕大部分功能,這些功能都是爲了構建一個快速、有吸引力和可靠的 Web 應用。
在第1章中,咱們討論過了 Web 應用應該具有的全部功能。讓咱們來回顧下到目前爲止 Twitter PWA 的細節。該應用是:
哇!真是個大清單,但幸運的是咱們所獲得的這些收益很多都是構建 PWA 的附屬品。
若是你曾經急於從網站上獲取緊急信息,那你會懂得等待網頁加載能夠是多麼一件使人沮喪的事情。事實上,在尼爾森諾曼集團的一項研究中,他們發現,10秒的延遲一般會使用戶當即離開網站,即便他們留下來,他們也很難了解將會發生什麼,讓他們在這種處境下繼續堅持下去直到網頁加載完彷佛變得不太可能。若是你經營的是一個基於在線的業務,你可能已經失去了將此人轉換爲銷售的機會。這就是爲何構建快速和有效工做的網頁如此重要,不管用戶設備是怎樣的。
在本書的第2部分,咱們會專一於如何使用 Service Workers 來提高 PWA 的性能。從緩存技術到備用圖片格式,Service Workers 的靈活性都足以應對各類狀況。
在第3章中,咱們會深刻 Service Worker 緩存並幫助你理解可應用於 Web 應用的不一樣緩存技術。咱們先從一個很是基礎的緩存示例入手,而後擴展爲不一樣的緩存方法。不管網站的前端代碼是如何編寫的,使用 Service Worker 緩存均可以大大改善頁面的加載速度。咱們還會看一些緩存相關的陷阱並提出建議以幫助你來處理它們。這一章會以Service Worker Toolbox 的簡短介紹來結尾,Service Worker Toolbox 是一個有用的庫,它使得編寫緩存代碼更加簡單。
在第4章中,咱們會深刻 Fetch API,並看看如何利用它來構建更快的 Web 應用。這一章涵蓋了一些小技巧,能夠使用它們來將你的網站性能提高至最佳。咱們還涉及到了一項返回輕量級圖片格式 (WebP) 的技術,而後還會看下如何在 Android 設備上接入 「Save-Data」 以減小網頁的總體體積。
現代瀏覽器真的十分聰明,它們能夠解釋和理解各類 HTTP 請求和響應,而且可以在須要數據以前進行存儲和緩存。我喜歡將瀏覽器緩存信息的能力看做牛奶上的最遲銷售日期。一樣的方式,你能夠將牛奶保存在冰箱中,直至到達保質期,瀏覽器也能夠在一段時間內緩存網站相關的信息。在過時後,它會去獲取更新後的版本。這能夠確保網頁加載更快並消耗更少的帶寬。
在深刻 Service Worker 緩存以前,前後退一步,瞭解下傳統 HTTP 緩存的工做原理是很重要的。自從20世紀90年代初引入 HTTP/1.0 以來,Web 開發者便已經可以使用 HTTP 緩存了。HTTP 緩存容許服務器發送正確的 HTTP 首部,這些首部信息將指示瀏覽器在一段時間內緩存響應。
Web 服務器能夠利用瀏覽器的能力來緩存數據,並使用它來減小重複請求的加載時間。若是一個用戶在一次會話中訪問同一個頁面兩次,若是數據沒有改變,一般不須要爲它們提供新的資源。這樣一來,Web 服務器能夠使用 Expires 首部來通知 Web 客戶端,它能夠使用資源的當前副本,直到指定的「過時時間」。反過來,瀏覽器能夠緩存此資源,而且只有在有效期滿後纔會再次檢查新版本。
圖3.1 當瀏覽器發起一個資源的 HTTP 請求時, 服務器會發送的 HTTP 響應會包含該資源相關的有用信息
在上圖中,你能夠看到當瀏覽器發起一個資源的 HTTP 請求時,服務器返回的資源還附帶一些 HTTP 首部。這些首部包含有用的信息,瀏覽器能夠經過它們來了解資源相關的更多信息。HTTP 響應告訴瀏覽器這個資源是什麼類型的,要緩存多長時間,它是否壓縮過,等等。
HTTP 緩存是提升網站性能的絕佳方式,但它也有自身的缺陷。使用 HTTP 緩存意味着你要依賴服務器來告訴你什麼時候緩存資源和什麼時候過時。若是你的內容具備相關性,任何更新均可能致使服務器發送的到期日期很容易變得不一樣步,並影響你的網站。
能力越大,責任越大, 對 HTTP 緩存來講,真是再正確不過了。當咱們對 HTML 進行重大更改時,咱們也可能會更改 CSS 以及對應的新的 HTML 結構,並更新任何 JavaScript 以適應樣式和內容的更改。若是你曾經在發佈更改後的網站時沒有獲得正確的 HTTP 緩存的話,我相信你會明白這是因爲錯誤的緩存資源所致使的網站被破壞。
下面的圖是個人我的博客在文件被錯誤緩存狀況下的樣子。
圖3.2 當緩存的文件不一樣步時,網站的感觀都會受其影響
你能夠想象一下,不管是對於開發者仍是用戶,這都是很是使人沮喪的!在上面的圖3.2中,你能夠看到頁面的 CSS 樣式沒有加載,這是因爲不正確的緩存而致使的文件不匹配。
本章讀到此處,你可能會思考,咱們都已經有 HTTP 緩存了,爲什麼還須要 Service Worker 緩存呢? Service Worker 緩存有何不一樣呢?好吧,它能夠替代服務器來告訴瀏覽器資源要緩存多久,做爲開發者的你能夠全權掌控。Service Worker 緩存極其強大,由於對於如何緩存資源,它賦予了你程序式的精準控制能力。與全部 PWA 的功能同樣,Service Worker 緩存是對 HTTP 緩存的加強,並能夠與之配合使用。
Service Workers 的強大在於它們攔截 HTTP 請求的能力。在本章中,咱們將使用這種攔截 HTTP 請求和響應的能力,從而爲用戶提供直接來自緩存的超快速響應!
使用 Service Workers,你能夠進入任何傳入的 HTTP 請求,並決定想要如何響應。在你的 Service Worker 中,能夠編寫邏輯來決定想要緩存的資源,以及須要知足什麼條件和資源須要緩存多久。一切盡歸你掌控!
在上一章中,咱們簡要地看過一個示例,以下面圖3.3所示。當用戶首次訪問網站時,Service Worker 會開始下載並安裝自身。在安裝階段中,咱們能夠進入這個事件,並準備緩存 Web 應用所需的全部重要資源。
圖3.3 在 Service Worker 安裝階段,咱們能夠獲取資源併爲下次訪問準備好緩存
以此圖爲例,咱們來建立一個基礎的緩存示例,以便更好地瞭解它是如何實際工做的。在下面的清單3.1中,我建立了一個簡單的 HTML 頁面,它註冊了 Service Worker 文件。
<!DOCTYPE html>
<html>
<head> <meta charset="UTF-8"> <title>Hello Caching World!</title> </head> <body> <!-- Image --> <img src="/images/hello.png" /> ❶ <!-- JavaScript --> <script async src="/js/script.js"></script> ❷ <script> // 註冊 service worker if ('serviceWorker' in navigator) { ❸ navigator.serviceWorker.register('/service-worker.js').then(function (registration) { // 註冊成功 console.log('ServiceWorker registration successful with scope: ', registration.scope); }).catch(function (err) { ❹ // 註冊失敗 :( console.log('ServiceWorker registration failed: ', err); }); } </script> </body> </html>
在上面的清單3.1中,你能夠看到一個引用了圖片和 JavaScript 文件的簡單網頁。該網頁沒有任何華麗的地方,但咱們會用它來學習如何使用 Service Worker 緩存來緩存資源。上面的代碼會檢查你的瀏覽器是否支持 Service Worker,若是支持,它會嘗試去註冊一個叫作 service-worker.js
的文件。
好了,咱們已經準備好了基礎頁面,下一步咱們須要建立緩存資源的代碼。清單3.2中的代碼會進入叫作 service-worker.js
的 Service Worker 文件。
var cacheName = 'helloWorld'; ❶ self.addEventListener('install', event => { ❷ event.waitUntil( caches.open(cacheName) ❸ .then(cache => cache.addAll([ ❹ '/js/script.js', '/images/hello.png' ])) ); });
在第1章中,咱們看過了 Service Worker 生命週期和它激活以前所經歷的不一樣階段。其中一個階段就是 install 事件,它發生在瀏覽器安裝並註冊 Service Worker 時。這是把資源添加到緩存中的絕佳時間,在後面的階段可能會用到這些資源。例如,若是我知道某個 JavaScript 文件可能整個網站都會使用它,咱們就能夠在安裝期間緩存它。這意味着另外一個引用此 JavaScript 文件的頁面可以在後面的階段輕鬆地從緩存中獲取文件。
清單3.2中的代碼進入了 install
事件,並在此階段將 JavaScript 文件和 hello 圖片添加到緩存中。在上面的清單中,我還引用了一個叫作 cacheName
的變量。這是一個字符串,我用它來設置緩存的名稱。你能夠爲每一個緩存取不一樣的名稱,甚至能夠擁有一個緩存的多個不一樣的副本,由於每一個新的字符串使其惟一。當看到本章後面的版本控制和緩存清除時,你將會感覺到它所帶來的便利。
在清單3.2中,你能夠看到一旦緩存被開啓,咱們就能夠開始把資源添加進去。接下來,咱們調用了 cache.addAll()
並傳入文件數組。event.waitUntil()
方法使用了 JavaScript 的 Promise 並用它來知曉安裝所需的時間以及是否安裝成功。
若是全部的文件都成功緩存了,那麼 Service Worker 便會安裝完成。若是任何文件下載失敗了,那麼安裝過程也會隨之失敗。這點很是重要,由於它意味着你須要依賴的全部資源都存在於服務器中,而且你須要注意決定在安裝步驟中緩存的文件列表。定義一個很長的文件列表便會增長緩存失敗的概率,多一個文件便多一份風險,從而致使你的 Servicer Worker 沒法安裝。
如今咱們的緩存已經準備好了,咱們可以開始從中讀取資源。咱們須要在清單3.3中添加代碼,讓 Service Worker 開始監聽 fetch 事件。
self.addEventListener('fetch', function (event) { ❶ event.respondWith( caches.match(event.request) ❷ .then(function (response) { if (response) { ❸ return response; ❹ } return fetch(event.request); ❺ }) ); });
清單3.3中的代碼是咱們 Service Worker 傑做的最後一部分。咱們首先爲 fetch 事件添加一個事件監聽器。接下來,咱們使用 caches.match()
函數來檢查傳入的請求 URL 是否匹配當前緩存中存在的任何內容。若是存在的話,咱們就簡單地返回緩存的資源。可是,若是資源並不存在於緩存當中,咱們就如往常同樣繼續,經過網絡來獲取資源。
若是你打開一個支持 Service Workers 的瀏覽器並導航至最新建立的頁面,你應該會注意到相似於下圖3.4中的內容。
圖3.4 示例代碼生成了帶有圖片和 JavaScript 文件的基礎網頁
請求的資源如今應該是能夠在 Service Worker 緩存中獲取的。當我刷新頁面時,Service Worker 會攔截 HTTP 請求並從緩存中當即加載合適的資源,而不是發起網絡請求到服務器端。Service Worker 中只需短短几行代碼,你便擁有了一個直接從緩存加載的網站,並能當即響應重複訪問!
附註一點,Service workers 只能在 HTTPS 這樣的安全來源中使用。然而,當開在本機上開發 Service Workers 時,你可以使用 http://localhost 。Service Workers 已經創建了這樣的方式,以確保發佈後的安全,並且同時還兼顧了靈活性,使開發者在本機上工做變得更加容易。
一些現代瀏覽器能夠使用瀏覽器內置的開發者工具來查看 Service Worker 緩存中的內容。例如,若是你打開 Google Chrome 的開發者工具並切換至 「Application」 標籤頁,你可以看到相似於下圖3.5中的內容。
圖3.5 當你想看緩存中存儲什麼時, Google Chrome 的開發者工具會很是有用
圖3.5展現了名稱爲 helloWorld
的緩存項中存儲了 scripts.js
和 hello.png
兩個文件。如今資源已經存儲在緩存中,從此這些資源的任何請求都會從緩存中當即取出。
在清單3.2中,咱們看過了如何在 Service Worker 安裝期間緩存任何重要的資源,這被稱之爲「預緩存」。當你確切地知道你要緩存的資源時,這個示例能很好地工做,可是資源多是動態的,或者你可能對資源徹底不瞭解呢?例如,你的網站多是一個體育新聞網站,它須要在比賽期間不斷更新,在 Service Worker 安裝期間你是不會知道這些文件的。
由於 Service Workers 可以攔截 HTTP 請求,對於咱們來講,這是發起請求而後將響應存儲在緩存中的絕佳機會。這意味着咱們改成先請求資源,而後當即緩存起來。這樣一來,對於一樣資源的發起的下一次 HTTP 請求,咱們能夠當即將其從 Service Worker 緩存中取出。
圖3.6 對於發起的任何 HTTP 請求,咱們能夠檢查資源是否在緩存中已經存在,若是沒有的話再經過網絡來獲取
咱們來更新下以前清單3.1中的代碼。
<!DOCTYPE html>
<html>
<head> <meta charset="UTF-8"> <title>Hello Caching World!</title> <link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet"> ❶ <style> #body { font-family: 'Lato', sans-serif; } </style> </head> <body> <h1>Hello Service Worker Cache!</h1> <!-- JavaScript --> <script async src="/js/script.js"></script> ❷ <script> if ('serviceWorker' in navigator) { ❸ navigator.serviceWorker.register('/service-worker.js').then(function (registration) { console.log('ServiceWorker registration successful with scope: ', registration.scope); }).catch(function (err) { ❹ console.log('ServiceWorker registration failed: ', err); }); } </script> </body> </html>
相比較於清單3.1,清單3.4中的代碼並無太大變化,除了咱們在 HEAD 標籤中添加了一個網絡字體的引用。因爲這是一個可能會發生變化的外部資源,因此咱們能夠在 HTTP 請求完成後緩存該資源。你還會注意到,咱們用來註冊 Service Worker 的代碼並無改變。事實上,除了一些例外,這段代碼是註冊 Service Worker 想當標準的方式。在本書中,咱們會反覆使用這段樣板代碼來註冊 Service Worker 。
如今頁面已經完成,咱們準備開始爲 Service Worker 文件添加一些代碼。下面的清單3.5展現了咱們要使用的代碼。
var cacheName = 'helloWorld'; ❶ self.addEventListener('fetch', function (event) { ❷ event.respondWith( caches.match(event.request) ❸ .then(function (response) { if (response) { ❹ return response; } var requestToCache = event.request.clone(); ❺ return fetch(requestToCache).then( ❻ function (response) { if (!response || response.status !== 200) { ❼ return response; } var responseToCache = response.clone(); ❽ caches.open(cacheName) ❾ .then(function (cache) { cache.put(requestToCache, responseToCache); ❿ }); return response; } ); }) ); });
清單3.5中的代碼看上去好多啊!咱們分解來看,並解釋每一個部分。代碼先經過添加事件監聽器來進入 fetch 事件。咱們首先要作的就是檢查請求的資源是否存在於緩存之中。若是存在,咱們能夠就此返回緩存並再也不繼續執行代碼。
然而,若是請求的資源於緩存之中沒有的話,咱們就按原計劃發起網絡請求。在代碼更進一步以前,咱們須要克隆請求。須要這麼作是由於請求是一個流,它只能消耗一次。由於咱們已經經過緩存消耗了一次,而後發起 HTTP 請求還要再消耗一次,因此咱們須要在此時克隆請求。而後,咱們須要檢查 HTTP 響應,確保服務器返回的是成功響應而且沒有任何問題。咱們毫不想緩存一個錯誤的結果!
若是成功響應,咱們會再次克隆響應。你可能會疑惑咱們爲何須要再次克隆響應,請記住響應是一個流,它只能消耗一次。由於咱們想要瀏覽器和緩存都可以消耗響應,因此咱們須要克隆它,這樣就有了兩個流。
最後,代碼中使用這個響應並將其添加至緩存中,以便下次再使用它。若是用戶刷新頁面或訪問網站另外一個請求了這些資源的頁面,它會當即從緩存中獲取資源,而再也不是經過網絡。
圖3.7 使用 Google Chrome 的開發者工具,咱們能夠看到網絡字體已經經過網絡獲取並添加至緩存中,以確保重複請求時速度更快
若是你仔細看下上面的圖3.7,你會注意到頁面上三個資源的緩存中有新項。在上面的代碼示例中,每次返回成功的 HTTP 響應時,咱們都可以動態地向緩存中添加資源。對於你可能想要緩存資源,但不太肯定它們可能更改的頻率或確切地來自哪裏,那麼這種技術多是完美的。
Service Workers 賦予做爲開發者的你經過代碼進行徹底的控制,並容許你輕鬆構建適合需求的自定義緩存解決方案。事實上,使用咱們前面提到的兩種緩存技術能夠組合起來,以使加載速度更快。徹底由你來掌控這一切。
例如,假設你正在構建一個使用應用外殼架構的新 Web 應用。你可能會想要使用咱們在清單3.2中的代碼來預緩存外殼。對於以後任何的 HTTP 請求都會使用攔截並緩存的技術進行緩存。或者你也許只想緩存現有網站中已知的、不會常常更改的部分。經過簡單地攔截並緩存這些資源,就能爲用戶提供更好的性能,卻只需短短几行代碼。取決於你的狀況,Service Worker 緩存能夠適用你的需求,並當即使用戶獲得的體驗全部提高。
到目前爲止,咱們已經運行的代碼示例都是有幫助的,可是單獨思考它們並不太容易。在第1章中,咱們討論了能夠用 Service Workers 來構建超棒 Web 應用的多種不一樣方式。報紙 Web 應用即是其中一個,咱們能夠在現實世界中使用所學到的關於 Service Workers 緩存的一切知識。我將咱們的示例應用命名爲 'Progressive Times'。該 Web 應用是一個新聞網站,人們會按期訪問並閱讀多個頁面,因此提早保存將來的頁面是有意義的,以便它們可以當即加載。咱們甚至能夠保存網站自己,以便用戶能夠離線瀏覽。
咱們的示例 Web 應用包含一些來自世界各地的有趣新聞。不管你是否相信,這個新聞網站的全部報道都是真實的,而且有可靠的信息來源!Web 應用包含絕大多數你能夠想象的網站元素,好比 CSS、JavaSript 和圖片。爲了保持示例代碼的基礎性,我還爲每篇文章準備了扁平的 JSON 文件,在現實生活中,這應該指向一個後端端點以獲取相似格式的數據。就自身而言,這個 Web 應用並不怎麼使人印象深入,可是當咱們開始利用 Service Workers 的能力時,即可以把它提高到一個新的水平。
Web 應用使用應用外殼架構來動態地獲取每篇文章的內容並將數據填充到頁面上。
圖3.8 Progressive Times 示例應用使用應用外殼架構
使用應用外殼架構還意味着咱們能夠利用預緩存技術,爲了確保 Web 應用的重複訪問可以當即加載。咱們還能夠假定訪問者會點擊連接和閱讀新聞文章的完整內容。若是當 Service Worker 安裝後咱們將這些內容緩存,這意味着對於他們下個頁面的加載速度會快不少。
讓咱們把到目前爲止在本章在所學的整合起來,併爲 Progressive Times 應用添加 Service Worker ,它將預處理重要資源,並緩存任何其餘請求。
var cacheName = 'latestNews-v1'; // 在安裝過程當中緩存咱們已知的資源 self.addEventListener('install', event => { event.waitUntil( caches.open(cacheName) .then(cache => cache.addAll([ ❶ './js/main.js', './js/article.js', './images/newspaper.svg', './css/site.css', './data/latest.json', './data/data-1.json', './article.html', './index.html' ])) ); }); // 緩存任何獲取的新資源 self.addEventListener('fetch', event => { ❷ event.respondWith( caches.match(event.request, { ignoreSearch: true }) ❸ .then(function (response) { if (response) { return response; ❹ } var requestToCache = event.request.clone(); return fetch(requestToCache).then( ❺ function (response) { if (!response || response.status !== 200) { return response; } var responseToCache = response.clone(); caches.open(cacheName) .then(function (cache) { cache.put(requestToCache, responseToCache); ❻ }); return response; }); }) ); });
清單3.6中的代碼是安裝期間的預緩存和獲取資源時進行緩存的組合應用。該 Web 應用使用了應用外殼架構,這意味着咱們能夠利用 Service Worker 緩存來只請求填充頁面所需的數據。咱們已經成功存儲了外殼的資源,因此剩下的就是來自服務器的動態新聞內容。
這個網頁是託管在 Github 的,若是想親身體驗,能夠導航至 bit.ly/chapter-pwa-3 輕鬆訪問。事實上,我將本書中所使用的全部代碼示例都添加到了這個 Github 倉庫中。
每章都有自述文件,它解釋了你須要作什麼來開始構建和實驗每章中的示例代碼。大約90%的章節都只是前端代碼,因此你所須要的只是啓動你的本地主機並開始使用。還有一點值得注意的是你須要在 http://localhost 環境上運行代碼,而不是 file:// 。
此刻,我但願我已經說服了你,Service Worker 緩存是多麼優秀。還沒!?好吧,但願這些使用緩存後得到的性能改善能令你改變主意!
以咱們的 Progressive Times 應用爲例,能夠比較使用 Service Worker 緩存先後的差異。我最喜歡的一種網站性能測試的方法是使用 webpagetest.org 這個工具。
圖3.9 WebPagetest.org 是一個免費工具,能夠使用來自世界各地的真實設備對你的網站進行測試
Webpagetest.org 是一個很棒的工具。只需簡單地輸入網站 URL,它就可讓你使用真實設備和各類類型的瀏覽器從世界任何地點來分析你的網站。測試運行在真實設備上,併爲你提供關於網站有用的分類和性能分析。最棒的是,它是開源而且徹底無償使用的。
若是我經過 Webpagetest.org 運行咱們的示例應用,它會生成相似下面圖3.10的表格。
圖3.10 WebPagetest.org 經過使用真實設備生成有關 Web 應用性能的有用信息
爲了在真實設備上測試咱們示例應用的性能,我在 Webpagetest 上使用了新加坡的 2G 節點。若是你曾經試圖經過緩慢的網絡訪問一個網站的話,那麼你會知道,等待網站完成是多麼使人討厭的過程。對於 Web 開發者來講,重要的是咱們應該像用戶同樣對網站進行測試,其中包括使用速度較慢的移動網絡及低端設備。一旦 Webpagetest 完成對 Web 應用的性能分析,它會生成你在上面圖3.10中看到的結果。
首次訪問,頁面加載須要大概12秒。這不怎麼理想,但對於很慢的 2G 網絡來講這也是意料之中的。可是,當你再次訪問時,頁面的加載時間將不到0.5秒,而且不會發送 HTTP 請求給服務器。示例應用使用了應用外殼架構,若是你還記得這種設計的話,你會知道從此任何請求都能快速響應,由於所需資源已經緩存了。若是使用正確的話, Service Worker 緩存會極大地提高應用的總體加載速度,而且不管用戶使用何種設備和網絡,都能加強他們的瀏覽體驗。
在本章中,咱們先看了如何使用 Service Worker 緩存來提高 Web 應用的性能。本章的剩餘部分,咱們將密切關注如何對文件進行版本控制,以確保沒有不匹配的緩存,以及一些在使用 Service Worker 緩存時可能遇到的問題。
你的 Service Worker 須要有一個更新的時間點。若是你更改了 Web 應用,並想要確保用戶接收到最新版本的文件,而不是老版本的。你能夠想象一下,錯誤地提供舊文件會對網站形成嚴重破壞!
Service Workers 的偉大之處在於每當對 Service Worker 文件自己作出任何更改時,都會自動觸發 Service Worker 的更新流程。在第1章中,咱們看過了 Service Worker 生命週期。記住當用戶導航至你的網站時,瀏覽器會嘗試在後臺從新下載 Service Worker 。即便下載的 Service Worker 文件與當前的相比只有一個字節的差異,瀏覽器也會認爲它是新的。
這個有用的功能給予咱們完美的機會來使用新文件更新緩存。更新緩存時能夠使用兩種方式。第一種方式,能夠更新用來存儲緩存的名稱。若是回看清單3.2中的代碼,能夠看到變量 cacheName 使用的值爲 'helloWorld' 。若是你把這個值更新爲 'helloWorld-2',這會自動建立一個新緩存並開始從這個緩存中提供文件。以前的緩存將被孤立並再也不使用。
第二種方式,就我我的感受,它應該是最實用的,就是實際上對文件進行版本控制。這種技術被稱爲「緩存破壞」,並且已經存在不少年了。當靜態文件被緩存時,它能夠存儲很長一段時間,而後才能到期。若是期間你對網站進行更新,這可能會形成困擾,由於文件的緩存版本存儲在訪問者的瀏覽器中,它們可能沒法看到所作的更改。經過使用一個惟一的文件版本標識符來告訴瀏覽器該文件有新版本可用,緩存破壞解決了這個問題。
例如,若是咱們想在 HTML 中添加一個 JavaScript 文件的引用,咱們可能但願在文件名末尾附加一個哈希字符串,相似於下面的代碼。
<script type="text/javascript" src="/js/main-xtvbas65.js"></script>
緩存破壞背後的理念是每次更改文件時建立一個全新的文件名,這樣以確保瀏覽器能夠獲取最新的內容。爲了更好地解釋緩存破壞,讓咱們想象一下,在咱們的報紙 Web 應用中有這樣一個場景。好比說你有一個文件叫作 main.js,你將它存儲在緩存之中。根據 Service Worker 的設置方式,每次都會從緩存中獲取這個版本的文件。若是你更新了 main.js 文件,Service Worker 仍然會攔截並返回老的緩存版本,儘管你想要的是新版本的文件!可是,若是你將文件重命名爲 main.v2.js 並在這個新版本文件中更新了代碼,你能夠確保瀏覽器每次都會獲得最新版本的文件。用這種方式,你的報紙應用永遠都會返回最新的結果給用戶。
實現此解決方案有多種不一樣的方法,使用哪一種都取決於你的編碼環境。一些開發者更喜歡在構建期間生成這些哈希文件名稱,而其餘的開發者可能會使用代碼並動態生成文件名。不管使用哪一種方式,這項技術都是通過驗證的,可確保你始終提供正確的文件。
當 Service Worker 檢查已緩存的響應時,它使用請求 URL 做爲鍵。默認狀況下,請求 URL 必須與用於存儲已緩存響應的 URL 徹底匹配,包括 URL 查詢部分的任何字符串。
若是對文件發起的 HTTP 請求附帶了任意查詢字符串,而且查詢字符串會更改,這可能會致使一些問題。例如,若是你對一個先前匹配的 URL 發起了請求,則可能會發現因爲查詢字符串略有不一樣而致使該 URL 找不到。當檢查緩存時想要忽略查詢字符串,使用 ignoreSearch
屬性並設置爲 true 。
self.addEventListener('fetch', function (event) { event.respondWith( caches.match(event.request, { ignoreSearch: true }).then(function (response) { return response || fetch(event.request); }) ); });
清單3.7中代碼使用了 ignoreSearch
選項來忽略請求參數和緩存請求的 URL 的查詢部分。你能夠經過使用其餘忽略選項 (如 ignoreMethod 和 ignoreVary) 進一步擴展。例如,ignoreMethod
選項會忽略請求參數的方法,因此 POST 請求能夠匹配緩存中的 GET 項。ignoreVary
選項會忽略已緩存響應中的 vary 首部。
每當我與開發者們談論 Service Worker 緩存時,常常會出現圍繞內存和存儲空間的問題。Service Worker 會使用多少空間來進行緩存?這種內存使用是否會影響到個人設備?
最誠實的回答是真的取決於你的設備和它的存儲狀況。就像全部瀏覽器的存儲,若是設備受到存儲壓力,瀏覽器能夠自由地丟棄它。這不必定是一個問題,由於數據能夠在須要時再次從網絡中獲取。在第7章中,咱們會來看看另外一種類型的存儲,它被稱之爲「持久化存儲」,它能夠用來更持久地存儲緩存數據。
如今,老的瀏覽器仍然可以在它們內存中存儲緩存的響應,而且使用的空間不一樣於 Service Worker 用來緩存資源的空間。惟一的不一樣的是 Service Worker 緩存讓你來掌控並容許你經過程序來建立、更新和刪除緩存項,從而使你能夠不經過網絡鏈接來訪問資源。
若是你發現本身在 Service Workers 中編寫緩存資源的代碼是有規律的,你可能會發現 Service Worker toolbox (https://github.com/GoogleChrome/sw-toolbox) 是有幫助的。它是由 Google 團隊編寫的,它是一個輔助庫,以使你快速開始建立本身的 Service Workers,內置的處理方法可以涵蓋最多見的網絡策略。只需短短几行代碼,你就能夠決定是否只是要從緩存中提供指定資源,或者從緩存中提供資源並提供備用方案,或者只能從網絡返回資源而且永不緩存。這個庫可讓你徹底控制緩存策略。
圖3.11 Service Worker toolbox 提供了用於建立 Service Workers 的輔助庫。
Service Worker toolbox 爲你提供了一種快速簡便的方式來複用常見的網絡緩存策略,而不是一次又一次地重寫。比方說,你但願確保始終從緩存中取出 CSS 文件,但若是獲取不到的話,則回退到經過網絡來獲取文件。使用 Service Worker toolbox,只需按照同本章中同樣的方式註冊 Service Worker 便可。接下來,在 Service Worker 文件中導入 toolbox 並開始定義你想要緩存的路由。
importScripts('/sw-toolbox/sw-toolbox.js'); ❶ toolbox.router.get('/css/(.*)', toolbox.cacheFirst); ❷
在清單3.8中,先使用 importScripts
函數導入 Service Worker toolbox 庫。Service Workers 能夠訪問一個叫作 importScripts()
的全局函數,它能夠將同一域名下腳本導入至它們的做用域。這是將另外一個腳本加載到現有腳本中的一種很是方便的方法。它保持代碼整潔,也意味着你只須要在須要時加載文件。
一旦腳本導入後,咱們就能夠開始定義想要緩存的路由。在上面的清單中,咱們定義了一個路由,它匹配路徑是 /css/
,而且永遠使用緩存優選的方式。這意味着資源會永遠從緩存中提供,若是不存在的話再回退成經過網絡獲取。Toolbox 還提供了一些其餘內置緩存策略,好比只經過緩存獲取、只經過網絡獲取、網絡優先、緩存 優先或者嘗試從緩存或網絡中找到最快的響應。每種策略均可以應用於不一樣的場景,甚至你能夠混用不一樣的路由來匹配不一樣的策略,以達到最佳效果。
Service Worker toolbox 還爲你提供了預緩存資源的功能。在清單3.2中,咱們在 Service Worker 安裝期間預緩存了資源,咱們能夠使用 Service Worker toolbox 以一樣的方式來實現,而且只須要一行代碼。
toolbox.precache(['/js/script.js', '/images/hello.png']);
清單3.9中的代碼接收在 Service Worker 安裝步驟中應該被緩存的 URL 數組。這行代碼會確保在 Service Worker 安裝階段資源被緩存。
每當我接手一個新項目時,毫無疑問我喜歡使用的庫就是 Service Worker toolbox 。它簡化了你的代碼併爲你提供通過驗證的緩存策略,只需幾行代碼即可實現。事實上,咱們在第2章剖析過的 Twitter PWA 也使用了 Service Worker toolbox 來使得代碼更容易理解,並依賴於這些通過驗證的緩存方法。
做爲 Web 開發者,爲了異步更新應用,咱們一般須要從服務器獲取數據的能力。傳統上,這個數據是經過使用 JavaScript 和 XMLHttpRequest 對象來獲取的。也被稱爲 AJAX,它是開發者求之不得的,由於它容許你更新網頁,而沒必要經過在後臺進行 HTTP 請求來從新加載頁面。在咱們的示例應用 Progressive Times 中,咱們也會異步獲取新聞文章列表。
若是你曾經實現過複雜的邏輯來從服務器獲取數據,使用 XMLHttpRequest 對象來編寫代碼會至關棘手。隨着開始添加愈來愈多的邏輯和回調函數,代碼很快就變得一團糟。
var request;
if (window.XMLHttpRequest) { request = new XMLHttpRequest(); } else if (window.ActiveXObject) { try { request = new ActiveXObject('Msxml2.XMLHTTP'); } catch (e) { try { request = new ActiveXObject('Microsoft.XMLHTTP'); } catch (e) {} } } request.onreadystatechange = function () { if (this.readyState == 4 && this.status == 200) { doSomething(this.responseText); } }; // 打開,發送 request.open('GET', '/some/url', true); request.send();
清單4.1中的代碼看上去有些多,只是簡單地發起一個 HTTP 請求而已!有趣的是,XMLHttpRequest 對象最初是由 Microsoft Exchange Server 的 Outlook Web Access 的開發人員建立的。通過一系列的置換以後,它最終成爲現在在 JavaScript 中發起 HTTP 請求的標準。上面的示例完成了它的目的,但代碼自己並不夠簡潔。上面的代碼還有另一個問題,你的邏輯越複雜,代碼就越複雜。過去,有許多庫和技術能夠使這種代碼更簡單,更易於閱讀,諸如 jQuery 和 Zepto 等流行庫,它們具備更簡潔的 API 。
幸運的是,現代瀏覽器廠商都意識到這是須要改進的,這也正是 Fetch API 誕生的緣由。Fetch API 是 Service Worker 全局做用域的一部分,它能夠用來在任何 Service Worker 中發起 HTTP 請求。截止到目前爲止,咱們一直在 Service Worker 代碼中使用 Fetch API,但咱們還不曾深刻研究它。咱們來看幾個代碼示例,以便更好地理解 Fetch API 。
fetch('/some/url', { ❶ method: 'GET' }).then(function (response) { ❷ // 成功 }).catch(function (err) { ❸ // 出問題了 });
清單4.2中的代碼是 Fetch API 實際應用的基礎示例。你可能還注意到了沒有回調函數和事件,取而代之的是 then()
方法。這個方法是 ES6 中新功能 Promises 的一部分,目的是使咱們的代碼更具可讀性、更便於開發者理解。Promise 表明異步操做的最終結果,咱們不知道實際的結果值是什麼,直到未來某個時刻操做完成。
上面的代碼看起來很容易理解,可是使用 Fetch API 發起 POST 請求呢?
fetch('/some/url', { ❶ method: 'POST', headers: { 'auth': '1234' ❷ }, body: JSON.stringify({ ❸ name: 'dean', login: 'dean123', }) }) .then(function (data) { ❹ console.log('Request success: ', data); }) .catch(function (error) { ❺ console.log('Request failure: ', error); });
假如說,你想要使用 POST 請求將某個用戶詳情發送到服務器。在上面的清單中,只需在 fetch 選項中將 method 更改成 POST 並添加 body 參數。使用 Promises 不只能夠使代碼更整潔,並且還能夠使用鏈式代碼,以便在 fetch 請求之間共享邏輯。
目前全部支持 Service Workers 的瀏覽器中均可以使用 Fetch API ,但若是想在不支持的瀏覽器上使用的話,你可能要考慮使用 polyfill 。它只是一段代碼,爲你提供你所指望的現代瀏覽器功能。例如,若是最新版本的 IE 具備一些你所須要的功能,但舊版本中沒有,polyfill 能夠用來爲老舊瀏覽器提供相似的功能。能夠把它看成是 API 的包裝,用來保持 API 完整可用。這有一個由 Github 團隊編寫的 polyfill (https://github.com/github/fetch),它會確保老舊瀏覽器可以使用 fetch API 發起請求。只須要將它放置在網頁中,你便可以開始使用 fetch API 來編寫代碼。
Service Worker 攔截任何發出的 HTTP 請求的能力使得它真的很是強大。屬於此 Service Worker 做用域內的每一個 HTTP 請求將觸發此事件,例如 HTML 頁面、腳本、圖片、CSS,等等。這可讓開發者徹底控制瀏覽器如何處理響應這些資源獲取的方式。
在第1章中,咱們在實戰中看過了 fetch 事件的基礎示例。還記得獨角獸嗎?
self.addEventListener('fetch', function (event) { ❶ if (/\.jpg$/.test(event.request.url)) { ❷ event.respondWith( fetch('/images/unicorn.jpg')); ❸ } });
在上面的代碼中,咱們監聽了 fetch 事件,若是 HTTP 請求的是 JPEG 文件,咱們就攔截請求並強制返回一張獨角獸圖片,而不是原始 URL 請求的圖片。上面的代碼會爲該網站上的每一個 JPEG 圖片請求執行一樣的操做。對於任何其餘文件類型,它會直接忽略並繼續執行。
同時清單4.4中的代碼是個有趣的示例,它並無展現出 Service Workers 的真正能力。咱們會在這個示例的基礎上更進一步,返回咱們自定義的 HTTP 響應。
self.addEventListener('fetch', function (event) { ❶ if (/\.jpg$/.test(event.request.url)) { ❷ event.respondWith( new Response('<p>This is a response that comes from your service worker!</p>', { headers: { 'Content-Type': 'text/html' } ❸ }); ); } });
在清單4.5中,代碼經過監聽 fetch 事件的觸發來攔截任何 HTTP 請求。接下來,它判斷傳入請求是不是 JPEG 文件,若是是的話,就使用自定義 HTTP 響應進行響應。使用 Service Workers ,你可以建立自定義 HTTP 響應,包括編輯響應首部。此功能使得 Service Workers 極其強大,同時也能夠理解爲何它們須要經過 HTTPS 請求才能獲取。想象一下,若是不是這樣的話,黑客動動手指即可以完成一些惡意的操做!
就在本書開頭的第1章中,你瞭解過了 Service Worker 生命週期以及在它構建 PWA 時所扮演的角色。再來仔細看一遍下面的圖。
圖4.1 Service Worker 生命週期
看過上面的圖,你會想到當用戶第一次訪問網站的時候,並不會有激活的 Service Worker 來控制頁面。只有當 Service Worker 安裝完成而且用戶刷新了頁面或跳轉至網站的其餘頁面,Service Worker 纔會激活並開始攔截請求。
爲了更清楚地解釋這個問題,咱們想象一下,一個單頁應用 (SPA) 或一個加載完成後使用 AJAX 進行交互的網頁。當註冊和安裝 Service Worker 時,咱們將使用到目前爲止在本書中所介紹的方法,頁面加載後發生的任何 HTTP 請求都將被忽略。只有當用戶刷新頁面,Service Worker 纔會激活並開始攔截請求。這並不理想,你但願 Service Worker 能儘快開始工做,幷包括在 Service Worker 未激活期間所發起的這些請求。
若是你想要 Service Worker 當即開始工做,而不是等待用戶跳轉至網站的其餘頁面或刷新本頁面,有一個小技巧,你能夠用它來當即激活你的 Service Worker 。
self.addEventListener('install', function(event) { event.waitUntil(self.skipWaiting()); });
清單4.6中的代碼重點在於 Service Worker 的 install 事件裏。經過使用 skipWaiting() 函數,最終會觸發 activate 事件,並告知 Service Worker 當即開始工做,而無需等待用戶跳轉或刷新頁面。
圖4.2 self.skipWaiting() 會使 Service Worker 解僱當前活動的 worker 而且一旦進入等待階段就會激活自身
skipWaiting() 函數強制等待中的 Service Worker 被激活 。self.skipWaiting() 函數還能夠與 self.clients.claim() 一塊兒使用,以確保底層 Service Worker 的更新當即生效。
下面清單4.7中的代碼能夠結合 skipWaiting()
函數,以確保 Service Worker 當即激活自身。
self.addEventListener('activate', function(event) { event.waitUntil(self.clients.claim()); });
同時使用清單4.6和4.7中的代碼,能夠快速激活 Service Worker。若是你的網站在頁面加載後會發起複雜的 AJAX 請求,那麼這些功能對你來講是完美的。若是你的網站主要是靜態頁面,而不是在頁面加載後發起 HTTP 請求,那麼你可能不須要使用這些功能。
正如咱們在本章中所看到的,Service Workers 爲開發者提供了無限的網絡控制權。攔截 HTTP 請求、修改 HTTP 響應以及自定義響應,這些只是經過進入 fetch 事件所獲取到的能力的一小部分。
直到此刻,絕大部分咱們所看過的代碼示例並不是真實世界的示例。在下一節中,咱們將深刻兩種有用的技術,可以使你的網站更快,更具吸引力和更富彈性。
現在,圖片在 Web 上扮演着重要的角色。能夠想象下一個網頁上徹底沒有圖片的世界!高質量的圖片能夠真正地使網站出彩,但不幸的是它們也是有代價的。因爲它們的文件大小較大,因此須要下載的內容多,致使頁面加載慢。若是你曾經使用過網絡鏈接較差的設備,那麼你會了解到這種體驗有多使人沮喪。
你可能熟悉 WebP 這種圖片格式。它是由 Google 團隊開發的,與 PNG 圖片相比,文件大小減小26%,與 JPEG 圖片相比,文件大小大約減小25-34%。這是一個至關不錯的節省,最棒的是,選擇這種格式圖像質量不會受到影響。
圖4.3 與原始格式相比,WebP 圖片的文件大小要小得多,而且圖片的質量沒有顯着差別
圖4.3並排展現了兩張內容相同、圖片質量無顯著差異的圖片,左邊是 JEPG,右邊是 WebP 。默認狀況下,只有 Chrome、Opera 和 Android 支持 WebP 圖片,不幸的是目前 Safari、Firefox 和 IE 還不支持。
支持 WebP 圖片的瀏覽會經過在每一個 HTTP 請求中傳遞 accept: image/webp
首部通知你它們可以支持。鑑於咱們擁有 Service Workers,這彷佛是一個完美的機會,以開始攔截請求,並將更輕,更精簡的圖片返回給可以渲染它們的瀏覽器。
假設有下面這樣一個基礎的網頁。它只是引用了一張紐約布魯克林大橋的圖片。
<!DOCTYPE html>
<html>
<head> <meta charset="UTF-8"> <title>Brooklyn Bridge - New York City</title> </head> <body> <h1>Brooklyn Bridge</h1> <img src="./images/brooklyn.jpg" alt="Brooklyn Bridge - New York"> <script> // 註冊 service worker if ('serviceWorker' in navigator) { navigator.serviceWorker.register('./service-worker.js').then(function (registration) { // 註冊成功 console.log('ServiceWorker registration successful with scope: ', registration.scope); }).catch(function (err) { // 註冊失敗 :( console.log('ServiceWorker registration failed: ', err); }); } </script> </body> </html>
清單4.8中的圖片是 JPEG 格式,大小爲137KB。若是將它轉換爲 WebP 格式的並存儲在服務器上,就可以爲支持 WebP 的瀏覽器返回 WebP 格式,爲不支持的返回原始格式。
咱們在 Service Worker 中構建代碼以開始攔截此圖片的 HTTP 請求。
"use strict"; // 監聽 fetch 事件 self.addEventListener('fetch', function(event) { if (/\.jpg$|.png$/.test(event.request.url)) { ❶ var supportsWebp = false; if (event.request.headers.has('accept')) { ❷ supportsWebp = event.request.headers .get('accept') .includes('webp'); } if (supportsWebp) { ❸ var req = event.request.clone(); var returnUrl = req.url.substr(0, req.url.lastIndexOf(".")) + ".webp"; ❹ event.respondWith( fetch(returnUrl, { mode: 'no-cors' }) ); } } });
上面清單中的代碼不少,咱們分解來看。最開始的幾行,我添加了事件監聽器來監放任何觸發的 fetch 事件。對於每一個發起的 HTTP 請求,我會檢查當前請求是不是 JEPG 或 PNG 圖片。若是我知道當前請求的是圖片,我能夠根據傳遞的 HTTP 首部來返回最適合的內容。在本例中,我檢查每一個首部並尋找 image/webp
的 mime 類型。一旦知道首部的值,我便能判斷出瀏覽器是否支持 WebP 並返回相應的 WebP 圖片。
一旦 Service Worker 激活並準備好,對於支持 WebP 的瀏覽器,任何 JPEG 或 PNG 圖片的請求都會返回一樣內容的 WebP 圖片。若是瀏覽器不支持 WebP 圖片,它不會在 HTTP 請求首部中聲明支持,Service Worker 會忽略該請求並繼續正常工做。
一樣內容的 WebP 圖片只有87KB,相比於原始的 JPEG 圖片,咱們節省了59KB,大約是原始文件大小的37%。對於使用移動設備的用戶,瀏覽整個網站會節省想當多的帶寬。
Service Workers 開啓了一個無限可能性的世界,這個示例能夠進行擴展,包含一些其餘的圖片格式,甚至是緩存。你能夠輕鬆地支持 叫作 JPEGXR 的 IE 改進圖片格式。咱們沒有理由不爲咱們的用戶提供更快的網頁!
我最近在出國旅行,當我迫切須要從航空公司的網站獲取一些信息時,我使用的是 2G 鏈接,頁面永遠在加載中,最終我完全放棄了。回國後,我還得向手機運營商支付平常服務費用,真是太讓人不爽了!
在全球範圍內,4G網絡覆蓋面正在迅速發展,但仍有很長的路要走。在2007年末,3G網絡僅覆蓋了孟加拉國、巴西、中國、印度、尼日利亞、巴基斯坦和俄羅斯等國家,將近全球人口的50%。雖然移動網絡覆蓋面愈來愈廣,但在印度一個500MB的數據包須要花費至關於17個小時的最低工資,這聽起來使人難以想象。
幸運的是,諸如 Google Chrome、Opera 和 Yandex 這樣的瀏覽器廠商已經意識到衆多用戶所面臨的痛苦。使用這些瀏覽器的最新版本,用戶將有一個選項,以容許他們「選擇性加入」節省數據的功能。經過啓動這項功能,瀏覽器會爲每一個 HTTP 請求添加一個新的首部。這是咱們這些開發者的機會,尋找這個首部並返回相應的內容,爲咱們的用戶節省數據。例如,若是用戶開啓了節省數據選項,你能夠返回更輕量的圖片、更小的視頻,甚至是不一樣的標記。這是一個簡單的概念,卻行之有效!
這聽上去是使用 Service Worker 的完美場景!在下節中,咱們將編寫代碼,它會攔截請求,檢查用戶是否「選擇性加入」節省數據並返回「輕量級」版本的 PWA 。
還記得咱們在第3章中構建的 PWA 嗎?它叫作 Progressive Times,包含來自世界各地的有趣新聞。
圖4.4 Progressive Times 示例應用是貫穿本書的基礎應用
在 Progressive Times 應用中,咱們使用網絡字體來提高應用的外觀感覺。
這些字體是從第三方服務下載的,大約30KB左右。雖然網絡字體真的能加強網頁的外觀感受,但若是用戶只是想節省數據和金錢,那麼網頁字體彷佛是沒必要要的。不管用戶的網絡鏈接狀況如何,你的 PWA 都沒有理由不去適應用戶。
不管你是使用臺式機仍是移動設備,啓用此功能都是至關簡單的。若是是在移動設備上,你能夠在菜單的設置裏開啓。
圖4.5 能夠在移動設備或手機上開啓節省數據功能。注意紅色標註的區域。
一旦設置啓用後,每一個發送到服務器的 HTTP 請求都會包含 Save-Data 首部。若是使用開發者工具查看,它看起來就以下圖所示。
圖4.6 啓用了節省數據功能,每一個 HTTP 請求都會包含 Save-Data 首部
一旦啓用了節省數據功能,有幾種不一樣的技術能夠將數據返回給用戶。由於每一個 HTTP 請求都會發送到服務器,你能夠根據來自服務器端代碼中 Save-Data 首部來決定提供不一樣的內容。然而,只需短短几行 JavaScript 代碼就能夠使用 Service Workers 的力量,你能夠輕鬆地攔截 HTTP 請求並相應地提供更輕量級的內容。若是你正在開發一個 API 驅動的前端應用,而且徹底沒有訪問服務器,那這就是個完美的選擇。
Service Workers 容許你攔截髮出的 HTTP 請求,進行檢測並根據信息採起行動。使用 Fetch API,你能夠輕鬆實現一個解決方案來檢測 Save-Data 首部並提供更輕量級的內容。
咱們開始建立一個名爲 service-worker.js 的 JavaScript 文件,並添加清單4.10中的代碼。
"use strict"; this.addEventListener('fetch', function (event) { if(event.request.headers.get('save-data')){ // 咱們想要節省數據,因此限制了圖標和字體 if (event.request.url.includes('fonts.googleapis.com')) { // 不返回任何內容 event.respondWith(new Response('', {status: 417, statusText: 'Ignore fonts to save data.' })); } } });
基於咱們已經看過的示例,清單4.10中代碼應該比較熟悉了。在代碼的開始幾行中,添加了事件監聽器以監放任何觸發的 fetch 事件。對於每一個發起的請求,都會檢查首部以查看是否啓用了 Save-Data 。
若是啓用了 Save-Data 首部,我會檢查當前 HTTP 請求是不是來自 「fonts.googleapis.com」 域名的網絡字體。由於我想爲用戶節省任何沒必要要的數據,我返回一個自定義的 HTTP 響應,狀態碼爲417,狀態文本是自定義的。HTTP 狀態代碼向用戶提供來自服務器的特定信息,而417狀態碼錶示「服務器不能知足 Expect 請求首部域的要求」。
經過使用這項簡單的技術和幾行代碼,咱們可以減小頁面的總體下載量,並確保用戶節省了任何沒必要要的數據。這項技術能夠進一步擴展,定製返回低質量的圖片或者網站上其餘更大的文件下載。
若是你想實際查看本章中的任何代碼,它託管在 Github 上,能夠經過導航至 bit.ly/chapter-pwa-4 輕鬆訪問。