编程崽

登录

一叶在编程苦海沉沦的扁舟之上,我是那只激情自射的崽

移动端开发-rem

移动端开发-rem

在开发移动端时,需要使用rem,就需要设置根字体大小(font-size),同时监听屏幕尺寸变化,实时调整根字体大小。

设置自适应的根字体大小

众所周知,根字体大小需要和屏幕宽度挂钩,这里就有两种方式。

一种是很简单,直接一行css就行。

第二种比较麻烦,就是使用js获取屏幕宽度,计算后再赋值,并且监听屏幕宽度变化,实时计算并修改覆盖。

我之前一直使用的都是第二种,后来再微信公众号中,看到了第一种纯css的写法,才豁然开朗。

这里推荐优先是第一种纯css的方式。

直接使用css配置(推荐)

直接使用css配置,方便快捷。

css 复制代码
html {
  font-size: calc(因数 * (100vw / UI图总宽度));
}

/* or */

/* 页面可视区域宽度小于等于 1000px 时,里面css生效 */
@media (max-width: 1000px) {
  html {
    font-size: calc(因数 * (100vw / UI图总宽度));
  }
}

其中,UI图总宽度不用多说,就是移动端页面的设计图的总宽度。

因数只是一个值,建议填写100,这样后面开发每个标签的样式时,直接把从UI图上测得的值,除以100,单位再改为rem即可,比如测得一个按钮的宽度是50px,那写这个按钮的宽度时,值就写成0.5rem

其中100这个因数必须要有,可以不是100。 因为,比如这里把因数改为1,且手机屏幕宽度恰好和UI图总宽度一样宽,这样计算完,font-size的值为1px,这里就应该设置根字体大小为1px。 但由于浏览器可能为了用户体验,可能会设置一个字体大小的最小限制,比如chrome的pc端限制字体最小为12px,小于此值时,仍然展示为12px。 由于移动端也可能会有类似的设置,所以这里乘以一个比例,让根字体的大小大一些,不过这样在写css时,也需要除以这个值,这是唯一的弊端,不过这个问题,在后面会解决。

e.g:因数设置为100,UI图总宽度为750,则写成下面即可,这样在使用时,7.5rem 就是正好宽度100%,因为 7.5rem * 因数100 = 750px

css 复制代码
html {
  font-size: calc(100 * (100vw / 750));
}

js指令

下面代码直接用<script>标签,插入到html的head中即可,因数也是设置的100,UI图总宽度uiWidth设置的是750,可以修改。

js 复制代码
(function (doc, win) {
  // 设置当前设置UI图的总宽度,直接看UI给的设计的宽度就行,固定的
  var uiWidth = 750
  var docEl = doc.documentElement
  var recalc = function () {
    // 乘以一个比例,作为rem的值,实现rem的值随着设别宽度的不同而不同
    var clientWidth = docEl.clientWidth || 0
    docEl.style.fontSize = 100 * (clientWidth / uiWidth) + 'px'
  }
  recalc() // 先自动触发一下
  win.addEventListener('resize', recalc, false) // 添加页面监听事件
})(document, window)

rem的用法

然后再使用的时候,量取了设计图的尺寸,写css时直接将这个值除以因数100,单位设置为rem。

比如在宽度为750的设计图上,量取一个按钮的宽度为125px,则写css时,宽度写为1.25rem即可,字体大小、圆角尺寸、阴影尺寸均可以以此类推。

css用法

css 复制代码
button {
  width: 1.25rem; /* (125px / 100 = 1.25rem) */
}

使用scss封装方法(暂不推荐使用了)

但对于开发着来说,每次写css还需要计算一下,哪怕这个计算是很简单的,也是很招人烦,所以如果使用了scss,就可以使用下面scss的语法,封装一个scss方法。

scss 复制代码
// phoneSet.scss 文件
$divisor: 100; // 需要除以的除数因子(js中的因数)

// 去掉单位 把传入的带单位的值的单位去掉
@function clearUnit($number) {
  @if type-of($number) == 'number' and not unitless($number) {
    @return $number / ($number * 0 + 1);
  }
  @return $number;
}

// 传过来一个带单位的值,比如1px,返回固定rem单位的值,比如3rem
@function r($numAndUnit) {
  @if not unitless($numAndUnit) {
    $numAndUnit: clearUnit($numAndUnit);
  }
  @return ($numAndUnit / $divisor) * 1rem;
}

