本规范定义了一个 API,允许网站将自身声明为 Web 共享目标,从而可以接收来自 [[[Web-Share]]] 或系统事件(例如来自 原生应用的共享)的共享内容。

这是一种类似于 {{NavigatorContentUtils/registerProtocolHandler()}} 的机制,因为它通过向用户代理注册网站来工作, 以便稍后由另一个网站或原生应用通过用户代理(可能由用户酌情决定)调用。 不同之处在于,{{NavigatorContentUtils/registerProtocolHandler()}} 通过编程 API 注册处理器, 而 Web 共享目标是在 [[[appmanifest]]] 中声明的,并在用户代理或用户选择的时间注册。

这是 Web Share Target 规范的早期草案。

前提条件

为了实现此 API,用户代理必须支持 [[[appmanifest]]]。本规范还复用了 [[[Web-Share]]] 规范中的一些定义。 但是,对 [[[Web-Share]]] 的支持是可选的。

使用示例

要将站点注册为共享目标,需要向 [[[appmanifest]]] 添加一个 [=manifest/share_target=] 条目, 如下所示:

      {
        "name": "Includinator",
        "share_target": {
          "action": "share.html",
          "params": {
            "title": "name",
            "text": "description",
            "url": "link"
          }
        }
      }
      

[=ShareTarget/params=] 键对应于 [[[Web-Share]]] 中 {{ShareData}} 的键名, 而值是共享目标启动时将用作查询参数的任意名称。

发生共享时,如果用户选择了此共享目标,用户代理会在 `action` URL 处打开一个新的浏览上下文, 查询参数值包含共享的数据,就像 HTML 表单提交一样。

在本示例中,我们假设清单位于 `https://example.org/includinator/manifest.webmanifest`。

      <html>
      <link rel="manifest" href="manifest.webmanifest">
      <script>
        window.addEventListener('load', () => {
          const parsedUrl = new URL(window.location);
          const { searchParams } = parsedUrl;
          console.log("Title shared:", searchParams.get('name'));
          console.log("Text shared:", searchParams.get('description'));
          console.log("URL shared:", searchParams.get('link'));
        });
      </script>
      

如果传入的共享包含标题 "My News" 和 URL `http://example.com/news`,用户代理将打开一个新窗口或标签页并导航到:

https://example.org/includinator/share.html?name=My+News&link=http%3A%2F%2Fexample.com%2Fnews

由于使用了 [=`application\/x-www-form-urlencoded`=] 编码,U+0020 (SPACE) 字符会被编码为 "`+`", 而不是可能预期的 "`%20`"。处理器必须注意将 U+002B (+) 字符解码为 U+0020 (SPACE), 一些 URL 解码库,包括 ECMAScript 的 `decodeURIComponent` 函数,可能不会自动这样做。

查询参数会用正在共享的 {{ShareData}} 中的信息填充。如果 {{ShareData}} 不包含某个给定成员的信息, 则省略该查询参数。

共享目标可能只关注 {{ShareData}} 成员的一个子集。本示例还展示了一个以 `POST` 请求接收数据的共享目标; 如果请求会造成即时副作用,就应当采用这种方式。

      {
        "name": "Bookmark",
        "share_target": {
          "action": "/bookmark",
          "method": "POST",
          "enctype": "multipart/form-data",
          "params": {
            "url": "link"
          }
        }
      }
      

共享的信息可以由 [=service worker=] 读取,而不是通过网络发送到服务器。

        self.addEventListener("fetch", (event) => {
          if (event.request.method !== "POST") {
            event.respondWith(fetch(event.request));
            return;
          }

          const formDataPromise = event.request.formData();
          event.respondWith(
            formDataPromise.then((formData) => {
              const link = formData.get("link") || "";
              saveBookmark(link);
              return new Response(`Bookmark saved: ${link}`);
            })
          );
        });
      

处理器如何处理共享数据由处理器自行决定,并且通常取决于应用的类型。下面是一些建议:

对 Web App Manifest 的扩展

由于 [=manifest=] 是 JSON,本规范依赖于 [[JSON]] 规范中定义的类型:即 objectstring

将以下步骤添加到 [=processing extension-point of web manifest=]:

  1. 令 |json| 和 |manifest| 为来自 [=processing a manifest=] 的对应变量。
  2. 使用 |json| 和 |manifest| [=Process the `share_target` member=]。

`share_target` 成员

清单的 share_target 成员是一个 [=object=]。当存在时,它声明此应用为一个 Web 共享目标, 并描述应用如何接收共享数据。

