Skip to content

HTML+CSS 移动端适配方案 -bilibili

移动端显示PC网页

视口概念 -MDN

  • 布局视口 layout viewport
  • 视觉视口 visual layout
  • 理想视口 ideal layout

🤔 让我们把时间倒回移动设备刚出来的时候,一台手机设备要显示PC网页(各种布局尺寸写死了px)的话,要如何展示?

答案是,想办法等比例缩放网页,网页代码里写死的尺寸也需要被缩放,那么就不是简单的把容器缩放了,还需要对所有尺寸都缩放

因此px单位的尺寸在移动端都需要被重新计算,也就是移动端浏览器另外计算尺寸

🤔 那么怎么缩放呢?

无论PC网页多宽都缩放到浏览器宽度,比例是 PC宽度/移动端宽度?

  • 一个PC网页最终的宽度也是未知的,只有PC浏览器运行了才知道
  • 因此移动端浏览器也不知道PC网页的宽度
  • 而假设直接把PC网页宽度设置为移动端设备的宽度的话
  • 各种px布局尺寸会乱版

希望保持PC网页在PC浏览器那样的布局而只是缩放大小的话

  • 移动端浏览器就需要设置一个假想的PC网页宽度
  • 把PC网页放在这个宽度里布局后
  • 再按照移动端宽度等比例缩放网页

👆 根据我们的思考设计

移动端浏览器需要对所有网页设置一个假想的网页宽度 根据设备的不同,有可能是768px980px1024px等 通过浏览器api可以获取 document.documentElement.clientWidth/clientHeight

👇 这个宽度就是布局视口 layout viewport

再按移动端设备宽度等比例缩放网页 通过浏览器api可以获取 window.innerWidth/innerHeight 👇 这个移动端设备宽度就是 视觉视口 visual layout

另外假设我们只针对某个设备宽度开发一个网页 这个网页不需要移动端浏览器来重新计算我们的尺寸

这种开发就是针对一个移动设备的理想视口 ideal layout进行的 👇 也就是理想视口,不同于布局视口视觉视口是个更概念的概念

虽然👆的设计帮我们自动适配了移动端显示PC网页

但是这并不是适合移动端的交互方式 直接缩放页面会导致页面字体变小,使得缩放后的页面显示效果都不会很理想 因此当我们网页是纯移动端时,并不希望用移动端浏览器默认为我们做重新计算单位 而是让它保持原尺寸并且网页宽度为移动端设备宽度,即使乱版

meta的viewport

👇 我们写一个干净的HTML

html
<!DOCTYPE html>
<html>
  <head> <title>Document</title> </head>
  <style>
  .test{
    width: 100px;
    height: 100px;
    background-color: #ccc;
  }
  </style>
  <body> <div class="test">100px*100px</div> </body>
</html>

👆 切换移动端/PC端,会发现选中元素都显示是100px,但是视觉上尺寸切换明显不一样

这就是移动端为了显示PC网页所做的等比例缩放处理,并且是默认处理的

就像我们说的这并不是适合纯移动端网页的交互方式

💻 我们需要关闭浏览器默认对网页缩放的处理

了解了 👆 HTMLmeta 元信息的设计 CSS 设备适配规范(CSS Device Adaptation specification)定义了 nameviewport 元数据名称,并且这个属性目前仅在移动设备上生效

👇 这是 vscode 自动生成的 html 模板

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

👆 我们看到只针对移动端生效的 meta 配置了两个属性 width initial-scale

viewport 中的 width

定义 viewport 的宽度,如果值为正整数,则单位为像素。 正整数,或字符串 ‘device-width’

这个宽度就是上面 移动端显示PC网页中的假想PC网页宽度 布局视口layout viewport

默认值根据设备的不同,有可能是768px、980px或1024px等

这个值的大小影响网页布局后缩放的尺寸

假设我们设置布局视口宽度为100px 里面的100px方块就会铺满100px,再进行缩放的效果将会是铺满的

👆 设置了 <meta name="viewport" content="width=100">

  • 但是指定具体的宽度值,并不能适配所有的移动设备尺寸
  • 这里有个特殊的值 ‘device-width’
  • 动态设置视口宽度为设备宽度

👆 这就是我们希望关闭浏览器默认对网页缩放的处理(就是缩放前后比例相等),这也是 vscode 为我们默认生成的模版中的值

效果上:不同的设备显示的方块大小将会一样,无论设备大小都不会缩放

viewport 中的 initial-scale

定义设备宽度(宽度和高度中更小的那个:如果是纵向屏幕,就是 device-width,如果是横向屏幕,就是 device-height)与 viewport 大小之间的缩放比例。 0.010.0 之间的正数 默认值 1.0

👆 会在移动端自动缩放的基础上再缩放一次,默认值就是 1.0 不缩放

但是这个值是移动端浏览器处理的,当不设置的时候,有可能会被浏览器设置成别的缩放值,如PC浏览器设置里可以设置默认缩放比

