Skip to content

可选运行时 ​

English · 简体中文

编译器本身不产生任何 JavaScript。这个模块是单独的入口,默认不需要,只有当 CSS 的视口单位和用户看到的可视区域对不上时才引入。

js
import { observeAdaptiveViewport } from 'postcss-adaptive-matrix/runtime'

const observer = observeAdaptiveViewport()
// 组件卸载 / 页面离开时
observer.destroy()

什么时候需要它 ​

CSS 的 vw / vh 指的是布局视口,浏览器有意让它在软键盘弹出、页面被捏合缩放时保持不动。多数情况这是对的行为。但有几种场景下它不是:

场景现象
移动端软键盘弹起100vh 仍是全屏高,底部按钮被键盘盖住
用户双指捏合缩放视口单位不变,固定元素飘出屏幕
iOS Safari 地址栏收起/展开100vh 与实际可视高度差一条地址栏
WebView / Capacitor / Tauri 外壳宿主给的可视区域与布局视口不一致

这些都是 VisualViewport 才能看到的信息,CSS 拿不到。这个观察器把它读出来写成 CSS 变量。

发布的变量 ​

调用后写在 document.documentElement 上(可用 target 改):

变量含义
--adaptive-width可视区域宽度(px 数值,无单位)
--adaptive-height可视区域高度
--adaptive-layout-height布局视口高度,即 window.innerHeight
--adaptive-keyboard-height当前缩放倍数下的可视高度损失估计值,并非权威的键盘几何尺寸
--adaptive-scale当前捏合缩放倍数
--adaptive-vh可视高度的 1%,带 px 单位
--adaptive-vw可视宽度的 1%,带 px 单位

前五个是裸数值,要参与计算得自己加单位(calc(var(--adaptive-keyboard-height) * 1px));后两个已经带单位,可以直接当 vh / vw 的替代品用。

典型用法 ​

真·全屏高度,不受地址栏影响:

css
.screen {
  min-block-size: calc(var(--adaptive-vh, 1vh) * 100);
}

双指缩放同样会让 VisualViewport.height 变小,但它不是键盘。观察器会先用 VisualViewport.scale 折算当前缩放下本来应有的可视高度:800px 布局在 2× 缩放时出现 400px 可视视口是正常的,键盘高度为 0;若键盘再把它压到 250px,才报告 150。这样普通缩放手势不会把底部操作栏无故顶到页面中间。

keyboardHeight 只是便于使用的名称,不是键盘检测 API。实现计算的是 max(0, layoutHeight / scale - visualHeight - offset),仅在缩放接近 1 时扣除 offsetTop。其他视口变化也可能产生相同差值;如果宿主同时缩小布局和可视高度,即使键盘打开也可能报告零。请在目标宿主验收操作栏行为,不要把这个估计值当作键盘已显示的证据或准确边界。

回退值 1vh 很重要——运行时没加载、或者在 SSR 首屏时,样式依然成立。

底部操作栏避开软键盘:

css
.action-bar {
  position: fixed;
  inset-block-end: calc(var(--adaptive-keyboard-height, 0) * 1px);
}

选项 ​

ts
observeAdaptiveViewport({
  prefix: 'adaptive',        // 变量前缀,写不写开头的 -- 都行
  target: document.documentElement,
  window: globalThis.window, // 多窗口 / 测试时注入
  document: globalThis.document,
  signal: abortController.signal, // 可选,自动解绑
})

prefix 必须是非空 CSS 标识符。开头的 -- 可写可不写,传入后会被移除;空串、空白或数字开头会在注册任何监听前被拒绝,不会生成无法使用的自定义属性名。

所有选项都不是必填。省略 window、document 或 target 时会使用对应的浏览器全局对象/默认元素;显式传 null 属于错误配置,不会被当成“省略”的另一种写法。只有需要让宿主生命周期接管清理时才传 AbortSignal;触发 abort 等同于调用 destroy(),已经 abort 的 signal 会得到不写入、不注册监听的惰性观察器。

使用同源 iframe 或其他窗口时,应同时传入该窗口的 document(或显式 target)和 window。这些默认值独立解析:只注入 window 不会把 CSS 写入目标从当前页面切换到该窗口的文档。

js
const observer = observeAdaptiveViewport({
  window: frameWindow,
  document: frameWindow.document,
})

返回:

ts
interface AdaptiveViewportObserver {
  update(): AdaptiveViewportSnapshot | null   // 手动触发一次,返回本次读数
  destroy(): void                             // 解绑全部监听
}

destroy() 可重复调用且永久生效:即使浏览器返回的动画帧句柄为 0,也会取消待执行帧;监听只解绑一次,之后再调用 update() 会返回 null 且不再写入。

生命周期与异常 ​

可以复用宿主的取消信号,不必单独保存清理回调:

js
const controller = new AbortController()
const observer = observeAdaptiveViewport({ signal: controller.signal })

// 当前视图销毁时:
controller.abort()
observer.update() // null;恢复观察需要创建新的 observer

销毁观察器会保留最后一次写入的 CSS 变量,不会删除或恢复样式。复用目标元素时,新观察器会写入最新读数。避免多个活跃观察器同时向同一目标写入相同前缀的变量。

配置无效或首次 CSS 写入失败会同步抛错。初始化失败时,会先尝试移除监听器,再抛出原始错误。后续动画帧中的写入失败会自动销毁观察器;手动调用 update() 的失败则直接抛给调用方。请按应用的异常处理方式保护手动更新,并在放弃观察器时调用 destroy()。

对于注入或嵌入式宿主,清理采用尽力释放策略:某个取消或移除监听器的方法抛错时,仍会尝试其余清理操作,不再抛出次生清理错误。异常宿主残留的回调在销毁后不能继续写入,但观察器无法保证宿主确实释放了这些引用。

SSR ​

没有 window 时构造函数不报错,返回的观察器什么也不做,update() 返回 null。所以可以无条件在模块顶层调用,不需要包 if (typeof window !== 'undefined')。

但服务端渲染的首屏 HTML 里不会有这些变量,因此每处使用都要写回退值,否则首帧会拿到空值。

开销 ​

视口事件通过 requestAnimationFrame 合并;显式调用 update() 会立即读取,不受帧合并限制。视口监听使用 passive。变量名在观察器初始化时准备一次,每个值单独缓存:读数未变化就不写 DOM。高度变化时只写实际改变的指标,根据宿主读数,可能涉及 height、layout-height、keyboard-height 和 --adaptive-vh。此辅助工具是独立入口,不会被主编译器包导入。

基于 MIT 协议发布