PWA - 微前端项目实践主要目的: 解决资源加载慢问题

2024-10-09
2245 字约 8 分钟
...

背景

平台各个子应用静态资源加载速度慢,用户体验有待提升

主要目的:

  • 解决资源加载慢问题,提升体验
  • 支持应用自启动(原生app体验)

项目使用到的技术栈为:React umi3 qiankun

PWA 介绍

简介

PWA 它不是特指某一项技术,而是应用多项技术来改善用户体验的 Web App,其核心技术包括 Web App Manifest,Service Worker 等,用户体验才是 PWA 的核心。

特点:

  • 可靠 - 即使在网络不稳定甚至断网的环境下,也能瞬间加载并展现
  • 用户体验 - 快速响应,具有平滑的过渡动画及用户操作的反馈
  • 用户黏性 - 和 Native App 一样,可以被添加到桌面,具有沉浸式的用户体验

核心技术

Web App Manifest

主要为项目配置manifest.json,提供浏览器安装PWA所需的信息,例如应用程序名称和图标等。Web app manifests允许开发者配置隐藏浏览器多余的 UI(地址栏,导航栏等),让PWA具有和Native App一样的沉浸式体验。

Service Worker

  • 使用到的时候浏览器会自动唤醒,不用的时候自动休眠
  • 可拦截并代理请求和处理返回,可以操作本地缓存,如 CacheStorage,IndexedDB 等
  • 离线内容开发者可控
  • 能接受服务器推送的离线消息
  • 必须在 HTTPS 环境下才能工作

实现过程

注册 Service Worker

  1. 根目录新建 service-worker.js 文件,用于编写 Service Worker 具体逻辑。

主要添加安装、激活、缓存捕获后的处理逻辑(会在后续缓存策略章节详细描述)

  1. 注册 service worker 服务

在 index.html 文件中注入以下代码

<script>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
      navigator.serviceWorker
        .register('/service-worker.js')
        .then(registration => {
          console.log('SW registered: ', registration);
        })
        .catch(registrationError => {
          console.log('SW registration failed: ', registrationError);
        });
    });
  }
</script>

我们项目是基于umi开发的,在umi3中在document.ejs添加script标签,在umi4中则需要在umirc.ts中的headScript添加内容。

安装到主屏幕

  1. 根目录(或是public目录)下新建manifest.json文件,添加需要的配置,具体可参考文档。 Web App Manifest,下面提供一份实例:
{
  "short_name": "灵思 Aicity",
  "name": "灵思 Aicity",
  "icons": [
    {
      "src": "https://xxx/144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "https://xxx/192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "maskable"
    }
  ],
  "start_url": ".",
  "display": "minimal-ui",
  "background_color": "#fff",
  "theme_color": "#fff"
}
  1. index.html中进行manifest引入
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta
      name="viewport"
      id="scale-view"
      content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
    />
    <link rel="manifest" href="/manifest.json" />
    <link rel="icon" href="/favicon.ico" type="image/x-icon" />
  </head>
</html>

到这一步启动项目之后就会在地址栏多出一个按钮

点击按钮就可以进行web app安装

安装完文件夹(或桌面)就会多出一个应用信息。

到这web应用的可安装操作就完成了,如果没有出现下载按钮,可打开控制台查看 Application-Manifest相关报错提示!!!

缓存策略

其核心是使用了 service worker 相关技术,详细可参考文档 Service Worker API。

为了减少一些处理细节上的时间,采用了google的插件workbox进行快速集成,省去了很多准备工作。

  1. 如果你只需要实现一些基本的缓存,不做预加载这些其他操作,workbox GenerateSW这个就足以满足你的需求,以下是一个建议配置:
import { defineConfig } from 'umi';
import { GenerateSW } from 'workbox-webpack-plugin';

export default defineConfig({
  
  
  chainWebpack(memo) {
    memo.plugin('workbox').use(GenerateSW, {
      
      cacheId: 'webpack-pwa', 
      
      skipWaiting: true, 
      clientsClaim: true, 
      swDest: 'service-wroker.js', 
      globPatterns: ['**/*.{html,js,css,png.jpg}'], 
      globIgnores: ['service-wroker.js'], 
      runtimeCaching: [
        
        {
          
          urlPattern: /.*\.js/, 
          
          handler: 'NetworkFirst' 
        }
      ]
    })
  }
})
  1. 遇到复杂点的场景,还是需要一定自由度去编写策略,我们可以选择workbox InjectManifest进行处理,详细实现如下:

umirc.ts

import { defineConfig } from 'umi';
import { InjectManifest } from 'workbox-webpack-plugin';