因此保险起见,手动设置成 1.0 这也是 vscode 为我们默认生成的模版中的考虑

viewport 禁止移动端缩放网页

<meta name="viewport" content="user-scalable=no">

设置为 no,用户将无法缩放当前页面 浏览器设置可以忽略此规则;iOS 10 开始,Safari iOS 默认忽略此规则 默认为 yes

一般纯移动端网页也不希望被人缩放导致乱版

移动端显示纯移动端网页

经过 👆 关闭浏览器默认对网页缩放的处理 <meta name="viewport" content="width=device-width"> 我们得到了一个无论设备大小都不会缩放的布局容器

但是我们希望不同移动端设备还是能根据设备不同有一点缩放处理


🤔 既然我们有能力设置决定缩放比例的视口宽度,为什么不把布局视口设置成750/375px,然后代码CSS写相应的布局视口下的尺寸呢

经过浏览器自动的缩放不是也能实现不同移动端适配的效果吗?

👆 我们把视口宽度设置为 375,设置方块宽高为 187.5 ,也就是屏幕的一半

拉伸设备宽度可以看到始终保持设备的一半,这不就是我们希望实现的不同设备适配了吗?

而且按照375编写的css尺寸,在PC浏览器上也会是希望的375尺寸(视口宽度只在移动端生效)

🤯 为什么啊?利用浏览器自动缩放实现不同设备不好吗?为什么要把移动端适配搞得这么复杂啊!

