原文:https://dev.to/parsajiravand/those-share-buttons-are-guessing-whats-on-my-phone-3dk1(作者 @parsajiravand)
把几乎任何博客文章拉到页面底部,你都会看到同一排五个图标:一只两年前改过名的鸟、一个圆圈里的小写 f、一个表示“复制链接”的链环图标,可能还有一个信封。这一排图标是某个开发者当初为当时重要的应用做的,从那以后,它一直在悄悄地对每一位读者撒谎。
它并不知道你手机上有没有装 Twitter——抱歉,是 X。它也不知道你其实想用 WhatsApp 发给自己、存进备忘录,或者 AirDrop 给旁边的笔记本。它只是扔出五个猜测,盼着有一个能命中。
有一个三行代码的替代方案,不再靠猜,而是直接问你的手机。大多数开发者从来没有用过它,因为多年来它只能算半可用。现在这个说法已经不再成立——而且它还能做到那一排图标在结构上永远做不到的事。
那一排永远跟不上的图标
最直观的修复方案——也是每个项目最先想到的方案——就是不断往里面加图标。有人要一个 WhatsApp 按钮,那就加;接着要 Threads,再加;然后团队里有人说一半读者都在 Reddit 上,于是又多一个图标。每一个图标都要记一套自己的分享 URL 格式,或者从 Stack Overflow 的回答里复制粘贴:
<a href="https://twitter.com/intent/tweet?text=...&url=...">Share on X</a> <a href="https://www.facebook.com/sharer/sharer.php?u=...">Share on Facebook</a> <a href="https://api.whatsapp.com/send?text=...">Share on WhatsApp</a>
这能用,意思是点击以后确实会打开某个东西。但它是一笔没有上限的维护开销:一个新平台冒出来,你就加一个链接;一个平台死了或改名,你就得把所有硬编码了旧 URL 方案的地方翻出来改一遍。而且无论你加多少图标,那一排都是有限的,读者手机上的应用列表却不是。
这排图标真正撑不住的地方
- 它永远只能是一个链接。 这些
<a>标签全都是为了传递一个 URL 而存在的。没有哪个图标能“分享我刚生成的 PDF”或“把画布作为图片分享”——以链接为基础的那一排图标,根本没有这种机制。 - 它永远匹配不上读者真正装的应用。 信息、备忘录、AirDrop、Slack、某个笔记应用、另一个聊天软件,全都不会出现,因为这一排被硬编码成了开发者预先想到的那几个平台。
- 它还是一个你没答应的追踪与授权问题。 有些分享 URL 模板会在读者还没点击任何东西之前,就把当前页面的数据悄悄递给目标站点。
- 它过时的方式既难堪又公开。 文章底部一个死了的改名图标,就是一个很小但很显眼的信号,说明这个网站的这片角落已经几年没人碰过了。
不如直接问操作系统
Web Share API 彻底跳过了猜这一步。你不再渲染一排固定的图标,而是交给浏览器一个很小的对象,让操作系统弹出读者自己的分享面板——上面是他们真正安装了的东西:
async function shareThisPage() {
try {
await navigator.share({
title: document.title,
text: "Worth a read:",
url: location.href,
});
} catch (err) {
console.error(`${err.name}: ${err.message}`);
}
}
shareButton.addEventListener("click", shareThisPage);
navigator.share() 返回一个 Promise,读者在面板里选好目标后,Promise 就会 resolve——目标是信息、WhatsApp、AirDrop,或者任何他们实际装了的应用。它要工作需要两个条件:安全上下文(HTTPS 或 localhost),以及一次真实的用户手势——所以它必须从点击处理函数里调用,不能写在页面加载时,也不能等某个无关的定时器触发后再调用。
那一排图标永远做不到的事
接下来说的不只是“体验更好”,而是一个能力上的差距。navigator.share() 还接受一个 files 数组,一旦浏览器支持,你就可以把真正的 File 对象交给操作系统:在 <canvas> 上生成的图片、在客户端构建的 PDF、截图。从来没有一个 <a> 标签能做到这一点;链接只能指向某个东西,永远不能携带它。
因为各平台和浏览器支持哪些文件类型,差异很大,JavaScript 无法提前枚举出来,所以要先用 canShare() 检查——它是同步的,直接返回一个布尔值,不需要解包 Promise:
const shareData = { files: [imageFile], title: "Generated chart" };
if (navigator.canShare?.(shareData)) {
await navigator.share(shareData);
} else {
// 回退到下载链接——不要猜操作系统会接受什么。
}
别自己去硬编码一份“安全”的 MIME 类型清单。规范把具体允许哪些文件类型交给了浏览器和操作系统决定,而且这份清单一直在变——canShare() 才是唯一能给出正确答案的地方。
陷阱:取消分享不算失败
你随手搜到的每个 navigator.share() 调用,几乎都是这个样子:
navigator.share(data).catch(() => {
showToast("Sharing failed. Please try again.");
});
不信你自己试试:点击分享,然后不选任何目标,直接点面板外侧把它关掉——toast 照样会弹出来,可读者明明什么也没做错。
取消分享面板会让 promise 以一个 AbortError 被拒绝。这既不是 bug,也不是失败;它恰恰是最常见的结果——因为很多人打开面板只是为了看看里面有什么。如果把每一次 rejection 都当成错误来处理,你就是在为一件读者自己主动去做的事向他们道歉:
navigator.share(data).catch((err) => {
if (err.name === "AbortError") return; // 他们只是关掉了面板——这不是错误
showToast("Sharing failed. Please try again.");
});
放心发布,别把任何人晾在一边
在碰这些 API 之前先做特性检测,并保留一条回退路径——这正是那一排旧图标仍有存在价值的地方:
if (navigator.share) {
shareButton.addEventListener("click", shareThisPage);
} else {
// 这里没有原生分享面板——改为显示复制链接 / mailto 那一排按钮
legacyShareRow.hidden = false;
}
移动端 Safari 和 Chrome 的支持已经很扎实,此后又扩展到了桌面版 Chrome 和 Edge——在桌面端会打开操作系统自己的分享面板。Firefox 从来就没有上线过 navigator.share()——在依赖它之前先去 caniuse.com 查一下最新情况,并且保留回退分支,不要假定所有人都能用上原生面板。如果你要分享的是一个动态生成的文件,还有一点值得知道:如果在调用 share() 之前花太长时间生成文件,API 需要的“用户手势”可能会过期——所以请尽量缩短从点击到调用的间隔。
那一排按钮,退休了
下一次你打算往代码里粘贴某个社交网络的分享 URL 时,问问自己真正想做什么:是给读者“一种”分享方式,还是给他们“他们自己”的方式。那一行五个图标,始终只是操作系统本可直接给出的答案的替身——只是平台花了一些时间才把这个答案提供出来。
页脚还留着那只 Twitter 小鸟吗?去检查一下吧——我打赌它还指着那套旧的 URL scheme。
<!-- quiz:start -->
测测自己
觉得自己已经掌握了?[来做这 7 道测验题 →](https://bestpractic.org/blog/web-share-api-native-sharing/quiz)
_即时反馈,每道题都有提示,每个答案都有解析——不管做对还是做错。_
<!-- quiz:end -->
原文:https://dev.to/parsajiravand/those-share-buttons-are-guessing-whats-on-my-phone-3dk1(作者 @parsajiravand)