export default defineConfig({
  
  
  chainWebpack(memo) {
    
    memo.plugin('workbox').use(InjectManifest, [
      {
        
        swSrc: './service-worker.js', 
        
        swDest: 'service-worker.js', 
        
        maximumFileSizeToCacheInBytes: 10 * 1024 * 1024
      },
    ]);
  }
})

service-worker.js

registerRoute和GenerateSW中的runtimeCaching配置是一样的,主要区分下集中缓存策略:

  • CacheFirst会在有缓存的时候返回缓存,没有缓存才会去请求并且把请求结果缓存
  • CacheOnly只返回缓存,不请求
  • NetworkFirst请求将会发出,成功的话就返回结果添加到缓存中,如果失败则返回立即缓存
  • NetworkOnly只请求,不读写缓存
  • StaleWhileRevalidate类似于 CacheFirst,区别在于在返回 Cache 缓存结果的同时会在后台发起网络请求拿到请求结果并更新 Cache 缓存,如果本来就没有 Cache 缓存的话,直接就发起网络请求并返回结果
import { registerRoute } from 'workbox-routing';
import {
  StaleWhileRevalidate,
  NetworkFirst,
  CacheFirst,
} from 'workbox-strategies';


self.__WB_DISABLE_DEV_LOGS = true;


registerRoute(
  /.*(gif|jpg|jpeg|png|svg|otf|woff|woff2|ttf|mp4|pbf).*/,
  new CacheFirst({
    cacheName: 'cache-static',
    expiration: {
      maxEntries: 1000, 
      maxAgeSeconds: 30 * 24 * 60 * 60, 
    },
  }),
);


registerRoute(
  /.*\.js.*/,
  new StaleWhileRevalidate({
    cacheName: 'cache-js',
    expiration: {
      maxEntries: 100, 
      maxAgeSeconds: 30 * 24 * 60 * 60, 
    },
  }),
);


registerRoute(
  /.*\.css.*/,
  new StaleWhileRevalidate({
    cacheName: 'cache-style',
    expiration: {
      maxEntries: 100,
      maxAgeSeconds: 30 * 24 * 60 * 60, 
    },
  }),
);


registerRoute(
  /^.*(-api\.).*$/,
  new NetworkFirst({
    cacheName: 'cache-api',
    cacheableResponse: {
      statuses: [200],
    },
  }),
);


registerRoute(
  ({ url }) => {
    
    const reg = /.*(\.flv|\.m3u8|\.ts|\.tsx).*/;
    return !reg.test(url.href);
  },
  new NetworkFirst({
    cacheName: 'cache-others',
    cacheableResponse: {
      statuses: [200],
    },
  }),
);

到这再请求页面的时候Application => Storage =>Cache storage里就能看到被缓存下来的文件了。

预加载实现

在上述基础上,我们对service-worker.js进行一些改造,让他可以进行预缓存。其实在配置InjectManifest的时候也提到了,workbox 会帮我们生成一份构建产物的映射(manifest)。借助这个我们可以实现预缓存。具体操作如下:

import { precacheAndRoute } from 'workbox-precaching';




const routes = ['/xxx', '/aaa'];

const precacheList = self.__WB_MANIFEST || [];
precacheAndRoute([...precacheList, ...routes]);




const precacheApps = ['portrait', 'map', 'graph'];
precacheApps.forEach((app) => {
  fetch(`/${app}/asset-manifest.json`)
    .then((response) => response.json())
    .then((data) => {
      const files = Object.values(data).filter(
        (file) => !file.includes('/index.html'),
      );
      precacheAndRoute(files);
    })
    .catch((e) =>
      console.error(
        `Error: can not fetch ${app} app asset-manifest.json`,
      ),
    );
});

Features

多 Tab 探索

这是目前比较常见的一个问题,在pwa应用内使用window.open()打开新tab会跳转到浏览器中,这样体验感是很不好的;对此我们想到了两种解决方案:

  1. 自己实现一套 Tab UI

大概长这样...

主要就是通过拦截window.open(),维护一份tabs的相关数据,使用iframe渲染每一个子页面

  • 优点:功能齐全,UI展示效果好
  • 缺点:性能差(主要原因你想想,开20个自定义tab,实际使用的是一个浏览器Tab性能,这不得炸了...);刷新、全屏、多个pwa应用等场景数据维护成本高;
  1. 使用chrome实验性api(目前楼主的方案)

在大概chrome@89版本左右开始,chrome 提出了 display: tabbed 模式,使用该模式需要手动开启:

这样在安装的时候会提示“在新标签页中打开”

大概长这样

  • 优点:原生交互体验好
  • 缺点:实验性api,用户使用成本大(目前我们平台还是小范围的推多tab形式,主要是内部人员使用,用于演示这些操作)

唤起方式

主要是针对pwa应用之间的相互唤起、开机启动这些

持续探索中,有好方案欢迎留言...