注:新版的scss,貌似有些数学计算方法不再支持,所以如果报错的话,就需要换种写法

scss 复制代码
// phoneSet.scss 文件
@use "sass:math";
$divisor: 100; // 需要除以的除数因子(js中的因数)

// 去掉单位 把传入的带单位的值的单位去掉
@function clearUnit($number) {
  @if type-of($number) == 'number' and not unitless($number) {
    @return math.div($number, ($number * 0 + 1));
  }
  @return $number;
}

// 传过来一个带单位的值,比如1px,返回固定rem单位的值,比如3rem
@function r($numAndUnit) {
  @if not unitless($numAndUnit) {
    $numAndUnit: clearUnit($numAndUnit);
  }
  @return calc((#{$numAndUnit} / #{$divisor}) * 1rem);
}

用法

scss 复制代码
// 其他scss文件
@import(./phoneSet.scss) // 引入scss方法文件

button {
  width: r(125); /* 直接传入测量的值即可 */
}

使用tailwindcss的问题

tailwindcss的移动端优先

tailwindcss提供sm、md、lg、xl、2xl这5个默认断点,但在开发pc端+移动端页面时,会发现 tailwindcss 有一个问题。

首先列举一下这5个断点的官方示意。

断点前缀 最小宽度 CSS
sm 640像素 @media (min-width: 640px) { ... }
md 768px @media (min-width: 768px) { ... }
lg 1024px @media (min-width: 1024px) { ... }
xl 1280像素 @media (min-width: 1280px) { ... }
2xl 1536px @media (min-width: 1536px) { ... }

需要注意的是:当使用 md:bg-white 时,指的并不是 屏幕宽度小于 768px 时,而是 屏幕宽度大于等于768px时,背景色为白色!

因为 tailwindcss 是 移动端优先,当不添加前缀时的样式,是屏幕宽度大于等于0时的样式,当使用了md前缀,指的是屏幕宽度大于等于 768px 时,其他断点同理。

即使在 tailwind.config.ts 文件中添加断点:

json 复制代码
theme: {
  extend: {
    screens: {
      mo: '500px',
      pc: '1200px'
    },
  },
},

当是用 mo 前缀时,指的也是屏幕尺寸大于等于 500px 时会应用的样式,比较蛋疼。

tailwindcss相关文档 也有说明:不要使用sm:定位移动端设备,而是使用无前缀的类名来定位移动端样式,并在较大的断点处覆盖它们!

解决:进行自由配置

官方文档在此:自定义屏幕断点

下面摘抄整理文档中的示例,在 tailwind.config.js 文件中配置:

js 复制代码
/** @type {import('tailwindcss').Config} */
module.exports = {
  theme: {
    screens: {
      'sm': '200px', // 可以自定义key,也可以覆盖默认值
      // => @media (min-width: 200px) { ... }
      // 意思是生效的范围的「最小值」是「200px」,也就是大于等于200px时生效

      'pad': {'max': '900px'},
      // => @media (max-width: 900px) { ... }
      // 意思是生效的范围的「最大值」是「900px」,也就是小于等于900px时生效
      
      'md': {'min': '768px', 'max': '1023px'},
      // => @media (min-width: 768px and max-width: 1023px) { ... }
      // 意思是生效的范围是 >= 768 且 <= 1023
      
      'tall': { 'raw': '(min-height: 800px)' },
      // 完全自定义媒体查询的值
    }
  }
}

总结

下面给出最适用的配置。

例子中将要完成的效果:

  • 大于900px时认为是pc端,小于等于900px时认为是移动端
  • 当时pc端时,给html添加类名pc,当移动端时类名为mo
  • 默认样式是所有端生效,pc前缀的是pc端生效,mo 前缀的是移动端生效
  • 移动端设计图的总宽度是375px,所以设定开发移动端时,需要375rem时正好是100vw,方便开发

1. 根字体大小、行高的修改

css 复制代码
html {
  font-size: 16px;
}

/* 小于等于900px时应用的样式 */
@media (max-width: 900px) {
  html {
    /* 设置小于等于900px时的根字体大小 */
    /* 按下面这样设置,当测量设计图的按钮宽度是100px时,开发时这个按钮的样式可以直接设置为 100rem */
    font-size: calc(100vw / 375);
  }
  body {
    font-size: 16rem;
  }
}

2. 在pc端、移动端时,给html添加不同的类名

ts 复制代码
/**
 * 获取设备宽度信息
 * @returns [宽度像素值, 是否是移动端]
 */
function getDeviceInfo(): [number, boolean] {
  const deviceWidth = document.documentElement.clientWidth
  let isMobile = false
  if (deviceWidth < screenPc) isMobile = true
  return [deviceWidth, isMobile]
}

// 监听设备宽度和处理
export function watchDevice() {
  // 处理方法
  const handle = () => {
    let result = getDeviceWidth()
    if (!result) return
    const [deviceWidth, isMobile] = result
    // 拿到了「设备宽度」和「是否是移动端」
    // 可以存储到全局状态中
		let classList = document.querySelector('html')?.classList
    if (!classList) return
    // 添加、删除类名
    if (isMobile) {
      if (classList.contains('pc')) classList.remove('pc')
      classList.add('mo')
    } else {
      if (classList.contains('mo')) classList.remove('mo')
      classList.add('pc')
    }
  }
  // 先执行一次
  handle()
  // 添加监听
  window.addEventListener('resize', handle)
}

3. 为tailwindcss添加断点

添加 pc 和 mo 的断点, tailwind.config.js 文件中配置:

js 复制代码
/** @type {import('tailwindcss').Config} */
module.exports = {
  theme: {
    screens: {
      // 设置屏幕尺寸大于 900px 时的前缀
      // 下面的媒体查询,是:「小于等于900px的取反」,也就是大于900px
      'pc': {'raw': 'not (max-width: 900px)'},

      // 设置屏幕尺寸小于等于 900px 时的前缀
      'mo': {'max': '900px'},
    }
  }
}

css中的px自动转为rem的功能

下面这个页面的功能,是先设定 1px 等于多少 rem,然后粘贴px为单位css,把里面的px自动计算转换为rem的页面。

还是有点用处的,偶尔使用。

html 复制代码
<!DOCTYPE html>
<html lang="zh-cn">
<head>
  <meta charset="UTF-8">
  <title>px计算转为rem</title>
  <style>
    .box {
      width: max-content;
      margin: 100px auto 0;
    }
    .tip {
      margin-bottom: 20px;
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    .tip .text {
      text-align: center;
      color: #999;
    }
    .tip .setRow {
      margin: 10px auto 0;
    }
    .tip .setRow > input {
      width: 200px;
      line-height: 2em;
      padding: 0 5px;
    }
    .tip .setRow > span {
      line-height: 2em;
      color: #999;
    }
    .inputBox {
      display: flex;
    }
    textarea {
      resize: none;
      padding: 20px 10px;
    }
    textarea + textarea {
      margin-left: 10px;
    }
  </style>
</head>
<body>
  <div class="box">
    <div class="tip">
      <h1>css 中的 px 自动计算转换为 rem</h1>
      <div class="text">左侧框贴入px为单位的代码,右侧自动计算转换为rem之后的代码,同时会自动复制。</div>
      <div class="setRow">
        <input type="text" placeholder="1px === ?rem">
        <span>输入1px等于几rem</span>
      </div>
    </div>
    <div class="inputBox">
      <textarea placeholder="请粘贴css代码" class="input" name="" id="" cols="50" rows="20"></textarea>
      <textarea placeholder="转换后代码" class="output" name="" id="" cols="50" rows="20"></textarea>
    </div>
  </div>
</body>
<script>
  let setDom = document.querySelector('.setRow input')
  let inputDom = document.querySelector('.input')
  let outputDom = document.querySelector('.output')

  let setRem = 0.01
  setDom.value = setRem

  // 监听修改了转换比例
  setDom.addEventListener('input', (e) => {
    let newSetRem = setDom.value.trim()
    if (!/[0-9.]/.test(newSetRem)) return
    setRem = Number(newSetRem)
    handler()
  })

  // focus 自动全选
  setDom.addEventListener('focus', () => {
    setDom.select()
  })
  inputDom.addEventListener('focus', () => {
    inputDom.select()
  })
  outputDom.addEventListener('focus', () => {
    outputDom.select()
  })

  // 监听修改了输入的css
  inputDom.addEventListener('input', () => {
    handler()
  })

  // 处理
  function handler() {
    let input = inputDom.value
    let result = input.replace(/[0-9.]+px/ig, (text, b) => {
      let a = parseFloat(text)
      let result = `${a * setRem}rem`
      return result
    })
    outputDom.value = result
    navigator.clipboard.writeText(result)
  }
</script>
</html>