PWA入門:手把手教你製作一個PWA應用
摘要:本文將通過一個簡單的列子(一個簡單的郵編查詢app)向大家展示PWA的開發流程,項目參考: Traversy Media - Build a PWA With Vue & Ionic4。在 src/views/Home.vue 中引入 ZipSearch 組件,當 Home 接收到 get-zip 事件時調用 https://www.zippopotam.us 的接口,獲取郵編對應的信息:。
簡介
Web前端的同學是否想過學習app開發,以彌補自己移動端能力的不足?但在面對一衆的選擇時很多同學略感迷茫,是學習ios還是android開發?是學習原生開發、混合開發(比如: Ionic ),還是使用 react native 或者 flutter 這樣的跨平臺框架?而app開發的學習週期長、學習成本高也讓一部分人望而卻步。得益於前端技術的飛速發展、瀏覽器性能的不斷提高,使用網頁技術開發出接近原生體驗的應用得以變爲現實,PWA就在這樣的背景下應運而生。可以用自己熟悉的HTML、CSS、Javascript開發出媲美原生app的網站,不僅擁有接近原生app的流暢程度,並且具備一些原生app纔有的特性,比如:a. 可以在主屏上安裝應用圖標,b. 離線狀態下訪問,c. 獲取消息通知,等等。。PWA的出現讓大家看到了希望!
對比原生應用
那PWA和原生應用相比到底有何競爭力呢?我們分別看一下原生應用和PWA的特點:
原生應用:
- 使用原生SDK和開發工具開發
- 需要考慮跨平臺,不同系統往往需要獨立開發
- 需要發佈到應用商店才能下載使用
- 可以安裝到手機主屏,生成應用圖標
- 直接運行於操作系統上,訪問系統資源方便
- 可以離線使用
- 可以獲取消息通知
PWA應用:
- 使用HTML,CSS,JS開發
- 無需考慮跨平臺,只需要考慮瀏覽器兼容性
- 通過url訪問,無需發佈到應用商店
- 可以安裝到手機主屏,生成應用圖標
- 運行於瀏覽器中,可訪問系統資源
- 可以離線使用
- 可以獲取消息通知
可以發現PWA具備了原生應用的主要能力,但是開發流程卻比原生應用更加簡潔:a. html/css/js的羣衆基礎更好,開發效率更高;b. 省去了爲不同系統開發獨立版本的大量成本;c. 省去了上架到應用市場的繁瑣流程;d. 無需前往應用商店下載,用戶使用起來也更加方便。但是值得注意的是,PWA還是相對比較新的技術,實現規範還有很多調整的空間,部分瀏覽器對PWA的支持也還不完善,但是PWA是一個趨勢,所以現在學習正合適!
本文將通過一個簡單的列子(一個簡單的郵編查詢app)向大家展示PWA的開發流程,項目參考: Traversy Media - Build a PWA With Vue & Ionic4 。完成後的效果是 這樣的 。
創建項目
項目使用Vue + Ionic的組合進行開發。本文主要關注PWA的搭建,因此vue、ionic等技術不做過多描述。使用VSCode的同學,建議安裝 Vetur 插件增加開發效率。
1. 首先全局安裝 @vue/cli
:
npm install -g @vue/cli
2. 初始化vue項目:
vue create vue-ionic-pwa
3. 因爲ionic的路由依賴於 vue-router
,所以接下來安裝 vue-router
:
vue add router
4. 安裝 @ionic/vue
npm install @ionic/vue
5. 在 src/main.js
中添加對ionic的引用:
... import Ionic from '@ionic/vue' import '@ionic/core/css/ionic.bundle.css' Vue.use(Ionic) ...
6. 在 src/router.js
中使用 IonicVueRouter
替換默認的vue router:
import Vue from 'vue' import { IonicVueRouter } from '@ionic/vue'; import Home from './views/Home.vue' Vue.use(IonicVueRouter) export default new IonicVueRouter({ mode: 'history', base: process.env.BASE_URL, routes: [ { path: '/', name: 'home', component: Home } ] })
7. 將 src/App.vue
內容修改爲:
<template> <div id="app"> <ion-app> <ion-vue-router/> </ion-app> </div> </template>
8. 將 src/views/Home.vue
內容修改爲:
<template> <div class="ion-page"> <ion-header> <ion-toolbar> <ion-title> ZipInfo </ion-title> </ion-toolbar> </ion-header> <ion-content class="ion-padding">My App</ion-content> </div> </template> <script> export default { name: 'home', components: {} } </script>
最後,我們運行 yarn serve
看下效果:
App功能實現
App主要有三部分組成:1. 搜索組件,用於輸入郵編並查詢,2. 展示組件,用於展示查詢到的郵編信息,3. 清除按鈕,用於清除查詢到的郵編信息
1. 搜索組件
我們在 src/components
下面新建 ZipSearch.vue
文件作爲郵編搜索組件,主要邏輯爲當用戶輸入一串字符,按下搜索按鈕,如果輸入合法則觸發 get-zip
事件,如果不合法則給出提示。
ZipSearch.vue
<template> <ion-grid> <form @submit="onSubmit"> <ion-col> <ion-item> <ion-label>ZipCode:</ion-label> <ion-input :value="zip" @input="zip = $event.target.value" name="zip" placeholder="Enter US ZipCode" /> </ion-item> </ion-col> <ion-col> <ion-button type="submit" color="primary" expand="block">Find</ion-button> </ion-col> </form> </ion-grid> </template> <script> export default { name: "ZipSearch", data() { return { zip: "" }; }, methods: { onSubmit(e) { e.preventDefault(); const zipRegex = /(^\d{5}$)|(^\d{5}-\d{4}$)/; const isValid = zipRegex.test(this.zip); if (!isValid) { this.showAlert(); } else { this.$emit("get-zip", this.zip); } this.zip = ""; }, showAlert() { return this.$ionic.alertController .create({ header: "Enter zipcode", message: "Please enter a valid US ZipCode", buttons: ["OK"] }) .then(a => a.present()); } } }; </script>
在 src/views/Home.vue
中引入 ZipSearch
組件,當 Home
接收到 get-zip
事件時調用 https://www.zippopotam.us 的接口,獲取郵編對應的信息:
... <ion-content class="ion-padding"> <ZipSearch v-on:get-zip="getZipInfo"/> </ion-content> ... <script> import ZipSearch from "../components/ZipSearch"; export default { name: "home", components: { ZipSearch }, data() { return { info: null }; }, methods: { async getZipInfo(zip) { const res = await fetch(`https://api.zippopotam.us/us/${zip}`); if (res.status == 404) { this.showAlert(); } this.info = await res.json(); }, showAlert() { return this.$ionic.alertController .create({ header: "Not Valid", message: "Please enter a valid US ZipCode", buttons: ["OK"] }) .then(a => a.present()); } } }; </script>
我們先看一下搜索組件的效果:
輸入郵編格式錯誤:
2. 信息展示和清除組件
獲取到郵編信息後我們需要一個展示郵編信息的組件和一個清除信息的按鈕,在 src/components
下面新建 ZipInfo.vue
和 ClearInfo.vue
。
ZipInfo.vue
<template> <ion-card v-if="info"> <ion-card-header> <ion-card-subtitle>{{info['post code']}}</ion-card-subtitle> <ion-card-title>{{info['places'][0]['place name']}}</ion-card-title> </ion-card-header> <ion-card-content> <ion-list> <ion-item> <ion-label> <strong>State:</strong> {{info['places'][0]['state']}} ({{info['places'][0]['state abbreviation']}}) </ion-label> </ion-item> <ion-item> <ion-label> <strong>Latitude:</strong> {{info['places'][0]['latitude']}} </ion-label> </ion-item> <ion-item> <ion-label> <strong>Longitude:</strong> {{info['places'][0]['longitude']}} </ion-label> </ion-item> </ion-list> </ion-card-content> </ion-card> </template> <script> export default { name: "ZipInfo", props: ["info"] }; </script>
ClearInfo.vue
<template> <ion-button color="light" expand="block" v-if="info" @click="$emit('clear-info')">Clear</ion-button> </template> <script> export default { name: "ClearInfo", props: ["info"] }; </script>
接着在 Home
中引入 ZipInfo
和 ClearInfo
組件:
src/views/Home.vue
... <ion-content class="ion-padding"> <ZipSearch v-on:get-zip="getZipInfo"/> <ZipInfo v-bind:info="info"/> <ClearInfo v-bind:info="info" v-on:clear-info="clearInfo"/> </ion-content> ... import ZipInfo from "../components/ZipInfo"; import ClearInfo from "../components/ClearInfo"; export default { name: "home", components: { ZipSearch, ZipInfo }, methods:{ ... clearInfo(){ this.info = null; } } }
到此,app的主體就完成了,效果如下:
實現PWA
我們使用現成的 @vue/pwa
插件來給我們的app增加PWA的能力。
安裝 @vue/pwa
:
vue add @vue/pwa
安裝完成後項目中增加了 public/manifest.json
和 registerServiceWorker.js
兩個文件。其中 public/manifest.json
文件內容如下:
{ "name": "vue-ionic-pwa", "short_name": "vue-ionic-pwa", "icons": [ { "src": "./img/icons/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "./img/icons/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ], "start_url": "./index.html", "display": "standalone", "background_color": "#000000", "theme_color": "#4DBA87" }
manifest.json
中主要包含app的基本信息,比如名稱(name)、圖標(icons)、顯示方式(display)等等,是web app能被以類似原生的方式安裝、展示的必要配置。更多的配置項可參考 MDN Web App Manifest 。
在Chrome瀏覽器控制檯中也可看到app的manifest配置:
registerServiceWorker.js
用於註冊service worker。service worker通俗來講就是在瀏覽器後臺獨立於網頁運行的一段腳本,service worker可以完成一些特殊的功能,比如:消息推送、後臺同步、攔截和處理網絡請求、管理網絡緩存等。Service worker之於pwa的意義在於能夠爲用戶提供離線體驗,即掉線狀態下用戶依舊能夠訪問網站並獲取已被緩存的數據。使用service worker需要HTTPS,並且考慮 瀏覽器兼容性 。
registerServiceWorker.js
import { register } from 'register-service-worker' if (process.env.NODE_ENV === 'production') { register(`${process.env.BASE_URL}service-worker.js`, { ready () { console.log( 'App is being served from cache by a service worker.\n' + 'For more details, visit https://goo.gl/AFskqB' ) }, registered () { console.log('Service worker has been registered.') }, cached () { console.log('Content has been cached for offline use.') }, updatefound () { console.log('New content is downloading.') }, updated () { console.log('New content is available; please refresh.') }, offline () { console.log('No internet connection found. App is running in offline mode.') }, error (error) { console.error('Error during service worker registration:', error) } }) }
在Chrome瀏覽器控制檯中也可看到service worker的狀態:
當然,只註冊service worker還不夠,我們還希望控制service worker的行爲,通過在 vue.config.js
中增加相關的配置我們可以設置service worker文件的名稱、緩存邏輯等等。
vue.config.js
module.exports = { pwa: { workboxPluginMode: 'GenerateSW', workboxOptions: { navigateFallback: '/index.html', runtimeCaching: [ { urlPattern: new RegExp('^https://api.zippopotam.us/us/'), handler: 'networkFirst', options: { networkTimeoutSeconds: 20, cacheName: 'api-cache', cacheableResponse: { statuses: [0, 200] } } } ] } } }
更多配置請參考: @vue/cli-plugin-pwa 和 workbox-webpack-plugin 。由於 @vue/cli-plugin-pwa
生成的service worker只在生產環境生效,所以建議將項目build之後部署到生產環境測試。本文示例使用 github pages 進行部署和展示。
到此,將普通web app轉成PWA的工作基本完成,我們部署到線上看下效果:
文件已被緩存用於離線訪問:
查詢一個郵編試試,可以發現請求被緩存了下來:
我們接着關掉網絡,再查詢剛剛的那個郵編,發現在網絡請求失敗之後立即切換用本地緩存的數據:
好了,一個簡單的PWA就已經制作完成了。當然PWA的功能遠不止本文所展示的,比如推送、安裝到手機,後續有機會再跟大家分享,謝謝:pray:。
本文demo地址: https://github.com/MudOnTire/...