標籤:

PWA 入門

什麼是PWA

PWA全稱Progressive Web App,直譯是漸進式WEB應用,是 Google 在 2015 年提出,2016年6月才推廣的項目。是結合了一系列現代Web技術的組合,在網頁應用中實現和原生應用相近的用戶體驗。

所謂的P(Progressive)這裡有兩層含義,一方面是漸進增強,讓WEB APP的體驗和功能能夠用漸進增強的方式來更接近原生APP的體驗及功能;另一方面是指下一代WEB技術,PWA並不是描述一個技術,而是一些技術的合集。

PWA特性

  • 漸進增強 - 能夠讓每一位用戶使用,無論用戶使用什麼瀏覽器,因為它是始終以漸進增強為原則。
  • 響應式用戶界面 - 適應任何環境:桌面電腦,智能手機,筆記本電腦,或者其他設備。
  • 不依賴網路連接 - 通過 service workers 可以在離線或者網速極差的環境下工作。
  • 類原生應用 - 有像原生應用般的交互和導航給用戶原生應用般的體驗,因為它是建立在 app shell model 上的。
  • 持續更新 - 受益於 service worker 的更新進程,應用能夠始終保持更新。
  • 安全 - 通過 HTTPS 來提供服務來防止網路窺探,保證內容不被篡改。
  • 可發現 - 得益於 W3C manifests 元數據和 service worker 的登記,讓搜索引擎能夠找到 web 應用。
  • 再次訪問 - 通過消息推送等特性讓用戶再次訪問變得容易。
  • 可安裝 - 允許用戶保留對他們有用的應用在主屏幕上,不需要通過應用商店。
  • 可連接性 - 通過 URL 可以輕鬆分享應用,不用複雜的安裝即可運行。

PWA優、缺點

優點:

  • 上面提到的,所有這些現代 Web 特性。
  • Web最重要的意義在於開放和去中心化,這才是萬維網的初衷

缺點:

  • 門檻不低。部署的伺服器要求HTTPS,ServiceWorker涉及API眾多,需要單獨學習
  • 瀏覽器支持不夠全面。蘋果Safari 短時間內不會支持,5 年計劃里可能實施
  • 用戶體驗習慣。網頁應用替代原生應用的方式,用戶短時間內不適應

PWA關鍵技術:

Manifest(應用清單)

Web App Manifest是一個W3C規範,定義了一個基於JSON的清單,為開發人員提供一個放置與Web應用程序關聯的元數據的集中地點。manifest 就是 PWA 概念的一環,它給你了控制你的應用如何出現在用戶期待出現的地方(比如用戶手機主屏幕),這直接影響到用戶能啟動什麼,以及更重要的,用戶如何啟動它。

使用 web 應用程序清單,你的應用可以:

  • 能夠真實存在於用戶主屏幕上
  • 在 Android 上能夠全屏啟動,不顯示地址欄
  • 控制屏幕方向已獲得最佳效果
  • 定義啟動畫面,為你的站點定義主題
  • 追蹤你的應用是從主屏幕還是 URL 啟動的

例如:

{ "lang": "en", "dir": "ltr", "name": "Donate App", "description": "This app helps you donate to worthy causes.", "short_name": "Donate", "icons": [{ "src": "icon/lowres.webp", "sizes": "64x64", "type": "image/webp" },{ "src": "icon/lowres.png", "sizes": "64x64" }, { "src": "icon/hd_hi", "sizes": "128x128" }], "scope": "/racer/", "start_url": "/racer/start.html", "display": "fullscreen", "orientation": "landscape", "theme_color": "aliceblue", "background_color": "red", "serviceworker": { "src": "sw.js", "scope": "/racer/", "use_cache": false }, "screenshots": [{ "src": "screenshots/in-game-1x.jpg", "sizes": "640x480", "type": "image/jpeg" },{ "src": "screenshots/in-game-2x.jpg", "sizes": "1280x920", "type": "image/jpeg" }]}

Service Workers

Service Worker是瀏覽器在後台獨立於網頁運行的腳本,它打開了通向不需要網頁或用戶交互的功能的大門。這個 API 之所以令人興奮,是因為它可以支持離線體驗,讓開發者能夠全面控制這一體驗。

