移动基础

289 阅读12分钟

像素

分辨率

屏幕分辨率指一个屏幕具体由多少个像素点组成。

image-20211014150105761

iPhone 13 Pro iPhone 13 Pro Max的分辨率分别为2532 x 11702778 x 1284。这表示手机分别在垂直和水平上所具有的像素点数。

当然分辨率高不代表屏幕就清晰,屏幕的清晰程度还与尺寸有关。

物理像素

物理像素(physical pixel)又叫做设备像素(dp:device pixel)。

image-20211014151350066

如图所示,每个方块用RGB三原色来表示一个物理像素点,通过控制像素点的明亮程度来显示我们想要的图形。

由于设备屏幕分辨率并不统一,所以我们开发使用 css 像素。

CSS像素

css 像素又称逻辑像素(logical pixel)或者设备独立像素(dip:device independent pixel)。

实际开发中使用的像素。

.box{
    width:200px;
    height:200px;
}

物理像素和 css 像素关系

image-20211014152252983

如图所示:

  • 普通屏,css 像素和物理像素是一对一的关系;
  • Retina屏,一个 css 像素需要 4 个物理像素来描述。

浏览器会自动推算出一个c ss 像素对应几个物理像素,不需要手动计算。

设备像素比

设备像素比(dpr:device pixel ratio)。

dpr = 设备像素 / css 像素 (缩放比是1的情况下)。

如上图可得,普通屏 dpr 1 ,Retina屏 dpr 2。

dpr = 2 表示 1个 css 像素用 2 x 2 个物理像素来绘制。

console.log(window.devicePixelRatio); //获取设备像素比

缩放

缩放改变的是 css 像素的大小。

1 css 像素对应 1 物理像素的情况下:

  • 放大为原来 2 倍,一个 css 像素对应 2 x 2 个物理像素;
  • 缩小为原来的 1/2 ,2 x 2 个 css 像素才能表示一个物理像素。

PPI

ppi ( ppi: pixels per inch),每英寸的物理像素点。

dpi (dpi: dots per inch) ,每英寸的点。

image-20211014155321329

viewport

width=device-width 很多时候和initial-scale=1.0是等效的,不过为了兼容性,我们需要一起写上。

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

user-scalable=no等同于maximum-scale=1,minimun-scale=1,禁止用户缩放,为了兼容性一般一起写上。

<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no,maximum-scale=1,minimun-scale=1" />

获取设备宽度

console.log(window.innerWidth);
console.log(document.documentElement.clientWidth);
console.log(document.documentElement.getBoundingClientRect().width);
console.log(screen.width); // 兼容性差,有的浏览器得到的值是物理像素的。
//一般这么使用
var viewWidth = document.documentElement.clientWidth || window.innerWidth;

媒体查询

什么是媒体查询

@media screen and (min-width:900px){
    body{
        background-color:red;
    }
}

为什么需要媒体查询

  • 一套样式不可能适应各种大小的屏幕;
  • 针对不同的屏幕大小写样式;
  • 让我们的页面在不同大小的屏幕上都能正常显示。

媒体类型

  • all(defalut)
  • screen / print / speech(屏幕阅读器)
/*默认all,不需要加and*/
@media (min-width:900px){
    body{
        background-color:red;
    }
}
/*print*/
@media print and (min-width:900px){
    body{
        background-color:red;
    }
}

媒体查询中的逻辑

与(and)/ 或(,)/ 非(not)

/*and*/
@media screen and (min-width:900px) and (max-width:1024px){
    body{
        background-color:red;
    }
}

/*,*/
@media screen and (min-width:1024px),(max-width:900px){
    body{
        background-color:red;
    }
}

/* not 和 and 联用,指对整体取反*/
@media not screen and (min-width:1024px) and (max-width:900px){
    body{
        background-color:red;
    }
}

/* not 和 and 一体,逗号后面自成一体 */
@media not screen and (min-width:1024px),(max-width:900px){
    body{
        background-color:red;
    }
}

媒体特征表达式

  • width/max-width/min-width;
  • -webkit-device-pixel-ratio/-webkit-max-device-pixel-ratio/-webkit-min-pixel-ratio;
  • orientation(定向);
    • landspace(横屏)/portrait(竖屏);
  • height(从来不用);
  • device-width/device-height(有可能获取物理像素,不建议使用);
  • screen.width/screen.height(有可能获取物理像素,不建议使用);
  • aspect-ratio(视口的宽高比)(很少用)。
