blog
blog copied to clipboard
移动端问题记录
图片错误
图片未返回或者加载失败出现默认头像
元素的行为表现:
- 可以给img添加字体样式,那些样式会被施加到alt属性中的替代文字里。如果图片正常显示,那么文字将不会出现;
- 是一个替换元素(样式和尺寸会被外部资源替代)。因为图片会被替代,那么
上的:before和:after这样的伪元素就不会成功显示,但是如果图片未加载成功,那么这些伪元素就会显示出来
#方案一
<img
src={rowData.avatar || loginIcon}
alt=""
onError={e => {
e.target.onerror = null; e.target.src = loginIcon;
}}
/>
# 方案二 使用after字体图标库
img:after {
content: "\f1c5" " " attr(alt);
font-size: 16px;
font-family: FontAwesome;
color: rgb(100, 100, 100);
display: block;
position: absolute;
z-index: 2;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #fff;
}
TextEllipsis
移动端需求,超出多少行出现全文,可以点击显示全部,然后可以收起
需求,超出多少行出现全文,可以点击显示全部,然后可以收起
组件实现思路:
- 接收文本和行数
- 根据行数判断是否需要显示隐藏
- 添加不同的样式控制显示行数
关键代码
//文本样式
.textContent {
font-size: px2rem(30);
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: rgba(0, 0, 0, 1);
line-height: px2rem(50);
word-wrap:break-word;
word-break:break-all;
white-space: pre-wrap;//支持文本输入带换行
}
//需要隐藏添加的样式
.hidden-text {
display: -webkit-box;
-webkit-line-clamp: 6;//超过6行隐藏
/*! autoprefixer: off */
-webkit-box-orient: vertical;
/* autoprefixer: on */
overflow: hidden;
}
//未计算完添加默认的灰色滚动效果
.bgLoad{
background-image: linear-gradient(90deg, #f0f0f0 25%, #e3e3e3 37%, #f0f0f0 63%);
background-size: 400% 100%;
height: px2rem(40);
animation: loading 1.4s ease infinite;
}
.bgLoad+.bgLoad{
width: 90%;
margin-top: px2rem(12);
}
// 效果https://ant.design/components/skeleton-cn/
@keyframes loading {
0% {
background-position: 100% 50%
}
to {
background-position: 0 50%
}
}
const [needHidden, setNeedHidden] = useState(false);
// 判断文本超出行数
const isElementCollision = (ele, rowCount = 6, cssStyles, removeChild) => {
if (!ele) {
return false;
}
const clonedNode = ele.cloneNode(true);//复制并返回调用它节点的副本,true标识递归复制所有子节点
// 给clone的dom增加样式
clonedNode.style.overflow = 'visible';
clonedNode.style.display = 'inline-block';
clonedNode.style.width = 'auto';
clonedNode.style.whiteSpace = 'nowrap';
clonedNode.style.visibility = 'hidden';
clonedNode.style.whiteSpace = 'pre-wrap';//支持输入的换行符
// 将传入的css字体样式赋值
if (cssStyles) {
Object.keys(cssStyles).forEach(item => {
clonedNode.style[item] = cssStyles[item];
});
}
// 给clone的dom增加id属性
const _time = new Date().getTime();
const containerID = `collision_node_id_${_time}`;
clonedNode.setAttribute('id', containerID);
const tmpNode = document.getElementById(containerID);
let newNode = clonedNode;
if (tmpNode) {
document.body.replaceChild(clonedNode, tmpNode); //用新节点替换某个子节点
} else {
newNode = document.body.appendChild(clonedNode);
}
// 新增的dom宽度与原dom的宽度*限制行数做对比
// 一行是25高度,根据样式TextEllipsis.scss的textContent的样式line-height: px2rem(50);
const defaulltHeight = rowCount * 25;
let differ = false;
if (newNode.offsetHeight > defaulltHeight){
differ = true;
}
if (removeChild) {
document.body.removeChild(newNode);
}
return differ;
};
//使用
useLayoutEffect(() => {
const cssStyles = { fontWeight: '400' };
const needHiddenValue = isElementCollision(contentRef.current, 6, cssStyles, true);
setNeedHidden(needHiddenValue);
setIsCompute(true);
}, [contentRef]);
return (
<div className={styles.textEllipsis}>
<div style={{ opacity: isCompute ? 1 : 0 }}>
<div
ref={contentRef}
className={`${styles.textContent} ${!showAll && needHidden ? styles['hidden-text'] : ''}`}
>
{headerText ? headerText() : null}
{content}
</div>
</div>
{
!isCompute && (
<Fragment>
<div className={styles.bgLoad} />
<div className={styles.bgLoad} />
</Fragment>
)
}
{isCompute && needHidden && (
<div
className={styles['content-btn']}
>
<span
onClick={e => {
handleContent(e);
}}
>
{!showAll ? '全文' : '收起'}
</span>
</div>
)}
</div>
)
IOS和安卓监听软键盘弹出并获取软键盘高度
安卓唤起软键盘,会改变视窗大小,IOS并不会,如果你在ios上设置一个100%高度的body,弹起键盘后你会发现这个body是可以上下滚动的,即100%高度的body超出了视窗;
IOS端唤起键盘,会自动把当前输入框滚动到可视区域(IOS系统优化),安卓则会出现键盘遮挡输入框的问题;
使用
window.scrollTo
会让输入框失去焦点;采用
e.scrollintoview(true/false)
滚动到可是区域的顶部或者底部
-
Android
-
监听
resize
事件,Android
唤起软键盘会改变clientHeight
,可以通过对比原clientHeight
的差距可以获得软键盘的高度export const useKeyboard = (nodeClassName, keyWordUp, keyWordDown) => { const originHeight = document.documentElement.clientHeight || document.body.clientHeight; const resizeHandler = () => { const resizeHeight = document.documentElement.clientHeight || document.body.clientHeight; if (resizeHeight < originHeight) { // 键盘弹起后逻辑 const val = originHeight - resizeHeight; prevHieght.current = val; setTimeout(() => { keyWordUp(val); }, 100); } else { // 键盘收起后逻辑 setTimeout(() => { keyWordDown(); prevHieght.current = 0; }, 100); } }; useEffect(() => { window.addEventListener('resize', resizeHandler); return () =>{ window.removeEventListener('resize', resizeHandler); } },[]) }
-
-
IOS
-
IOS端软键盘弹出不会改变布局高度,不会触发
resize
,需要使用focusin
和focusout
-
页面底部放一个透明且层级为负数的div,通过该元素距离顶部的高度差异可以获得软键盘的高度
// 上一次键盘弹起之后的屏幕的值 const prevHieght = useRef(0); const allNodeClassName = document.querySelector(nodeClassName); if (allNodeClassName){ screenHieght.current = allNodeClassName.getBoundingClientRect().top; } const iosFocusinHandler = () => { // 键盘展开 setTimeout(() => { const nodeHeight = document.querySelector(nodeClassName); if (nodeHeight){ const nodeTop = nodeHeight.getBoundingClientRect().top; const sum = screenHieght.current - nodeTop; prevHieght.current = sum; keyWordUp(prevHieght.current, 'IOS', screenHieght.current); } }, 300); }; const iosFocusoutHandler = () => { // 键盘收起 setTimeout(() => { keyWordDown('IOS'); prevHieght.current = 0; }, 100); }; useEffect(() => { document.body.addEventListener('focusin', iosFocusinHandler); document.body.addEventListener('focusout', iosFocusoutHandler); return () => { document.body.removeEventListener('focusin', iosFocusinHandler); document.body.removeEventListener('focusout', iosFocusoutHandler); } },[])
-
IOS端软键盘弹出行为
-
输入框在顶部
-
当输入框在页面中上的位置,比键盘高度高时,软键盘弹起,不会引起页面往上滚,而是出现一个覆盖层
-
-
输入框在底部
- 顶上去
- IOS设计为输入目标被软键盘遮挡优化的方案
键盘弹起时页面各属性的变化:
scrollHeight:504 不变
offsetHeight:504 不变
clientHeight: 504 不变
innerHeight: 206 改变
scrollTop:298 改变
改变的值有文档显示高度innerHeight、被卷去的高度scrollTop这两个值
如何解决
主动避开键盘后再聚焦
解决方案:当键盘弹起时,就先手动让输入框弹上来,那么页面就不会滚了,然后缩短页面高度,让输入框落到页面底部
handleFocus =()=>{
input.style.bottom=`${window.innerHeight}px`;
setTimeout(() => rest(),100)
}
rest = () =>{
document.body.style.height = `${window.innerHeight}px`
input..style.bottom = 0
}
图片示例
反向滚动
其他解决方案
iOS 13 VisualViewport
API 与新思路
- 这是一个可以反映实际可视区域的实验性标准;
- 体现页面上不含键盘的可视区域所在的位置;
-
VisualViewport
API 在 Android 和 iOS 两端,都完整反映了在缩放和键盘弹出等一系列影响下,实际可视区域在页面中的位置和大小
IOS端微信loacation.href跳转之后点击返回页面不会刷新
由于 IOS 系统的页面缓存机制,经常会遇到在移动端返回到上一个页面不刷新的情况。(一加手机也会)
现象
-
开发微信 H5 页面的时候,在IOS微信内置浏览器中返回上一页时,上一个页面不会被刷新。 而通常在浏览器缓存机制中,在返回上一页的操作中, html/css/js/接口 等动静态资源不会重新请求,但是js会重新加载。
-
但在IOS微信页面中js也会保存上一页面最后执行的状态,不会重新执行js。 使用这种模式的缓存机制可以加快渲染速度,但是部分数据需要经常展示和编辑的情况下会导致不同步。
-
比如‘详情页’跳转到‘编辑页’,编辑完后再返回到‘详情页’,如果‘详情页’数据展示未进行同步修改那肯定是不能接受的。 在webview和5+的混合app模式中,也会遇到这种返回上一个页面不刷新的问题
场景
- 在页面A点击支付按钮跳转到页面B,从页面B返回时要获取当前订单的支付状态防止重复支付;
- 家校平台,因外链访问拥堵,为了防止用户点击第三方应用后一直停留在当前页面需要进行弹窗提示,当前访问人数较多,是否刷新后重试;
产生原因
浏览器前进/后退缓存
-
浏览器前进/后退缓存(Backward/Forward Cache, BF Cache);
-
BF Cache 是一种浏览器优化
- 是浏览器为了在用户页面间执行前进后退操作时拥有更加流畅体验的一种策略;
- 该策略具体表现为,当用户前往新页面时,将当前页面的浏览器DOM状态保存到
bfcache
中;当用户点击后退按钮的时候,将页面直接从bfcache
中加载,节省了网络请求的时间。
-
HTML 标准并未指定其如何进行缓存,因此缓存行为是各浏览器各自实现,所以不尽相同。 由于不是 HTTP 缓存,所以通过头文件缓存设置 no-cache 是无效的
解决方案
#方法一
try {
const bfWorker = new Worker(window.URL.createObjectURL(new Blob(["1"])));
window.addEventListener("unload", function () {
// 这里绑个事件,构造一个闭包,以免 worker 被垃圾回收导致逻辑失效
bfWorker.terminate(); //终止
});
} catch (e) {
}
#方法二 pageshow方法 onpageshow 事件在用户浏览网页时触发
const isWXAndIos = isWeiXinAndIos();
if(isWXAndIos && !window.onpageshow){
// 如果是ios,强制刷新页面,解决ios跳转到第三方应用不刷新页面
window.onpageshow = event => {
if (event.persisted) {
window.onpageshow = null;
window.location.reload();
}
};
}
#方法三 window.performance.navigation.type(该属性返回一个整数值,表示网页的加载来源)
# window.performance.navigation.type值的类型
# YPE_NAVIGATE (0):当前页面是通过点击链接,书签和表单提交,或者脚本操作,或者在url中直接输入地址,type值为0
# TYPE_RELOAD (1):击刷新页面按钮或者通过Location.reload()方法显示的页面,type值为1
# TYPE_BACK_FORWARD (2):页面通过历史记录和前进后退访问时。type值为2
# TYPE_RESERVED (255):任何其他方式,type值为255
window.addEventListener('pageshow', () => {
if (e.persisted || (window.performance &&
window.performance.navigation.type == 2)) {
location.reload()
}
}, false)
# 场景
A---------中转页面----------B
中转页面如何知道自己是从A跳过来的还是B跳过来的呢?
移动端绑定区分长按和点击事件
移动端H5没有长按事件
实现思路
通过*onTouchStart*
,*onTouchMove*
,*onTouchEnd*
三个事件来组合加上定时器判断
代码实现
export const useLongPressAndClick = (pressHandle, clickHandle) => {
const flagRef = useRef(0);
const interVal = useRef(0);
const longPress = () => {
clearInterval(interVal.current);
interVal.current = 0;
flagRef.current = 0;
pressHandle();
};
const onClick = () => {
clickHandle();
};
const touchStart = () => {
// 设置定时器
interVal.current = setInterval(() => {
flagRef.current += 1;
}, 500);
};
const touchEnd = () => {
// 这里执行点击的操作,长按和点击互不影响
if (!interVal.current){
return false;
}
if (flagRef.current) {
longPress();
} else {
clearInterval(interVal.current);
flagRef.current = 0;
interVal.current = 0;
onClick();
}
};
const touchmove = () => {
if (interVal.current) {
clearInterval(interVal.current);
interVal.current = 0;
flagRef.current = 0;
}
};
return {
touchStart,
touchEnd,
touchmove
};
};
#使用
const clickHandle = () =>{
点击
}
const pressHandle = () =>{
长按
}
const { touchStart, touchEnd, touchmove } = useLongPressAndClick(pressHandle, clickHandle);
<div onTouchStart={touchStart} onTouchMove={touchmove} onTouchEnd={touchEnd}>
</div>
获取mshareui的listView组件的滚动的位置和设置位置
export const useListViewScollTop = () => {
const listViewDOM = useRef({
listviewRef: {
ListViewRef: {
ScrollViewRef: {
}
}
}
});
// 获取滚动位置
const getScrollTop = () => {
const { ScrollViewRef } = listViewDOM.current.listviewRef.ListViewRef;
return ScrollViewRef.scrollTop;
};
// 设置滚动位置
const setScrollTop = scrollTopVal => {
const { ScrollViewRef } = listViewDOM.current.listviewRef.ListViewRef;
if (ScrollViewRef){
ScrollViewRef.scrollTo(0, scrollTopVal);
}
};
return {
listViewDOM,
getScrollTop,
setScrollTop
};
};
# 使用
const {
listViewDOM,
getScrollTop,
setScrollTop
} = useListViewScollTop();
<ListView
dataSource={dataSource}
renderSectionHeader={sectionHeader}
ref={r => (listViewDOM.current = r)}
/>
mshareui的listView如何更新数据
# dataRef.current保存列表数据,dataSource数据默认是不可见的
const changeDataSource = async ({ current }, callback) => {
const { rowID } = current;
const currRow = await callback(dataRef.current[rowID]);
// 如果有返回值是删除里一行里面的内容
if (currRow){
let changeRowIndex = '';
// 查找需要替换的数据
dataRef.current.forEach((row, index) => {
if (row[idx] === currRow[idx]){
changeRowIndex = index;
}
});
//数据替换
if (changeRowIndex || changeRowIndex === 0){
dataRef.current[changeRowIndex] = currRow;
}
refreshRowId.current = dataRef.current[rowID][idx];
setDataSource(dataSource.cloneWithRows(dataRef.current));//更新数据
} else {
// 如果没有返回值,说明删除的是改行
// 允许刷新
isDownPullRefresh.current = true;
dataRef.current.splice(Number(rowID), 1);//删除该行
setDataSource(dataSource.cloneWithRows(dataRef.current));//更新数据
}
};
mshareui的listView的rowHasChanged优化
代码
# 这是mshareui的listView的代码示例
import { ListView } from '@mshare/mshareui';
class Demo extends React.Component {
constructor(props) {
super(props);
const getSectionData = (dataBlob, sectionID) => dataBlob[sectionID];
const getRowData = (dataBlob, sectionID, rowID) => dataBlob[rowID];
const dataSource = new ListView.DataSource({
getRowData,
getSectionHeaderData: getSectionData,
rowHasChanged: (row1, row2) => row1 !== row2, //行改变的时候触发
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
});
this.state = {
dataSource,
isLoading: true,
height: document.documentElement.clientHeight * 3 / 4,
};
}
。。。。
结论
row1 !== row2
这个写法是错误的,这样写每次都是返回true
,会导致每次都更新,会导致每次都渲染;
名词解释
rowHasChanged(prevRowData, nextRowData)
:根据此定义自己的数据格式并实现rowHasChanged
,每次更改数据源行时,基本上都会为每个数据源行条目调用rowHasChanged
,然后只重新呈现要更改的行(当rowHasChanged
返回true时);
正确做法应该是去比较哪一行要修改,需要修改返回true
,否则返回false
代码示例:
const dataRef = useRef([]);//存储列表数据
const logNum = useRef(0);//计数,rowHasChanged比较到第几个
const refreshRowId = useRef('');// 用来判断当前需要刷新的行
const isDownPullRefresh = useRef(false);//是否下拉刷新或者第一次加载
const [dataSource, setDataSource] = useState(() => {
return new ListView.DataSource({
rowHasChanged: (row1, row2) => {
// 第一次加载或者下拉刷新
if (isDownPullRefresh.current){
// 如果是最后一个就更新
if (dataRef.current.length === logNum.current){
isDownPullRefresh.current = false;
logNum.current = 0;
}
logNum.current += 1;
return true;
}
// 如果当前刷新的这一行等于需要刷新的行,返回true,表示需要更新该列
if (refreshRowId.current && (row1[idx] === refreshRowId.current || row2[idx] === refreshRowId.current)){
refreshRowId.current = '';
return true;
}
if (!row1 || !row2){
return true;
}
return row1[idx] !== row2[idx];
} // rowHasChanged(prevRowData, nextRowData); 用其进行数据变更的比较
});
});