ios pwa的踩坑记录

背景介绍

因为ios审查的原因,导致ios app短时间内无法上线,但是领导们并不想放弃这群ios用户,所以就提出了pwa,用来短时间内达到替换ios app的目的,以下是我在做pwa时踩到的一些坑,如果对你有些帮助,不胜荣幸!

因为是踩坑记录,所以本文并不会对pwa的相关基础知识做过多介绍😂

配置相关

manifest.json

通用配置如下(使用时,只需要将其添加到public目录下即可)

json 复制代码
{
  "name": "xxx",  // 项目名称,一般是全名
  "short_name": "xxx", // 项目简称,一般是缩写
  "icons": [ // 项目保存在桌面上时的icon,通常至少需要三种类型的png 72x72 96x96 144x144 因为ios的分辨率更高,一般会推荐加上 192x192
    {
      "src": "./logo144.png",
      "sizes": "144x144",
      "type": "image/png"
    }
  ],
  "start_url": "/", // 首页路径,可以根据自己需要进行调整
  "display": "standalone", //  访问网站窗口展示模式,如:fullscreen/standalone 详情见下图
  "theme_color": "#fff", // 主色调
  "background_color": "#3d7eff", // 背景色
  "lang": "id", // 国际化
  "orientation": "portrait-primary", // 手机的展示方向,any(不限制屏幕方向)/ natural(应用程序的默认屏幕方向与设备的自然方向相同)/ portrait-primary(应用程序的默认屏幕方向为垂直方向) / landscape(应用程序的默认屏幕方向为水平方向)
}

其中需要注意的有以下三点:

  1. icons 需要多加几个以适配不同版本的ios手机
  2. start_url 代表了在pwa模式下的首页,可以根据自己的需要进行调整,如下twitter的配置

虽然 iOS 11.3/ Safari 11.1 宣布支持了 Web App Manifest ,但是根据实际的测试,目前只有部分属性得到了一定程度的支持🤣 需要在html中添加下列代码来完成兼容,否则在低版本中只会出现截图,如下图keep

html 复制代码
<!-- 是否隐藏 Safari 地址栏等-->
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- 桌面上的title-->
<meta name="apple-mobile-web-app-title" content="xxx">
<!-- 桌面上的图标 推荐 192x192 -->
<link rel="apple-touch-icon" href="./logo192.png">
<link rel="apple-touch-icon" href="./logo192.png" sizes="192x192">
<link rel="apple-touch-icon" href="./logo152.png" sizes="152x152">
<!-- 
告诉苹果,pwa的主色调,注:导航栏的颜色也是由这个字段控制
并且这个配置的优先级高于manifest.json中的theme_color配置
-->
<meta name="theme-color" content="#fff">

nginx配置

因为pwa本地做了缓存,那么nginx就不需要添加缓存了,防止多级缓存导致本地更新不及时 我的配置如下,其他可以根据自己的需求自行添加

xml 复制代码
location / {
    # 设置Cache-Control头,控制缓存行为
    add_header Cache-Control "private, no-cache, no-store, must-revalidate, proxy-revalidate";
    # 设置Pragma头,确保不缓存
    add_header Pragma "no-cache";
    # 禁用响应的过期时间
    expires off;
    # 如果请求的文件匹配指定的文件扩展名
    if ($request_filename ~* .*\.(?:gif|jpg|jpeg|png|bmp|swf|js|css)$) {
        # 设置Cache-Control头,允许对静态资源进行缓存
        add_header Cache-Control public;
    }
    # 设置文件的根目录
    root /opt/nginx/dist/;
    # 指定默认的索引文件
    index index.html index.htm;
    # 尝试查找请求的文件,如果不存在则返回index.html
    try_files $uri $uri/ /index.html;
}

运行时配置

配置webpack,为了快速启动,采用了workbox插件workbox-webpack-plugin,具体配置如下

js 复制代码
chainWebpack(memo) {
      const isDev = process.env.NODE_ENV === 'development';
        // workbox 配置
        memo.plugin('workbox').use(GenerateSW, [
          {
            cacheId: 'webpack-pwa', // 设置前缀
            skipWaiting: true, // 强制等待中的 Service Worker 被激活
            clientsClaim: true, // Service Worker 被激活后使其立即获得页面控制权
            cleanupOutdatedCaches: true, //删除过时、老版本的缓存
            swDest: 'service-wroker.js', // 输出 Service worker 文件
            include: ['**/*.{html,js,css,png.jpg}'], // 匹配的文件
            exclude: ['service-wroker.js', 'version.js', 'index.html'], // 忽略的文件,
            runtimeCaching: [
              {
                urlPattern: new RegExp(isDev ? proxyUrl.dev : proxyUrl.test),
                handler: 'NetworkOnly',
                options: {
                  cacheName: 'api-cache'
                },
              },
              {
                urlPattern: /.*\.js.*/i,
                handler: isDev ? 'NetworkFirst': 'StaleWhileRevalidate',
                options: {
                  cacheName: 'seed-js',
                  expiration: {
                    maxEntries: 20, //最多缓存20个,超过的按照LRU原则删除
                    maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
                  },
                },
              },
              {
                urlPattern: /.*css.*/,
                handler: isDev ? 'NetworkFirst': 'StaleWhileRevalidate',
                options: {
                  cacheName: 'seed-css',
                  expiration: {
                    maxEntries: 30, //最多缓存30个,超过的按照LRU原则删除
                    maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
                  },
                },
              },
              {
                urlPattern: /.*(png|svga).*/,
                handler: isDev ? 'NetworkFirst': 'StaleWhileRevalidate',
                options: {
                  cacheName: 'seed-image',
                  expiration: {
                    maxEntries: 30, //最多缓存30个,超过的按照LRU原则删除
                    maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
                  },
                },
              },
              {
                urlPattern: /.*html.*/,
                handler: isDev ? 'NetworkFirst': 'StaleWhileRevalidate',
                options: {
                  cacheName: 'seed-html',
                  expiration: {
                    maxEntries: 30, //最多缓存30个,超过的按照LRU原则删除
                    maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
                  },
                },
              },
              ],
          },
        ]);
    },

