一站式解决:H5开发中的各类坑与技巧【2】
常见问题
1px 问题
现象
在 H5 页面中,可能需要设置边框宽度为 1px,但在 Retina 屏幕上,1px 可能会看起来比实际要粗。
原因
这是因为移动设备的物理像素密度与 CSS 像素的比例(设备像素比)导致的。
解决方案
利用伪元素和 scale 来实现 0.5px 的效果。
.border-1px {
position: relative;
}
.border-1px:after {
content: "";
position: absolute;
left: 0;
top: 0;
width: 200%;
height: 200%;
border: 1px solid #000;
transform: scale(0.5);
transform-origin: left top;
box-sizing: border-box;
}
sticky 的兼容性问题
在某些 Android 设备的原生浏览器中,使用 position: sticky 实现的元素不能正常吸顶。
这是因为这些浏览器不支持 position: sticky。
解决方案
使用 react-sticky 组件:通过计算 <Sticky> 组件相对于<StickyContai ner>组件的位置进行工作。
npm install react-sticky
<StickyContainer>
<Sticky>{({ style }) => <h1 style={style}>Sticky element</h1>}</Sticky>
</StickyContainer>
使用 JS:通过自定义滚动事件的监听,根据 top 的改变来实现吸顶层 fixed 和 absolute 的转换。
<div id="stickyElement">吸顶bar</div>
<div id="content">这是主要内容</div>
<script>
window.addEventListener('scroll', function() {
var stickyElement = document.getElementById('stickyElement');
var stickyElementRect = stickyElement.getBoundingClientRect();
if (stickyElementRect.top <= 0) {
// 当元素到达顶部,将其定位方式改为固定
stickyElement.style.position = 'fixed';
stickyElement.style.top = '0';
} else {
// 当元素离开顶部,将其定位方式改回绝对
stickyElement.style.position = 'absolute';
stickyElement.style.top = 'initial';
}
});
</script>
``
`
3. 在 Vue 项目中,可以直接使用 vue-sticky 组件。
```js
npm install vue-sticky --save
directives: {
'sticky': VueSticky,
}
<ELEMENT v-sticky="{ zIndex: NUMBER, stickyTop: NUMBER, disabled: [true|false]}">
<div> <!-- sticky wrapper, IMPORTANT -->
CONTENT
</div>
</ELEMENT>
软键盘将页面顶起来、收起未回落问题
在 Android 设备上,点击 input 框弹出键盘时,可能会将页面顶起来,导致页面样式错乱。失去焦点时,键盘收起,键盘区域空白,未回落。
原因
键盘不能回落问题出现在 iOS 12+ 和 wechat 6.7.4+ 中,而在微信 H5 开发中是比较常见的 Bug。
兼容原理,1.判断版本类型 2.更改滚动的可视区域
解决方案
通过监听页面高度变化,强制恢复成弹出前的高度。
const originalHeight = document.documentElement.clientHeight || document.body.clientHeight;
window.onresize = function() {
const resizeHeight = document.documentElement.clientHeight || document.body.clientHeight;
if (resizeHeight < originalHeight) {
document.documentElement.style.height = originalHeight + 'px';
document.body.style.height = originalHeight + 'px';
}
}
使用 line-height 实现文字垂直居中,发现文字偏上
实际这个Bug一直存在,没有好的解决方案,详情见Android浏览器下line-height垂直居中为什么会偏离?
采用 flex 布局,align-items: center 来替代,兼容性更高。
.elem {
display: flex;
justify-content: center;
align-items: center;
}
border-radius 画出的圆在移动端有毛边
给元素添加 overflow: hidden 属性。
.elem {
overflow: hidden;
}
安卓上去掉语音输入按钮
input::-webkit-input-speech-button {
display: none;
}
Vue 单页应用在 iOS 上微信分享失效,图片,标题和描述均未正常显示,安卓上分享正常
我们一般在 APP.vue 的 mounted 生命周期中初始化微信 SDK,此时页面的地址 hash 是#/,而首页的 hash 是#/home,导致初始化微信 SDK 时传入的分享 url 和用户实际触发分享操作时页面的 url 不一致,致使在 iOS 上分享失败。
解决:初始化微信分享 SDK 时传入的地址,和实际触发分享时页面的地址保持一致。
iOS safari 被点击元素会出现半透明灰色遮罩
给 html 或者 body 加入以下 css 代码。
body {
-webkit-tap-highlight-color: rgba(0,0,0,0);
-webkit-user-modify: read-write-plaintext-only;
}
iOS 禁止保存或拷贝图像
长按图片保存场景下,禁止 IOS 默认识别图像行为。
img {
-webkit-touch-callout: none;
}
iOS 端微信 H5 页面上下滑动时卡顿
给滚动元素加上-webkit-overflow-scrolling
属性。
body {
-webkit-overflow-scrolling:touch;
}
iOS 默认输入框内阴影重置
阻止 iOS 默认的美化页面的策略-webkit-appearance:none;
input {
border: 0;
-webkit-appearance:none;
}
对非可点击元素(div,span 等)监听 click 事件,部分 ios 版本不会触发事件
- 添加 css 属性 cursor: pointer;
- 换成 button 元素。
cursor: pointer;
<button></button>
手机底部刘海存在背景,和页面背景色不一致
通过指定 body 的背景色来解决。
body {
background-color: #fff;
// or 暗色模式
// background-color: #000;
}
对于带有 hash 的 H5 链接,部分手机厂商的 webview 打开 H5 页面会加载两次
这是部分 webview 对于特殊 url 有独特的解析和加载逻辑,去掉 hash 即可
https://www.example.com/a/b#/
body存在默认背景色
body 标签在大部分浏览器中的默认背景色是白色,但在极少数浏览器中的背景颜色是淡绿色或者其他颜色。通过指定 body 背景色为#fff,来兼容更多设备。
body {
background-color: #fff;
}
旋转屏幕的时候,字体大小调整的问题
css
body {
-webkit-text-size-adjust: 100%;
}
IOS解析日期问题
在某些情况下,苹果系统上解析YYYY-MM-DD HH:mm:ss
格式的日期会报错Invalid Date
,而安卓系统则没有这个问题。解决这个问题的一种方法是将日期字符串中的-
替换为/
。
const dateString = "2023-07-16 00:00:00";
const fixedDateString = dateString.replace(/-/g, "/");
const date = new Date(fixedDateString);
滚动穿透
滚动穿透(scrolling through)是指在一个固定区域内滚动时,滚动事件透过该区域继续传递到其下方的元素,导致同时滚动两个区域的现象。滚动穿透可能会对用户体验产生负面影响,因为用户可能意外地滚动到不相关的内容。
这个问题一直很无解,只能hack去兼容
overflow: hidden
先锁住body
.modal-open {
&,
body {
overflow: hidden;
height: 100%;
}
}
还原body滚动区域
// 获取滚动区域的容器元素
const container = document.querySelector('.container');
// 获取滚动区域的内容元素
const content = document.querySelector('.content');
// 记录滚动位置
let scrollTop = 0;
// 禁止滚动穿透
function disableScroll() {
// 记录当前滚动位置
scrollTop = window.pageYOffset || document.documentElement.scrollTop;
// 设置滚动区域容器的样式,将其高度设置为固定值,并设置滚动条样式
container.style.height = '100%';
container.style.overflow = 'hidden';
// 阻止窗口滚动
document.body.classList.add('no-scroll');
document.body.style.top = `-${scrollTop}px`;
}
// 启用滚动穿透
function enableScroll() {
// 恢复滚动区域容器的样式
container.style.height = '';
container.style.overflow = '';
// 允许窗口滚动
document.body.classList.remove('no-scroll');
document.body.style.top = '';
// 恢复滚动位置
window.scrollTo(0, scrollTop);
}
// 示例使用,当某个事件触发时禁止滚动穿透
function disableScrollEvent() {
disableScroll();
}
// 示例使用,当某个事件触发时启用滚动穿透
function enableScrollEvent() {
enableScroll();
}
ant-mobile组件库解决方式
touchmove
,通过监听滑动方向和滚动元素的状态,决定是否阻止默认的滑动行为,从而防止滚动穿透。document
添加touchstart
和touchmove
事件的监听器,通过捕获触摸滑动事件,并根据情况阻止默认行为,从而避免滚动穿透。document
移除对触摸事件的监听器,恢复默认的滑动行为。
// 移植自vant:https://github.com/youzan/vant/blob/HEAD/src/composables/use-lock-scroll.ts
export function useLockScroll(
rootRef: RefObject<HTMLElement>,
shouldLock: boolean | 'strict'
) {
const touch = useTouch()
const onTouchMove = (event: TouchEvent) => {
touch.move(event)
const direction = touch.deltaY.current > 0 ? '10' : '01'
const el = getScrollParent(
event.target as Element,
rootRef.current
) as HTMLElement
if (!el) return
// This has perf cost but we have to compatible with iOS 12
if (shouldLock === 'strict') {
const scrollableParent = getScrollableElement(event.target as HTMLElement)
if (
scrollableParent === document.body ||
scrollableParent === document.documentElement
) {
event.preventDefault()
return
}
}
const { scrollHeight, offsetHeight, scrollTop } = el
let status = '11'
if (scrollTop === 0) {
status = offsetHeight >= scrollHeight ? '00' : '01'
} else if (scrollTop + offsetHeight >= scrollHeight) {
status = '10'
}
if (
status !== '11' &&
touch.isVertical() &&
!(parseInt(status, 2) & parseInt(direction, 2))
) {
if (event.cancelable) {
event.preventDefault()
}
}
}
const lock = () => {
document.addEventListener('touchstart', touch.start)
document.addEventListener(
'touchmove',
onTouchMove,
supportsPassive ? { passive: false } : false
)
if (!totalLockCount) {
document.body.classList.add(BODY_LOCK_CLASS)
}
totalLockCount++
}
const unlock = () => {
if (totalLockCount) {
document.removeEventListener('touchstart', touch.start)
document.removeEventListener('touchmove', onTouchMove)
totalLockCount--
if (!totalLockCount) {
document.body.classList.remove(BODY_LOCK_CLASS)
}
}
}
useEffect(() => {
if (shouldLock) {
lock()
return () => {
unlock()
}
}
}, [shouldLock])
}