@media screen and (max-width:900px){
    body{
        background-color:red;
    }
}

媒体查询策略

断点

  • xs: < 576px;
  • sm: 576px ~ 768px;
  • md: 768px ~ 992px;
  • lg: 992px ~ 1200px;
  • xl: > 1200px.

断点怎么来的

改变屏幕大小,当页面显示不正常的时候,你就需要设置断点了。

/* 第一种写法 */
@media (max-width:576px){
    .col{
        width:100%;
    }
}
@media (min-width:577px) and (max-width:768px){
     .col{
        width:50%;
    }
}
@media (min-width:769px) and (max-width:992px){
    .col{
        width:25%;
    }
}
@media (min-width:993px) and (max-width:1200px){
    .col{
        width:16.666666667%;
    }
}
@media (max-width:1201px){
     .col{
        width:8.3333333333%;
    }
}

/* pc first */
.col{
    width:8.333333333%;
}
@media (max-width:1200px){
     .col{
        width:16.666666667%;
    }
}
@media (max-width:992px){
     .col{
         width:25%;
    }
}
@media (max-width:768px){
     .col{
        width:50%;
    }
}

@media (max-width:576px){
     .col{
        width:100%;
    }
}

/* mobile first */
.col{
    width:100%;
}
@media (min-width:576px){
     .col{
        width:50%;
    }
}
@media (min-width:768px){
     .col{
        width:25%;
    }
}
@media (min-width:992px){
     .col{
         width:16.666666667%;
    }
}
@media (min-width:1200px){
     .col{
        width:8.333333333%;
    }
}

移动端常用单位

  • px css 像素;
  • % 百分比可以自适应;
  • em 元素自身的 font-size大小;
  • rem root元素的font-size的大小,在html中就是指html元素的font-size
  • vw 视口宽度,1vw指视口宽度的百分之一;
  • vh 视口高度,1vw指视口高度的百分之一。

等比例放大缩小

height

  • 使用px

    1. 375px -> 100%width(375px) x 给定的高height50px;
    2. 750px -> 100%width(750px) x 给定的高也要等比例增加height100px;
    3. 视口宽度 = document.documentElement.clientWidth;
    4. height = (document.documentElement.clientWidth / 375 ) * 50px;
    5. height = (document.documentElement.clientWidth / 750 ) * 100px;
  • 使用rem

    1. width375px -> 指定1rem = 20px,可以随便指定,只要方便计算

      height = 50px / 20px = 2.5rem;

    2. width750px -> 1rem = 40px 宽度扩大一倍,rem代表的像素等比例扩大一倍

      height = 100 / 40 = 2.5rem;

    3. 视口宽度 = document.documentElement.clientWidth;

    4. 1rem = (document.documentElement.clientWidth / 375 ) * 20px ;

    5. 1rem = (document.documentElement.clientWidth / 750 ) * 40px ;

响应式布局

什么是响应式布局

2010年5月由 Ethan Marcotte提出的一个概念,一个网站兼容多种终端。

对不同屏幕尺寸(大小)做出响应,并进行相应布局的一种移动Web开发方式。

响应式布局的原理

通过媒体查询设置断点,不同的断点代表不同屏幕尺寸的范围。我们在不同的断点下来写相应的css样式来实现不一样的布局效果。

image-20211015150759956

栅格布局

优点:不用为每种终端开发一个网站,一个网站能够兼容多种终端。

缺点:

  1. 需要兼容各种终端,工作量大,效率低下;
  2. 为了所有终端开发的代码累赘,加载时间加长,在特定终端会出现无用代码。

适用:展示性质的网站,结构比较简单的页面;

不适用:淘宝等 功能性页面,结构复杂、交互多的页面。

移动端屏幕适配

使页面在移动端各种大小的屏幕上都能够正常显示的一种移动端开发方案。

与响应式布局的关系

相同点:适应各种屏幕尺寸;

不同点:

移动端适配响应式布局
终端仅移动端PC端和移动端
常用单位宽高:rem / %宽:% 高、字体:px
宽和高宽和高都随着屏幕大小变化等比例宽度变化,高度不变非等比例

简单适配

<meta name="viewport" content="width=device-width, initial-scale=1.0,maximum-scale=1,minimum-scale=1,user-scalable=no" />

