最近在做一个 web 端的阅读器,缘由暂且不提,适配 SM (移动端 Safari) 时简直要了 👴 的小命!
浏览器高度
首先是浏览器高度,在 SM 中,vh
在计算时会包括顶部的导航栏
也就是说,如果你想让内容填满整个可视区域,用 height: 100vh
会溢出一大截,解决的方式很简单,用 height: 100%
或者别的方式替代(比如 flex
布局)
为什么
在我们滚动页面的时候,导航栏可以透过毛玻璃看到滚动到后面的内容,所以 SM 的实际可视区域还包括了状态栏
经过测试,iOS 平台的 Chrome 也能复现高度问题,可以推测出 iOS 平台上的浏览器都需要注意这点,因为 iOS 限制了第三方浏览器只能使用系统内置的 🧐WebKit 内核
上下文菜单
这个问题就比较重量级了,通过触屏长按目标触发 contextmenu
事件在 SM 上直接不支持
从图中也可以看出,应该是为了 iOS 中的跨应用拽托功能做出了让步(在以前是为无障碍功能-放大镜让步)
一开始我不信邪,用 Chat-GPT 查了一圈上下文菜单的库,例如 react-contexify
、react-menu
,他们都没有处理这个问题
最后找到 Quasar
和 radix-ui
做了处理(还得是知名的组件库),但方法并不出彩,是我已有的思路:监听触控事件时长来判断是否显示菜单
算了,又不是不能用,最后我在 touch
和 pointer
事件中选择 pointer
来实现,理由很简单,它兼容性更好
最后贴一下 react
中的实现,先是一个 hook
// useContextMenu.ts
import { useCallback, useEffect, useRef, useState } from 'react';
function whenTouchOrPen<E>(
handler: React.PointerEventHandler<E>
): React.PointerEventHandler<E> {
return (event) =>
event.pointerType !== 'mouse' ? handler(event) : undefined;
}
const useContextMenu = <
T extends React.DOMAttributes<HTMLElement> = React.DOMAttributes<HTMLElement>
>(
onOpen?: (position: { x: number; y: number }) => void
) => {
const [disabled, setDisabled] = useState(false);
const position = useRef({ x: -999, y: -999 });
useEffect(() => {
if (disabled) clearLongPress();
}, [disabled]);
const handleOpen = (e: React.MouseEvent | React.PointerEvent) => {
position.current = { x: e.clientX, y: e.clientY };
onOpen?.(position.current);
};
const longPressTimer = useRef(0);
const clearLongPress = useCallback(() => {
window.clearTimeout(longPressTimer.current);
}, []);
const onContextMenu: T['onContextMenu'] = disabled
? undefined
: (e) => {
// 大部分触控平台都支持长按触发 `contextmenu` 事件,清除计时器
clearLongPress();
handleOpen(e);
e.preventDefault();
};
const onPointerDown: T['onPointerDown'] = disabled
? undefined
: whenTouchOrPen((e) => {
clearLongPress();
// 在触发 iOS 跨应用拽托前响应,500ms 比较合适
longPressTimer.current = window.setTimeout(() => handleOpen(e), 500);
});
const onPointerMove: T['onPointerMove'] = disabled
? undefined
: whenTouchOrPen(clearLongPress);
const onPointerCancel: T['onPointerCancel'] = disabled
? undefined
: whenTouchOrPen(clearLongPress);
const onPointerUp: T['onPointerUp'] = disabled
? undefined
: whenTouchOrPen(clearLongPress);
return [
{
triggerProps: {
onContextMenu,
onPointerDown,
onPointerMove,
onPointerCancel,
onPointerUp,
},
position: position.current,
},
{ setDisabled },
] as const;
};
export default useContextMenu;
然后是一个利用该 hook 封装的高阶组件:
// ContextMenu.tsx
import useContextMenu from '@/hooks/useContextMenu';
import type { FC } from 'react';
const ContextMenu: FC<{
onOpen?: Parameters<typeof useContextMenu>[0];
children:
| React.ReactNode
| ((...props: ReturnType<typeof useContextMenu>) => React.ReactNode);
}> = (props) => {
const { onOpen, children } = props;
const hookRes = useContextMenu(onOpen);
return (
<>{typeof children === 'function' ? children(...hookRes) : children}</>
);
};
export default ContextMenu;
不可靠的 BLOB
SM 存放在
indexedDB
(web 浏览器数据库)的blob
并不可靠
在页面间来回跳转的时候,blob
引用中的数据会莫名其妙的消失
为了让 blob
更可靠,我们在做持久化的时候,需要抽取 blob
中的 arrayBuffer
和 type
,使用的时候再组装
简单写个伪代码例子
// 存储
const book; // Blob
fs.saveBlob('一本书', {
arrayBuffer: await book.arrayBuffer(),
type: book.type,
});
// 使用
const origin = fs.getBlob('一本书');
const book = new Blob([origin.arrayBuffer], { type: origin.type });
为什么
从 iOS-13 和 Safari-13.1 开始,indexedDB 的数据不再具有持久性
参考:
子元素溢出
子元素应用 CSS3 动画属性后,会无视祖先元素的
border-radius
溢出
当你需要写一个带圆角且子元素触发了硬件加速的元素时
我们期望效果如下:
html
<div class="rounded-wrapper">
<div class="content">
</div>
</div>
css
.rounded-wrapper {
overflow: hidden;
border-radius: 50%;
height: 200px;
width: 200px;
}
.content {
transform: translatez(0);
height: 100%;
width: 150%;
background: gray;
}
但实际在 SM 中圆角会无效
此时我们需要在其声明了 overflow: hidden
和 border-radius
的元素上增加一个样式
.rounded-wrapper {
...
isolation: isolate;
}
为什么
这是一个 🧐WebKit 的 BUG,所以没有为什么
解决思路主要是让其通过其它方式计算溢出,isolation: isolate
可以新建一个独立于其它部分的层叠上下文,自然就触发了别的计算方式
同理 transform
也是可行的,但是有缺陷(阴影失效等等),不推荐使用
参考:
Overflow: hidden with border radius not working on Safari
overflow: hidden + border radius does not work when transform is added to child