消灭一切BUG,大功告橙🍊

恰逢今天是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~~~

相关推荐
Bug缔造者4 小时前
Element-ui el-table 全局表格排序
前端·javascript·vue.js
xnian_5 小时前
解决ruoyi-vue-pro-master框架引入报错,启动报错问题
前端·javascript·vue.js
罗政5 小时前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端
阿树梢6 小时前
【Vue】VueRouter路由
前端·javascript·vue.js
随笔写7 小时前
vue使用关于speak-tss插件的详细介绍
前端·javascript·vue.js
陈小唬8 小时前
html页面整合vue2或vue3
前端·vue.js·html
花下的晚风8 小时前
Vue实用操作篇-1-第一个 Vue 程序
前端·javascript·vue.js
我是Superman丶10 小时前
【前端UI框架】VUE ElementUI 离线文档 可不联网打开
前端·vue.js·elementui
growdu_real11 小时前
pandoc自定义过滤器
vue.js
天下无贼!11 小时前
2024年最新版TypeScript学习笔记——泛型、接口、枚举、自定义类型等知识点
前端·javascript·vue.js·笔记·学习·typescript·html