# scope 清单成员

> scope 成员是一个 URL 前缀，标记哪些页面属于已安装的 Web 应用；导航到它之外的页面仍保留应用窗口，但浏览器界面会重新出现。

`scope` 是一个 URL 字符串，指定已安装 Web 应用的导航范围：哪些页面可以获得没有浏览器控件的
应用式窗口。一个 URL 与 scope URL 同源、且路径以 scope URL 的路径开头时，就在范围内；其他页面
都在范围之外，仍然可以访问，但浏览器会恢复自己的界面来提示用户。

Chrome 73（Android 与桌面端）、Edge 79、Samsung Internet 6.2、iOS 上的 Safari 16.4 与 macOS
上的 Safari 17 会把该成员应用到已安装应用（BCD `html.manifest.scope`）。桌面端 Firefox 157 没有
基于清单的安装路径，这个值在那里没有消费者。在 Android 上，同一个字符串还决定已安装的 WebAPK
会从其他应用接管哪些链接。

## 成员

- **类型**：持有 URL 的字符串，绝对或相对均可；相对值相对于清单文件的 URL 解析，而不是文档的
  URL。
- **默认值**：去掉文件名、查询串与片段后的 `start_url`。`start_url` 为
  `/app/index.html?user=1#home` 时，有效 scope 是 `/app/`。
- **示例值**：`"/app/"`。

两条规则会让该成员被丢弃并回到默认值。scope 必须与文档同源，否则记录
`property 'scope' ignored, should be same origin as document.`；`start_url` 必须位于 scope 之内，否则记录
`property 'scope' ignored. Start url should be within scope of scope URL.`。
两条消息都来自 Chromium 的 `manifest_parser.cc`。

匹配是对路径做纯字符串前缀比较，而不是目录判断。`"/app"` 既匹配 `/app/`，也匹配 `/app-admin/`
与 `/application.html`；以 `/` 结尾才能把范围限定在一个目录内。scope 不是安全边界：范围外导航
从不被拦截；Service Worker 的 scope 是在 `register()` 时另行设定的值（MDN，
`ServiceWorkerRegistration.scope`），可以比清单的 scope 更宽或更窄。

范围外页面的呈现方式因平台而异。Android 上的 Chrome 以类似自定义标签页的视图打开页面，顶部显示
URL；桌面端的 Chrome 与 Edge 加一条带来源和"在浏览器中打开"控件的工具栏；iOS 上的 Safari 16.4
在主屏幕应用内显示一条横幅，而不是完整地址栏（BCD 注记）。跨源页面只能通过
`scope_extensions` 纳入范围。

:::observed
Chrome 155（macOS 26，英文界面）的 DevTools > Application > Manifest，清单写
`"start_url": "/"` 与 `"scope": "/app/"` 时：**Errors and warnings** 一节显示
`property 'scope' ignored. Start url should be within scope of scope URL.`，**Presentation** 一节的
**Start URL** 为 `/`，没有 scope 行，这正是面板表示默认 scope（`/`）已生效的方式。
:::

## 示例

每个示例都把一份清单和它在页面层面造成的结果放在一起。

### 把应用限定在子路径下

一个部署在 `/app/` 下的仪表盘，站点的营销页面在根路径。指向 `/pricing/` 的链接在应用窗口内打开，
但浏览器控件恢复显示，访问者能看出自己已经离开了应用。

```json
{
  "name": "Ledger",
  "start_url": "/app/",
  "scope": "/app/",
  "display": "standalone"
}
```

结尾的斜杠很重要：`"scope": "/app"` 还会把 `/app-status/` 与 `/apply.html` 也划进来，因为匹配是
路径前缀比较。

### 让默认 scope 从带查询串的 start_url 推导出来

一份用查询串标记启动来源、但没有声明 `scope` 的清单。浏览器从 `start_url` 去掉文件名、查询串与
片段，于是有效 scope 是 `/app/`，整个目录都在范围内，包括用户通过导航到达的、不带标记的 `/app/`。

```json
{
  "name": "Ledger",
  "start_url": "/app/index.html?source=homescreen",
  "display": "standalone"
}
```

如果同一份清单写了 `"start_url": "/app/index.html?source=homescreen"` 和
`"scope": "/dashboard/"`，scope 会被丢弃（起始 URL 不在其中），有效 scope 仍是 `/app/`；这个错误
除了上面引用的 DevTools 那一行之外没有任何提示。

### 在运行时检测范围外环境

清单的 scope 不暴露给脚本，但 `display-mode` 媒体特性会报告当前页面是否带着浏览器界面显示。在
已安装应用里，这意味着页面在范围之外（或者用户在标签页里打开了它）；在没有该特性的引擎里查询
什么都不匹配，代码走与浏览器标签页相同的回退分支。

```js
function isWithinScope(target, scope) {
  const t = new URL(target, location.href);
  const s = new URL(scope, location.href);
  return t.origin === s.origin && t.pathname.startsWith(s.pathname);
}

const APP_SCOPE = '/app/';
const inAppWindow = matchMedia('(display-mode: standalone)').matches;

if (!inAppWindow || !isWithinScope(location.href, APP_SCOPE)) {
  // 浏览器标签页、不支持的引擎或范围外页面：显示完整的站点页头。
  document.documentElement.dataset.chrome = 'site';
} else {
  document.documentElement.dataset.chrome = 'app';
}
```

把 `APP_SCOPE` 放进与清单构建共享的同一个常量里，两者才不会漂移；它们不一致时浏览器不会给出
任何警告。

## 另请参阅

- [start_url 清单成员](/zh/reference/manifest/start-url/)
- [scope_extensions 清单成员](/zh/reference/manifest/scope-extensions/)
- [display 清单成员](/zh/reference/manifest/display/)
- [Service Worker 注册与作用域](/zh/reference/service-worker/registration-scope/)
- [Chrome 与 Android 上的 PWA（WebAPK、TWA）](/zh/reference/platforms/chrome-android/)
- [Web Application Manifest: scope member](https://www.w3.org/TR/appmanifest/#scope-member)（w3.org）
- [WebAPKs on Android](https://web.dev/articles/webapks)（web.dev）
- [scope](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/scope)（developer.mozilla.org）