溯源flexible的成长
在移动端适配中,flexible占据着非常重要的位置,尽管它有许多不足。但是其适配的思想仍然是非常值得学习的,这里我尝试着从最初版本的flexible源码开始解读,看看它的前世今生。
----如果只是想知道现在的flexible是怎么运作的,那就没必要往下看了,直接跳转到这篇文章即可
这里的所有代码都在github的amfe/lib-flexible中,这里有flexible比较详尽的发展过程。
2014.06.05
可以看到第一次提交是在2014年6月5日,最初版本的flexible就是这么质朴,一些变量的定义和处理和现在大相径庭。。
-
首先dpr和scale在初始化时就有了值,dpr是通过win.navigator.appVersion.match(/iphone/gi)来捕获iPhone信息,对于iPhone以外的设备,dpr默认为1
var dpr = win.navigator.appVersion.match(/iphone/gi)?win.devicePixelRatio:1; var scale = 1 / dpr;
-
setUnitA(),这个A是有特殊含义的,在README中可以看到,
a是视觉稿中的单位,1a = 屏幕宽度/160 ,所以代码中这样计算根字体的大小是有道理的,这样能保证 1a = 0.1rem 。(因为刚工作没多长时间,不知道现在的视觉稿是不是这样,或者说现在还有视觉稿这种东西吗?请大佬指正)function setUnitA(){ win.rem = window.innerWidth / 16; document.documentElement.style.fontSize = win.rem + 'px'; }
-
好的,至此根字体已经设置完成了,就是这么easy,或者说有些“草率”,后面的就比较简单了,监听页面变化去执行setUnitA(),给html标签添加data-dpr属性,根据scale创建meta标签去做页面初始配置。
win.dpr = dpr; win.addEventListener('resize', function() { clearTimeout(h); h = setTimeout(setUnitA, 300); }, false); document.documentElement.setAttribute('data-dpr', dpr); document.write('<meta name="viewport" content="initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no"/>'); setUnitA();
-
到此,最初版本的flexible就这样结束了,看过0.3.2版本的小伙伴肯定一眼就能看出其中有诸多不合理的地方,但是在2014年,这样的适配思想已经非常难得了。
更新浏览器回退bug(2014.06.27)
-
官方很快的意识到了第一个问题,仅仅监听页面变化是不行的,所以我们来看下这次代码的变更。
-
新定义了文档对象和meta标签对象
var docEl = document.documentElement; var metaEl = document.createElement('meta');
-
重点在这里,除了监听resize,还监听了pageshow,这样在浏览器回退时仍然能正常的适配。e.persisted就是用来判定当前页面是否是通过回退达到的。
win.addEventListener('pageshow', function(e) { if (e.persisted) { clearTimeout(h); h = setTimeout(setUnitA, 300); } }, false);
-
最后,既然定义了文档对象和meta标签对象,那么最后的文档配置也要做一定修改。
docEl.setAttribute('data-dpr', dpr); 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 = document.createElement('div'); wrap.appendChild(metaEl); document.write(wrap.innerHTML); }
-
这里有一个迷惑了我好长时间的问题,就是这个docEl.firstElementChild,难道还会不存在第一个子元素吗,那这个页面不就空了吗?后来才得知,这是为了兼容firstChild,我们要获取某个标签的第一个子元素时,一般都会用firstElementChild,但是这个属性在一些浏览器版本中是不兼容的,“Can I Use”中的介绍是这样的。在低版本的浏览器中必须使用firstChild获取第一个子元素,或者使用上面的方法,无脑把标签扔进去。
支持手动设置meta标签(2014.09.19)
-
我们在html都会习惯性的使用这样的配置,那么如果已经存在viewport配置后,flexible要做出有效的判断,从而有不同的处理方式,而不是一味的新增meta标签
<meta name="viewport" content="width=device-width, initial-scale=1.0">
-
这里摈弃了之前初始化赋值的操作,开始的定义只用于初始化。这样做也是为了之后的判断,当我们没有viewport配置的时候,dpr和scale自然为空,可以对此进行一定处理;反之,就不需要处理,通过这种方法区分出了开发者手动配置meta的情况。另外,对于meta标签对象,也有了更细致的查询。
var docEl = document.documentElement; var metaEl = document.querySelector('meta[name="viewport"]'); var dpr; var scale; var tid;
-
这里的一系列判断和当前版本的代码已经非常像了。识别出已有viewport配置的情况,然后进行特殊的处理。
if (metaEl) { console.warn('将根据已有的meta标签来设置缩放比例'); var match = metaEl.getAttribute('content').match(/initial\-scale=(["']?)([\d\.]+)\1?/); if (match) { scale = parseFloat(match[2]); dpr = 1 / scale; } } if (!dpr && !scale) { dpr = win.navigator.appVersion.match(/iphone/gi)?win.devicePixelRatio:1; scale = 1 / dpr; } docEl.setAttribute('data-dpr', dpr); if (!metaEl) { metaEl = document.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 = document.createElement('div'); wrap.appendChild(metaEl); document.write(wrap.innerHTML); } }
-
需要注意的是“ scale = parseFloat(match[2]); ”这个地方,match很好理解,就是content内容拆分出来的数组,为什么是索引2呢,按照现在的源码来看应该是1才对呀。原因是这样的,我在当时那个版本的html文件中看到,viewport的配置是这样的。索引为2获取到的是最小缩放倍数,当然可以用作scale的计算。
<meta name="viewport" content="initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
更新rem方案,安卓在大于2倍的屏幕下用2的方案,其余用1的方案。ios在2和3倍的屏幕下用2的方案,其余用1的方案。(2014.10.14)
- 之前的所有版本,仅有iPhone一家受宠。终于在这个版本,安卓也被重视起来。之所以做这样的区分,为的是获取不同的dpr,dpr用来影响后面meta标签的创建,所以很容易想到,这些判断应该出现在开发没有手动创建meta标签的情况下,恰巧,这就是上个版本主要做的事。
- 另外,这个版本还做了一个更新,就是对页面中字体单位的把控,之前并没有详细介绍,其实在flexible中元素的宽高布局单位推荐使用rem,而字体大小是推荐使用px的。原因是rem计算后的数值精确度较差,这种较差的精确度反应在字体上会更加明显。所以flexible的做法是在body中根据dpr设置font-size,这样一来,字体的px遵循body,元素的rem遵循html,能够非常明显的做出区分。
andriod暂时只用1的方案(2014.10.14)
- 所谓“天妒英才”,短短一天内,安卓再次被打入“冷宫”,时至今日,大家在源码中可以看到,依旧没有对安卓dpr的判断。为什么要做这样的设计?我在文件中并没有找到答案。事实上,在flexible的实践中,面对部分高dpr的安卓机,确实会出现一些适配的问题,只能手动去解决。这里flexible设计师到底是出于怎样的考虑,恳请大佬指明!
更新为flexible,更改dpr的计算规则(2014.10.22)
-
这个版本和我们现在看到的差别已经不大了,希望大家能记住“flexible”的本名叫“tbm”(^ _ ^)。
-
这里除了一些变量名上的变更,主要有两点更新。第一,对最大宽度做了限制。这样做的目的主要是避免元素无限变大,在特大屏的设备上显示对应比例的元素,并不一定是明智之举,这样会一定程度的影响美观。
function setUnitA(){ var width = docEl.getBoundingClientRect().width; if (width / dpr > 540) { width = 540 * dpr; } win.rem = width / 16; docEl.style.fontSize = win.rem + 'px'; }
-
第二,将body中font-size的设置抽离出来。能够更好的根据页面状态去设置字体大小,也能使字体大小和元素宽高两者的适配更明确的分开。
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); }
更新1/100的方案(2014.11.04)
- 上面提到过,依据方便视觉稿中a的计算,使用“width/16”来解决这个问题,这里将16改为10,可能是视觉稿中a的含义发生变化。因为这里只做了一个数值上的变化,没有做其他解释,如果这里理解有误,希望大佬批评指正。
END
- 至此,flexible的基本功能已经实现了,之后的更新没有什么大的改动,都是一些小的优化,比如变量名更加规范,正则bug的修复…有兴趣的小伙伴可以去追一下源码。截至0.3.2版本,flexible停止了更新,在适配功能上,它也许有很多弊端没有解决,当前也有很多更好的方法能够取代它,但是去看一下源码,了解一套方案的成长过程,学习学习大佬的思维模式和编程方式,这才是我们看源码的目的,如果只是单纯的使用,Duck不必看这么深。