核心原理是获取viewport的宽度,除以一个系数。

  1. 1remhtml元素的font-size
  2. 设计稿宽度为 750px ,高度为 100px,这里假设1rem = 750px;
  3. 那么 height = 100px / 750px = 0.13333333333333333333333 rem
  4. 这里的rem值是一个非常不方便计算的小数,我们可以给它一个系数,让它可以方便计算;
  5. 假设系数为 18.75,设计稿宽度 = 750px ,1rem = 750px / 18.75 = 40px;
  6. height = 100px / 40px = 2.5rem

所以 1rem的宽度 = viewWidth / 18.75(这个系数只要方便整除,可取任意值)。

以适配 IP6 为例,我们可以使用等比缩放,750px -> 375px ,40px -> 20px。

如果想动态获取视口宽度,可以使用JS函数。

(function () {
    'use strict';

    setRemUnit();

    window.addEventListener('resize', setRemUnit);

    function setRemUnit() {
        var docEl = document.documentElement;
        var ratio = 18.75;
        var viewWidth = docEl.getBoundingClientRect().width || window.innerWidth;

        docEl.style.fontSize = viewWidth / ratio + 'px';
    }
})();

px转化rem方式:

  1. 使用CSS预处理器,比如SASSLESSStylus
  2. 通过工程化的手段,自动转化为rem

简单适配方案缺点:并不能解决1px边框问题。

通用适配

原理跟简单适配方案一样,只是为了解决1px边框问题做了一些修改。

先看看1px的问题:

dpr:1,1px边框就是1个物理像素渲染。

image-20211018104442652

dpr:2,1px边框就是2个物理像素渲染。

image-20211018104545903

dpr:3,1px边框就是3个物理像素渲染。

image-20211018104613674

为了解决边框问题,我们要使用缩放。

dpr:2,1px边框就是2个物理像素渲染,这个时候缩放成原来0.5倍。

 <meta name="viewport" content="width=device-width, initial-scale=0.5maximum-scale=0.5,minimum-scale=0.5,user-scalable=no" />

边框就变成我们像样的样子了。

image-20211018104907246

dpr:3同理。

修改一下之前的JS函数

;(function () {
  'use strict'
  // 获取viewport属性
  var docEl = document.documentElement
  var viewportEl = document.querySelector('meta[name="viewport"]')
  // 获取设备像素比
  var dpr = window.devicePixelRatio || 1
  // 设置最大最小值,保证布局不会因为过小或过大而失真
  var maxWidth = 540
  var minWidth = 320

  // 如果设备像素比大于 3 取整数 3 ,大于 2 取整数 2 ,大于 1 取整数 1
  dpr = dpr >= 3 ? 3 : dpr >= 2 ? 2 : 1
  // 将设别像素比作为属性添加到html,不想使用 rem 单位,使用 px 单位时使用
  docEl.setAttribute('data-dpr', dpr)
  // 将最大最小值添加进html
  docEl.setAttribute('max-width', maxWidth)
  docEl.setAttribute('min-width', minWidth)

  // 缩放比
  var scale = 1 / dpr
  // 将新缩放比保存
  var content = 'width=device-width, initial-scale= ' + scale + ',maximum-scale=' + scale + ',minimum-scale=' + scale + ',user-scalable=no'
  //   判断viewport标签是否存在,如果存在,将content传入,如果不存在,创建meta标签,设置name和viewport属性,将新缩放比存入meta标签的content,将meta标签写入head标签
  if (viewportEl) {
    viewportEl.setAttribute('content', content)
  } else {
    viewportEl = document.createElement('meta')
    viewportEl.setAttribute('name', 'viewport')
    viewportEl.setAttribute('content', content)

    document.head.appendChild(viewportEl)
  }

  setRemUnit()

  window.addEventListener('resize', setRemUnit)

  function setRemUnit() {
    // 系数,选择能整除的,可以修改
    var ratio = 18.75
    var docEl = document.documentElement
    var viewWidth = docEl.getBoundingClientRect().width || window.innerWidth

    if (maxWidth && viewWidth / dpr > maxWidth) {
      viewWidth = maxWidth * dpr
    } else if (minWidth && viewWidth / dpr < minWidth) {
      viewWidth = minWidth * dpr
    }

    docEl.style.fontSize = viewWidth / ratio + 'px'
  }
})()

其它适配方案

固定宽高

内容简单的移动网站,使用px实现也可以。

vw/vh

具体看扩展链接。