其中有两点需要注意:

  1. Workbox内部封装了以下五种缓存策略:
    • NetworkFirst:网络优先
    • CacheFirst:缓存优先
    • NetworkOnly:仅使用正常的网络请求
    • CacheOnly:仅使用缓存中的资源
    • StaleWhileRevalidate:从缓存中读取资源的同时发送网络请求更新本地缓存

具体可根据自己的需求来选择具体的配置

  1. version.js version.js的作用主要是为了记录service-wroker.js的版本,尽管nginx上已经做了配置,但是浏览器也存在缓存,为了排除这些缓存的影响,特意新建一个version.js来记录版本,当版本不一致时则更新缓存,而version.js本身因为只是处理version相关,所以只有1kb,并不会造成太多的传输损耗,其他详情见下面的缓存问题的讲述

缓存问题

service-work 的缓存

因为上面已经配置了service-work的缓存,与此同时,缓存相关的问题也因此产生了,每次刷新时的请求都会被service-work拦截,这样会导致代码永远无法更新最新版本

所以此时需要新建一个文件,用来保存每次service-work的hash值,本地在每次切换页面时去发送请求并比对,如果hash不一致,则说明本地的是老代码,需要更新,主要思路和代码如下

js 复制代码
let flag = false;
const getVersionJs = () => {
    if(flag) return;
    flag = true;
    const xhr = new XMLHttpRequest();
    const url = '/version.js?v=' + Date.now();
    xhr.open('GET', url, true);
    // 添加超时逻辑,防止阻塞页面渲染
    const timeoutDuration = 3000;
    const timeoutId = setTimeout(() => {
        flag = false;
        xhr.abort(); // 中止请求
        // 处理超时逻辑
    }, timeoutDuration);
    xhr.onreadystatechange = () => {
        if (xhr.readyState === 4) {
            clearTimeout(timeoutId); // 请求完成,清除超时定时器
            flag = false;
            if (xhr.status === 200) {
                const responseText = xhr.responseText;
                // @ts-ignore
                const projectVersion = window.projectVersion
                // 比较当前页面缓存中的版本信息与最新的版本信息
                //  如果当前的 version 和 最新 的不一致,则直接刷新当前页
                if (responseText.indexOf(projectVersion) < 0) {
                    window.location.reload();
                    sessionStorage.setItem('@refreshByVersion', String(Date.now()));
                }
                // 处理响应数据
            } else {
                // 处理请求失败逻辑
                console.error('请求最新JS资源版本信息失败');
            }
        }
    };
    xhr.send();
};
export const refreshByVersion = () => {
    /** 开发环境禁止刷新,防止每次修改都会产生连刷两次的情况 */
    if (process.env.NODE_ENV === 'development') return;
    const preRefreshTime = sessionStorage.getItem('@refreshByVersion');
    const currentTime = Date.now();
    // 间隔 60s 内的不会再次刷新,防止一直重刷而导致网页卡顿
    if (!preRefreshTime || Number(preRefreshTime) < currentTime - 60 * 1000) {
        // 添加异步,防止阻塞渲染
        setTimeout(() => {
            getVersionJs();
        }, 500);
    }
};

每次在跳转页面的时候就请求一次,这样尽可能的保证,当服务器上的service-work文件更新,本地能够第一时间知晓并更新

本地代码缓存

因为现在的项目都是pwa项目的原因,当服务器上面的资源更新,而客户端在更新之前就在当前页面了,此时如果用户直接操作,则会请求资源报错,所以需要拦截这种报错

js 复制代码
/** 资源加载失败,说明服务器上面没有这个资源了,最大的可能是服务器上面更新了资源,需要刷新本地 */
export function sourceErrorHandle() {
    window.addEventListener('error', handleListenerError, true);
    /** Safari浏览器在处理error事件方面与其他浏览器有所不同。在Safari中,error事件不会在<script>标签加载错误时触发 */
    const scriptElements = document.getElementsByTagName('script');
    for (let i = 0; i < scriptElements.length; i++) {
      scriptElements[i].addEventListener('error', ()=>{
        const pageErrorReloadNum = getPageErrorReloadNum();
        beforeReload();
        if(!pageErrorReloadNum || pageErrorReloadNum < 2){
            reloadHandle(pageErrorReloadNum);
        }
      });
    }
}

以上就是前一段时间做ios的pwa项目的一个踩坑总结,如果上面有失误的地方还望不吝赐教😁

相关推荐
hackeroink13 分钟前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者2 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-2 小时前
验证码机制
前端·后端
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235244 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240255 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar5 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人6 小时前
前端知识补充—CSS
前端·css