缺点是不能局部控制不缩放,比如1px希望所有设备都是1px不缩放(其他方案也是通过特殊处理1px不缩放的

那设置viewport的方案也可以特殊处理1px吧,比如说伪元素

移动端开发适配不同尺寸方案

不依赖浏览器自动对所有尺寸做的缩放,而是我们自己决定所有尺寸的缩放

因此着手的地方就是每个写尺寸的地方 写成rem/vw这种动态的单位

vw

按视觉视口(设备宽度)为375px,那么 1vw = 3.75px ,这时UI给定一个元素的宽为75px(设备独立像素),我们只需要将它设置为 75 / 3.75 = 20vw

设计稿总宽度px / 100vw = 目标样式px / 得出的vw

得出的vw = 目标样式px / (设计稿总宽度px / 100vw)

👇 scss

scss
@vv = 375/100

@function vw($px) {
  @return ($px / @vv) vw;
}

.class-name{
  width: vw(187.5);
}

👇 css

css
root {
  --vv : calc(375 / 100); /* 不能写单位 */
}

.class-name{
  width: calc(187.5vw / var(--vv)); /* 要写单位vw */
}

rem

把设计稿总宽度375,分为 100rem 则样式css187.5px,占50rem

  • 设计稿总宽度px / 100 = 目标样式px / 得出的rem
  • 得出的rem = 目标样式px / (设计稿总宽度px / 100)

👆 其实和vw的做法相同,需要编写css函数来计算

👇 另外还要做的是把1rem对应的尺寸得出来

  • 设备宽度/100 = 1rem = 根元素font-size
html
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1.0" >
</head>
<style>
* {
  padding: 0;
  margin: 0;
  --vv : calc(375 / 100);
}
body{
  font-size: 16px; /* 还原文字默认大小 */
}
.test{
  width: calc(187.5rem / var(--vv));
  height: 187.5px;
  background-color: #ccc;
}
</style>
<script>
  function setRem() {
    const deviceWidth = window.innerWidth
    const htmlEl = document.documentElement;
    // 将设备宽度分为100份,作为1rem的大小 font-size
    htmlEl.style.fontSize = (deviceWidth/100) + 'px';
  };
  // 第一次进入页面调用
  setRem();
  window.addEventListener('resize', setRem);
</script>

<body> <div class="test">187.5*187.5</div> </body>
</html>

因为1rem对应多少由我们决定,因此我们可以让1rem等于设计稿的``1px

可以想办法让开发直接写rem,而减少写css函数的繁琐

375的设计稿,187.5px的样式css,开发代码可以写成 width: 187.5rem

而不是 width: calc(187.5rem / var(--vv))

👇 设计稿375px1rem1px设备宽度/设计稿宽度 = 1rem = 根元素font-size

js
function setRem() {
  const deviceWidth = window.innerWidth
  const htmlEl = document.documentElement;
  // 将设备宽度氛围设计稿宽度的份数,使设计稿1px对应1rem font-size
  htmlEl.style.fontSize = (deviceWidth/375) + 'px';
};
setRem();
window.addEventListener('resize', setRem);

1px问题

1px问题也就是有时候希望样式尺寸不缩放,如1px的边框在无论大小的设备上都是一样的大小

vwrem的方案里

  • 不用css函数rem单位写的尺寸
  • px写尺寸就实现了不缩放的尺寸

这个问题更多的是针对使用webpack编译插件扫描所有px转化为vw/rem导致的全局缩放问题

这些插件都有相应的配置项,设置如大于2px才转化为vw/rem

🤔 问题是如果用的viewport width=设计稿宽度,利用移动端浏览器自动缩放的方案

所有px都会被自动缩放,没办法局部设置不缩放

0.5px问题

0.5px问题不属于移动端适配的问题,这里作为题外话

设计稿中有时会有

  • 750设计稿1px
  • 375设计稿0.5px

👆的需求

而浏览器不支持1以下的px,有些浏览器会当作1px显示,有些浏览器会不显示

因此假如要实现这种0.5px

需要绘制不被缩放的1px,利用css的缩放,缩小一半来实现

物理像素

名词

  • 抽象像素 - 代码CSS像素
  • 设备物理像素 - 宽高像素值(随设备不同变化)
  • 设备独立像素 - CSS1px对应的物理像素(随设备不同变化)

代码css设置的1px并不是真实的设备上的1像素 而是运行时根据设备的物理像素和CSS像素的比例计算出来的物理像素,如 显示物理像素 = 代码CSS像素 * (设备物理像素和设备独立像素的比例)

👇 PC浏览器调试模式下,iphone6的设备尺寸

这里的375px并不是iphone6的物理像素,而是设备独立像素 而查询到设备物理像素是750px 也就是1个CSS像素对应2个物理像素 👆 这就是我们要的比例

而浏览器有提供相应的api给我们获取 devicePixelRatio -MDN

返回当前显示设备的物理像素分辨率与CSS 像素分辨率之比 此值也可以解释为像素大小的比率:一个 CSS 像素的大小与一个物理像素的大小 简单来说,它告诉浏览器应使用多少屏幕实际像素来绘制单个 CSS 像素

console.log(window.devicePixelRatio)输出了2,即375对应750物理像素

👇 常见的设备像素比:

  • 普通密度桌面显示屏:devicePixelRatio = 1
  • 高密度桌面显示屏(Mac Retina):devicePixelRatio = 2
  • 主流手机显示屏:devicePixelRatio = 2 or 3

决定我们使用几倍图会清晰

lib-flexible.js

lib-flexible -github

1. 🔧 工具库代码结构上是立即执行函数IIFE

js
(function flexible (window, document) {
  // ...
}(window, document))

👆 我们可以马上联想到其他直接通过<script>标签引入的第三方库

vue.min.js当中的模块化

但是这里的 windowdocument 参数没必要传递吧 如模块化文章中,参数一般用于在UMD中区分全局变量环境使用 也就是传递进一个this的话,适合这样传

但是这个🔧工具函数的使用场景只有浏览器 👇 感觉用普通立即执行函数就可以了

js
(()=>{
  // ...
})()

🤔 为什么要用立即执行函数,引入一个js不是本来就会立即执行吗?

因为直接引入的js作用域是全局的,会造成变量污染,用闭包包住后,变量不会影响其他js

2. 立即设置1rem的font-size并且监听窗口事件

js
var docEl = document.documentElement
// set 1rem = viewWidth / 10
function setRemUnit () {
  var rem = docEl.clientWidth / 10
  docEl.style.fontSize = rem + 'px'
}

setRemUnit()
window.addEventListener('resize', setRemUnit)

3. 还原默认字体大小

js
// adjust body font size
function setBodyFontSize () {
  if (document.body) {
    document.body.style.fontSize = (12 * dpr) + 'px'
  }
  else {
    document.addEventListener('DOMContentLoaded', setBodyFontSize)
  }
}
setBodyFontSize();

4. 处理跳转第三方网页调整大小后返回不触发重新计算问题-监听返回

pageshow -MDN

js
window.addEventListener('pageshow', function (e) {
  if (e.persisted) {
    setRemUnit()
  }
})

5. 判断设备支不支持0.5px

👇 设备像素比 大于等于2 并且 两条0.5px占文档流1px偏移量

js
var docEl = document.documentElement
var dpr = window.devicePixelRatio || 1

// 测试 0.5px supports
if (dpr >= 2) { // 设备像素比大于等于2
  var fakeBody = document.createElement('body')
  var testElement = document.createElement('div')
  testElement.style.border = '.5px solid transparent'
  fakeBody.appendChild(testElement)
  docEl.appendChild(fakeBody)
  // 两条0.5px占文档流1px偏移量
  if (testElement.offsetHeight === 1) {
    docEl.classList.add('hairlines') // 往全局dom添加一个 `hairlines` 的 className
  }
  docEl.removeChild(fakeBody)
}

👆 只是往全局dom添加了一个 hairlinesclassName

🤔 TODO: 并不会做什么操作,这个className要用来做什么

设置一个css变量不是更好用吗? 如 --hair-unit = 0.5 or 1,使用时设备支持则显示 0.5px 不支持则显示 1px

参考资料