屏幕适配解决方案

1,325 阅读9分钟

前言

项目是基于vue框架构建的,使用了elementUI组件库和echarts插件。开发过程中需要实现的一个需求是希望能够让首页实现大屏幕的适配效果。大概的完成思路借鉴了手机淘宝之前的flexible.js,就是获取不同屏幕的宽度,然后修改html根元素的字体大小;这样跟根元素字体大小绑定的rem就能够实时地监听屏幕的变化,实现不同屏幕的适配。

其实移动端的适配也是同理,不过好像有很多的坑,但本项目没有移动端适配的需求,因此就暂且采用了简单的rem适配的方法。由于首页中有echarts表单,需要也能够监听屏幕的变化,因此除了flexible.js以外,我们还需要自定义一个resize.js混入在首页的每个echarts上,让其也会实时跟踪监听屏幕变化,这样才能够实现首页所有元素的屏幕适配。

解决方案

基础定义

最简单最直接的方式就是直接用百分端来设置元素的尺寸;可以实现元素大小的自适应,但无法实现字体大小的自适应,而且尺寸转为百分比计算十分麻烦。其实我们需要的是一个和屏幕宽度正相关的单位,而且这个单位要和px很容易互相转化。这样我们就可以使用这种单位进行元素尺寸和字体大小的设置。

em单位为相对长度单位,是根据当前元素的父元素的字体大小来计算的;但父级元素改变时,则em会经常改变,因此后面推出了rem来代替em单位的功能。

rem单位也是一个相对长度单位,1rem等于html元素上字体设置的大小;我们只要设置html上font-size的大小就可以改变rem所代表的大小。

vw、vh都是viewport视窗的相对长度单位,100vw代表着viewport视窗的宽度,100vh代表着viewport视窗的高度。

设备像素比device pixel ratio简称dpr,即物理像素和设备独立像素的比值,设备像素比越大意味着你的手机屏幕越高清。

例如:电脑的dpr都为1,而iphone7的dpr为2,因此设计稿上的1px,要想让iphone7实现适配,CSS应该为0.5px。而有的浏览器在解析0.5px 的时候会把他解析成1px,所以呈现出来会变成2px。这就是经典的1px问题。

1px问题

解决方案:既然1个css像素代表两个物理像素,设备又不认0.5px的写法,那就画1px,然后再想尽各种办法将线宽减少一半。

1、图片大法及背景渐变。这两种方案原理一样,都是设置元素一半有颜色,一半透明,比如做一个2px高度的图片,其中1px是我们想要的颜色,1px设置为透明。

2、缩放大法。这也是flexible.js的解决方案,根据对应的dpr调整对应的缩放比例,从而达到适配的目的,直接缩放页面。

3、使用伪元素缩放。transform: scale(1, 0.5);实现缩放的功能。

flex弹性盒子布局

当我们采用flex布局时,flex会自己根据屏幕的宽度进行适配。关于flex适配的方案比较容易,通常跟rem一起来实现屏幕宽度不同时的界面适配。这里就只介绍一下flex的基础概念,具体的布局在理解定义后较为简单,就不列举实例了。

简要介绍下flex常用的属性:父容器:display:flex; flex-direction用于确定flex主轴布局的方向;接下来的 justify-content, align-items, align-content 用于确定 flex 项对于 flex 容器空间的空白如何处理。

flex容器中的子元素会成为flex项。flex属性是flex-grow, flex-shrink, flex-basis 三个属性的简写属性。grow、shrink分别代表着增长和收缩因子;basis代表着初始基准大小。默认值为:flex: 0 1 auto。flex-basis 指定固定的长度值时,其优先级高于width;flex-basis 指定百分比值时,其参考对象是 main size.所以其计算值 flex-basis: percent * mainSize px。

rem适配

原理其实前面已经讲过了,就是识别不同的屏幕长宽来设置不同的html根元素的字体大小,从而用动态的rem来实现界面的配置。关键在于如何识别不同的屏幕宽度。

1、利用媒体查询:@media screen and (min-width:XXX)来判断设备的尺寸,进而设置html的fontSize,比较麻烦且需要考虑较多。

2、利用js获取并设置fontSize,简单实例如下。以下代码是以iphone6为设计稿,结果是1rem=100px的实际像素,因为iphone6的设备像素比是2所以1rem在浏览器的预览中是50px,也就是实现了1rem和设备宽度成7.5倍的关系,设备宽度改变1rem的实际大小也会改变,

