theme: juejin
highlight: a11y-dark
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,不再止步于会用!
- 构思中…
本文正在参加「金石计划 . 瓜分6万现金大奖」
暂无评论内容