ServiceWorker是由兩部分構成,一部分是 cache,還有一部分則是 Worker。

它被設計為一個相對底層、高度可編程、子概念眾多,也因此異常靈活且強大的 API,它就像一個位於客戶端和網路之間的代理,可以攔截、處理、響應流經的網路請求,配合Cache API,你可以自由管理網路請求文件的緩存,這使得 Service Worker 可以從緩存中向 web 應用提供資源,即使是在離線的環境下。這樣,在離線和網速低的情況下也能秒開,說白了,之前的Hybrid架構的出現不就是為了這個功能么。之前雖然有AppCache,但它具有相當多的缺陷,這裡就不說了。

來張官網的形象圖:

Push Notification(推送通知)

Push 和 Notification是兩個不同的功能,涉及到兩個API,但是它們之前有依賴關係。

Notification這塊應該大家多少了解一些,屬於瀏覽器發出的通知消息,之前需要瀏覽器一直開著才能實現這種通知,但是現在有了上面提到的Service Worker,就可以駐留在進程裡面操作了。

Push & Notification關係:

  • Push : 伺服器端將更新的信息傳遞給 Service Worker
  • Notification: Service Worker 將更新的信息推送給用戶


PWA與其它App的對比

目前的移動端APP:

  • Native APP
  • Web App
  • Hybrid App

Native APP

Native APP,指原生App,是一個完整的App,可拓展性強,需要用戶下載安裝使用。

優點:

  • 可使用移動設備所有功能
  • 速度快、性能高、用戶體驗好
  • 離線使用

缺點:

  • 開發成本高、維護成本高
  • 每個不通的平台都要重新開發
  • 應用商店審核複雜,效率低

Web APP

Web App 指採用Html5語言寫出的App,生活在瀏覽器里的應用,不需要下載安裝。

優點:

  • 跨平台開發、無需下載、無需安裝,開發速度快
  • 發布靈活,因為根本不需要應用商店的審核
  • 較低的開發成本
  • 可即時上線
  • 用戶可以直接使用最新版本
  • 支持設備廣泛

缺點:

  • 只能使用有限的移動設備API
  • 瀏覽器兼容問題
  • 無法上傳到應用商店
  • 用戶暫時不適用

Hybrid App

Hybrid APP指的是半原生半Web的混合類App,需要下載安裝。

優點:

  • 兼容多平台
  • Web前端工作人員就可快速構建
  • 可以上傳到應用商店
  • 可以基於瀏覽器的方式進行頁面調試
  • 可使用的移動設備的API多

缺點:

  • 用戶體驗不如原生應用
  • 為模擬原生樣式,需要大量的html和css
  • 性能稍慢
  • 技術不是很成熟

PWA兼容性

Blink內核(Chrome、Oprea、Samsung Internet 等)和 Gecko內核(Firefox)和Microsoft Edge都已經實現了 PWA 所需的所有關鍵技術的支持,但IOS的Safari(Webkit),目前不支持PWA的API。

不過在2015年Webkit的5年計劃裡面,已經提及了Service Worker,相信很快就能實現。


PWA在中國

  • 國在IPhone不在少數,而IOS目前是不支持PWA的
  • 國內Android系統,大部分早已把Google框架移除了,所以兼容性會出問題
  • 推送依賴於GCM,而國內Google是無法訪問的

Google的技術在國內推進是很痛苦的,Android雖然近年來在國內不錯,但PWA在國內的發展有很多困難。


PWA的未來

總的來說,PWA還是很不錯的,雖然PWA在國內的體驗目前有一些限制,但相信PWA在國內的春天肯定會來的。

WA關鍵技術Manifest

前言

前面說過,讓Web App能夠達到Native App外觀體驗的主要實現技術就是PWA中的manifest技術,本章會詳細說明manifest的實現,及各個參數的具體含義,還將了解如何定義Web App的啟動圖標、啟動樣式等。

簡介

