【XIUNOX插件】PWA 应用 v1.0.2

管理员组

XNX PWA 应用

为 Xiuno BBS X 提供完整的 PWA(渐进式 Web 应用)支持,包含应用清单、Service Worker 离线缓存、安装提示横幅、Web Push 推送通知等能力,让站点可被用户"安装"到桌面/主屏幕,并在断网、后台、关闭页面场景下仍能触达用户。

  • 插件目录:plugin/xnx_pwa/
  • 适用版本:Xiuno BBS X ≥ 1.1
  • 当前版本:1.0.0
  • 作者:twelve

功能特性

1. PWA 基础能力

  • 自动生成 manifest.json,支持应用名称、简称、主题色、背景色、启动 URL、显示模式(独立窗口/全屏)
  • 支持 display_override(Window Controls Overlay 自定义标题栏)
  • 注册 web+share / web+tg 协议处理(兼容 Chrome 协议白名单)
  • 支持 Richer Install UI:自动扫描 upload/pwa/screenshot-*.png 作为应用截图(桌面端 wide、移动端默认)

2. Service Worker 离线缓存

  • 静态资源(css/js/png/jpg/gif/svg/woff/woff2/ico)→ 缓存优先
  • HTML 页面 → 网络优先,失败回退缓存,再失败返回离线页面
  • 页面缓存 LRU 淘汰,上限 50 条,避免无限增长
  • 可配置不缓存路径(默认 admin/api/
  • 缓存版本号变更时自动清理旧版本缓存

3. 安装提示横幅

  • 顶部居中浮窗样式(最大宽度 480px,不占满宽度)
  • 用户滑动页面时自动隐藏
  • 可配置自动隐藏开关与秒数(建议 5-15 秒)
  • 显示动效基于 anime.js(回弹/滑入)
  • 可配置触发延迟次数(默认 1 次)
  • 可配置关闭后不提醒天数(0 = 永久不提醒)
  • 安装成功后写入 localStorage 标记,浏览器访问也自动隐藏横幅

4. Web Push 推送通知

  • PHP 原生实现 VAPID 密钥对生成(P-256 ECDSA,无需第三方扩展)
  • 实现Web Push 协议(JWT + AES128GCM 加密)
  • 拦截 Xiuno notify_create 钩子,支持:
- 回复提醒:有人回复我的主题时推送

- @提及提醒:帖子内容 @ 到我时推送

  • 后台支持"发送测试推送"按钮
  • 用户订阅信息存储于 bbs_pwa_subscription

环境要求

| 项目 | 要求 | |------|------| | Xiuno BBS | ≥ 1.1 | | PHP | ≥ 8.0(推荐 8.2+) | | OpenSSL 扩展 | 必需(生成 VAPID 密钥、Web Push 加密) | | HTTPS | 推送功能强制要求 HTTPS(Service Worker 也要求 HTTPS 或 localhost) | | 浏览器 | Chrome/Edge ≥ 88、Firefox ≥ 75、Safari ≥ 16.4 |

> 内网或本地开发可用 http://localhost,生产环境必须 HTTPS。


安装步骤

  1. 将插件目录放入 plugin/xnx_pwa/
  2. 登录后台 → 插件管理 → 找到「PWA 应用」→ 点击安装
  3. 安装脚本会自动:
- 写入默认配置

- 生成 VAPID 密钥对(写入 push_vapid_public_key / push_vapid_private_key) - 创建 upload/pwa/ 目录并写入 .htaccess - 创建 bbs_pwa_subscription 推送订阅表

  1. 进入「插件设置」→ 勾选启用 PWA → 保存
  2. 在「图标管理」Tab 上传 192x192 和 512x512 的 PNG 图标
  3. 保存后插件会自动在站点根目录生成 sw.js,在 upload/pwa/ 生成 manifest.json

后台配置说明

后台路径:后台 → 插件管理 → PWA 应用 → 设置

Tab 1:基本设置

| 配置项 | 说明 | |--------|------| | 启用 PWA | 总开关,关闭后所有 PWA 功能停用 | | 应用名称 | manifest 的 name 字段,安装横幅、应用图标下方显示 | | 应用简称 | manifest 的 short_name,桌面图标名称 | | 主题色 | theme-color meta 与 manifest theme_color,影响浏览器地址栏颜色 | | 背景色 | manifest background_color,启动屏背景色 | | 启动 URL | 应用启动时打开的相对路径,默认 / | | 显示模式 | standalone(独立窗口,含状态栏)/ fullscreen(全屏) |

Tab 2:图标管理

  • 点击图标预览区直接选择文件上传(无需额外点击"上传"按钮)
  • 要求 PNG 格式,192x192 与 512x512 各一张
  • 单文件不超过 1MB
  • 鼠标悬停显示"更换"遮罩,点击即可替换
  • 上传后立即生效,自动重建 manifest.json

Tab 3:缓存设置

| 配置项 | 说明 | |--------|------| | 启用 Service Worker 缓存 | 关闭后不注册 SW,无离线能力 | | 不缓存 URL 路径 | 每行一个相对路径,匹配的 URL 不走 SW 缓存(默认 admin/api/) | | 缓存版本号 | 修改后强制刷新所有缓存(旧版本缓存自动清理) |

Tab 4:安装提示

| 配置项 | 说明 | |--------|------| | 显示安装提示 | 是否在页面顶部显示安装横幅 | | 提示延迟次数 | 用户访问达到该次数后显示(0 = 每次都显示,默认 1) | | 关闭后不提醒天数 | 用户点关闭后多少天内不再显示(0 = 永久不提醒) | | 自动隐藏 | 显示后到时自动隐藏(不标记为已关闭) | | 自动隐藏秒数 | 显示多少秒后隐藏(建议 5-15 秒) |

Tab 5:推送通知

| 配置项 | 说明 | |--------|------| | 启用推送通知 | 推送总开关 | | 通知类型 | 勾选启用的类型:回复提醒 / @提及提醒 | | VAPID 公钥 | 安装时自动生成,只读,用于前端订阅 | | VAPID Subject | 推送服务联系方式,mailto:https:// URL | | 发送测试推送 | 向当前已订阅设备发送一条测试通知 |


技术实现

静态文件生成

为避免 Xiuno 路由解析导致 SW 脚本被 301 重定向(The script resource is behind a redirect)以及 SW 作用域问题,插件不通过动态 URL 输出 manifest 和 SW,而是在保存设置时生成静态文件:

站点根目录/sw.js              ← Service Worker 脚本(作用域默认 /)
upload/pwa/manifest.json      ← 应用清单
upload/pwa/.cache_key         ← 配置指纹,用于判断是否需要重建

每次 saveSettings() 会计算配置指纹(含设置项 + 图标存在状态),指纹变化才重建文件,避免每次保存都写盘。

Service Worker 缓存策略

静态资源请求 ─→ 查 STATIC_CACHE
                ├─ 命中 → 返回缓存
                └─ 未命中 → fetch → 写入 STATIC_CACHE → 返回

HTML 页面请求 ─→ fetch 网络请求
                ├─ 成功 → 返回 + 异步写入 PAGE_CACHE + LRU 淘汰
                └─ 失败 → 查 PAGE_CACHE
                          ├─ 命中 → 返回缓存
                          └─ 未命中 → 返回内嵌离线 HTML

Web Push 协议实现

  1. VAPID 密钥对:PHP openssl_pkey_new() 生成 P-256 ECDSA 密钥,通过 DER 解析提取公钥和私钥(兼容 PHP 8.5+,不再依赖 ec.pubkey 字段)
  2. JWT 签名:用 VAPID 私钥对 { aud, exp, sub } 进行 ES256 签名,作为 Authorization 头
  3. payload 加密:使用客户端 p256dh + auth 进行 AES128GCM 加密
  4. 推送发送curl POST 到客户端 endpoint,携带 Authorization: WebPush

推送触发点

通过 post_post_end.php 钩子拦截 notify_create

  • 回复主题时:向主题作者推送
  • 帖子内容含 @username 时:向被@用户推送
  • 同一用户多设备订阅时,遍历 bbs_pwa_subscription 表逐个推送

目录结构

plugin/xnx_pwa/
├── conf.json                       # 插件元信息
├── install.php                     # 安装脚本(建表、生成密钥、写默认配置)
├── uninstall.php                   # 卸载脚本
├── setting.php                     # 后台设置处理(保存、上传图标、测试推送)
├── setting.htm                     # 后台设置页面模板(5 个 Tab)
├── update.md                       # 更新日志
├── README.md                       # 本文档
├── hook/
│   ├── header_meta_before.htm      # 注入 manifest 链接、theme-color meta
│   ├── footer_js_after.htm         # 注入 SW 注册脚本、安装横幅 HTML
│   ├── model_inc_file.php          # 自动 include PwaService / WebPushService
│   ├── model_route_table_end.php   # 注册 PWA 路由
│   ├── index_route_case_end.php    # 前台路由扩展
│   ├── post_post_end.php           # 拦截 notify_create 触发推送
│   └── lang_zh_cn_bbs.php          # 中文语言包
├── model/
│   ├── PwaService.php              # PWA 核心服务(配置、manifest、SW 脚本生成)
│   └── WebPushService.php          # Web Push 服务(VAPID、加密、发送)
├── route/
│   ├── manifest.php                # 旧版动态 manifest 路由(已弃用,保留兼容)
│   ├── sw.php                      # 旧版动态 SW 路由(已弃用,保留兼容)
│   └── push.php                    # 推送订阅/取消订阅接口
└── static/
    └── js/
        └── pwa.js                  # 前台 PWA 交互(SW 注册、横幅、订阅)

常见问题

Q1:Service Worker 注册失败 The script resource is behind a redirect

原因:Xiuno 的伪静态路由把 ?pwa-sw.htm 这类动态 URL 重定向了。

解决:插件已改为生成静态 sw.js 到站点根目录,直接访问 https://your-domain/sw.js。若仍报错,检查:

  1. 站点根目录是否存在 sw.js 文件(保存设置后会自动生成)
  2. Web 服务器是否对 sw.js 做了重定向规则

Q2:The path of the provided scope ('/') is not under the max scope allowed

原因:SW 脚本放在子目录(如 /upload/pwa/sw.js),作用域默认只能覆盖该子目录。

解决:插件已将 sw.js 放在站点根目录,作用域自动覆盖 /。无需额外配置 Service-Worker-Allowed 头。

Q3:Manifest Line: 2, column: 1, Syntax error

原因:manifest.json 被服务器以非 application/manifest+jsonapplication/json 类型返回,或被 PHP 路由拦截输出 HTML。

解决:插件已改为生成静态 upload/pwa/manifest.json 文件,直接由 Web 服务器返回。如仍报错,检查服务器是否对该路径配置了 MIME 类型。

Q4:安装横幅不显示

排查清单:

  1. 后台「启用 PWA」是否勾选
  2. 后台「显示安装提示」是否勾选
  3. 是否已上传 192x192 图标(manifest 需要至少一个图标)
  4. 浏览器是否已安装过该 PWA(检查 localStoragexnx_pwa_installed 标记,清除后重试)
  5. 访问次数是否达到「提示延迟次数」配置值
  6. 上次关闭横幅是否在「关闭后不提醒天数」内

Q5:推送通知收不到

排查清单:

  1. 站点是否 HTTPS(推送强制要求)
  2. 后台「启用推送通知」是否勾选,是否勾选了对应通知类型
  3. 浏览器是否已授权通知权限(地址栏左侧图标 → 通知 → 允许)
  4. VAPID 公钥是否已生成(后台「推送通知」Tab 可查看)
  5. 用户是否已订阅(前端会自动调用 pwa-push-subscribe.htm 订阅接口)
  6. PWA 安装后会自动延迟 1 秒请求通知权限,用户必须点"允许"

Q6:Class "PwaService" not found

原因:install.php 执行时插件 hook 尚未编译生效,类未加载。

解决:install.php 顶部已显式 include_once PwaService.php 和 WebPushService.php,并用 class_exists 守卫。若仍报错,检查文件权限和 APP_PATH 定义。

Q7:图标上传失败

排查清单:

  1. upload/pwa/ 目录是否存在且可写(权限 755)
  2. 文件是否为 PNG 格式
  3. 文件大小是否超过 1MB
  4. PHP upload_max_filesizepost_max_size 是否足够

Q8:已安装 PWA 后横幅仍然显示

原因:浏览器通过 display-mode: standalone 检测,但浏览器访问时 display-modebrowser

解决:pwa.js 已监听 appinstalled 事件,安装成功后写入 localStorage.xnx_pwa_installed = 1,横幅显示前会检查该标记。若仍显示,清除浏览器缓存或手动删除该 localStorage 项后重新安装。


卸载说明

后台插件管理点击「卸载」会自动:

  1. 删除 bbs_pwa_subscription 推送订阅表
  2. 清除 xnx_pwa 配置项
  3. 删除 upload/pwa/ 目录下的 manifest.json、图标、截图等文件
  4. 删除站点根目录的 sw.js

> 卸载不会删除用户已安装的 PWA 应用(用户需手动在系统设置中移除)。


浏览器兼容性

| 功能 | Chrome | Edge | Firefox | Safari | |------|--------|------|---------|--------| | manifest.json | ✓ | ✓ | ✓ | ✓ (16.4+) | | Service Worker | ✓ | ✓ | ✓ | ✓ (16.4+) | | 离线访问 | ✓ | ✓ | ✓ | ✓ | | 安装到桌面 | ✓ | ✓ | ✗ | ✓ (iOS 16.4+) | | Web Push | ✓ | ✓ | ✓ | ✓ (16.4+) | | Window Controls Overlay | ✓ (104+) | ✓ | ✗ | ✗ | | Richer Install UI | ✓ (✓) | ✓ | ✗ | ✗ |


开发者说明

重建静态文件

当手动修改了 PwaService.php 中的 buildManifest()buildSwScript() 方法后,需要触发重建:

  • 后台任意 Tab 保存一次设置即可
  • 或删除 upload/pwa/.cache_key 文件,下次访问前台时自动重建

自定义截图

将截图文件放入 upload/pwa/,命名规则:

  • screenshot-mobile-1.png → 移动端截图(form_factor 不设)
  • screenshot-wide-1.png → 桌面端截图(form_factor=wide)

建议尺寸 1280x720,PNG 格式。

缓存版本号策略

当发布站点更新(如改了 CSS/JS)后,用户浏览器可能仍用旧缓存。此时:

  1. 后台「缓存设置」→ 修改「缓存版本号」(如 v1v2
  2. 保存后 SW 的 activate 事件会自动清理 xnx_pwa_v1_* 旧缓存
  3. 用户下次访问会重新拉取所有资源

更新日志

update.md

版本 1.0.2 | 兼容 Xiuno 1.1+



版本:v1.0.2
适用版本:1.1
开发者:贰先生

请登录后查看此内容
最新回复

请先登录后再回复 登录