PWA入门:手把手教你制作一个PWA应用
摘要: PWA图文教程
- 原文:PWA入门:手把手教你制作一个PWA应用
- 作者:MudOnTire
Fundebug经授权转载,版权归原作者所有。

简介
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的功能远不止本文所展示的,比如推送、安装到手机,后续有机会再跟大家分享,谢谢
PWA入门:手把手教你制作一个PWA应用的更多相关文章
- 手把手教你制作AppPreview视频并上传到appStore进行审核
手把手教你制作AppPreview视频并上传到appStore进行审核 注意,你需要使用iMovie才能够制作AppPreview视频文件,用QuickTime录制的无效! 最终效果 1. 新建一个事 ...
- 只有20行Javascript代码!手把手教你写一个页面模板引擎
http://www.toobug.net/article/how_to_design_front_end_template_engine.html http://barretlee.com/webs ...
- iOS回顾笔记(05) -- 手把手教你封装一个广告轮播图框架
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,bi ...
- R数据分析:跟随top期刊手把手教你做一个临床预测模型
临床预测模型也是大家比较感兴趣的,今天就带着大家看一篇临床预测模型的文章,并且用一个例子给大家过一遍做法. 这篇文章来自护理领域顶级期刊的文章,文章名在下面 Ballesta-Castillejos ...
- 手把手教你画一个 逼格满满圆形水波纹loadingview Android
才没有完结呢o( ̄︶ ̄)n .大家好,这里是番外篇. 拜读了爱哥的博客,又学到不少东西.爱哥曾经说过: 要站在巨人的丁丁上. 那么今天,我们就站在爱哥的丁丁上来学习制作一款自定义view(开个玩笑,爱 ...
- 用Python手把手教你搭一个Transformer!
来源商业新知网,原标题:百闻不如一码!手把手教你用Python搭一个Transformer 与基于RNN的方法相比,Transformer 不需要循环,主要是由Attention 机制组成,因而可以充 ...
- PWA 入门: 写个非常简单的 PWA 页面
Progressive Web Apps 是 Google 提出的用前沿的 Web 技术为网页提供 App 般使用体验的一系列方案. 这篇文章里我们来完成一个非常简单的 PWA 页面. 一个 PWA ...
- 手把手教你制作微信小程序,开源、免费、快速搞定
最近做了个"罗孚传车"的小程序 一时兴起,做了一个小程序,将个人收集的同汽车相关的行业资讯和学习资料,分享到小程序中,既作为历史资料保存,又提供给更多的人学习和了解,还能装一下:) ...
- 手把手教你写一个RN小程序!
时间过得真快,眨眼已经快3年了! 1.我的第一个App 还记得我14年初写的第一个iOS小程序,当时是给别人写的一个单机的相册,也是我开发的第一个完整的app,虽然功能挺少,但是耐不住心中的激动啊,现 ...
随机推荐
- Add an Action that Displays a Pop-up Window 添加显示弹出窗口按钮
In this lesson, you will learn how to create an Action that shows a pop-up window. This type of Acti ...
- git命令教程
git教程笔记 Git是什么? Git是一个分布式版本控制系统 版本控制方式 集中式版本控制:从版本库中先取得最新的版本,改完之后再上传到版本库中,需要联网 分布式版本控制:每个合作者电脑上都有一个版 ...
- vue中使用router打开一个新的窗口
一个单页应用打开一个新的窗口不是很好控制,比如权限的处理,因为原先的页面不会自动刷新,方法很简单: let routeData = this.$router.resolve({ name: " ...
- iOS中计算字符串NSString的高度
根据固定宽度计算字符串高度: NSString *info = @"但是公司的高度是广东省公司的广东省高速度来开个大帅哥多撒谎个爱好就跟他说噶三公司噶是的刚好是我哥如果黑暗如果坏都干撒降低公 ...
- SpringBoot高版本修改为低版本时测试类报错解决
有时在使用idea通过Spring Initailizr创建项目时,默认只能创建最近的版本的SpringBoot项目. 这是如果想要换成版本,就可以在项目创建好了之后,在pom文件中直接将版本修改过来 ...
- 如何将RAC数据库的 RMAN Disk 备份 Restore 到另一个节点上的单个实例 (Doc ID 415579.1)
HowTo Restore RMAN Disk backups of RAC Database to Single Instance On Another Node (Doc ID 415579.1) ...
- Octave计算数据
设A=[1 2;3 4;5 6] B=[11 12;13 14;15 16] A.*B = :对A以及B中的对应的元素进行相乘 11 24 39 56 75 96 A.^2 :对A中的每一个元 ...
- java-11-Stream优化并行流
并行流 多线程 把一个内容分成多个数据块 不同线程分别处理每个数据块的流 串行流 单线程 一个线程处理所有数据 java8 对并行流优化 StreamAPI 通过pa ...
- go语言设计模式之Command
package main import ( "fmt" ) type Command interface { Execute() } type ConsoleOutput stru ...
- ACWING 95 费解的开关 解题记录
你玩过“拉灯”游戏吗?25盏灯排成一个5x5的方形.每一个灯都有一个开关,游戏者可以改变它的状态.每一步,游戏者可以改变某一个灯的状态.游戏者改变一个灯的状态会产生连锁反应:和这个灯上下左右相邻的灯也 ...