事件

  • PC端事件:鼠标事件、键盘事件;

  • 移动端事件:

    1. 触摸事件
      • touch事件;
        • 先被提出来,各大厂商纷纷跟进;
      • pointer事件。
        • 微软提出来的,把鼠标事件和触摸事件统一成指针事件,兼容性有问题。
    2. 手势(gesture)事件;
    3. 传感器(sensor)事件。

    touch事件

     <div id="box" class="box"></div>
        <script>
          var boxEl = document.getElementById('box')
    
          // 一般使用 addEventListener,如果不支持,使用下面这种方式
          // boxEl.ontouchstart = handleStart
          // boxEl.ontouchmove = handleMove
          // boxEl.ontouchend = handleEnd
          // boxEl.ontouchcancel = handleCancel
    
          boxEl.addEventListener('touchstart', handleStart, false)
          boxEl.addEventListener('touchmove', handleMove, false)
          boxEl.addEventListener('touchend', handleEnd, false)
    
          function handleStart(event) {
            console.log('touchstart', event)
            console.log(event.touches, event.targetTouches, event.changedTouches)
          }
          function handleMove(event) {
            console.log('touchmove', event)
          }
          function handleEnd(event) {
            console.log('touchend', event)
          }
        </script>
    

    image-20211018134753958

其中:

  • event.touches:所有的触摸点,会捕获到4个触摸点;
  • event.targetTouches:目标上的触摸点,会捕获到2个触摸点;
  • event.changedTouches:发生变化的触摸点,会捕获到1个触摸点。

image-20211018135909556

这里面需要注意的一点是:

<script>
      var boxEl = document.getElementById('box')

      boxEl.addEventListener('touchend', handleEnd, false)

      function handleEnd(event) {
        console.log(event.touches, event.targetTouches, event.changedTouches)
      }
</script>

touchend事件触发时,touchestargetToucheslength都是0,只有changedTouches的length有长度。所以推荐在所有事件中使用changedTouches

image-20211018141540373

<style>
      body {
        height: 2000px;
      }
      .box {
        width: 150px;
        height: 150px;
        background-color: red;
        margin: 20px auto;
      }
</style> 

<body>
    <div id="box" class="box"></div>
    <script>
      var boxEl = document.getElementById('box')

      boxEl.addEventListener('touchstart', handleStart, false)

      function handleStart(event) {
        var touch = event.changedTouches[0]
        console.log(touch)
        console.log(touch.pageX, touch.pageY)
      }

    </script>
  </body>

image-20211018142704184

其中:

  • clientXclientY都是可视区域范围内的坐标;
  • pageXpageY都是考虑滚动条的,这两个元素计算需要滚动条的距离+可视区域范围内的距离。
  • 所以我们看图clientY是3.07px,而pageY是103.07px,这是加过滚动条滚动的距离,常用的是pageXpageY

单指拖拽

<style>
      /* 为了显示滚动条 */
      body {
        height: 2000px;
      }
      .backtop {
        position: fixed;
        right: 20px;
        bottom: 20px;
        width: 45px;
        height: 45px;
        line-height: 45px;
        text-align: center;
        background-color: rgba(0, 0, 0, 0.6);
        border-radius: 50%;
        color: #fff;
        font-size: 30px;
        -webkit-tap-highlight-color: transparent;
        /* 使用translate3d是因为移动端可以开启GPU加速 */
        transform: translate3d(x, y, 0);
      }
 </style>

 <a href="#" id="backtop" class="backtop">&uarr;</a>
 <script>
      // 点击拖动
      function drag(el, options) {
        // 默认可拖动x轴,不可拖动y轴
        options.x = typeof options.x !== 'undefined' ? options.x : true
        options.y = typeof options.y !== 'undefined' ? options.y : false
        // 都不允许拖动,直接返回
        if (!options.x && !options.y) return
        // 实时更新当前坐标
        var curPoint = {
          x: 0,
          y: 0
        }
        // 初始坐标点
        var startPoint = {}
        // 标志位,解决只触发开始结束不移动的情况,默认false
        var isTouchMove = false

        // 绑定事件
        el.addEventListener('touchstart', handleStart, false)
        el.addEventListener('touchmove', handleMove, false)
        el.addEventListener('touchend', handleEnd, false)

        function handleStart(ev) {
          // 单指,所以选第0个
          var touch = ev.changedTouches[0]

          startPoint.x = touch.pageX
          startPoint.y = touch.pageY
        }

        function handleMove(ev) {
          // 阻止默认行为
          ev.preventDefault()
          // 触发Move事件,改为true
          isTouchMove = true

          var touch = ev.changedTouches[0]

          // 变量之差
          var diffPoint = {}
          // move函数使用
          var movePoint = {
            x: 0,
            y: 0
          }
          // 计算差值
          diffPoint.x = touch.pageX - startPoint.x
          diffPoint.y = touch.pageY - startPoint.y
          // 如果允许X轴拖动,拖动距离是差值+当前坐标点
          if (options.x) {
            movePoint.x = diffPoint.x + curPoint.x
          }
          // 如果允许Y轴拖动,同上
          if (options.y) {
            movePoint.y = diffPoint.y + curPoint.y
          }

          move(el, movePoint.x, movePoint.y)
        }

        function handleEnd(ev) {
          // 没有触发touchMove事件,直接返回
          if (!isTouchMove) return

          var touch = ev.changedTouches[0]
          //  最终值是差值加上当前点坐标
          curPoint.x += touch.pageX - startPoint.x
          curPoint.y += touch.pageY - startPoint.y
          // 还原标志位
          isTouchMove = false
        }

        function move(el, x, y) {
          x = x || 0
          y = y || 0
          el.style.transform = 'translate3d(' + x + 'px,' + y + 'px, 0)'
        }
      }
    </script>
