🚂🚂🚂 ServiceWorker -> PWA的基石,在线离线都能玩!

简介: PWA是Progressive Web App的缩写,翻译过来就是渐进式网络应用,它是一种新的网络应用模式,它结合了Web App和Native App的优点

PWAProgressive Web App的缩写,翻译过来就是渐进式网络应用,它是一种新的网络应用模式,它结合了Web AppNative App的优点,它可以让用户像使用Native App一样使用Web App,并且它可以在离线状态下使用。

PWA的简介

PWA是一种新的网络应用模式,它结合了Web AppNative 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

image.png

接着再将这个文件拖到命令行中,然后后面跟上localhost 127.0.0.1:

mkcert-v1.4.3-windows-amd64.exe localhost 127.0.0.1

image.png

这样就会在当前目录下生成两个文件,一个是/localhost+1.pem,一个是localhost+1-key.pem

由于我这里使用node环境来启动的服务,所以还需要设置一下NODE_EXTRA_CA_CERTS环境变量,这个环境变量是用来指定额外的证书的,这样我们就可以在本地使用HTTPS了;

set NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"

上述方案参考自:使用mkcert 为(nodejs)本地后端服务器的https安装ssl证书

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事件,所以才说ServiceWorkerPWA的基石。

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

like192.png

图标可以在这里生成,我这里就放上了我使用的图标。

一切准备就绪,我们就可以启动了,如果浏览器正常识别之后,会在地址栏上出现一个安装的按钮,点击之后就可以安装了,如下图所示:

image.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应用安装到桌面,但是并不会将应用程序的资源文件下载到本地,而是通过浏览器的缓存手段来实现的,这些缓存手段包括但不限于indexedDBlocalStorageService 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,比如Workboxsw-precache等,这些工具可以帮助我们自动化的生成Service Worker,以及缓存策略等。

历史章节和预告