Web 共享目标 是一个具有有效 [=manifest=] 且包含 [=manifest/share_target=] 成员的网站。

Web 共享目标是一种 share target(还可以有其他类型,例如某些系统应用)。

给定 [=object=] |json:JSON| 和 [=ordered map=] |manifest:ordered map|, 要处理 `share_target` 成员

  1. 如果 |json|["share_target"] 不是 [=object=],则返回。
  2. 令 |target:object| 为 |json|["share_target"]。
  3. 如果 |target|["action"] 或 |target|["params"] 缺失,则返回。
  4. 处理 [=ShareTarget/action=]:
    1. 令 |action:URL| 为以 |manifest URL| 为基准、且不使用编码覆盖, [=URL parser|parsing=] |share target|["action"] 的结果。 如果结果为失败,则返回。
    2. 如果 |action| 不在 |manifest|["scope"] 的 [=URL/within scope=] 内,则返回。
    3. 如果 |action| 的 [=url/origin=] 不是 [=potentially trustworthy origin=],则返回。
  5. 令 |method:string| 为 "GET"。
  6. 如果 |target|["method"] 存在,则处理 [=ShareTarget/method=]:
    1. 如果 |target|["method"] 既不是与字符串 `"GET"` 的 [=ASCII case-insensitive=] 匹配,也不是与 `"POST"` 的匹配, 则返回。
    2. 将 |method| 设置为 [=ASCII uppercase=] |target|["method"]。
  7. 令 |enctype:string| 为 "application/x-www-form-urlencoded"。
  8. 如果 |method| 为 `"POST"`:
    1. 如果 |target|["enctype"] 既不是与字符串 `"application/x-www-form-urlencoded"` 的 [=ASCII case-insensitive=] 匹配,也不是与 `"multipart/form-data"` 的匹配,则返回。
    2. 将 |enctype| 设置为 [=ASCII lowercase=] |target|["enctype"]。
  9. 令 |params:ordered map| 为一个新的 [=ordered map=]。
  10. 处理 [=ShareTarget/params=]:
    1. [=List/For each=] |member:string| of « "title", "text", "url" »:
      1. 如果 |target|["param"] 不具有属性 |member|, 则继续。
      2. 如果 |target|["param"][member] 不是 [=string=],则返回。
      3. 将 |params|[member] 设置为 |target|["param"][member]。
  11. 将 |manifest|["share_target"] 设置为 [=ordered map=] «[
    "action" → [=URL serializer|serialize=] |action|,
    "enctype" → |enctype|,
    "method" → |method|,
    "params" → |params|,
    ]»。

`ShareTarget` 及其成员

ShareTarget [=object=] 可以具有以下成员:

action 成员
一个 [=string=],用于指定 [=web share target=] 的 [=URL=]。
method 成员
一个 [=string=],用于指定 [=web share target=] 的 HTTP [=request=] [=request/method=]。
enctype 成员
一个 [=string=],用于指定在 `POST` 请求的正文中如何编码共享数据。 当 [=method=] 为 `"GET"` 时,它会被忽略。
params 成员
一个 ShareTargetParams [=object=]。

`ShareTargetParams` 及其成员

ShareTargetParams [=object=] 可以具有以下成员:

title 成员
一个 [=string=],用于指定共享文档标题所使用的查询参数名称。
text 成员
一个 [=string=],用于指定构成被共享消息正文的任意文本所使用的查询参数名称。
url 成员
一个 [=string=],用于指定引用被共享资源的 URL 字符串所使用的查询参数名称。

Web 共享目标的注册

Web 共享目标如何以及何时被“注册”,由用户代理和/或最终用户自行决定。事实上, “注册”是一个用户代理特有的概念,此处并未正式定义;用户代理完全不要求“注册”Web 共享目标; 它们只被要求提供某种机制,将共享数据传递给最终用户所选择的 Web 共享目标。 即使 Web 共享目标不是 [=installed web application|installed=],用户代理也可以认为它已“注册”。

用户代理可以在用户访问站点时自动注册所有 Web 共享目标,但建议采用更多酌情判断, 以避免让大量目标选项压倒用户。

用户代理可以采用的注册策略示例包括:

在向最终用户呈现 Web 共享目标 列表时,用户代理可以使用已预先索引清单的在线服务, 从而向用户显示他们从未访问或明确注册过的目标。

处理传入共享

当最终用户正在共享一些面向通用应用的数据,并指明特定 Web 共享目标作为该数据的接收者时, Web 共享目标就会被调用

数据来自何处,或最终用户如何指明 Web 共享目标作为接收者,并未被指定。不过,一个可能的来源是 在同一用户代理中调用 {{Navigator}} 的 {{Navigator/share()}} 方法。