<script>
      // 单指拖拽
      var backtop = document.getElementById('backtop')
      drag(backtop, {
        x: true,
        y: true
      })
</script>

其它触摸事件(高级事件)

image-20211018160540438

可以使用封装好的库hammer.js

幻灯片

<!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, maximum-scale=1, minimum-scale=1, user-scalable=no" />
    <title>幻灯片</title>
    <link rel="stylesheet" href="css/slider.css" />
  </head>
  <body>
    <div class="slider" id="slider">
      <div class="slider-item-container">
        <div class="slider-item">
          <a href="#" class="slider-link">
            <img src="img/1.jpg" alt="slider" class="slider-img" />
          </a>
        </div>
        <div class="slider-item">
          <a href="#" class="slider-link">
            <img src="img/2.jpg" alt="slider" class="slider-img" />
          </a>
        </div>
        <div class="slider-item">
          <a href="#" class="slider-link">
            <img src="img/3.jpg" alt="slider" class="slider-img" />
          </a>
        </div>
        <div class="slider-item">
          <a href="#" class="slider-link">
            <img src="img/4.jpg" alt="slider" class="slider-img" />
          </a>
        </div>
        <div class="slider-item">
          <a href="#" class="slider-link">
            <img src="img/5.jpg" alt="slider" class="slider-img" />
          </a>
        </div>
      </div>
      <!-- 使用js动态生成 -->
      <!-- 幻灯片指示器 -->
      <!-- <div class="slider-indicator-container">
        <span class="slider-indicator slider-indicator-active"></span>
        <span class="slider-indicator"></span>
        <span class="slider-indicator"></span>
        <span class="slider-indicator"></span>
        <span class="slider-indicator"></span>
      </div> -->
    </div>

    <button id="prev">prev</button>
    <button id="next">next</button>

    <!-- js文件需要放在最下面,不然会报Uncaught TypeError: Cannot read properties of null (reading 'querySelector')
错 -->
    <script src="js/slider.js"></script>
    <script src="js/hammer.min.js"></script>
    <script>
      var slider = new Slider(document.getElementById('slider'), {
        initIndex: 2, // 初始显示第几张幻灯片,从0开始
        speed: 300, // 幻灯片的切换速度
        hasIndicator: true // 是否需要指示器indicator
      })
      // 是否滑动,加上判定防止 panend 和 swipeleft 或者 swiperight 切换两次。
      var isSwiping = false
      // 点击事件
      document.getElementById('prev').addEventListener(
        'click',
        function () {
          slider.prev()
        },
        false
      )

      document.getElementById('next').addEventListener(
        'click',
        function () {
          slider.next()
        },
        false
      )

      var hammer = new Hammer(slider.getItemContainer())

      // 手指滑动
      hammer.on('panmove', function (ev) {
        // console.log('panmove', ev)
        // 当前幻灯片滑动距离+之前滑动的幻灯片数量
        var distance = ev.deltaX + slider.getDistanceByIndex(slider.getIndex())
        slider.move(distance)
      })
      // 手指松开
      hammer.on('panend', function (ev) {
        // 同时触发,直接返回
        if (isSwiping) return
        // console.log('panend', ev)
        var distance = ev.deltaX + slider.getDistanceByIndex(slider.getIndex())
        var index = getIndexByDistance(distance)

        slider.to(index)
      })

      // 根据容器的移动距离获取索引
      function getIndexByDistance(distance) {
        if (distance > 0) {
          return 0
        } else {
          return Math.round(-distance / slider.getDistancePerSlide())
        }
      }

      hammer.on('swipeleft', function (ev) {
        // 滑动时触发
        isSwiping = true
        slider.next(function () {
          // 切换完成后改成false
          isSwiping = false
        })
      })

      hammer.on('swiperight', function (ev) {
        isSwiping = true
        slider.prev(function () {
          isSwiping = false
        })
      })
    </script>
  </body>