manifest是一種簡單的json數據風格的配置文件,通過對其相應的屬性進行配置,才實現相應的功能,這裡可以稱manifest為WEB應用清單。WEB應用清單可以實現自定義啟動畫面、打開URL、設置界面顏色、設置桌面圖標等等。

大概就是下面這樣:

{ "short_name": "短名稱", "name": "這是一個完整名稱", "icons": [ { "src": "144x144.png", "type": "image/png", "sizes": "144x144" } ], "background_color": "#2196f3", "display": "standalone", "start_url": "index.html"}

部署到瀏覽器

好么manifest.js如何讓瀏覽器去執行呢?

只需要用link標記引用即可:

<link rel="manifest" href="manifest.js">

目前各大瀏覽器對manifest的支持程度:

成員

下面對manifest涉及到的各個屬性詳細說一下。

name

name: {string},用來描述應用的名稱,會顯示在各類提示的標題位置和啟動畫面中。

short_name

short_name: {string},用來描述應用的短名字。當應用的名字過長,在桌面圖標上無法全部顯示時,會用short_name中定義的來顯示。

start_url

start_url: {string},用來描述當用戶從設備的主屏幕點擊圖標進入時,出現的第一個畫面。

  • 如果設置為空字元串,則會以manifest.js的地址做為URL
  • 如果設置的URL打開失敗,則和正常顯示的網頁打開錯誤的樣式一下(可以通過後面講的ServiceWorker改善)
  • 如果設置的URL與當前的項目不在一個域下,也不能正常顯示
  • start_url 必須在scope的作用域範圍內
  • 如果 start_url 是相對地址,那麼根路徑基於manifest的路徑
  • 如果 start_url 為絕對地址,則該地址將永遠以 / 作為根路徑

scope

scope : {string},用來設置manifest對於網站的作用範圍。

下面列一下,scope的作用範圍及對start_url的影響:

icons

icons: {Array.<ImageObject>},用來設置Web App的圖標集合。

ImageObject 包含屬性:

  • src: {string},圖標的地址
  • type {string},圖標的 mime 類型,可以不填寫。這個欄位會讓瀏覽器不使用定義類型外的圖標
  • sizes {string},圖標的大小,用來表示widthxheight,單位為px,如果圖標要適配多個尺寸,則第個尺寸間用空格分割,如12x12 24x24 100x100

sizes適配規則:

  • 在PWA添加到桌面的時候,瀏覽器會適配最合適尺寸的圖標。瀏覽器首先會去找與顯示密度相匹配且尺寸調整到 48dp 屏幕密度的圖標,例如它會在 2 倍像素的設備上使用 96px,在 3 倍像素的設備上使用 144px。。
  • 如果沒有找到任何符合的圖標,則會查找與設備特性匹配度最高的圖標。
  • 如果匹配到的圖標路徑錯誤,將會顯示瀏覽器默認 icon。

需要注意的是,圖標中必須要有一張尺寸為144x144的,圖標的 mime 類型為 image/png的。

background_color

background_color: {Color},值為CSS的顏色值,用來設置Web App啟動畫面的背景顏色。

可以像正常寫CSS顏色那樣定義:

// 完整色值"background_color": "#0000ff"// 縮寫"background_color": "#00f"// 預設色值"background_color": "yellow"// rgb"background_color": "rgb(0, 255, 255)"// transparent 背景色顯示為黑色"background_color": "transparent"

其他的定義rgbahslhsla等顏色定義方式瀏覽器不支持,未設置時,背景色均顯示白色。

theme_color

theme_color: {Color},定義和background_color一樣的CSS顏色值,用於顯示Web App的主題色,顯示在banner位置。

display

display: {string},用來指定 Web App 從主屏幕點擊啟動後的顯示類型

顯示類型描述降級顯示類型fullscreen應用的顯示界面將佔滿整個屏幕standalonestandalone瀏覽器相關UI(如導航欄、工具欄等)將會被隱藏minimal-uiminimal-ui顯示形式與standalone類似,瀏覽器相關UI會最小化為一個按鈕,不同瀏覽器在實現上略有不同browserbrowser瀏覽器模式,與普通網頁在瀏覽器中打開的顯示一致(None)

