PWA
是Progressive Web App
的缩写,翻译过来就是渐进式网络应用,它是一种新的网络应用模式,它结合了Web App
和Native App
的优点,它可以让用户像使用Native App
一样使用Web App
,并且它可以在离线状态下使用。
PWA的简介
PWA
是一种新的网络应用模式,它结合了Web App
和Native App
的优点,它可以让用户像使用Native App
一样使用Web App
,并且它可以在离线状态下使用。
PWA
并不是只用一种技术实现的,它表达的是一种网络应用模式,它代表了构建 Web 应用程序的新理念,一个 PWA 应该具有以下特点:
- 可发现:通过 URL 可以访问,可以被搜索引擎抓取到
- 可安装:可以添加到桌面,可以在离线状态下使用
- 可链接:可以通过 URL 进行分享
- 独立与网络:可以在离线状态或者是在网速很差的情况下运行
- 渐进式:适配老版本的浏览器,在新版本的浏览器中可以使用更多的新特性
- 可重入:无论何时有内容更新,都可以及时更新
- 响应式:可以适配不同的屏幕尺寸
- 安全:通过 HTTPS 进行传输,保证用户的数据安全
上面的简介重要的是最后一点,PWA
是通过HTTPS
进行传输的,所以我们在使用PWA
的时候,需要在本地配置一个HTTPS
的服务;
开发环境下的HTTPS
网上虽然有很多关于证书如何申请和配置的文章,但是这些文章都是在生产环境下的,需要有服务器,域名,固定的IP等等;
但是我们开发怎么办?肯定得尽量和生产环境保持一致,而且PWA
应用是一定需要HTTPS
的,所以我们需要在本地配置一个HTTPS
的开发环境。
1. 生成证书
证书我们可以使用mkcert
这个包来生成,这个包在npm
上就有,但是这里生成的证书是不受信任的,不过它有一个衍生的exe
文件;
可以通过这里下载证书生成程序,根据自己电脑环境下载对应的版本;
我的电脑是Windows
,所以我下载的是mkcert-v1.4.3-windows-amd64.exe
;
下载完成之后,在你需要生成证书的目录下,打开命令行工具,然后将这个文件拖到命令行中,然后后面跟上-install
:
mkcert-v1.4.3-windows-amd64.exe -install
接着再将这个文件拖到命令行中,然后后面跟上localhost 127.0.0.1
:
mkcert-v1.4.3-windows-amd64.exe localhost 127.0.0.1
这样就会在当前目录下生成两个文件,一个是/localhost+1.pem
,一个是localhost+1-key.pem
;
由于我这里使用node
环境来启动的服务,所以还需要设置一下NODE_EXTRA_CA_CERTS
环境变量,这个环境变量是用来指定额外的证书的,这样我们就可以在本地使用HTTPS
了;
set NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"
2. 启动服务
完成之后,就需要使用node
来启动服务了,完整代码如下:
import express from 'express';
import * as https from "https";
import path from "path";
import fs from "fs";
const app = express();
app.use(express.json())
app.use(express.urlencoded({
extended: false}))
const __dirname = path.resolve();
app.use('/', express.static(__dirname + '/public'));
app.all('/getData', (req, res) => {
// 百万级数据
const data = [];
for (var i = 0; i < 1000000; i++) {
data.push({
name: 'name' + i,
age: i
});
}
res.send(data);
})
//为https请求添加ssl证书
const httpsOption = {
key: fs.readFileSync("./cert/key.pem"),
cert: fs.readFileSync("./cert/cert.pem"),
}
//https请求
https.createServer(httpsOption, app).listen(443, () => {
const hostname = 'localhost';
const port = 443;
console.log(`Server running at https://${hostname}:${port}/`);
});
process.on('uncaughtException', (e) => {
console.error(e); // Error: uncaughtException
// do something: 释放相关资源(例如文件描述符、句柄等)
// process.exit(1); // 手动退出进程
});
PWA的实现
PWA
的实现并不一定需要使用ServiceWorker
,但是ServiceWorker
可以提供网络能力,如果在断网的情况下,我们想要让PWA
能够正常运行,那么就需要使用ServiceWorker
了。
使用PWA
很简单,只需要在HTML
中的head
中使用link
标签引用一个manifest.json
文件即可,这个文件就是PWA
的配置文件,它的内容如下:
{
"name": "my-pwa-app",
"short_name": "pwa-app",
"start_url": "/",
"display": "standalone",
"background_color": "#fff",
"theme_color": "#fff",
"description": "PWA demo",
"icons": [
{
"src": "/images/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png"
}
]
}
先来看看配置的含义:
name
:应用的名称short_name
:应用的简称start_url
:应用的启动页display
:应用的显示模式,有以下几种模式:fullscreen
:全屏模式standalone
:独立模式minimal-ui
:最小化模式browser
:浏览器模式
background_color
:应用的背景颜色theme_color
:应用的主题颜色description
:应用的描述icons
:应用的图标,可以配置多个图标,每个图标都有自己的尺寸和类型src
:图标的路径sizes
:图标的尺寸type
:图标的类型
manifest.json
文件中有很多属性配置,通常情况下只需要提供name
和一组icons
属性就好了,但是尽可能多的提供一些属性,可以让PWA
在不同的设备上有更好的表现。
更多的manifest.json
的配置可以参考:https://developer.mozilla.org/zh-CN/docs/Web/Manifest
PWA的使用
上面说到了PWA
的实现就是通过link
标签引用一个manifest.webmanifest
文件,在我们之前写到的ServiceWorker
的文章中,直接加上link
标签即可:
<link rel="manifest" href="/manifest.webmanifest">
根据规范,
manifest.json
这个文件的后缀应该是.webmanifest
,内容依旧是JSON
格式;所以很多网站的引用的是
manifest.webmanifest
,还有一些历史的后缀名.webapp
;所以按照规范来说,我们应该使用
.webmanifest
作为后缀名。
一个PWA
的应用的manifest.webmanifest
必须包含以下属性,才能有效的被浏览器识别:
name
:应用的名称short_name
:应用的简称start_url
:应用的启动页display
:应用的显示模式icons
:正确的图标background_color
:应用的背景颜色
图标需要遵守Google Play 图标设计规范,这样才能在不同的设备上有更好的表现,当然图标并不是一定要完全遵守,但是大小还是会识别的。
除此之外,还需要有ServiceWorker
的支持,并且ServiceWorker
必须监听了fetch
事件,所以才说ServiceWorker
是PWA
的基石。
PWA的实际应用
上面说了这么多,我们来实际看看PWA
的应用启动好了会是什么样子;
根据我上面提供的HTTPS
启动方案,然后再加上正确的manifest.webmanifest
文件,接着就是安装一个ServiceWorker
,然后就可以启动了。
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="manifest" href="manifest.webmanifest" />
</head>
<body>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js', {
scope: '/'
}).then(function (registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
});
}
</script>
</body>
</html>
- manifest.webmanifest
{
"name": "my-pwa-app",
"short_name": "pwa-app",
"description": "这是我的第一个PWA应用",
"start_url": "/index.html",
"background_color": "purple",
"display": "fullscreen",
"icons": [
{
"src": "/icons/like192.png",
"sizes": "192x192",
"type": "image/png"
}
]
}
- service-worker.js
self.addEventListener('install', function (event) {
caches.open('v1').then(function (cache) {
return cache.addAll([
'/index.html',
'/manifest.webmanifest'
]);
});
})
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
- icons/like192.png
图标可以在这里生成,我这里就放上了我使用的图标。
一切准备就绪,我们就可以启动了,如果浏览器正常识别之后,会在地址栏上出现一个安装
的按钮,点击之后就可以安装了,如下图所示:
现在我们就可以把这个应用添加到桌面了,大家可以试试,我这里就不截图了。
手动安装PWA
除了可以通过浏览器自动识别之后,通过点击图标安装之外,我们还可以通过代码的方式来安装PWA
,这样就可以在我们的应用中自动安装了。
这里通过监听beforeinstallprompt
事件来实现,代码如下:
window.addEventListener('beforeinstallprompt', (e) => {
e.prompt();
});
这个事件是在浏览器识别到可以安装之后触发的,我们可以在这个事件中调用prompt()
方法来安装;
通常情况下考虑用户体验,我们会在用户点击某个按钮之后再安装,这样就可以避免用户不知道这个应用是什么的情况。
<button id="install" style="display: none;">安装</button>
<script>
window.addEventListener('beforeinstallprompt', (e) => {
// 防止 Chrome 67 及更早版本自动显示安装提示
e.preventDefault();
const installBtn = document.getElementById('install');
installBtn.addEventListener('click', () => {
// 隐藏显示 A2HS 按钮的界面
installBtn.style.display = 'none';
// 显示安装提示
e.prompt();
// 等待用户反馈
e.userChoice.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the A2HS prompt');
installBtn.partentNode.removeChild(installBtn);
} else {
console.log('User dismissed the A2HS prompt');
}
});
});
});
</script>
这里最开始隐藏install
按钮是因为防止目标浏览器不支持PWA
。
A2HS
说到安装PWA
应用还是有必要了解一下A2HS
的概念;
A2HS
全称是Add to Home Screen
,即添加到主屏幕,这个是PWA
的一个特性,可以让我们的应用在安装之后,可以像原生应用一样,可以在桌面上显示图标,可以在应用列表中显示,可以在应用列表中卸载等等。
A2HS
只是将PWA
应用安装到桌面,但是并不会将应用程序的资源文件下载到本地,而是通过浏览器的缓存手段来实现的,这些缓存手段包括但不限于indexedDB
、localStorage
、Service Worker
等等。
实战
上面讲了这么多,还是实战最好玩,这里依然用最开始Web Worker
的百万级数据计算的例子来实现一个PWA
应用。
这里只需要稍微修改一下index.html
文件,然后再在service-worker.js
中添加一些缓存就好了:
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="manifest" href="manifest.webmanifest"/>
<style>
#install {
position: absolute;
left: 0;
top: 0;
}
.table-wrapper {
margin-top: 40px;
height: 800px;
overflow: auto;
width: 200px;
}
table {
width: 100%;
}
thead tr {
position: sticky;
left: 0;
top: 0;
background: #fff;
}
</style>
</head>
<body>
<button id="install" style="display: none;">安装</button>
<div class="table-wrapper">
<table border id="table">
<thead>
<tr>
<th>姓名</th>
<th>年龄</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<script src="./axios.js"></script>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js', {
scope: '/'
}).then(function (registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
});
}
window.addEventListener('beforeinstallprompt', (e) => {
// 防止 Chrome 67 及更早版本自动显示安装提示
e.preventDefault();
const installBtn = document.getElementById('install');
installBtn.style.display = 'block';
installBtn.addEventListener('click', (e) => {
// 显示安装提示
e.prompt();
// 等待用户反馈
e.userChoice.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the A2HS prompt');
installBtn.parentNode.removeChild(installBtn);
} else {
console.log('User dismissed the A2HS prompt');
}
});
});
});
const table = document.getElementById('table');
const tbody = table.getElementsByTagName('tbody')[0];
let result = [];
axios.get('/getData').then(function (response) {
result = response.data;
result.slice(0 , 300).forEach(function (item) {
const tr = document.createElement('tr');
const td1 = document.createElement('td');
const td2 = document.createElement('td');
td1.innerText = item.name;
td2.innerText = item.age;
tr.appendChild(td1);
tr.appendChild(td2);
tbody.appendChild(tr);
});
});
// 虚拟滚动
const tableWrapper = document.querySelector('.table-wrapper');
let scrollTop = tableWrapper.scrollTop;
let firstPage = 1, lastPage = 3;
tableWrapper.addEventListener('scroll', function (e) {
const _scrollTop = e.target.scrollTop;
if (_scrollTop - scrollTop > 0) {
// 向下滚动
if ((_scrollTop + tableWrapper.clientHeight + 10) >= tableWrapper.scrollHeight) {
if (lastPage === result.length / 100) return;
// 滚动到底部
firstPage++;
lastPage++;
const data = result.slice(lastPage * 100, (lastPage + 1) * 100);
appendToTBody(data, 'bottom');
removeTr('top');
}
} else {
// 向上滚动
if (_scrollTop <= 10) {
// 滚动到顶部
if (firstPage === 1) return;
firstPage--;
lastPage--;
const data = result.slice(firstPage * 100, (firstPage + 1) * 100);
appendToTBody(data, 'top');
removeTr('bottom');
// 移除头部需要重新定位
const tr = table.getElementsByTagName('tr')[0];
tableWrapper.scrollTop = tr.clientHeight * 100;
}
}
});
const appendToTBody = (data, direction = 'bottom') => {
const fragment = document.createDocumentFragment();
data.forEach((item) => {
const tr = document.createElement('tr');
const td1 = document.createElement('td');
const td2 = document.createElement('td');
td1.innerText = item.name;
td2.innerText = item.age;
tr.appendChild(td1);
tr.appendChild(td2);
fragment.appendChild(tr);
});
if (direction === 'bottom') {
tbody.appendChild(fragment);
} else {
tbody.insertBefore(fragment, tbody.firstChild);
}
}
const removeTr = (direction = 'top') => {
const tr = tbody.getElementsByTagName('tr');
let removeNum = 100;
while (removeNum--) {
if (direction === 'top') {
tbody.removeChild(tr[0]);
} else {
tbody.removeChild(tr[tr.length - 1]);
}
}
}
</script>
</body>
</html>
- service-worker.js
self.addEventListener('install', function (event) {
caches.open('v1').then(function (cache) {
return cache.addAll([
'/index.html',
'/manifest.webmanifest',
'/axios.js',
'/getData',
]);
});
})
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
上面的代码都弄好了之后,先刷新一下页面,然后再切断网络,再刷新一下页面,就可以看到效果了。
总结
本篇已经了解了PWA
应用如何工作在浏览器中,需要做哪些前置工作,需要满足哪些必要条件,以及如何实现一个简单的PWA
应用。
PWA
应用实现起来还是挺简单的,只需要一个配置文件,以及启动一个Service Worker
,然后就可以实现离线缓存,桌面安装等功能了。
PWA
只是一个概念,目前也有很多工具可以帮助我们实现PWA
,比如Workbox
、sw-precache
等,这些工具可以帮助我们自动化的生成Service Worker
,以及缓存策略等。
历史章节和预告
- 🎉🎉🎉 Web Workers 使用秘籍,祝您早日通关前端多线程!
- 🥳🥳🥳Worker中还可以创建多个Worker,打开多线程编程的大门
- ✨✨✨ ServiceWorker 让你的网页拥抱服务端的能力
- 🎊🎊🎊深入 ServiceWorker,消息推送,后台同步,一网打尽!
- 当前章节
- SharedWorker让你多个页面相互通信
- 详解 Web Worker,不再止步于会用!
- 构思中...