# orientation 清单成员

> orientation 成员为已安装 Web 应用的窗口请求默认屏幕方向；Android 上的 Chrome 会遵循它，桌面浏览器与 Safari 则不会。

`orientation` 请求浏览器把已安装 Web 应用的顶层窗口锁定到一个默认屏幕方向，例如 `portrait` 或
`landscape`，只要应用运行在应用式显示模式下就一直生效。它是 `screen.orientation.lock()` 的声明式
对应物：清单在启动时设置一次初始锁定，API 则在运行时改变它。

Android 上的 Chrome 39、Samsung Internet 4.0、Android WebView 39 与 Firefox for Android 79 会把
该成员应用到已安装应用（BCD `html.manifest.orientation`）。桌面端的 Chrome 39 与 Edge 79 解析它但
不会锁定桌面窗口，iOS 与 macOS 上的 Safari 忽略它，桌面端 Firefox 157 不从清单安装任何东西。锁定
只在 `fullscreen`、`standalone` 或 `minimal-ui` 下生效；在浏览器标签页里打开的页面永远不会被
自己的清单旋转。

## 成员

- **类型**：字符串，取 `OrientationLockType` 之一：`any`、`natural`、`landscape`、
  `landscape-primary`、`landscape-secondary`、`portrait`、`portrait-primary` 或
  `portrait-secondary`。
- **默认值**：缺省，即不锁定。窗口跟随设备传感器与用户的旋转锁定设置，和浏览器标签页完全一样。
- **示例值**：`"portrait"`。

`natural` 是设备自身的默认方向（多数手机是竖屏，多数平板和所有笔记本是横屏）。`portrait` 与
`landscape` 允许该轴向上的两种变体，所以手机倒过来拿仍然可读；`-primary` 与 `-secondary` 形式
只钉住其中一种。`any` 用于显式声明"不在乎"，效果与省略该成员相同。

列表之外的值会被丢弃，而不是让整份清单出错：Chromium 记录 `unknown 'orientation' value ignored.`
然后以不锁定的状态继续。锁定是应用窗口的属性，所以它作用于该窗口展示的每一个 scope 内文档，而
永远不作用于同一批 URL 在浏览器标签页里的打开方式。

:::observed
Chrome 155（macOS 26，英文界面）的 DevTools > Application > Manifest：**Presentation** 一节有一行
**Orientation**，显示声明的值；清单写 `"orientation": "sideways"` 时，**Errors and warnings** 下
多出 `unknown 'orientation' value ignored.`，而该行为空。在 Chrome 155 for Android（Android 16）
上，`chrome://webapks` 为每个已安装的 WebAPK 在 **Orientation** 行列出同一个值，那是打包该 APK 时
用的值，不是线上清单里的值。
:::

## 示例

声明只有一行；有意思的代码在于锁定未被遵循时页面要做什么。

### 为只用竖屏的外勤日志声明方向

一个表单按手机竖持布局的录入应用锁定为 `portrait`。在 Chrome for Android 上，已安装应用会无视
设备旋转；在浏览器标签页、桌面端和 iOS 上，同一份清单没有效果，布局在横屏下仍然必须可用。

```json
{
  "name": "Field Journal",
  "short_name": "Journal",
  "start_url": "/",
  "display": "standalone",
  "orientation": "portrait"
}
```

手机应用选 `portrait` 比 `portrait-primary` 更好：用户把设备掉头时屏幕可以翻转 180 度，而
`portrait-primary` 会阻止这一点。

### 读取当前方向并响应变化

清单的锁定对脚本不可见，但它产生的方向是可见的。[Screen Orientation API](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Orientation_API)（MDN）通过
`screen.orientation.type` 报告方向；没有该 API 的浏览器（iOS 16.4 之前的 Safari）走回退分支，用
媒体查询读取，而两条分支里每次旋转都会触发同一个媒体查询。

```js
function currentOrientation() {
  if ('orientation' in screen && screen.orientation?.type) {
    return screen.orientation.type; // 例如 "portrait-primary"
  }
  // 回退：没有 Screen Orientation API，从视口推断轴向。
  return matchMedia('(orientation: portrait)').matches ? 'portrait' : 'landscape';
}

const query = matchMedia('(orientation: portrait)');
query.addEventListener('change', () => {
  document.documentElement.dataset.orientation = currentOrientation();
});
document.documentElement.dataset.orientation = currentOrientation();
```

由于清单锁定把应用固定在一个轴向上，`change` 监听器只会在没有遵循锁定的环境里触发，这正是切换到
双栏布局的自然位置。

### 在清单被忽略的环境里于运行时锁定

`screen.orientation.lock()` 可以从脚本施加同样的锁定，但有两个清单没有的限制：在 Android 上文档必须
先进入全屏，桌面浏览器则以 `NotSupportedError` 拒绝调用（MDN，`ScreenOrientation.lock()`）。
回退做法是什么都不做，依靠响应式布局。

```js
async function lockPortraitIfPossible() {
  if (!('orientation' in screen) || typeof screen.orientation.lock !== 'function') {
    return 'unsupported'; // Safari：没有 lock()，布局保持响应式
  }
  try {
    await document.documentElement.requestFullscreen();
    await screen.orientation.lock('portrait');
    return 'locked';
  } catch (err) {
    // 桌面端 Chrome 与 Edge 以不支持为由拒绝；沙箱 iframe 中以安全错误拒绝。
    return err.name;
  }
}
```

从 click 处理函数里调用它：`requestFullscreen()` 和锁定都需要用户激活，返回的字符串告诉界面是否
保留"请旋转设备"的提示。

## 另请参阅

- [display 清单成员](/zh/reference/manifest/display/)
- [scope 清单成员](/zh/reference/manifest/scope/)
- [WebAPK：Chrome 在 Android 上安装 PWA 的方式](/zh/reference/installation/webapk/)
- [Web Application Manifest: orientation member](https://www.w3.org/TR/appmanifest/#orientation-member)（w3.org）
- [ScreenOrientation: lock() method](https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation/lock)（developer.mozilla.org）