</html>
function Slider(el, options) {
  // 默认参数
  var defaults = {
    initIndex: 0,
    speed: 300,
    hasIndicator: false
  }
  // 用户参数
  this.options = {}
  this.options.initIndex = typeof options.initIndex !== 'undefined' ? options.initIndex : defaults.initIndex
  this.options.speed = typeof options.speed !== 'undefined' ? options.speed : defaults.speed
  this.options.hasIndicator = typeof options.hasIndicator !== 'undefined' ? options.hasIndicator : defaults.hasIndicator

  // 保存一下el,原型方法可能会用
  this.el = el
  // 获取指示器
  this.itemContainer = el.querySelector('.slider-item-container')
  // 获取指示器子元素列表
  this.items = this.itemContainer.children
  // 切换距离
  this.distancePerSlide = this.items[0].offsetWidth

  this.minIndex = 0
  this.maxIndex = this.items.length - 1

  // 校正初始索引
  this.index = this._adjustIndex(this.options.initIndex)
  // 切换到当前幻灯片,不带动画
  this.move(this.getDistanceByIndex(this.index))

  // 点亮索引
  if (this.options.hasIndicator) {
    this._createIndicators()
    this._setIndicatorActive(this.index)
  }
}
// 切换到指定索引幻灯片,带动画
// 回调是为了解决同时触发 panend 和 swiper 的问题
Slider.prototype.to = function (index, cb) {
  // 校正传入的索引,不然就会一直往下走,走到空元素
  this.index = this._adjustIndex(index)

  this._setTransitionSpeed(this.options.speed)
  this.move(this.getDistanceByIndex(this.index))

  // 先保存一下this,这时指的slider对象
  var self = this
  this.itemContainer.addEventListener(
    'transitionend',
    function () {
      self._setTransitionSpeed(0)
      // 将传入的标识符改成 isSwiping = false
      if (typeof cb === 'function') {
        cb()
      }
    },

    false
  )
  if (this.options.hasIndicator) {
    this._setIndicatorActive(this.index)
  }
}

Slider.prototype._setTransitionSpeed = function (speed) {
  this.itemContainer.style.transitonDuration = speed + 'ms'
}

// 上一张
Slider.prototype.prev = function (cb) {
  this.to(this.index - 1, cb)
}
// 下一张
Slider.prototype.next = function (cb) {
  this.to(this.index + 1, cb)
}

// 校正索引
Slider.prototype._adjustIndex = function (index) {
  // 索引如果过大或过小,返回最大或最小索引,让它继续循环
  if (index < this.minIndex) return this.maxIndex
  if (index > this.maxIndex) return this.minIndex

  return index
}
// 移动
Slider.prototype.move = function (distance) {
  this.itemContainer.style.transform = 'translate3d(' + distance + 'px ,0 ,0)'
}
// 通过索引获取距离
Slider.prototype.getDistanceByIndex = function (index) {
  return -index * this.distancePerSlide
}

// 创建指示器的html结构
Slider.prototype._createIndicators = function () {
  var indicatorContainer = document.createElement('div')
  var html = ''

  indicatorContainer.className = 'slider-indicator-container'

  for (var i = 0; i <= this.maxIndex; i++) {
    html += '<span class="slider-indicator"></span>'
  }
  // 将span元素放入父容器 slider-indicator-container
  indicatorContainer.innerHTML = html
  // 将父容器 slider-indicator-container 放入它的父容器 slider
  this.el.appendChild(indicatorContainer)
}

