移动端的一些技能

454 阅读7分钟

刚在做OKR的一个项目,连续3天熬夜凌晨一两点肝到怀疑人生。一上线发现 bug,解决一下,再上线,我屮艸芔茻,竟然没有改对,还是有问题。我整个人就是下面的动图状态,哭爹喊娘,身后空无一人(基本一个项目一个前端)。只能硬着头皮分析问题,看代码。

41bf4b929ad69e8f433e40892e46cd55.gif

之所以出现这种反复上线的问题,主要的原因是时间紧,任务重,测试不充分,还有就是自身能力不够,🙃 不能迅速及时对问题做出正确、快速的应对。第一次在互联网公司受到了现实的“毒打”。

既然苦都吃了,总得学着成长。所以用这篇文章记录一下,让自己长点记性。

上中下布局

前端面试有一个流行面试题目:是实现左、中、右布局,左侧和右侧固定,中间部分自适应。在做 OKR 项目中,我发现 UI 同学给的页面有一种很明显的特点就是上、中、下布局,上部分页面一般固定在顶部,下部分固定在页面底部,中间部分是随着内容的增多会出现滚动条,这种布局。哈哈哈,如果有幸成为面试官,我会出一道这种题目,来看下面试者的 CSS 能力。

image.png

主要是2点:

  1. 使用 flex 布局实现,中间部分设置flex: 1; ,上、下使用固定高度;实现页面整体布局。
  2. 最外层 container 设置height: 100vh; overflow: hidden;; 中间部分的overflow: auto; 即可。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>中间内容自适应</title>
</head>
<body>
  <div class="demo-container">
    <div class="demo-container__head border">头部标题固定</div>
    <div class="demo-container__content">
      <div class="card card-item">card1</div>
          ...
      <div class="card card-item">card6</div>
    </div>
    <div class="demo-container__footer border">
      <button type="button" class="btn btn-primary">提交</button>
    </div>
  </div>
</body>
</html>
<style>
  .demo-container{
    height:100vh;
    overflow: hidden;
    background-color: #F6F6F6;
    display: flex;
    flex-direction: column;
  }
  .demo-container__head{
    height: 66px;
    background-color: #FFFFFF;
  }
  .demo-container__footer{
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: #FFFFFF;
    padding: 10px 16px; 
  }
  .demo-container__content{
    flex: 1;
    overflow: auto;
  }
  .card-item{
    width: 100%;
    height: 400px;
    margin-top: 10px;
  }
</style>

image.png

自定义字体

项目中如果 UI 同学指定了想使用某种字体的时候,前端最好不要说 不,说“不”显得我们不专业,我们要说 没问题。哈哈哈哈~ 那么怎么实现项目中使用自定义字体呢?主要分两步:

  1. 先下载该字体库的字体。下载地址:www.fontke.com/ ,搜索对应的字体,比如:SF Pro Rounded

image.png

image.png

  1. 一般项目中只是某几个字需要使用上述字体;还需要使用 fontmin(ecomfe.github.io/fontmin/) 抽取出想要的字集

image.png

点击生成即可得到相应的字集:

image.png

根据项目对浏览器的兼容情况,大家按需使用即可:

image.png

iphon X 适配

image.png

iPhone X 需要适配的地方有两个一个是顶部的齐刘海,还有一个是底部的黑色的线。对于顶部齐刘海浏览器已经为我们做了适配。对于底部的黑色的线要做的适配通常需要两步:

第一步:设置 viewport-fit=cover, 使得网页内容占据整个手机屏幕可视区域,效果如下:

image.png

<meta name='viewport'  content="width=device-width, viewport-fit=cover"  />

第二步:通过设置 padding 使得网页内容在黑色线条之上正常显示:

image.png

body {
  /* 适配齐刘海*/
  padding-top: constant(safe-area-inset-top);  
  padding-top: env(safe-area-inset-top);  
 /* 适配底部黑条*/
  padding-bottom: constant(safe-area-inset-bottom);
  padding-bottom: env(safe-area-inset-bottom);  
}

juejin.cn/post/684490…

bobtung.medium.com/%E9%9D%A2%E…

判断是否为空

周五上完最后一波儿线后,前方用户竟然还发现了 bug,新 bug,惊不惊喜,意不意外,没有按时下班走成。我大致看了一眼,是少了非空的判断,加一下就 OK 啦,再上线,我擦,复用了界面,数据为空的话,没有重新赋值,这就导致了脏数据的问题,继续改啊,继续上线啊,最终解决了 bug,安心下班了。

按照我之前写代码的习性,我都会做非空判断,唯独这个接口没有,为什么呢?因为按理来说,有了 ID,根据 ID 去查后端数据的话,肯定是有值的,然鹅,偏偏后端偶现了有数据却查询失败的情况,无奈啊,前端提示“系统异常”,F12 打开后:

image.png

这么小概率的 bug 都遇到了,我深感自己写代码一定一定得严谨,不能忽视 0.001% 可能存在的情况。

如果在写代码的时候偷懒,后面生产出了问题,还要看代码,解决 bug,花费的时间和精力更多,还不如一开始就不要怕麻烦,不要省事,认认真真写好代码。

