# 接收分享的内容（share target）

> 把已安装的 PWA 注册为分享目标：在 manifest 中声明 share_target，在 Service Worker 中处理 POST，并在应用里读取分享的文件。

import CompatTable from '@components/CompatTable.astro';

完成本指南后，你的已安装 PWA 会出现在操作系统的分享面板中，从别的应用分享过来的一张照片会以 `File`
的形式落到你的页面里，供预览与导入。Web App Manifest 中的 `share_target` 声明接收 URL；浏览器随后像
表单提交一样向它发起 `GET` 或 `POST`，剩下的就是你熟悉的请求处理。

你需要一个带 Service Worker 的可安装 PWA（见[让它可安装](/zh/guides/installable/)）；在所有平台上，
分享面板都只在用户安装之后才列出 Web 应用。每个 manifest 只允许一个 `share_target`，所以不同类型的分享
在落地页上分流，而不是声明多个。

## 1. 在 manifest 中声明 share_target

`action`（接收 URL，须在 manifest 的 `scope` 之内）与 `params`（分享字段到请求参数名的映射）是必需的。
`method` 默认为 `GET`，`enctype` 默认为 `application/x-www-form-urlencoded`。接收文件要求 `POST` 配合
`multipart/form-data`，并提供一个 `files` 数组，每一项写明字段名与接受的 MIME 类型或扩展名；两种写法都列上，
因为各操作系统匹配的方式不同。

```json
{
  "name": "Scrapbook",
  "start_url": "/",
  "display": "standalone",
  "share_target": {
    "action": "/share",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": {
      "title": "title",
      "text": "text",
      "url": "url",
      "files": [{ "name": "media", "accept": ["image/*", ".png", ".jpg", "video/*"] }]
    }
  }
}
```

纯文本目标可以保留 `GET` 默认值：浏览器以 `?title=…&text=…&url=…` 打开 `action`，页面读取
`new URL(location).searchParams`。`GET` 更容易调试，但会把分享的文本泄露到历史记录与服务器日志里；
`POST` 把它放在请求体中，而且是接收文件的唯一方式。

## 2. 在 Service Worker 中处理 POST

页面读不到 `POST` 请求体，所以由 Service Worker 在 `fetch` 中拦截请求、读取 `formData()`、保存文件，
然后以 `303 See Other` 重定向到展示它们的页面。重定向很重要：它阻止刷新页面时再次提交这次分享。
重定向目标带一个标记（`?shared=1`），页面据此知道要去缓存里找。

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

  event.respondWith(
    (async () => {
      const formData = await event.request.formData();
      const files = formData.getAll('media');
      const cache = await caches.open('shared-content');
      await Promise.all(files.map((file, i) => cache.put(`/shared/${i}`, new Response(file))));
      const text = formData.get('text') || formData.get('url') || '';
      return Response.redirect(`/share/view?shared=1&text=${encodeURIComponent(text)}`, 303);
    })(),
  );
});
```

像对待任何表单提交一样校验收到的内容：别的应用会把内容放进意料之外的参数，而请求体来自你无法控制的软件。

:::observed
在 Android 上把一个 URL 分享进 Web 应用时，`url` 参数到达时为空，因为 Android 的分享系统没有 URL 字段；
链接会出现在 `text` 里，偶尔在 `title` 里。上面的处理函数正因如此先读 `text` 再读 `url`。
Chrome 的 share target 文档记录了同样的行为。
:::

## 3. 在页面中读取分享的文件

被重定向到的页面检查标记，把保存的响应读回为 `Blob`，展示出来供确认，然后删除它们，
以免第二次分享捡到第一次的文件。不带标记直接打开 `/share/view` 时渲染正常视图。

```js
function isSharedLaunch() {
  return new URLSearchParams(location.search).has('shared');
}

async function takeSharedFiles() {
  if (!('caches' in window)) return [];
  const cache = await caches.open('shared-content');
  const requests = await cache.keys();
  const files = await Promise.all(requests.map((request) => cache.match(request).then((r) => r.blob())));
  await Promise.all(requests.map((request) => cache.delete(request)));
  return files;
}

if (isSharedLaunch()) {
  takeSharedFiles().then(renderSharedFiles);
} else {
  renderDefaultView();
}
```

## 4. 安装，然后分享到应用里

从浏览器的 **Install**（英文界面）入口安装 PWA，打开任意相册应用，选择 **Share**，在系统面板中选中你的
应用名。重定向落到 `/share/view`，预览图由缓存的文件渲染出来。再从浏览器分享一个 URL，就能看到 Android 上
`text` 回退的实际效果。会在分享面板中提供已安装 Web 应用的浏览器列在下表：

<CompatTable feature="manifest-share-target" />

## 另请参阅

- [Manifest share_target 字段](/zh/reference/manifest/share-target/)
- [Web Share API：从浏览器调起原生分享面板](/zh/reference/capabilities/web-share/)
- [manifest: share_target 支持情况](/zh/compatibility/manifest-share-target/)
- [Receiving shared data with the Web Share Target API](https://developer.chrome.com/docs/capabilities/web-apis/web-share-target)（developer.chrome.com）
- [share_target](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/share_target)（developer.mozilla.org）

← 返回[指南](/zh/guides/)总览。