對於不同的顯示樣式,可以通過CSS的媒體查詢進行設置:

@media all and (display-mode: fullscreen) { div { padding: 0; }}@media all and (display-mode: standalone) { div { padding: 1px; }}@media all and (display-mode: minimal-ui) { div { padding: 2px; }}@media all and (display-mode: browser) { div { padding: 3px; }}

orientation

orientation: {string},Web App的在屏幕上的顯示方向。

  • landscape-primary,當視窗寬度大於高度時,當前應用處於「橫屏」狀態
  • landscape-secondarylandscape-primary的180°方向
  • landscape,根據屏幕的方向,自動橫屏幕180°切換
  • portrait-primary,當視窗寬度小於高度時,當前應用處於「豎屏」狀態
  • portrait-secondaryportrait-primary的180°方向
  • portrait,根據屏幕方向,自動豎屏180°切換
  • natural, 根據不同平台的規則,顯示為當前平台的0°方向
  • any,任意方向切換

dir

dir: {string},設置文字的顯示方向。

- ltr,文本顯示方向,左到右

- rtl,文本顯示方向,右到左

- auto,根據系統的方向顯示

related_applications

related_applications: {Array.<AppInfo>},用於定義對應的原生應用,類似應用安裝橫幅的形式去推廣、引流。

AppInfo結構:

- platform: {string}, 應用平台

- id: {string} 應用id

如安卓可以這麼定義:

"related_applications": [ { "platform": "play", "id": "com.app.xxx" }]

prefer_related_applications

prefer_related_applications:{Boolean},用於設置只允許用戶安裝原生應用。

實例

下面寫一下相關的常用實例。

目錄結構

項目結構:

根路徑 / | |----manifest.json // 清單文件 | |----index.html | |----144x144.png // 圖標文件 | |----sw.js

因為瀏覽器要安裝manifest中的定義,需要一些其他的代碼環境條件,以上目錄中,我們只討論manifest.json文件,其他文件後面會做說明。

index.html

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width_=device-width, initial-scale=1.0"> <title>Manifest</title> <link rel="manifest" href="./manifest.json"> <!-- 引用manifest文件 --></head><body> <h1>Manifest Page</h1> <script> // 此處代碼 後面相關章節會去說明 if (serviceWorker in navigator) { window.addEventListener(load, function() { navigator.serviceWorker.register(sw.js) .then(function(registration) {}) .catch(function(err) {}) }) } </script></body></html>

sw.js

// 此處代碼 後面相關章節會去說明var cacheName = helloWorldself.addEventListener(install, event => { event.waitUntil( caches.open(cacheName) .then(cache => cache.addAll([ index.html ])) )})self.addEventListener(fetch, function (event) { event.respondWith( caches.match(event.request) .then(function (response) { if (response) { return response; } return fetch(event.request); }) )})

144x144.png

將應用添加到桌面

manifest.js

{ "short_name": "短名稱", "name": "這是一個完整名稱", "icons": [ { "src": "144x144.png", "type": "image/png", "sizes": "144x144" } ], "display": "standalone", "start_url": "index.html"}

按照上面方式配置,在移動端Chrome上訪問,效果如下:

瀏覽器會提示一個「添加到主屏幕」的提示。提示的標題顯示的是在manifest中定義的name,當點擊時,就會將應用添加到桌面:

桌面圖標上顯示的文本為manifest中定義的short_name。

點擊應用圖標,打開應用:

可以看到根據設置的display屬性,打開的應用去除了瀏覽器的地址欄。

注意

瀏覽器「添加到主屏幕」的提示是需要滿足下麵條件才會顯示的:

  1. 需要manifest文件
  2. manifest中需要定義start_url
  3. 需要包含144x144的png圖標文件
  4. 網站是通過Https訪問的
  5. 並且網站中運行ServiceWorker
  6. 用戶需要至少瀏覽過網站兩次,並且兩次的間隔大於5分鐘
  7. 如果修改了 manifest 的配置,已添加到主屏幕的名稱並不會改變,只有當用戶重新添加到桌面時,更改後的配置才會生效。但是在未來版本的 Chrome 瀏覽器將支持自動更新

