官方不提供现金交易,线下交易风险自担,请仔细鉴别!
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。
安装步骤
将插件目录放入
plugin/xnx_pwa/登录后台 → 插件管理 → 找到「PWA 应用」→ 点击安装
安装脚本会自动:
- 写入默认配置
- 生成 VAPID 密钥对(写入 push_vapid_public_key / push_vapid_private_key) - 创建 upload/pwa/ 目录并写入 .htaccess - 创建 bbs_pwa_subscription 推送订阅表
进入「插件设置」→ 勾选启用 PWA → 保存
在「图标管理」Tab 上传 192x192 和 512x512 的 PNG 图标
保存后插件会自动在站点根目录生成
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
├─ 命中 → 返回缓存
└─ 未命中 → 返回内嵌离线 HTMLWeb Push 协议实现
VAPID 密钥对:PHP
openssl_pkey_new()生成 P-256 ECDSA 密钥,通过 DER 解析提取公钥和私钥(兼容 PHP 8.5+,不再依赖ec.pubkey字段)JWT 签名:用 VAPID 私钥对
{ aud, exp, sub }进行 ES256 签名,作为 Authorization 头payload 加密:使用客户端
p256dh+auth进行 AES128GCM 加密推送发送:
curlPOST 到客户端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。若仍报错,检查:
站点根目录是否存在
sw.js文件(保存设置后会自动生成)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+json 或 application/json 类型返回,或被 PHP 路由拦截输出 HTML。
解决:插件已改为生成静态 upload/pwa/manifest.json 文件,直接由 Web 服务器返回。如仍报错,检查服务器是否对该路径配置了 MIME 类型。
Q4:安装横幅不显示
排查清单:
后台「启用 PWA」是否勾选
后台「显示安装提示」是否勾选
是否已上传 192x192 图标(manifest 需要至少一个图标)
浏览器是否已安装过该 PWA(检查
localStorage的xnx_pwa_installed标记,清除后重试)访问次数是否达到「提示延迟次数」配置值
上次关闭横幅是否在「关闭后不提醒天数」内
Q5:推送通知收不到
排查清单:
站点是否 HTTPS(推送强制要求)
后台「启用推送通知」是否勾选,是否勾选了对应通知类型
浏览器是否已授权通知权限(地址栏左侧图标 → 通知 → 允许)
VAPID 公钥是否已生成(后台「推送通知」Tab 可查看)
用户是否已订阅(前端会自动调用
pwa-push-subscribe.htm订阅接口)PWA 安装后会自动延迟 1 秒请求通知权限,用户必须点"允许"
Q6:Class "PwaService" not found
原因:install.php 执行时插件 hook 尚未编译生效,类未加载。
解决:install.php 顶部已显式 include_once PwaService.php 和 WebPushService.php,并用 class_exists 守卫。若仍报错,检查文件权限和 APP_PATH 定义。
Q7:图标上传失败
排查清单:
upload/pwa/目录是否存在且可写(权限 755)文件是否为 PNG 格式
文件大小是否超过 1MB
PHP
upload_max_filesize和post_max_size是否足够
Q8:已安装 PWA 后横幅仍然显示
原因:浏览器通过 display-mode: standalone 检测,但浏览器访问时 display-mode 为 browser。
解决:pwa.js 已监听 appinstalled 事件,安装成功后写入 localStorage.xnx_pwa_installed = 1,横幅显示前会检查该标记。若仍显示,清除浏览器缓存或手动删除该 localStorage 项后重新安装。
卸载说明
后台插件管理点击「卸载」会自动:
删除
bbs_pwa_subscription推送订阅表清除
xnx_pwa配置项删除
upload/pwa/目录下的 manifest.json、图标、截图等文件删除站点根目录的
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)后,用户浏览器可能仍用旧缓存。此时:
后台「缓存设置」→ 修改「缓存版本号」(如
v1→v2)保存后 SW 的
activate事件会自动清理xnx_pwa_v1_*旧缓存用户下次访问会重新拉取所有资源
更新日志
见 update.md。
版本 1.0.2 | 兼容 Xiuno 1.1+