// 激活指示器
Slider.prototype._setIndicatorActive = function (index) {
  // indicators 有元素列表就使用现有元素列表,没有就获取元素列表
  this.indicators = this.indicators || this.el.querySelectorAll('.slider-indicator')

  for (var i = 0; i < this.indicators.length; i++) {
    // 遍历 indicators ,去掉 slider-indicator-active 样式
    this.indicators[i].classList.remove('slider-indicator-active')
  }
  // 添加 slider-indicator-active 样式
  this.indicators[index].classList.add('slider-indicator-active')
}

Slider.prototype.getItemContainer = function () {
  return this.itemContainer
}

Slider.prototype.getIndex = function () {
  return this.index
}

Slider.prototype.getDistancePerSlide = function () {
  return this.distancePerSlide
}
/* css reset */
* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

img {
  border: none;
  vertical-align: top;
}

a {
  -webkit-tap-highlight-color: transparent;
}

/* slider */
.slider {
  overflow: hidden;
  position: relative;
  width: 100%;
  height: 183px;
}
.slider-item-container,
.slider-item,
.slider-link,
.slider-img {
  width: 100%;
  height: 100%;
}

.slider-item-container {
  display: flex;
  transition: transform 0;
}

.slider-item {
  /* flex以后会压缩,关闭压缩重新显示 */
  flex-shrink: 0;
}
.slider-link {
  display: block;
}

.slider-indicator-container {
  position: absolute;
  bottom: 10px;
  width: 100%;
  text-align: center;
}

.slider-indicator {
  display: inline-block;
  width: 8px;
  height: 8px;
  background-color: #000;
  opacity: 0.2;
  border-radius: 50%;
  margin-right: 8px;
}

.slider-indicator-active {
  background-color: #007aff;
  opacity: 1;
}

移动端常见问题

浏览器兼容性问题

html5shiv:让浏览器支持 HTML5 标签;

modernizr:HTML5 / CSS3 特性检测库。

/* 浏览器兼容前缀 */
display: -webkit-flex;
display: -moz-flex;
display: -ms-flex;
display: -o-flex;
display: flex;
justify-content: center;
-ms-align-items: center;
align-items: center;

单行和多行溢出省略

