# share_target 清单成员

> share_target 成员把已安装的 PWA 注册进系统分享面板，并通过 GET 或 multipart POST 把分享来的文本、URL 与文件送到作用域内的某个 URL。

import Figure from '@components/Figure.astro';
import shareTargetDiagram from '@assets/diagrams/share-target.svg';

`share_target` 把已安装的 Web 应用注册为操作系统分享面板中的一个目标。用户选中该应用时，浏览器
把分享来的标题、文本、URL 与文件送到应用作用域内的一个 URL：要么作为 GET 导航的查询参数，要么
作为可被 Service Worker 拦截的 `multipart/form-data` POST。

Android 上的 Chrome 76、桌面端的 Edge 89（GET，文件支持有限）与 Samsung Internet 12.0 读取该
成员（BCD `html.manifest.share_target`）。iOS 与 macOS 上的 Safari 27 以及 Firefox 157 没有实现
它，应用不会出现在它们的分享面板里。该成员是分享的接收端；发送端是 `navigator.share()`。

<Figure src={shareTargetDiagram} alt="分享目标流程图：manifest 以 action、method、enctype 与 params 声明 share_target；应用安装后系统分享面板会列出它，用户选中后浏览器要么带着 title、text、url 查询参数导航到 action URL（GET），要么提交 multipart/form-data，由 service worker 的 fetch 处理器用 formData() 读取、保存并以 303 重定向回应（POST）；两条路径最终都落在展示分享内容的页面上。" caption="分享目标：从 manifest 声明，经系统分享面板，到 GET 或 POST 处理器。" />

## 成员

- **类型**：对象，含 `action`（接收分享的作用域内 URL）、`method`（`"GET"` 或 `"POST"`）、
  `enctype`（`"application/x-www-form-urlencoded"` 或 `"multipart/form-data"`）以及 `params`：
  一个把分享字段映射到参数名的对象，包括 `title`、`text`、`url`，以及 `files`，后者是
  `{ "name", "accept" }` 数组，`accept` 列出 MIME 类型或扩展名。
- **默认值**：缺省，应用不是分享目标。对象内部 `method` 默认 `"GET"`，`enctype` 默认
  `"application/x-www-form-urlencoded"`。
- **示例值**：`{ "action": "/share", "method": "GET", "params": { "title": "title", "text": "text", "url": "url" } }`。

Chromium 校验这些组合，组合不合法时丢弃整个成员，并记入清单的错误列表：
`invalid method. Allowed methods are: GET and POST.`、
`invalid enctype for GET method. Only application/x-www-form-urlencoded is supported.`、
`files are only supported with multipart/form-data POST.`，以及 `action` 跨源或在 `scope` 之外时的
`property 'share_target' ignored. Property 'action' is invalid.`。因此文件要求 `"method": "POST"` 搭配
`"enctype": "multipart/form-data"`；纯文本目标用默认值即可。

应用必须先安装才会出现在分享面板里，条目使用清单的 `short_name` 与图标。Android 把它与原生应用
并列显示；没有安装这个 PWA 的用户无法向它分享，所以分享目标页面还需要一条接收粘贴内容的普通
路由。分享载荷只有 `text` 没有 `url` 时，Android 上的 Chrome 经常把链接放在 `text` 里，处理器
要两个字段都解析。

:::observed
Chrome 155（macOS 26，英文界面）的 DevTools > Application > Manifest，清单写
`"share_target": { "action": "/share", "method": "GET", "params": { "files": [{ "name": "f", "accept": ["image/*"] }] } }` 时：**Errors and warnings** 一节显示
`files are only supported with multipart/form-data POST.`，清单被视为没有分享目标。改为
`"method": "POST", "enctype": "multipart/form-data"` 后这一行消失。
:::

## 示例

示例沿着两条投递路径展开，最后处理两条路径都够不着的用户。

### 通过 GET 导航接收链接与文本

一个稍后阅读应用接收三个文本字段。分享会在已安装应用中打开 `/share?title=…&text=…&url=…`，
页面在加载时读取查询串。由于 Android 有时把链接放在 `text` 里，处理器回退到在其中找到的第一个
URL。

```json
{
  "share_target": {
    "action": "/share",
    "method": "GET",
    "params": { "title": "title", "text": "text", "url": "url" }
  }
}
```

```js
const params = new URLSearchParams(location.search);
const text = params.get('text') ?? '';
const url = params.get('url') ?? text.match(/https?:\/\/\S+/)?.[0] ?? null;

if (url) {
  saveLink({ title: params.get('title') ?? '', url });
} else {
  showPasteForm(text); // 没有可分享的内容到达：让用户改为粘贴链接
}
```

保存后用 `history.replaceState` 清掉查询串，这样重新加载不会把同一个链接存两次。

### 通过 Service Worker 接收文件

图片需要 POST 与 multipart 编码。Service Worker 拦截发往 `action` 的 POST，读取表单数据，保存
文件，然后用 303 重定向回应，让客户端落在一个普通的 GET 页面上；没有这次重定向，重新加载会
重复提交这次分享。

```json
{
  "share_target": {
    "action": "/share/upload",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": {
      "title": "title",
      "files": [{ "name": "photos", "accept": ["image/jpeg", "image/png", "image/webp"] }]
    }
  }
}
```

```js
// service-worker.js
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  if (event.request.method !== 'POST' || url.pathname !== '/share/upload') return;

  event.respondWith((async () => {
    const form = await event.request.formData();
    const files = form.getAll('photos');
    const cache = await caches.open('shared-inbox');
    await Promise.all(files.map((file, i) => cache.put(`/shared/${Date.now()}-${i}`, new Response(file))));
    return Response.redirect('/share/review', 303);
  })());
});
```

`/share/review` 页面列出 `caches.open('shared-inbox')` 里的内容，让用户确认后再上传到服务器，
这也覆盖了设备离线时到达的分享。

### 应用未安装时的回退

分享面板只列出已安装的应用，Safari 27 与 Firefox 157 则根本不列出 Web 应用。一条接受粘贴内容、并在
`navigator.share` 存在时提供发送方向的路由，能让这项功能在任何地方都可用。

```js
const canReceive = matchMedia('(display-mode: standalone)').matches; // 已安装：系统可以向我们分享
const canSend = 'share' in navigator;

document.querySelector('#hint').textContent = canReceive
  ? '从任意应用分享并选择本应用。'
  : canSend
    ? '安装应用即可接收分享；现在仍可用按钮向外分享。'
    : '在输入框中粘贴链接或文本。';
```

`display-mode: standalone` 只是"已安装"的代理信号，不保证分享目标已经注册；但它是页面能拿到的
最好信号。

## 另请参阅

- [接收分享内容（分享目标）](/zh/guides/share-target/)
- [Web Share API：浏览器调起原生分享面板](/zh/reference/capabilities/web-share/)
- [scope 清单成员](/zh/reference/manifest/scope/)
- [file_handlers 清单成员](/zh/reference/manifest/file-handlers/)
- [Web Share Target API](https://w3c.github.io/web-share-target/)（w3c.github.io）
- [Receiving shared data with the Web Share Target API](https://developer.chrome.com/docs/capabilities/web-apis/web-share-target)（developer.chrome.com）