手机分享按钮别等上线才测:Web Share API 做邀请卡原型,用 cpolar 给同事真机验收
手机端的“分享”看起来只是一个按钮,真正容易出问题的部分却在按钮之外:浏览器是否实现 Web Share API,页面是不是 HTTPS,调用是否紧跟用户点击,用户取消后页面怎样提示,以及桌面浏览器该走哪条回退路径。只在电脑上看截图,验证不了系统分享面板,更验证不了不同手机的取消行为。
本文做一个虚构的“社区开放日”邀请卡。页面只有静态文案和原生 HTML、CSS、JavaScript,不读取通讯录,不收集联系人,不上传文件,也不连接账号或接口。先在本机跑通,再通过 cpolar 生成短时 HTTPS 地址,让同事用各自手机完成一次有边界的验收。
Web Share API 分享的其实是一组文本数据
navigator.share()接收的核心字段是title、text和url。浏览器把这些字段交给系统分享面板,用户自己选择目标应用和接收对象。网页不会因此获得联系人列表,也不会知道用户最终选了谁。
这条边界要在产品说明里写清楚。我们的示例只传虚构活动标题、简短说明和当前页面的 HTTPS 地址,不调用文件分享能力。验收者点“分享邀请”后,系统面板出现;完成分享、主动取消或浏览器拒绝调用,页面都给出可读状态。
Web Share API 通常要求安全上下文。localhost便于本机开发,但同事从手机访问开发机地址时并不等同于本机访问。cpolar 提供的 HTTPS 入口正好用于短时真机检查,同时本地静态服务仍只监听127.0.0.1。
先做一张不依赖外部资源的邀请卡
新建index.html,把样式和脚本都放进同一个文件。这样验收包只有一个静态页面,不会带出额外配置或内部资源。
<!doctype html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>微风社区开放日邀请</title> <style> :root { color-scheme: light; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; background: #eef5ef; color: #18332a; } * { box-sizing: border-box; } body { min-height: 100vh; margin: 0; display: grid; place-items: center; padding: 24px; } .card { width: min(100%, 560px); padding: 32px; border: 1px solid #c9ddd0; border-radius: 24px; background: #fff; box-shadow: 0 18px 50px rgba(24, 51, 42, .12); } .eyebrow { color: #35705a; font-weight: 700; letter-spacing: .12em; } h1 { margin: 10px 0 12px; font-size: clamp(30px, 8vw, 48px); } .meta { padding: 16px; border-radius: 14px; background: #f2f8f4; line-height: 1.8; } .actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 24px; } button { min-height: 46px; padding: 0 18px; border: 1px solid #246b51; border-radius: 999px; font: inherit; font-weight: 700; cursor: pointer; } #shareButton { background: #246b51; color: #fff; } #copyButton { background: #fff; color: #205b46; } button:focus-visible { outline: 3px solid #efb94c; outline-offset: 3px; } #status { min-height: 1.6em; margin: 16px 0 0; color: #315f50; } </style> </head> <body> <main class="card"> <p class="eyebrow">仅供原型验收</p> <h1>微风社区开放日</h1> <p>一起看手作摊位、旧物交换角和小型音乐会。</p> <div class="meta"> <strong>日期:</strong>9 月 12 日 14:00—17:00<br> <strong>地点:</strong>示例社区共享花园<br> <strong>说明:</strong>本活动与地点均为虚构内容 </div> <div class="actions"> <button id="shareButton" type="button">分享邀请</button> <button id="copyButton" type="button">复制邀请链接</button> </div> <p id="status" role="status" aria-live="polite"></p> </main> <script> const shareButton = document.querySelector('#shareButton'); const copyButton = document.querySelector('#copyButton'); const status = document.querySelector('#status'); const setStatus = (message) => { status.textContent = message; }; const shareData = () => ({ title: '微风社区开放日邀请', text: '来看看这个虚构的社区开放日原型。', url: window.location.href }); async function copyCurrentUrl() { const url = window.location.href; try { await navigator.clipboard.writeText(url); setStatus('邀请链接已复制,可以手动粘贴发送。'); } catch (error) { const input = document.createElement('textarea'); input.value = url; input.setAttribute('readonly', ''); input.style.position = 'fixed'; input.style.opacity = '0'; document.body.appendChild(input); input.select(); const copied = document.execCommand('copy'); input.remove(); setStatus(copied ? '邀请链接已复制,可以手动粘贴发送。' : `请手动复制:${url}`); } } shareButton.addEventListener('click', async () => { if (typeof navigator.share !== 'function') { setStatus('当前浏览器不支持系统分享,请使用复制链接。'); copyButton.focus(); return; } try { await navigator.share(shareData()); setStatus('系统分享操作已完成。'); } catch (error) { if (error.name === 'AbortError') { setStatus('你已取消分享,页面内容没有变化。'); return; } setStatus('系统分享未完成,请改用复制链接。'); } }); copyButton.addEventListener('click', copyCurrentUrl); if (typeof navigator.share !== 'function') { shareButton.hidden = true; setStatus('当前浏览器不支持系统分享,请复制邀请链接。'); } else { setStatus('此浏览器支持系统分享,可以开始验收。'); } </script> </body> </html>这里没有把分享数据写成页面加载时的常量,而是在点击时读取window.location.href。本机访问时分享本机链接,经 cpolar 访问时分享临时 HTTPS 链接,验收者不会误拿旧地址。
用户手势不能被定时器和页面初始化替代
系统分享属于受保护能力,调用应直接发生在点击事件里。不要在页面打开后自动弹出,也不要先启动一个延时任务再调用。示例在click监听器中立即执行navigator.share(),中间没有网络请求,用户手势链条清楚。
能力检测也不能只看设备名称。手机浏览器不等于一定支持,桌面浏览器也不等于一定不支持。用typeof navigator.share === 'function'判断更可靠。示例在不支持时隐藏主分享按钮,保留“复制邀请链接”,并用状态区域解释当前路径。
role="status"与aria-live="polite"能让读屏软件在合适时机朗读结果。状态文案不只靠颜色区分,键盘焦点也有清晰轮廓。回退发生时把焦点移到复制按钮,键盘用户无需重新寻找操作入口。
取消分享不是程序故障
用户打开系统面板后点取消,Promise 通常以名为AbortError的异常结束。这是正常选择,不能显示成红色报错,更不该反复弹出面板。示例单独识别取消,并提示“页面内容没有变化”。
其他异常统一引导到复制链接,但页面不展示原始异常详情。这样既避免把实现信息暴露给验收者,也能让流程继续。复制功能优先使用 Clipboard API;若不可用,再借助临时文本框尝试复制;仍失败时直接把当前地址显示出来,方便手动选择。
要注意,系统面板返回成功只表示浏览器完成了分享调用,不代表网页能确认接收方已阅读。验收记录应写“面板已拉起并完成操作”,不要写成“消息已送达”。网页也不应追踪分享对象。
分享数据要以当前页面为准
分享参数很短,仍值得逐项检查。title用活动名称,不要照搬网页里过长的标题;text用一句完整说明,避免把日期、地点和操作要求全塞进去;url则取当前地址。不同接收应用对三项内容的组合方式不同,有的会保留标题,有的主要展示说明和链接,因此关键信息不能只存在于某一个字段中。
本例没有额外拼接查询参数。若原型以后加入页面状态,也不要把内部标识、调试开关或会话信息带入分享地址。验收前可以在浏览器地址栏直接核对链接,确保它只指向这张邀请卡。发送给同事的验收说明也应独立于页面链接,例如写清“请在今晚六点前测试,结束后不要继续转发”,而不是把管理要求编码进网址。
页面刷新后状态提示会恢复为能力检测结果,这是有意设计。它不使用本地存储,不保留分享次数,也不建立访问者画像。若需要统计测试结果,采用人工验收表即可,记录设备型号、系统版本、浏览器版本和观察结果,不写接收人身份。
本机跑通后再开放短时 HTTPS 地址
在index.html所在目录启动静态服务:
python3 -m http.server 8080 --bind 127.0.0.1先访问http://127.0.0.1:8080,检查卡片排版、复制按钮、键盘焦点和状态提示。确认页面没有外链请求后,再开另一个终端执行:
cpolar http 8080从 cpolar 输出中取 HTTPS 地址,用它打开页面。不要把地址改写成固定域名,也不要长期复用。把这条临时链接只发给本次验收同事,并附上截止时间。静态服务仍只绑定回环地址,对外入口由隧道在本次会话内转发。
桌面回退也要当成主流程测试
团队常犯的错误是把复制链接当作一句“浏览器不支持”的善后文案,直到桌面验收时才发现按钮不可聚焦、剪贴板被拒绝后没有提示。这个示例让复制按钮始终保留,即使系统分享可用,用户仍可主动选择复制。这样更符合桌面沟通习惯,也方便验收者直接粘贴到测试群。
测试复制分支时,可以先正常复制,再在浏览器站点设置中拒绝剪贴板权限并重试。页面应进入备用复制逻辑;若浏览器连备用操作也拒绝,则显示完整当前地址供手动选择。整个过程中不应清空邀请卡、不应刷新页面,也不应让焦点凭空丢失。
开发者工具里的设备模拟只适合检查窄屏排版,不能替代系统面板测试。模拟视口仍运行在桌面浏览器内核和桌面系统环境中,分享目标、权限处理与手机不同。它是排版预检,不是真机验收结论。
真机验收不要只检查“能弹出来”
建议至少准备一台 iOS 手机、一台 Android 手机和一个桌面浏览器,按同一张表记录:
- 通过 HTTPS 地址打开,页面没有安全上下文警告。
- 点击“分享邀请”,系统面板由这次点击触发,没有自动弹出。
- 面板中的标题、说明和链接均为虚构内容,链接与地址栏当前地址一致。
- 选择取消,页面读出取消状态,按钮仍可再次使用。
- 完成一次分享,页面显示完成状态,但不宣称接收方已阅读。
- 在不支持系统分享的浏览器中,复制入口可见,复制成功与失败都有文字反馈。
- 使用键盘或读屏操作时,焦点顺序、按钮名称和状态播报清楚。
测试期间不要加入真实客户资料、订单、支付、库存、手机号、Token、私有链接或任何凭据。这个页面也不连接数据库、容器控制接口、本机目录及任何服务端业务能力。原型只回答一个问题:分享与回退体验在真实手机上是否可用。
临时开放后的收尾清单
- 结束 cpolar 进程,使临时 HTTPS 入口立即失效。
- 停止本地静态服务,确认 8080 端口不再监听。
- 在验收群撤回临时链接,并明确告知入口已经关闭。
- 清理聊天中的链接预览、终端输出截图和临时验收记录。
- 保留的结论只写设备、浏览器、分享结果与回退结果,不记录联系人或接收对象。
- 再次检查源码,确保只有虚构活动与脱敏文案,没有误带内部地址、凭据或私有资源。
分享按钮越接近系统能力,越不该等部署后再碰运气。把页面限制为单一静态原型,把服务锁在本机回环地址,再用短时 HTTPS 入口做跨手机验收,既能提前发现兼容与交互问题,也能在验收结束后干净收口。