/* 单行溢出省略 */
/* 当在 flex 容器使用时,需要另外包裹一层才生效 */
.text-ellipsis {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* 多行溢出省略 */
/* 只兼容 webkit 浏览器,移动端开发可以这么做*/
/* 不要配合高度使用,容器高度过高,省略号后面还会显示文字,容器高度过低,显示不出省略号 */
.multiline-ellipsis {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: normal !important;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  word-wrap: break-word;
}

移动性能优化

HTML中的CSS和JavaScript

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
    <title>2.1 HTML中的CSS和JavaScript</title>
    <!-- 1. 内联和外部文件引入 -->
    <style>
        /*
        内联
            优点:
                减少HTTP请求
            缺点:
                1.没办法复用
                2.使得HTML文件变大,加载时间变长
                3.代码都写到HTML文件中,不利于后期的维护?
                    工程化的手段可以解决这个问题
                        源码到可以上线的代码
        */
    </style>
    <!--
        通过外部文件引入
            优点:
                1.很好的复用代码
                    有效利用浏览器的静态资源缓存
                2.代码分离,利于后期维护
            缺点:
                增加了HTTP请求
    -->
    <link rel="stylesheet" href="css/base.css">

    <!-- 3. 避免重复资源,防止增加多余的请求 -->
    <!-- <link rel="stylesheet" href="css/base.css"> -->
    <script>
        // 内联
    </script>
    <!-- 通过外部文件引入(没有dom操作的情况下放head里) -->
    <script src="js/flexible.js"></script>
    <script>
        // inline flexible
        // 内联首屏数据请求的代码 (为了增加用户体验,让用户不论网速快慢都能第一时间看到)
    </script>
</head>
<body>

    <div></div>
    <div></div>

    <link rel="stylesheet" href="css/base.css">


    <div></div>


    <!-- 2. 放置位置 -->
    <script src="js/index.js"></script>
    <script>
        
    </script>
</body>
</html>

图片和其他优化

图片优化

图片大、多

  • HTTP请求大
    • 图片压缩处理
    • 使用更高压缩比格式的图片webp
    • 尽量少用图片
      • 使用图标字体代替图片图标
      • CSS画图
  • HTTP请求多
    • 合理使用base64内嵌图片
    • 合并静态资源图片
    • 雪碧图

其它优化

  • 减少DOM元素的嵌套层级
  • 避免空链接hrefsrc
  • 尽量避免使用table/iframe等慢元素
  • 主要内容(写前面)和侧边栏(写后面)的位置

CSS选择器优化

  • 避免通配符选择器

  • 移除无匹配的样式

  • 避免类正则的属性选择器

  • 不要使用类选择器和ID选择器修饰元素标签,这样多此一举,还会降低效率 div#slider.slider

  • 保持简单,不要使用嵌套过多过于复杂的选择器

    • 浏览器从右向左解析CSS(也就是从最里层向外解析)

    • 为了避免冲突,可以在团队开发时使用个人名字起名.sun-slider-item

CSS属性和其他优化

  • 提取公用部分
  • 使用CSS简写形式合并多条属性
  • 避免使用CSS @import引用加载CSS
  • 使用CSS3动画,代替DOM动画
  • 优先考虑flex 不滥用float

DOM操作优化

DOM操作很奢侈,很耗性能

  • 加快单次DOM操作(例如使用父元素的querySelector而不使用document

    • 尽量使用id选择器
  • 减少DOM操作的次数

    • 合理缓存DOM对象/操作(例如var sliderItemContainer = sliderEl.querySelector('.slider-item-container')
    • 缓存 DOM.length
      • 每次.length都要计算,用一个变量保存这个值(例如var i = 0, num = sliderItem.length; i < num; i++
    • 使用DocumentFragment优化多次的appendChild
    • 使用一次innerHTML
    • 不要直接修改style,通过添加class修改(这样可以减少重绘)

事件优化

  • 动态插入/删除节点,需要动态的重新绑定/移除节点;
  • 给多个元素同时绑定事件处理函数,我们可以通过绑定父元素。
// 使用事件代理,避免直接事件绑定
sliderIndicatorContainer.addEventListener('click', function (ev) {
    // console.log(ev.target);
    if (ev.target && /(^|\s)slider\-indicator($|\s)/.test(ev.target.className)) {
        console.log('click');
    }
}, false);

// 事件节流
// scroll resize mousemove touchmove
var timer = null
window.addEventListener(
'scroll',
function () {
    clearTimeout(timer)
    timer = setTimeout(function () {
    console.log('scroll')
    }, 100)
    // ....
},
false
)

资源按需加载和预加载

// 1. 图片的按需加载
var lazyLoadClass = '.lazyload-img'
// 将选择器找到的元素列表复制到数组
var imgArr = Array.prototype.slice.call(document.querySelectorAll(lazyLoadClass))
// 调用函数
lazyLoadImgs()
// 事件节流,先声明一个定时器
var timer = null
window.addEventListener(
  'scroll',
  function () {
    // 清理定时器
    clearTimeout(timer)
    timer = setTimeout(function () {
      // 每隔100ms执行一次
      lazyLoadImgs()
    }, 100)
  },
  false
)

function lazyLoadImgs() {
  for (var i = 0; i < imgArr.length; i++) {
    if (isInVisibleArea(imgArr[i])) {
      imgArr[i].src = imgArr[i].getAttribute('data-src')
      imgArr.splice(i, 1)
      i--
    }
  }
}

// 是否在页面可视区内
function isInVisibleArea(el) {
  // Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置
  var rect = el.getBoundingClientRect()
  // 返回在可视区域范围内的区域	
  return rect.bottom > 0 && rect.top < window.innerHeight && rect.right > 0 && rect.left < window.innerWidth
}

// 2. 其他内容的按需加载
loadProduct()
window.addEventListener('scroll', loadProduct, false)

function loadProduct() {
  if (isInVisibleArea(document.getElementById('product'))) {
    var script = document.createElement('script')
    script.src = 'js/loadProduct.js'
    // 为了模拟演示
    // setTimeout(function () {
    //   script.src = 'js/loadProduct.js'
    // }, 1000)
    document.body.appendChild(script)
	// 移除执行过的 loadProduct 方法
    window.removeEventListener('scroll', loadProduct, false)
  }
}

// 3. 图片预加载
var img = new Image()
img.src = 'img/recommend/5.jpg'

参考:

慕课前端课程

扩展:

关于移动端适配,你必须要知道的

前端基础知识概述 -- 移动端开发的屏幕、图像、字体与布局的兼容适配