所以在查询数据的时候一定要先判断数据是否为 ;在 try 代码后加catch,不要影响页面后续操作。

页面返回定位到离开时的位置

三种高度(宽度)

详细说明:

  • clientHeight: 包括 padding, 但是不包括水平 scrollbar 的高度、border 和 margin;
    clientHeight = content + padding

  • offsetHeight:包括 padding、border、水平 scrollbar 的高度。
    offsetHeight = content + padding + border + scrollbar height

  • scrollHeight:DOM 元素实际内部的总高度

可以看出offsetHeightclientHeight包括了 border 和 scrollbar height 更能反映出 DOM 的实际大小。

到这里大家可能会想起来 style 样式的height,那 style 样式里面的 height 指的是什么呢?这里的 height 根据 box-sizing 的不同取值表示不同的含义。盒模型传送门

一个关于三个高度计算的例子,注意例子中 box-sizing 是默认的取值 content-box

英文版本解释

scroll 的一些属性

image.png

截图来自《高程》,比较形象的说明了scrollTop\scrollLeft 的含义了。

scrollTop 表示隐藏内容到滚动条的距离;在页面离开的时候,先记录下该值,页面返回的时候将滚动条移动到该值的位置,就可以实现离开页面是什么位置回来还在那里的效果了。

scroll 的一些方法

scrollBy(options?: ScrollToOptions): void;
scrollBy(x: number, y: number): void;

scrollTo(options?: ScrollToOptions): void;
scrollTo(x: number, y: number): void;

scrollIntoView(arg?: boolean | ScrollIntoViewOptions): void;

scrollBy 和 scrollTo 使用方法类似,作用于有滚动条的元素上,将滚动条滚动到指定位置;

scrollIntoView 方法会滚动元素的父容器,使被调用scrollIntoView()的元素对用户可见。

// 方式一:
const dom = document.getElementById('scrollWrap')
dom.scrollTo({ 
  top: 420, 
  behavior: "smooth" 
});

// 方式二:
dom.scrollBy(0, 420)

// 方式三:
const card2 = document.getElementById('card2')
card2.scrollIntoView()

scrollTop 是一个可读可写的属性,也可以直接设置 scrollTop 让滚动条到指定位置

const dom = document.getElementById('scrollWrap')
dom.scrollTop = 420

scroll 会失效的问题

发现一个 scroll 比较奇怪的点,你所不知道的scroll事件:为什么scroll事件会失效?

safari 返回不会重新加载数据

OKR 项目中,由于复盘使用了旧系统;点击复盘按钮跳入旧系统,然后从旧系统再返回回来。测试同学发现,已经有了复盘内容,但是页面并没有显示出来。进行分析后发现,旧系统采用router.back() 函数返回的,也就是window.history.back() 返回的。

在 Chrome 浏览器,window.history.back()是会触发 window.load 事件的;然鹅在 Safari下,window.history.back() 并不会触发 window.load 事件,这就导致数据不是最新的,还是之前缓存的数据。

Google 了一波儿后,发现手机上的浏览器有一个特性,名叫“往返缓存”(back-forward cache,或bfcache),可以在用户使用浏览器的“后退”和“前进”按钮时加快页面的转换速度。这个缓存中不仅保存着页面数据,还保存了DOM和JavaScript的状态;实际上是将整个页面都保存在了内存里。如果页面位于bfcache中,那么再次打开该页面就不会触发load事件。

解决方法: 监听 window 的pageshow 事件,如果是来自 cache,则执行 window 的reload方法。

const onPageShow = (event: any)=>{
   if (event.persisted) {
     window.location.reload() 
   }
}

window.addEventListener('pageshow', onPageShow)

需要注意的一点是:单纯的 Vue2 和 Vue3 中在createdsetup 中绑定 window 的pageshow 是没有问题的;但是项目中往往会使用Vue Router,这个时候还在 Vue 页面组件的生命周期中注册 pageshow 是不会生效,要在实例化 APP 之前进行 pageshow 事件的注册。如下:

const onPageShow = (event)=>{
  if (event.persisted) {
    window.location.reload() 
  }
}

window.addEventListener('pageshow', onPageShow)

createApp(App).use(store).use(router).mount('#app')

Vue Router 4 + keep-alive

在 Vue Router 4 中使用keep-alive 的正确姿势:

利用 include 页面组件 Contact 的 name 一定要写成 “Contact” 与 include 后面的字符串保持一致。

  <router-view v-slot="{ Component }">
    <keep-alive include="Contact">
      <component :is="Component" />
    </keep-alive>
  </router-view>

或者利用 meta: {keepAlive: true}, 统一设置:

<router-view v-slot="{ Component, route }">
  <template v-if="route.meta.keepAlive">
    <keep-alive>
      <component :is="Component" />
    </keep-alive>
  </template>
  <template v-else>
    <component :is="Component" />
  </template>
</router-view>
const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  {
    path: '/contact/:id',
    name: 'Contact',
    meta: {
      keepAlive: true
    },
    component: () => import('../views/Contact.vue'),
  }
]