POST 缓存方案

由于cache storage不支持缓存 POST 请求,所以在首页展示有POST相关接口进行数据拉取时,离线效果不理想。这个问题更多的是探讨是否有必要这样做,理论上遵守restful api规范的话就不会存在这个问题(具体的原因就不进行深究了...)。

抛开业务,我们只探讨技术方案;对此找了下相关文档,还真有实现方案,大致分以下几部:

  • Service Worker 拦截一个 POST 请求,并根据request中的查询字符串组成一个 MD5 加密的缓存key。
  • Service Worker 使用缓存key将新的 JSON 响应存储在 IndexedDB 中。
  • 如果POST请求失败,Service Worker 使用缓存key检查 IndexedDB。如果密钥存在,则返回缓存的 JSON。

具体的可以参考原文Service worker无法缓存POST请求?来我教你

结语

这是一次很好的学习过程,上述所提到的插件是Webpack的,Vite也有相关插件vite-plugin-pwa,配置大同小异。有什么问题欢迎评论区交流👏👏👏

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
pwa2024-10-09
一次性完整学完搭建PWA项目
最近在开发一个简单的个人记录网站,技术栈是使用 Vite + Vue3,由于使用的单台服务器,有时候服务器会被限制带宽,所以平时都会比较访问比较慢。所以想实现一个离线应用,而 PWA 应用则是目前最佳方案。 本文涉及知识点如下: PWA 的概念 Service Worker使用 用构建工具搭建 PWA 应用 渐进式 Web 应用(Progressive Web App,PWA)是一个使用 web 平台技术构建的应用程序,但它提供的用户体...
学习
pwa2024-10-09
通过PWA,我的博客也能离线阅读啦
前言 我经常看到有些博客站点拥有离线观看的功能,就算是没有网络,刷新页面依然可以获得数据。我对这一功能甚为好奇,了解后才发现这一技术是PWA。 何为PWA PWA又为渐进式Web应用,在MDN上是这样说的, 如何实现 构建一个PWA程序主要是用到 技术,这一项技术在现代浏览器中默认开启。 以上是一张官方的图,讲解了 的工作流程,简单来说就是 注册程序 安装程序 激活程序 获取数据 版本更新后再次获取数据 。 注册程序 安装程序 定义版本...
学习
http-网络2026-10-15
OAuth 2.0是什么,以及如何正确接入
一、先从一个需求理解 OAuth 2.0 假设你开发了一个相册整理工具,用户希望它读取某个云盘里的照片。 把云盘密码交给工具,会带来几个问题:工具拿到的权限太大,密码可能泄漏,用户也难以只撤销这个工具的访问。 OAuth 2.0 提供一套 委托授权机制 :用户在云盘自己的页面确认授权,工具取得一个有权限范围和有效期的访问令牌,再用它调用云盘 API。用户的云盘密码不交给工具。 可以把它理解为:给工具一张“限定用途的通行证”。但令牌泄漏同...
学习
前端2026-10-15
前端动画全景指南
一、前端动画到底是什么 前端动画,就是让画面随时间、用户输入或页面状态连续变化。变化的对象可以是按钮的位置、弹窗的透明度、SVG 的线条、Canvas 里的粒子,也可以是三维场景中的镜头。 先把三个问题分开: | 问题 | 常见答案 | | | | | 画什么? | DOM/CSS、SVG、Canvas 2D、GPU 渲染的场景、预制动画素材 | | 怎样变化? | CSS 过渡/关键帧、Web Animations API、逐帧计算、...
学习
flutter2026-10-14
Flutter Android 正式签名实战:生成密钥、配置构建与验证 APK
Flutter Android 正式签名实战:生成密钥、配置构建与验证 APK Android 正式签名不需要购买证书。开发者可以自己生成密钥,用它给 APK 签名;密钥的用途是建立稳定的应用身份。签名有效并不等于应用已经通过商店审核,也不等于应用功能已完成真机验收。 本文面向 Flutter Android 项目,介绍从开发签名切换到独立正式签名的完整流程。应用名称、目录、包名和证书信息均使用通用示例,不包含真实项目密码、私钥、签名指...
学习
flutter2026-10-14
Flutter 新版本发布实战:完整安装包与 Shorebird 热更新
一、先判断该发完整包还是补丁 | 这次改动 | 发布方式 | | | | | Dart 文案、Flutter 页面布局、业务逻辑修复 | 通常可以发布补丁 | | 纯 Dart 依赖变化 | 可以尝试补丁构建,仍需验证兼容性 | | Kotlin、Java、Swift、Objective C 或原生构建配置变化 | 发布完整包 | | 插件的原生实现变化、新增原生插件、权限变化 | 发布完整包 | | 新增或修改打包图片、字体、应用图标...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录