背景介绍
因为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(应用程序的默认屏幕方向为水平方向)
}
其中需要注意的有以下三点:
- icons 需要多加几个以适配不同版本的ios手机
- 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
},
},
},
],
},
]);
},
其中有两点需要注意:
Workbox
内部封装了以下五种缓存策略:- NetworkFirst:网络优先
- CacheFirst:缓存优先
- NetworkOnly:仅使用正常的网络请求
- CacheOnly:仅使用缓存中的资源
- StaleWhileRevalidate:从缓存中读取资源的同时发送网络请求更新本地缓存
具体可根据自己的需求来选择具体的配置
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项目的一个踩坑总结,如果上面有失误的地方还望不吝赐教😁