關於上面提到的第4點,我們可以建立一個https網站或者可以直接用github的pages服務來實現。

關於第6點,是為了防止每次打開網址都有這個提示,對用戶造成較差的體驗。

設置主題色

"theme_color": "red"

給主題色設置個紅色:

可以發現App的header上已經變成了設置的紅色。

設置啟動界面

啟動界面是由icon、background_color和name構成的。

"background_color": "#2196f3"

效果:

icon也會根據屏幕的尺寸,瀏覽器來適配最佳的圖標。

添加到主屏幕 觸發的事件

當執行」添加到主屏幕「的操作時,內部會觸發相應的事件beforinstallprompt。可以利用這個事件做一些事情,例如App判斷流量入口:

window.addEventListener(beforeinstallprompt, function(e) { e.userChoice.then(function (result){ if (result.outcome === dismissed){ // 發送數據進行分析 } else { // 發送數據進行分析 } })})

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

事件中的userChoice對象用來返回用戶的選擇信息,處理是基於Promise的,這個後面章節會詳說。

本地調試

上面是為了實現真是效果,所以在真是移動端上實現的。其實在測試的時候,是可以通過Chrome的開發者工具來測試的。

首先,打開上一章里下載的」WebServer for Chrome」本地伺服器工具,並把項目加下到裡面,然後開始服務

然後,在Chrome瀏覽器中訪問」Web Server URL(s)「下的地址http://127.0.0.1:8887

然後,打開開發者工具,打開Application選項卡,選擇Manifest,就可以後到配置的信息了。

並可以通過點擊」Add to homescreen「觸發添加圖標到桌面的事件。

授權後即可添加到桌面

線上manifest驗證

除去本地調試外,還可以通過在線清單驗證工具來實現驗證,例如:Web Manifest Validator

總結

到這裡,總結一下:

  • manifest是一種簡單的JSON文件,通過對屬性進行相應的配置,可以實現很多類Native的體驗
  • 添加到主屏幕會觸發相應的事件,可以基於這些事件做相應的處理
  • 調試的時候,可以基於Chrome的開發者工具,或者通過線上的測試驗證工具
  • https可以藉助github pages功能實現

準備工作

建議安裝 http-server 和 ngrok 以便調試和查看。

準備一個 HTML 文件, 以及相應的 CSS 等:

<head> <title>Minimal PWA</title> <meta name="viewport" content="width_=device-width, user-scalable=no" /> <link rel="stylesheet" type="text/css" href="main.css"></head><body> <h3>Revision 1</h3> <div class="main-text">Minimal PWA, open Console for more~~~</div></body>

添加 manifest.json 文件

為了讓 PWA 應用被添加到主屏幕, 使用 manifest.json 定義應用的名稱, 圖標等等信息。

{ "name": "Minimal app to try PWA", "short_name": "Minimal PWA", "display": "standalone", "start_url": "/", "theme_color": "#8888ff", "background_color": "#aaaaff", "icons": [ { "src": "e.png", "sizes": "256x256", "type": "image/png" } ]}

然後在 HTML 文件當中引入配置:

<link rel="manifest" href="manifest.json" />

添加 Service Worker

Service Worker 在網頁已經關閉的情況下還可以運行, 用來實現頁面的緩存和離線, 後台通知等等功能。sw.js 文件需要在 HTML 當中引入:

<script> if (navigator.serviceWorker != null) { navigator.serviceWorker.register(sw.js) .then(function(registration) { console.log(Registered events at scope: , registration.scope); }); }</script>

後面我們會往 sw.js 文件當中添加邏輯代碼。在 Service Worker 當中會用到一些全局變數:

  • self: 表示 Service Worker 作用域, 也是全局變數

  • caches: 表示緩存
  • skipWaiting: 表示強制當前處在 waiting 狀態的腳本進入 activate 狀態

  • clients: 表示 Service Worker 接管的頁面

處理靜態緩存

首先定義需要緩存的路徑, 以及需要緩存的靜態文件的列表, 這個列表也可以通過 Webpack 插件生成。

var cacheStorageKey = minimal-pwa-1var cacheList = [ /, "index.html", "main.css", "e.png"]

藉助 Service Worker, 可以在註冊完成安裝 Service Worker 時, 抓取資源寫入緩存:

self.addEventListener(install, e => { e.waitUntil( caches.open(cacheStorageKey) .then(cache => cache.addAll(cacheList)) .then(() => self.skipWaiting()) )})

調用 self.skipWaiting() 方法是為了在頁面更新的過程當中, 新的 Service Worker 腳本能立即激活和生效。

處理動態緩存

網頁抓取資源的過程中, 在 Service Worker 可以捕獲到 fetch 事件, 可以編寫代碼決定如何響應資源的請求:

self.addEventListener(fetch, function(e) { e.respondWith( caches.match(e.request).then(function(response) { if (response != null) { return response } return fetch(e.request.url) }) )})

真實的項目當中, 可以根據資源的類型, 站點的特點, 可以專門設計複雜的策略。fetch 事件當中甚至可以手動生成 Response 返回給頁面。

更新靜態資源

緩存的資源隨著版本的更新會過期, 所以會根據緩存的字元串名稱(這裡變數為 cacheStorageKey, 值用了 "minimal-pwa-1")清除舊緩存, 可以遍歷所有的緩存名稱逐一判斷決決定是否清除(備註: 簡化的寫法, Promise.all 中 return undefined 可能出錯, 見評論):

self.addEventListener(activate, function(e) { e.waitUntil( Promise.all( caches.keys().then(cacheNames => { return cacheNames.map(name => { if (name !== cacheStorageKey) { return caches.delete(name) } }) }) ).then(() => { return self.clients.claim() }) )})

在新安裝的 Service Worker 中通過調用 self.clients.claim() 取得頁面的控制權, 這樣之後打開頁面都會使用版本更新的緩存。舊的 Service Worker 腳本不再控制著頁面之後會被停止。

查看 Demo

執行命令:

http-server -c-1 # 注意設置關閉緩存, 這裡用參數 -c-1# 用另一個終端ngrok http 8080

桌面瀏覽器可以直接通過 localhost:8080 訪問, 從 DevTools 的 Application 標籤可以看到 Service Worker。

由於 Service Worker 限制了使用 HTTPS 地址或者 localhost 地址, 在 Android Chrome 打開需要藉助 ngrok 生成的 HTTPS 地址, 這樣才能把 demo 添加到首屏。添加到首屏之後, 即便在離線狀態下, 頁面也可以打開。

從 DevTools 可以看到, 普通頁面刷新時, 列表當中的靜態資源都是從 Service Worker 獲取的:

更新頁面

頁面被緩存之後, 就需要適當處理緩存失效時頁面的更新。在這個 Demo 當中, 被緩存的資源是無法發起請求判斷是否被更新的, 只有 sw.js 會自動根據 HTTP 緩存的機制嘗試去判斷應用是否被更新。

所以當頁面發生修改時, 要同時對 sw.js 文件進行一次修改。比如在 HTML 當中更新版本到 2:

<h3>Revision 2</h3>

同時 sw.js 文件當中也要進行一次修改, 保證文件發生改變, 同時緩存的名稱也變改變了:

var cacheStorageKey = minimal-pwa-2

然後重新打開一次頁面, 這個時候渲染的頁面依然是舊的, 不過可以從 DevTools 看到 sw.js 被安裝和激活。之後關閉頁面, 再次打開, 就可以見到網頁上的顯示版本變成了 2。

注意: Demo 當中如果直接啟動 http-server 而不使用 -c-1 關閉緩存, sw.js 可能被緩存住, 導致更新方案失敗。這種情況下存在 Caches API 和 HTML caching 兩層緩存, 需要進行清理才能完成更新。

推薦閱讀:

Progressive Web Apps - Part.2 PWA 是什麼?
U4 2.0 新特性 —— PWA 完整支持
深入了解 Service Worker ,看這篇就夠了
Chrome Dev Summit 2017參會筆記

TAG:PWA |