前言
项目是基于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,稍微修改后便实现了需求,代码比这个实例复杂许多,具体代码会贴在文章的最后。
1 2 3 4 5
| function setRem () { let htmlRem = document.documentElement.clientWidth document.documentElement.style.fontSize = htmlRem/7.5 + 'px' } setRem()
|
3、使用vm、vh:vw、vh是新的一种相对单位是把可视区域分的宽高为100份类似于百分比布局,这种方案它不用去写js,不过兼容性有点差。
1 2 3
| html{ font-size: 10vw }
|
px适配
根据不同的屏幕宽度,计算处不同的px值,所以当我们改变苹果的大小时,网站就会刷新动态计算出对应的px值,从而达到适配的目的。具体的实施代码没有找到,但其实原理逻辑相差不大。
项目实践
项目的适配大概有2部分吧:1、使用flexible.js来实现不同屏幕的适配;在基于vue框架的项目中,这里的flexible.js直接import进main.js即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
| (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 = {});
if (metaEl) { console.warn("将根据已有的meta标签来设置缩放比例"); var match = metaEl .getAttribute("content") .match(/initial\-scale=([\d\.]+)/); if (match) { scale = parseFloat(match[1]); dpr = parseInt(1 / scale); } } else if (flexibleEl) { var content = flexibleEl.getAttribute("content"); if (content) { var initialDpr = content.match(/initial\-dpr=([\d\.]+)/); 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)); } } }
if (!dpr && !scale) { var isAndroid = win.navigator.appVersion.match(/android/gi); var isIPhone = win.navigator.appVersion.match(/iphone/gi); var devicePixelRatio = win.devicePixelRatio; if (isIPhone) { if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) { dpr = 3; } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)) { dpr = 2; } else { dpr = 1; } } else { dpr = 1; } scale = 1 / dpr; } docEl.setAttribute("data-dpr", 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); } }
function refreshRem() { var width = docEl.getBoundingClientRect().width; if (width / dpr < 1366) { width = 1366 * dpr; } else if (width / dpr > 2560) { width = 2560 * dpr; } 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 );
if (doc.readyState === "complete") { doc.body.style.fontSize = 12 * dpr + "px"; } else { doc.addEventListener( "DOMContentLoaded", function(e) { doc.body.style.fontSize = 12 * dpr + "px"; }, false ); }
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文件中即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| export function debounce(func, wait, immediate) { let timeout, args, context, timestamp, result;
const later = function() { const last = +new Date() - timestamp;
if (last < wait && last > 0) { timeout = setTimeout(later, wait - last); } else { timeout = null; 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() { return { chart: null, }; }, created() { window.addEventListener('resize', this[resizeChartMethod], false); }, beforeDestroy() { window.removeEventListener('reisze', this[resizeChartMethod]); }, methods: { [resizeChartMethod]: debounce(function() { if (this.chart) { this.chart.resize(); } }, 100), }, };
|