Web 共享目标调用的其他可能来源示例包括:

获取 `ShareData`

Web 共享目标调用时,数据可以采用未指定的格式。 用户代理必须首先将数据转换为 {{ShareData}} 对象(如果它还不是), 方法是从宿主系统中的等价概念映射到 `ShareData` 的成员。如果来源是对 {{Navigator/share()}} 的调用,用户代理应当原样使用 {{ShareData}} 实参 (但这并不总是可能,因为它可能必须以有损方式在某种其他格式中往返)。 用户代理可以采用启发式方法,尽可能好地将数据映射到 `ShareData` 字段。

例如,宿主共享系统可能没有专门的 URL 字段,而是采用一种约定: 纯文本和 URL 有时都会在 "text" 字段中传输。Android 就是这种情况。 用户代理可以检查 "text" 字段的全部或部分是否为 [=valid URL string=],如果是,则将该部分从 "text" 字段移动到 {{ShareData}} 的 {{ShareData/url}} 成员。

启动 Web 共享目标

当具有 [=ordered map=] |manifest| 的 Web 共享目标 使用 {{ShareData}} |data| 被调用时,运行以下步骤:

  1. 令 |url:URL| 为 [=URL parser|parsing=] |manifest|["share_target"]["action"] 的结果。
  2. 令 |entries:list| 为一个新的空 [=list=]。
  3. [=List/For each=] |member:string| of « "title", "text", "url" »:
    1. 令 |name:string| 为 |manifest|["share_target"]["params"][|member|] 的值。
    2. 如果 |name| 为 `undefined` 或空字符串,则继续。
    3. 如果 |data|[|member|] 为 `undefined`,则继续。
    4. 令 |value:string| 为 ToString(|data|[|member|])。
    5. 将 [=tuple=] (|name|, |value|) [=List/Append=] 到 |entry list|。
  4. 令 |header list| 为一个新创建的 [=Headers/header list=]。
  5. 令 |method:string| 为 |manifest|["share_target"]["method"]。
  6. 令 |enctype:string| 为 |manifest|["share_target"]["enctype"]。
  7. 如果 |method| 为 `"GET"`:
    1. 令 |query| 为在 |entries| 上运行 [=urlencoded serializer=] 且不使用编码覆盖的结果。
    2. 将 |url| 的 [=URL/query=] 组件设置为 |query|。
    3. 令 |body| 为 null。
  8. 否则,如果 |method| 为 `"POST"` 且 |enctype| 为 `"application/x-www-form-urlencoded"`:
    1. 令 |body:string| 为在 |entries| 上运行 [=urlencoded serializer=] 且不使用编码覆盖的结果。
    2. 将 |body| 设置为 [=UTF-8 encode=] |body| 的结果。
    3. 将 `"Content-Type"`/`"application/x-www-form-urlencoded"` [=header list/Append=] 到 |header list|。
  9. 否则,如果 |method| 为 `"POST"` 且 | enctype| 为 `"multipart/form-data"`:
    1. 令 |body| 为使用 |entries| 和 [=UTF-8=] 编码运行 multipart/form-data encoding algorithm 的结果。
    2. 令 |MIME type:string| 为字符串 `"multipart/form-data;"`、一个 U+0020 SPACE 字符、字符串 `"boundary="`,以及由 [=`multipart\/form-data` encoding algorithm=] 生成的 [=`multipart\/form-data` boundary string=] 的拼接结果。
    3. 将 `"Content-Type"`/|MIME type| [=header list/Append=] 到 |header list|。
  10. 令 |browsing context| 为创建一个 新的 [=top-level browsing context=] 的结果。
  11. 令 |request:Request| 为一个新的 [=Request=],其 method 为 |method|,url 为 |url|,header list 为 |header list|,body 为 |body|。
  12. [=Navigate=] |browsing context| 到 |request =|

此算法假设 |manifest| 已经对其运行过 [=process the `share_target` member=] 算法,并且之后仍具有 [=manifest/share_target=]。

无障碍

本规范没有已知的无障碍注意事项。

安全与隐私注意事项

致谢

感谢 [[[WEBINTENTS]]] 团队,他们为 Web 应用互操作用例奠定了基础。特别感谢 Paul Kinlan, 他为 Web Share 和 Web Share Target 做了大量早期倡导工作。

感谢 Connie Pyromallis,她撰写了本规范的早期草案,并帮助设计和原型化此 API。

感谢 Alex Russell 和 David Baron 对本规范早期草案的反馈。