但我自己则是用了手机淘宝开源的flexible.js,稍微修改后便实现了需求,代码比这个实例复杂许多,具体代码会贴在文章的最后。

function setRem () {
        let htmlRem = document.documentElement.clientWidth
        document.documentElement.style.fontSize = htmlRem/7.5 + 'px'
      }
setRem()

3、使用vm、vh:vw、vh是新的一种相对单位是把可视区域分的宽高为100份类似于百分比布局,这种方案它不用去写js,不过兼容性有点差。

html{
    font-size: 10vw
}

px适配

根据不同的屏幕宽度,计算处不同的px值,所以当我们改变苹果的大小时,网站就会刷新动态计算出对应的px值,从而达到适配的目的。具体的实施代码没有找到,但其实原理逻辑相差不大。

项目实践

项目的适配大概有2部分吧:1、使用flexible.js来实现不同屏幕的适配;在基于vue框架的项目中,这里的flexible.js直接import进main.js即可。


(function(win, lib) {
  var doc = win.document;
  var docEl = doc.documentElement;
  var metaEl = doc.querySelector('meta[name="viewport"]');
  var flexibleEl = doc.querySelector('meta[name="flexible"]');
  var dpr = 0;
  var scale = 0;
  var tid;
  var flexible = lib.flexible || (lib.flexible = {});
  /*
  获取dom树:win.document.documentElement,后续向HTML插入dpr、font-size;
  分别取meta标签中元素,判断用户是否曾经设置过;viewport的meta标签,其主要用来告诉浏览器如何规范的渲染Web页面,而你则需要告诉它视窗有多大
   设备像素比简称为dpr,其定义了物理像素和设备独立像素的对应关系 = 物理像素 / 设备独立像素
   */

  if (metaEl) {
    console.warn("将根据已有的meta标签来设置缩放比例");
    var match = metaEl
      .getAttribute("content")
      // eslint-disable-next-line no-useless-escape
      .match(/initial\-scale=([\d\.]+)/);
    if (match) {
      scale = parseFloat(match[1]);
      dpr = parseInt(1 / scale);
    }
  } else if (flexibleEl) {
    var content = flexibleEl.getAttribute("content");
    if (content) {
      // eslint-disable-next-line no-useless-escape
      var initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
      // eslint-disable-next-line no-useless-escape
      var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
      if (initialDpr) {
        dpr = parseFloat(initialDpr[1]);
        scale = parseFloat((1 / dpr).toFixed(2));
      }
      if (maximumDpr) {
        dpr = parseFloat(maximumDpr[1]);
        scale = parseFloat((1 / dpr).toFixed(2));
      }
    }
  }
  /*
   这段代码是判断你的meta标签里面是不是设置了name=viewport属性,如果你设置了viewport
   并且设置了initial-scale(初始屏幕的大小)我们将取到这个值作为dp
   */

  if (!dpr && !scale) {
    // eslint-disable-next-line no-unused-vars
    var isAndroid = win.navigator.appVersion.match(/android/gi);
    var isIPhone = win.navigator.appVersion.match(/iphone/gi);
    var devicePixelRatio = win.devicePixelRatio;
    if (isIPhone) {
      // iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
      if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
        dpr = 3;
      } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)) {
        dpr = 2;
      } else {
        dpr = 1;
      }
    } else {
      // 其他设备下,仍旧使用1倍的方案
      dpr = 1;
    }
    scale = 1 / dpr;
  }
  docEl.setAttribute("data-dpr", dpr);
  /*
  之后如果我们动态设置了scale或者设置了meta标签里面的name=flexible的inital-scale,
  那么我们就根据自己设置的dpr在判断iphone手机的retina屏幕的dpr比值判断不同型号的倍数,最后我们在html上设置了data-dpr自定义属性。
  */

  if (!metaEl) {
    metaEl = doc.createElement("meta");
    metaEl.setAttribute("name", "viewport");
    metaEl.setAttribute(
      "content",
      "initial-scale=" +
        scale +
        ", maximum-scale=" +
        scale +
        ", minimum-scale=" +
        scale +
        ", user-scalable=no"
    );
    if (docEl.firstElementChild) {
      docEl.firstElementChild.appendChild(metaEl);
    } else {
      var wrap = doc.createElement("div");
      wrap.appendChild(metaEl);
      doc.write(wrap.innerHTML);
    }
  }
  /*
  之后当我们之前没有设置metaEl标签的话,那么需要我们手动的去创建meta标签,实现移动端的适配
  */

  function refreshRem() {
    var width = docEl.getBoundingClientRect().width;
    // 最小1366px,最大适配2560px
    if (width / dpr < 1366) {
      width = 1366 * dpr;
    } else if (width / dpr > 2560) {
      width = 2560 * dpr;
    }
    // 设置成24等份,设计稿时1920px的,这样1rem就是80px
    var rem = width / 24;
    docEl.style.fontSize = rem + "px";
    flexible.rem = win.rem = rem;
  }

  win.addEventListener(
    "resize",
    function() {
      clearTimeout(tid);
      tid = setTimeout(refreshRem, 300);
    },
    false
  );
  win.addEventListener(
    "pageshow",
    function(e) {
      if (e.persisted) {
        clearTimeout(tid);
        tid = setTimeout(refreshRem, 300);
      }
    },
    false
  );
  /*
  这段代码的目的就是监听window里面的resize和pageshow方法来实现css样式的重绘。
  函数里面就是实现取到当前设备的width之后根据width计算出rem的具体值,rem代表html的font-size,
  这里的rem代表的是一个自定义的rem,而不是rem属性!
  */

  if (doc.readyState === "complete") {
    doc.body.style.fontSize = 12 * dpr + "px";
  } else {
    doc.addEventListener(
      "DOMContentLoaded",
      // eslint-disable-next-line no-unused-vars
      function(e) {
        doc.body.style.fontSize = 12 * dpr + "px";
      },
      false
    );
  }
  /*
  之后我们判断document对象是否处于complete状态,
  如果完成状态我们给body一个font-size=12*dpr的值,否则我们判断dom加载方法来实现body中的font-size的设置。
  这个设置是为了页面中字体的大小,而html中的font-size是为了设置页面的height,width等属性。
  */

  refreshRem();

  flexible.dpr = win.dpr = dpr;
  flexible.refreshRem = refreshRem;
  flexible.rem2px = function(d) {
    var val = parseFloat(d) * this.rem;
    if (typeof d === "string" && d.match(/rem$/)) {
      val += "px";
    }
    return val;
  };
  flexible.px2rem = function(d) {
    var val = parseFloat(d) / this.rem;
    if (typeof d === "string" && d.match(/px$/)) {
      val += "rem";
    }
    return val;
  };
})(window, window["lib"] || (window["lib"] = {}));

