手淘 flexible.js 移动端适配原理与源码解析
本文详细解析手淘 flexible.js 框架,阐述 viewport、物理像素与设备独立像素等核心概念,介绍基于 dpr 和 rem 的适配原理。通过拆解 flexible.js 源码,展示其如何动态计算 font-size 并设置 meta 标签,实现不同屏幕尺寸下的样式统一与高清渲染。
flexible.js 概述与基本概念
手淘 flexible.js 是一套专门用于移动端页面适配的 JavaScript 框架,下文将详细介绍其具体使用方法。
基本概念
1、视窗 viewport
从事过移动端开发的人员通常了解 viewport 的含义。
若对此概念陌生,可简单将其理解为浏览器的可视区域窗口。在 PC 端,viewport 通常指浏览器窗口的宽度和高度;而在移动端设备上,其机制更为复杂,具体细节建议自行查阅相关资料。
2、物理像素
物理像素也被称为设备像素,是显示设备中构成图像的最小物理单位。每个物理像素可根据操作系统设置其颜色和亮度,正是这些微小像素点的排列欺骗了人眼,形成了我们看到的图像效果。
3、设备独立像素
设备独立像素(Device Independent Pixels)也称为密度无关像素,可视为计算机坐标系统中的一个点。它代表一个由程序使用的虚拟像素(如 CSS 像素),随后由相关系统将其转换为物理像素。
4、CSS 像素
CSS 像素是一个抽象单位,主要用于浏览器中精确度量 Web 页面内容。通常情况下,CSS 像素被称为与设备无关的像素(Device-Independent Pixels,简称 DIPs)。
5、屏幕密度
屏幕密度指设备表面上存在的像素数量,通常以每英寸像素数(PPI)来计算。
6、设备像素比
设备像素比(Device Pixel Ratio,简称 dpr)定义了物理像素与设备独立像素之间的对应关系。其值可通过以下公式计算:
设备像素比 = 物理像素 / 设备独立像素
以 iPhone 6 为例,其设备宽度为 375pt * 667pt,这代表设备的独立像素;其 dpr 为 2。根据上述公式,可轻松计算出其物理像素为 750pt * 1334pt。
适配原理与设备参数分析
手淘框架的核心原理在于:根据不同的屏幕宽度,为网页的 html 根节点设置不同的 font-size,并将所有距离大小用 rem 单位代替。这样即可实现不同尺寸屏幕对相同样式的自适应。首先我们来了解常用的移动设备参数。
iPhone 6: 375px*667px 实际像素:750px*1334px
iPhone 5: 320px*568px 实际像素:640px*1136px
iPhone 4: 320px*480px 实际像素:640px*960px
Nexus 5X (安卓): 411px *731px 实际像素:411px*731px
以上数据均来源于 Chrome 浏览器测试。
实际上,iPhone 手机均采用视网膜屏幕(Retina Display),因此其实际像素应为逻辑像素乘以视网膜屏幕的倍率。
在实际开发中,UI 设计师提供的切图尺寸通常为 750x1334。iPhone 6 的像素与 UI 设计图的像素大小一致,但如果开发时直接按照 iPhone 6 的 px 进行设计,其他比 iPhone 6 尺寸小的设备将面临宽度不足的问题。flexible 框架完美解决了这一适配难题。
在应用中,只需设置好基准像素即可。例如,若 UI 图尺寸为 750,则基准像素为 750/10 = 75。所有固定宽度的 px 均可转换为 rem 单位以实现样式统一。例如,若 width 需要 200px,可这样编写:
width=200rem/75;从而实现样式的兼容(特别注意:由于 CSS 不支持直接进行样式计算,我们需要使用 Less 或 Sass 等 CSS 预处理器编译执行,以得到最终的 rem 值)。
此外,我们可以根据不同的 dpr 设置不同的样式,以实现视网膜屏幕的高清显示效果。
[data-dpr="1"] .selector {
width: 10px;
height: 32px;
font-size: 14px;
}
[data-dpr="2"] .selector {
width: 20px;
height: 64px;
font-size: 28px;
}
我们根据不同的自定义属性 data-dpr 来设置不同的 width 和 height,从而让不同 dpr 的屏幕拥有不同的属性(此方法通常用于处理图片)。
flexible.js 源码深度解析
接下来,我们将深入讲解 flexible 的源代码实现。
;(function(win, lib) {
})(window, window['lib'] || (window['lib'] = {}));
首先,最外层结构是一个基本的封装类库方法:函数立即调用(IIFE)。这样做可以防止封装的方法污染全局变量,jQuery 的源码也采用了相同的原理。
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 = {});
doc 变量获取文档的 document 对象。
docEl 变量获取以 html 为根的整个 DOM 树,后续我们需要向 html 插入 dpr 和 font-size 属性时将使用此对象。
metaEl 变量获取 meta 标签中 name 为 viewport 的元素,若不存在则返回空,用于判断用户是否自行设置了 meta 值以执行相应逻辑。
flexibleEl 变量获取 meta 标签中 name 为 flexible 的元素,若不存在则返回空,用于判断用户是否手动设置了相关 meta 值。
dpr 变量表示获取手机屏幕的 dpr 值。
scale 变量获取 meta 标签中的 scale 值,将根据不同的 scale 设置对应的 dpr。
我们需要了解的大致就是上述这些在代码中用到的关键属性。
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));
}
}
}
这段代码用于判断 meta 标签中是否设置了 name=viewport 属性。如果设置了 viewport 且包含 initial-scale(初始缩放比例),我们将获取该值作为 dpr(此处进行了逻辑运算,如果页面初始放大倍率为 2,则 dpr 会被设置为 0)。
同理,如果我们动态设置了 meta 标签,则直接从中取出值并设置 dpr 和 scale。
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) {
// 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 的 initial-scale,则根据自行设置的 dpr,结合 iPhone 视网膜屏幕的 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;
if (width / dpr > 540) {
width = 540 * dpr;
}
var rem = width / 10;
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 概念,而非 CSS 中的 rem 属性。
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);
}
随后判断 document 对象是否处于 complete 状态。如果已完成加载,则给 body 设置 font-size=12*dpr 的值;否则判断 DOM 加载方法以设置 body 中的 font-size。此设置旨在控制页面中字体的大小,而 html 中的 font-size 则是为了设置页面的 height、width 等属性。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
JavaScript闭包与类(Class)的区别及适用场景对比
2026-11-05 15:48:57
JavaScript入门要学ES6吗?let与箭头函数等新特性解析
2026-11-05 14:49:16
JavaScript 8种数据类型详解:基本类型与引用类型的区别
2026-11-05 14:25:15
怎么在JavaScript中使用async/await优化Ajax异步流程
2026-11-05 13:56:15
JavaScript replace回调函数实现动态正则替换逻辑
2026-11-05 13:52:13
