在移动端开发中,有一个让所有前端都头皮发麻的场景:
“页面底部有一个 position: fixed 的提交按钮。当用户点击输入框,软键盘弹起时……”
- 在 Android 上:浏览器窗口高度被压缩,底部的按钮直接“骑”在了输入框上面,遮挡了视线。
- 在 iOS 上:页面整体往上推,键盘收起后,页面经常“回不来”,底部留下一大片空白。
面试官问:“移动端软键盘兼容性问题,你是怎么处理的?”如果你只回答“监听 resize 隐藏按钮”,那思路就有些老旧了。今天我们来聊聊现代的 Viewport meta 新属性和 VisualViewport API。
01. 核心差异:Android 与 iOS 的机制不同
要解决问题,先懂原理。
- Android (Webview):
- 键盘弹起时,视口高度 (Visual Viewport) 真的变小了。
- 相当于页面被挤压了。所以
fixed 到底部的按钮,会跟着视口底部一起跑上来。
- iOS (Safari/WKWebview):
- 键盘弹起时,视口高度不变。
- 它只是把页面整体向上推 (Scroll) 了一段距离。按钮还在原来的位置(虽然视觉上可能被遮挡)。
02. 解法一:最简单的 CSS 新标准 (推荐) 🌟
以前我们为了解决这事,写了一堆 JavaScript。现在,Chrome 和 Safari 都支持了一个新的 viewport 属性:interactive-widget。
你只需要在 HTML 的 <head> 标签里加一段话:
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content"
>
取值说明:
resizes-content (推荐):键盘弹起时,视口变小(模拟 Android 行为)。这能保证你的 Flex 布局自动适应剩余空间。
overlays-content:键盘覆盖在页面上,不改变视口大小。
面试话术:
“对于现代浏览器,我优先使用 interactive-widget 属性来统一 Android 和 iOS 的视口行为,这比写 JS 监听性能更好。”
03. 解法二:解决“按钮顶上来” (经典兼容方案)
如果你需要兼容老旧的 App 容器,或者 meta 标签不生效。核心逻辑很简单:键盘弹起时,把底部按钮隐藏;键盘收起时,再显示。
代码实现 (Vue 3 Hook):
import { ref, onMounted, onUnmounted } from 'vue';
export function useKeyboard() {
const isKeyboardOpen = ref(false);
const originalHeight = ref(0);
const handleResize = () => {
const currentHeight = window.innerHeight;
// 阈值:如果高度变小超过 20%,认为键盘弹起了
if (originalHeight.value - currentHeight > originalHeight.value * 0.2) {
isKeyboardOpen.value = true;
} else {
isKeyboardOpen.value = false;
}
};
onMounted(() => {
originalHeight.value = window.innerHeight;
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
return { isKeyboardOpen };
}
模板中使用:
<button v-show="!isKeyboardOpen" class="fixed-bottom-btn">提交</button>
04. 解法三:解决“输入框被遮挡” (VisualViewport API)
有时候输入框在页面中下部,键盘一弹起来,正好挡住输入框。用户只能盲打。我们需要手动把输入框滚到可视区域。
现代标准 API:window.visualViewport。它比 window.innerHeight 更精准,专门为这种移动端交互场景设计。
window.visualViewport.addEventListener('resize', () => {
// 键盘弹起,视口变矮
if (window.visualViewport.height < window.innerHeight) {
// 获取当前聚焦的输入框
const activeElement = document.activeElement;
if (activeElement && activeElement.tagName === 'INPUT') {
// 延迟一下,等待键盘完全弹出
setTimeout(() => {
// 核心:把输入框滚到视口中间
activeElement.scrollIntoView({
block: 'center',
behavior: 'smooth'
});
}, 300);
}
}
});
05. 解决 iOS “回不来”的 Bug
现象:iOS 收起键盘后,页面还停留在上面,底部留白,点击页面错位。
原因:window.scrollY 没有自动恢复。
暴力解法:监听 focusout 事件,通过 DOM 操作手动滚一下。
// 在 input 的 onBlur 或 focusout 中调用
function fixIOSScroll() {
const isIOS = /iPhone|iPad|iPod/.test(navigator.userAgent);
if (isIOS) {
setTimeout(() => {
window.scrollTo(0, window.scrollY); // 原地滚一下,触发重绘
}, 100);
}
}
结语
移动端兼容性是前端最琐碎、也最显功底的地方。从 resize 监听,到 scrollIntoView,再到现在的 interactive-widget,我们看到了 Web 标准在不断进步,开发者体验也在变好。
最后给一个忠告:如果可以,尽量不要设计“底部 Fixed 按钮 + 大量输入框”的页面布局。把按钮放在表单流的最下面,利用浏览器的原生滚动,往往才是最稳健的方案。