2、由于首页中还有echarts组件的展示,需要自定义一个函数来监听屏幕的变化,当屏幕变化时则修改echarts组件中chart的大小;这个resize的效果需要用防抖的函数来控制resize的频率。这里我把防抖写在了一个公共库里,就能方便复用,对于resize.js,直接混入在有echarts插件的vue文件中即可。

export function debounce(func, wait, immediate) {
  let timeout, args, context, timestamp, result;

  const later = function() {
    // 据上一次触发时间间隔
    const last = +new Date() - timestamp;

    // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
    if (last < wait && last > 0) {
      timeout = setTimeout(later, wait - last);
    } else {
      timeout = null;
      // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
      if (!immediate) {
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      }
    }
  };

  return function(...args) {
    context = this;
    timestamp = +new Date();
    const callNow = immediate && !timeout;
    // 如果延时不存在,重新设定延时
    if (!timeout) timeout = setTimeout(later, wait);
    if (callNow) {
      result = func.apply(context, args);
      context = args = null;
    }

    return result;
  };
}

import { debounce } from '@/XXX/XXXX';
const resizeChartMethod = '$__resizeChartMethod';
export default {
  data() {
    // 在组件内部将图表init的引用映射到chart属性上
    return {
      chart: null,
    };
  },
  created() {
    window.addEventListener('resize', this[resizeChartMethod], false);
  },
  beforeDestroy() {
    window.removeEventListener('reisze', this[resizeChartMethod]);
  },
  methods: {
    // 通过lodash的防抖函数来控制resize的频率
    [resizeChartMethod]: debounce(function() {
      if (this.chart) {
        this.chart.resize();
      }
    }, 100),
  },
};