目录
相关文章
|
1月前
|
编解码 前端开发 算法
实时云渲染方案为虚拟仿真教学搭建共享平台
实时云渲染技术的应用也日益重要,平行云作为唯一提供云渲染技术服务的企业,参与制定《虚拟仿真实验教学课程建设与共享应用规范(试用版·2020)》,有效解决下载、算力和盗版等痛点,实现随时随地的在线访问,保护知识产权,降低终端硬件要求,兼容性强,助力学校构建统一入口云平台。
|
1月前
|
存储 数据处理 对象存储
云端问道方案教学4期—多媒体数据存储与分发
本文整理自阿里云存储服务产品团队关于多媒体数据存储与分发的分享,涵盖以下四部分内容:1)行业痛点及背景:分析Web 2.0到AIGC时代下多媒体行业的存储挑战;2)方案优势介绍:结合对象存储(OSS)、智能媒体管理(IMM)和内容分发网络(CDN),提供高效、低成本的解决方案;3)典型场景应用:包括音视频、在线教育、网站/APP/小程序、游戏下载等场景的具体应用;4)选型推荐:根据业务需求选择合适的产品配置。该方案通过动静分离、智能处理和全球加速,帮助企业在数据存储与分发中实现降本增效。
|
1月前
|
存储 编解码 数据处理
云端问道第4期实践教学——多媒体数据存储与分发方案部署演示
该文档详细介绍了阿里云一键部署和手动部署多媒体数据存储与分发方案的步骤。一键部署通过资源编排服务(ROS)实现自动化,涵盖注册账号、开通服务、创建OSS Bucket、配置CDN加速及绑定IMM等功能,简化了复杂操作。手动部署则更细致地展示了每个配置环节,包括网络规划、资源创建、域名绑定、CDN配置、证书加密及最终的验证与清理,确保用户对整个流程有清晰理解。两种方式均以OSS为核心,支持数据上传、转码处理和加速分发,保障高效稳定的用户体验。
|
3月前
|
存储 缓存 前端开发
PWA 如何实现离线功能
PWA(渐进式Web应用)通过Service Worker技术实现离线功能。Service Worker作为浏览器和网络之间的代理,可以缓存网页资源,在用户离线时提供缓存内容,确保应用正常运行。
|
4月前
|
人工智能 Android开发 iOS开发
移动应用与系统:构建高效移动体验的关键技术
【10月更文挑战第3天】 在当今数字化时代,移动应用已成为人们生活中不可或缺的一部分。无论是社交、购物、娱乐还是学习,移动应用都扮演着重要角色。然而,要实现出色的用户体验并非易事。本文将深入探讨移动应用开发和移动操作系统的关键技术,揭示如何通过优化性能、提升安全性和增强用户交互来构建高效的移动应用环境。我们将从移动应用的开发流程、主流移动操作系统的特点,以及未来的发展趋势三个方面进行详细阐述。
63 3
|
4月前
|
开发框架 Android开发 UED
移动应用与系统:构建高效、可靠的移动生态
在当今数字化时代,移动应用已经成为人们日常生活和工作中不可或缺的一部分。本文将探讨移动应用开发和移动操作系统的重要性,以及如何构建一个高效、可靠的移动生态系统。我们将介绍移动应用开发的基本概念和流程,包括需求分析、设计、编码、测试和发布等环节。在此基础上,我们将深入探讨移动操作系统的作用和功能,以及它们如何为移动应用提供支持和保障。最后,我们将提出一些建议,以帮助开发者和企业更好地应对移动应用开发和系统优化的挑战。
|
6月前
|
弹性计算 关系型数据库 Serverless
云端架构下的高效多媒体文件处理方案测评体验
传统的服务器部署模式在处理高并发、大数据量的文件转换任务时,常面临资源瓶颈和成本上升的问题。使用函数计算,利用事件驱动和异步任务的方式,将文件处理任务与核心应用解耦,同时依靠函数计算自动弹性扩展和按使用付费的优势可以快速对多媒体文件进行处理。
|
6月前
|
SQL 监控 大数据
"解锁实时大数据处理新境界:Google Dataflow——构建高效、可扩展的实时数据管道实践"
【8月更文挑战第10天】随着大数据时代的发展,企业急需高效处理数据以实现即时响应。Google Dataflow作为Google Cloud Platform的强大服务,提供了一个完全托管的流处理与批处理方案。它采用Apache Beam编程模型,支持自动扩展、高可用性,并能与GCP服务无缝集成。例如,电商平台可通过Dataflow实时分析用户行为日志:首先利用Pub/Sub收集数据;接着构建管道处理并分析这些日志;最后将结果输出至BigQuery。Dataflow因此成为构建实时数据处理系统的理想选择,助力企业快速响应业务需求。
358 6
|
6月前
|
UED 存储 数据管理
深度解析 Uno Platform 离线状态处理技巧:从网络检测到本地存储同步,全方位提升跨平台应用在无网环境下的用户体验与数据管理策略
【8月更文挑战第31天】处理离线状态下的用户体验是现代应用开发的关键。本文通过在线笔记应用案例,介绍如何使用 Uno Platform 优雅地应对离线状态。首先,利用 `NetworkInformation` 类检测网络状态;其次,使用 SQLite 实现离线存储;然后,在网络恢复时同步数据;最后,通过 UI 反馈提升用户体验。
148 0
|
7月前
|
数据采集 开发工具 Android开发
构建高效移动应用:从开发到部署的全面指南构建高效Python爬虫的实战指南
【7月更文挑战第31天】在数字时代,移动应用已成为我们日常生活和工作不可或缺的一部分。本文将引导读者穿越移动应用开发的迷宫,探索如何从零开始构建一个高效的移动应用。我们将深入讨论移动操作系统的选择、开发工具的应用、以及实际编码过程中的最佳实践。通过本文,你不仅能够获得理论知识,还将通过代码示例加深理解,最终能够独立完成一个移动应用的构建和部署。
81 2