恰逢今天是1024程序员节,祝各位程序员小哥哥小姐姐节日快乐,BUG多多,填坑多多😏😏😏~~~~ 话说,凭实力写的BUG,为什么要改呢❓
没错,我这个填坑小能手又来填坑啦~~ 这段时间又回到了h5的坑里,相对于已经没有IE的pc端来说,h5反而更像是当年pc端IE时代,单是安卓IOS两端兼容就会有很多问题,更别说各个系统版本、各个浏览器版本的奇怪问题了。今天暂满依旧不讨论那些太高深的问题,基础填坑走起。
一、1px边框问题
这个可以说是老生常谈问题了,如果要求不严格,那么直接1px也未尝不可,但是毕竟咱们是严谨的程序员(UI不同意),这里就再次赘述一下,直接上代码。
css
.button {
position:relative;
width:300px;
height:300px;
}
.button:before {
content:"";
box-sizing:border-box;
position:absolute;
top:0;
left:0;
right:0;
bottom:0;
font-size:0;
line-height:0;
border-radius:0;
border:1px solid #F80B0F;
-webkit-transform-origin:0 0;
transform-origin:0 0;
}
@media only screen and (-webkit-min-device-pixel-ratio:1.5){
.button:before {
width:150%;
-webkit-transform:scale(0.66666667);
transform:scale(0.66666667);
}
}
@media only screen and (-webkit-min-device-pixel-ratio:2){
.button:before {
width:200%;
-webkit-transform:scale(0.5);
transform:scale(0.5);
}
}
@media only screen and (-webkit-min-device-pixel-ratio:3){
.button:before {
width:300%;
-webkit-transform:scale(0.3333);
transform:scale(0.33333);
}
}
二、自定义单选框多选框边框重叠问题
开发使用的是vant,虽然有自带的单选多选,但是出于UI样式的问题,样式需要重新写,于是就出现了边框重叠问题。
如果单单是每个元素边框变色,那么衔接位置会很明显的出现两条边线,效果很不好;如果单纯通过margin负值方式处理,对于单选多选还会出现兼容问题。于是这里采用了边框和阴影结合处理方式。
css
// 单选框
<van-radio-group v-model="indexData.remarkForm['customerValue' + index]">
<van-radio class="user_remark_radio" :class="indexData.remarkForm['customerValue' + index] === radio.value ? 'user_remark_radio_select' : ''" v-for="radio in item.radioOptions" :name="radio.value">
{{ radio.label }}
</van-radio>
</van-radio-group>
// 多选框
<van-checkbox-group v-model="indexData.remarkForm['customerValue' + index]">
<van-checkbox class="user_remark_checkbox" :class="indexData.remarkForm['customerValue' + index].indexOf(check.value) !== -1 ? 'user_remark_checkbox_select' : ''" v-for="check in item.checkboxOptions" :name="check.value">
{{ check.label }}
</van-checkbox>
</van-checkbox-group>
// css样式
.user_remark_radio,
.user_remark_checkbox{
padding: 10px 12px;
background: #FFFFFF;
border: 1px solid #E5E5E5;
border-top: 0px;
&:first-child{
border-top: 1px solid #E5E5E5;
}
.radio_img_icon{
width: 20px;
height: 20px;
margin-right: 2px;
}
}
.user_remark_radio_select,
.user_remark_checkbox_select{
background: #FFFAF7;
border-color: #FFC395;
box-shadow: 0 -1px 0 0 #FFC395;
&:first-child{
border-top: 1px solid transparent;
}
}
三、页面有input必填校验,但是点击页面其他元素不需要校验input必填
页面input输入框有必填校验,通常都会在失焦的时候进行校验。但是这样有个问题就是,在输入框获取焦点未填值的时候,点击页面任何东西都会进行校验,在某些情况下,并不需要进行校验,网上查了有添加native的,又通过mousedown方法的,都没效果。于是这里采取了添加变量方式控制。
ini
<van-field
ref="createMoneyRef"
v-model="dataObj.formData.amount"
label="¥"
placeholder="请输入金额"
type="number"
@blur="dataObj.isBlur && inputMoneyChange($event)"
/>
<button @click='changeFun'>不需要校验必填</button>
<script setup>
const dataObj = reactive({
formData: {
amount: ''
},
isBlur: true
}
const changeFun = ()=>{
dataObj.isBlur = false
// 其他逻辑
}
</script>
四、ios移动端底部fixed按钮被键盘顶起
当页面有输入框,并且底部有fixed定位按钮时,ios端按钮会被弹出的键盘顶起,可能会造成误操作,因此需要进行处理。
因为ios在输入法弹出的时候,页面高度实际会发生变化,可以通过监听resize方式进行控制。
ini
// 输入框
<van-field
class="create_money_field"
v-model="dataObj.formData.description"
label="说明"
placeholder="请输入说明"
input-align="right"
rows="1"
autosize
type="textarea"
/>
// 底部fixed按钮
<div class="create_page_button" v-if="dataObj.btnFixed">
<span @click="viewCustom">预览顾客端</span>
</div>
<script setup>
const dataObj = reactive({
btnFixed: true,
windowHeight: 0
})
const resizeWin = ()=>{
if(window.innerHeight < dataObj.windowHeight){
dataObj.btnFixed = false
} else {
dataObj.btnFixed = true
}
}
onMounted(()=>{
dataObj.windowHeight = window.innerHeight
window.addEventListener('resize', resizeWin)
})
onBeforeUnmount(()=>{
window.removeEventListener('resize', resizeWin)
})
</script>
五、输入框输入时,输入法出现"搜索"字样
给input输入框外层加上一层form
表单即可
ini
<form action="/">
<van-search
class="search_input"
ref="searchInput"
v-model="dataObj.searchVal"
show-action
placeholder="请输入名称"
@search="onSearch"
@cancel="onCancel"
@update:model-value="onChange"
/>
</form>
六、h5进入页面是输入框获取焦点并且自动弹出输入法
这个问题可以说是比较坑的了,尤其是ios端。对于点击跳转的页面,可以实现部分系统自动弹出;对于直接打开的页面完全不能自动弹出输入框(暂未找到合适方法,有知道的小伙伴可以留言解答下,感谢~~
)
这里说两种网上说的可用的方法,但是亲测,并不是所有的ios都能够实现自动唤起输入法,可以说大部分ios都不能正常唤起。
方法一:
ini
<van-search
class="search_input"
ref="searchInput"
v-model="dataObj.searchVal"
show-action
placeholder="请输入名称"
@search="onSearch"
@cancel="onCancel"
@update:model-value="onChange"
/>
onMounted(()=>{
if(searchInput && searchInput.value){
searchInput.value.focus()
let inp = document.getElementsByClassName('van-field__control')[0];
inp.focus();
}
})
方法二:跳转页面前,采用点击唤起输入法方式提前触发
javascript
// 首页
<input class="van-field__control" />
<span class="index_nav_right" @click="goSearch()">搜索</span>
// 搜索
const goSearch = ()=>{
setTimeout(()=>{
let inp = document.getElementsByClassName('van-field__control')[0];
inp.focus();
router.push({
name: 'Search'
})
}, 20)
}
// css
.van-field__control{
width: 0;
height: 0;
}
七、van-pull-refresh和vant-list上下滑动冲突问题
当在一个页面中同时使用两个组件,并且vant-list是局部滚动的时候,这个问题尤其明显,会导致vant-list只能向上滑动,当向下滑动的时候,就会触发refresh,导致列表部分无法正常滚动。对此vant组件也没给出什么好的解决方法。
对于网上多数的方法,都是通过监听vant-list的滚动距离,当有滚动的时候,就把refresh的下拉刷新方法disabled掉,当滚动到顶部的时候再打开。但是这又引发了另一个问题,就是list列表不滚动到顶部,就无法触发下拉刷新,对于整页的滚动,这种交互是没问题的,但是对于局部滚动,这种效果是很不友好的。
针对这种局部滚动上拉加载更多+下拉刷新的问题并没有找到合理的处理方法,目前采用的方式是修改了交互,改为整页滚动,交互没问题。有相关处理经验的小伙伴,望予以指点~
八、h5端实现拍照或选择图片上传
因为我们的h5是嵌入在app里的,开始我觉得这个方法就是app端实现,h5端调用相关方法就可以了(实际也证明,这种方法更合理),但是基于之前的开发,相关功能都是h5自己实现的,app没有暴露相关方法,只能硬着头皮上了。
这里依旧使用vant的van-uploader
方法,点击按钮在移动端就会出现拍照和上传图片的选项,进行相关的图片压缩上传就可以了。还需要注意一点,ios端拍照会有个默认旋转问题,就是正常拍照之后,图片会自己旋转一定的角度,这个需要特殊处理下(目前只有在ios13.4之前
有这种问题),直接上代码吧,毕竟也是copy来的😏
xml
<van-uploader v-model="dataObj.formData.imgUrl" reupload max-count="1" :preview-full-image="false" :after-read="onImgRead" accept="image/*">
<span>
<i>拍照</i>
</span>
</van-uploader>
<script setup>
import Exif from 'exif-js'
const dataObj = reactive({
formData: {
imgUrl: []
},
imgData: {
name: '',
type: '',
imgFile: null,
imgObj: null
}
})
// 拍照上传
const onImgRead = async (file)=>{
console.log('upfile', file)
dataObj.imgData.name = file.file.name; // 获取文件名
dataObj.imgData.type = file.file.type; // 获取类型
dataObj.imgData.imgFile = file.file; // 文件流
imgPreview(dataObj.imgData.imgFile);
}
// 处理图片
const imgPreview = (file) => {
let Orientation;
//去获取拍照时的信息,解决拍出来的照片旋转问题
Exif.getData(file, function () {
Orientation = Exif.getTag(this, "Orientation");
});
// 看支持不支持FileReader
if (!file || !window.FileReader) return;
if (/^image/.test(file.type)) {
// 创建一个reader
let reader = new FileReader();
// 将图片2将转成 base64 格式
reader.readAsDataURL(file);
console.log('reader', reader)
// 读取成功后的回调
reader.onloadend = function () {
let result = this.result;
let img = new Image();
img.src = result;
//判断图片是否小于1M,是就直接上传,反之压缩图片
if (this.result.length <= 1 * 1024 * 1024) {
dataObj.imgData.imgObj = this.result;
postImg();
} else {
img.onload = function () {
let data = compress(img, Orientation);
dataObj.imgData.imgObj = data;
postImg();
};
}
}
}
}
// 压缩图片
const compress = (img, Orientation) => {
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
//瓦片canvas
let tCanvas = document.createElement("canvas");
let tctx = tCanvas.getContext("2d");
// let initSize = img.src.length;
let width = img.width;
let height = img.height;
//如果图片大于四百万像素,计算压缩比并将大小压至400万以下
let ratio;
if ((ratio = (width * height) / 4000000) > 1) {
// console.log("大于400万像素");
ratio = Math.sqrt(ratio);
width /= ratio;
height /= ratio;
} else {
ratio = 1;
}
canvas.width = width;
canvas.height = height;
// 铺底色
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
//如果图片像素大于100万则使用瓦片绘制
let count;
if ((count = (width * height) / 1000000) > 1) {
// console.log("超过100W像素");
count = ~~(Math.sqrt(count) + 1); //计算要分成多少块瓦片
// 计算每块瓦片的宽和高
let nw = ~~(width / count);
let nh = ~~(height / count);
tCanvas.width = nw;
tCanvas.height = nh;
for (let i = 0; i < count; i++) {
for (let j = 0; j < count; j++) {
tctx.drawImage(img, i * nw * ratio, j * nh * ratio, nw * ratio, nh * ratio, 0, 0, nw, nh);
ctx.drawImage(tCanvas, i * nw, j * nh, nw, nh);
}
}
} else {
ctx.drawImage(img, 0, 0, width, height);
}
console.log('Orientation====>', Orientation)
//修复ios上传图片的时候 被旋转的问题,ios13.4之后可以自动回正
var str= navigator.userAgent.toLowerCase();
var ver=str.match(/cpu iphone os (.*?) like mac os/);
if (ver && ver.length && ver[1].replace(/_/g,".") < '13.4' && Orientation != "" && Orientation != 1) {
switch (Orientation) {
case 6: //需要顺时针(向左)90度旋转
rotateImg(img, "left", canvas);
break;
case 8: //需要逆时针(向右)90度旋转
rotateImg(img, "right", canvas);
break;
case 3: //需要180度旋转
rotateImg(img, "right", canvas); //转两次
rotateImg(img, "right", canvas);
break;
}
} else {
ctx.drawImage(img, 0, 0, width, height);
}
//进行最小压缩
let ndata = canvas.toDataURL("image/jpeg", 0.1);
tCanvas.width = tCanvas.height = canvas.width = canvas.height = 0;
return ndata;
}
// 旋转图片
const rotateImg = (img, direction, canvas) => {
//最小与最大旋转方向,图片旋转4次后回到原方向
const min_step = 0;
const max_step = 3;
if (img == null) return;
//img的高度和宽度不能在img元素隐藏后获取,否则会出错
let height = img.height;
let width = img.width;
let step = 2;
if (step == null) {
step = min_step;
}
if (direction == "right") {
step++;
//旋转到原位置,即超过最大值
step > max_step && (step = min_step);
} else {
step--;
step < min_step && (step = max_step);
}
//旋转角度以弧度值为参数
let degree = (step * 90 * Math.PI) / 180;
let ctx = canvas.getContext("2d");
switch (step) {
case 0:
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0);
break;
case 1:
canvas.width = height;
canvas.height = width;
ctx.rotate(degree);
ctx.drawImage(img, 0, -height);
break;
case 2:
canvas.width = width;
canvas.height = height;
ctx.rotate(degree);
ctx.drawImage(img, -width, -height);
break;
case 3:
canvas.width = height;
canvas.height = width;
ctx.rotate(degree);
ctx.drawImage(img, -width, 0);
break;
}
}
//将base64转换为文件
const dataURLtoFile = (dataurl) => {
let arr = dataurl.split(",")
let bstr = atob(arr[1])
let n = bstr.length
let u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], dataObj.imgData.name, {
type: dataObj.imgData.type
});
}
// 上传图片
const postImg = () => {
let file = dataURLtoFile(dataObj.imgData.imgObj);
let formData = new window.FormData();
formData.append("file", file);
axiosHttp.uploadImg(formData).then((res)=>{
if(res && res.code === 200 && res.data){
dataObj.formData.imgUrl = [{url: res.data}]
} else {
setTimeFailToast(res.message || '上传失败')
}
}).catch((err)=>{
console.log('img upload err==>', err)
})
}
</script>
九、页面浏览时长统计
这部分是涉及到了监听页面浏览时长并上报,涉及pc端、h5嵌入app端、h5在微信/支付宝端。这里就不说开发的过程了,真的是简直了......直接给出结论:
PC端:
监听 window 的 visibilitychange 事件:可以统计页面显示隐藏、tab切换,但是不能统计到页面被覆盖
监听 window 的 focus/blur 事件:可以统计页面显示隐藏、tab切换、页面覆盖。但是问题是如果页面不被激活,就无法进行初始记录。
针对需求,此处采用的是focus/blur方法:
scss
// 页面失去焦点上报(包括页面隐藏。tab切换)
const windowBlurHandler = ()=>{
let outTime = new Date().getTime()
if((outTime - dataObj.enterTime)/1000 >= 0.01){
// 上报埋点
}
}
// 页面获取焦点开始计时
const windowFocusHandler = ()=>{
dataObj.enterTime = new Date().getTime()
}
onBeforeMount(()=>{
dataObj.enterTime = new Date().getTime()
window.addEventListener("blur", windowBlurHandler)
window.addEventListener("focus", windowFocusHandler)
})
onBeforeUnmount(()=>{
if(dataObj.enterTime){
let outTime = new Date().getTime()
if((outTime - dataObj.enterTime)/1000 >= 0.01){
// 上报埋点
}
};
window.addEventListener("blur", windowBlurHandler)
window.addEventListener("focus", windowFocusHandler)
})
h5内嵌app
监听 document 的 visibilitychange 事件,可以统计息屏、退到后台、页面覆盖。
h5在微信和支付宝端
安卓支付宝:
document.pause:获取隐藏、息屏
document.back:获取左上角返回,并且需要通过 e.preventDefault() 阻止默认关闭事件,然后通过 AlipayJSBridge.call('popWindow') 手动关闭,否则埋点数据无法正常上报。
无法获取左上角x号关闭事件
安卓微信:document的 visibilitychange: 能抓取后台、物理返回、左上角关闭
iOS支付宝:document.pause:获取隐藏、息屏
document.back:获取左上角返回,并且需要通过e.preventDefault() 阻止默认关闭事件,然后通过 AlipayJSBridge.call('popWindow') 手动关闭,否则埋点数据无法正常上报。
无法获取左上角x号关闭事件
iOS微信:document的 visibilitychange:能抓取后台、息屏。无法抓取物理返回、左上角关闭
h5的监听方法和pc方法基本一致,具体代码就不写了,可以参考pc端。
十、vue的keep-alive不生效
这是一个pc短的问题,在此简单记录一下。众所周知keep-alive的作用,但是在使用element 的 el-tab-pane
组件时,每次切换tab页面都会重新刷新一次,即使页面在keep-alive内部,依旧不生效。起初代码是这样子的
ini
// 部分代码
<el-tabs
v-if="$route.meta.isTab"
v-model="commonStores.mainTabsActiveName"
:closable="true"
@tab-change="selectedTabHandle"
@tab-remove="removeTabHandle">
<el-tab-pane
v-for="item in commonStores.mainTabs"
:key="item.name"
:label="item.title"
:name="item.name">
<el-card :body-style="siteContentViewHeight">
<iframe
v-if="item.type === 'iframe'"
:src="item.iframeUrl"
width="100%" height="100%" frameborder="0" scrolling="yes">
</iframe>
<router-view v-else v-slot="{ Component }">
<keep-alive>
<component v-if="$route.meta.keep" :key="$route.name" :is="Component" />
</keep-alive>
</router-view>
</el-card>
</el-tab-pane>
</el-tabs>
网上查阅了不少文章,写法上也都没有问题。各个页面也都单独道出了name进行尝试,依旧是不生效,直到一句话给了提示。
上面的写法是把card放在了tab内部,这样每次切换tab相当于重新创建了一个tabpane组件,里边并没有已经缓存的内容,每次都会重新加载,导致keep-alive不生效,可以改成下面的写法。
ini
<el-tabs
v-if="$route.meta.isTab"
v-model="commonStores.mainTabsActiveName"
:closable="true"
@tab-change="selectedTabHandle"
@tab-remove="removeTabHandle">
<!-- TabPane只用来切换当前tab的值显示组件
如果将tab-content内容也放到tabPane里,就会出现第一次无法被缓存情况
因为每点击tab一次,就会生成一个新的tabPane组件,新tabPane里就没有前面的组件缓存,导致点击tab切换时还是会刷新 -->
<el-tab-pane
v-for="item in commonStores.mainTabs"
:key="item.name"
:label="item.title"
:name="item.name">
</el-tab-pane>
<el-card :body-style="siteContentViewHeight">
<router-view v-slot="{ Component }">
<keep-alive :exclude="excludeRoute">
<component v-if="$route.meta.keep" :key="$route.name" :is="Component" />
</keep-alive>
</router-view>
</el-card>
</el-tabs>
好了,坑又填完了,最后再次预祝各位程序猿(程序媛)节日快乐,永无BUG~~~