Rain's Blog

unocss实现多端的rem适配

Rain, Wed Oct 11 2023back

在taro中有关设计稿及尺寸单位中有这样一段话

在 Taro 中书写尺寸按照 1:1 的关系来进行书写,即从设计稿上量的长度 100px,那么尺寸书写就是 100px,当转成微信小程序的时候,尺寸将默认转换为 100rpx,当转成 H5 时将默认转换为以 rem 为单位的值。

本文会在这段话的基础上带你实现react+unocss的rem适配,则pc中正常使用固定单位,而在移动端中,设计稿的尺寸是多少,移动端开发中的尺寸就写入多少,通过改动配置去实现移动端的不同屏幕的等比例缩放。

传统开发

如何让一个项目同时支持pc端和移动端,在以往的开发中有多种实现方式。

1.项目分离 即pc端一套代码,移动端一套代码,外接一个启动页,当用户去访问网址的时候,判断用户当前的访问设备和尺寸,根据这些参数去重定向对应的网站。比如 https://xxx.com/pchttps://xxx.com/phone。这种方式利弊分明。 好处是代码分离,结构清晰明了,维护起来不复杂。坏处则是大部分的ui代码效果不一致,而他们的业务逻辑是差不多是相同的,当业务发生变动的时候,一样的代码需要在分离的两套代码中书写两次。

2.项目不分离 在单页面应用中,对相似度特别接近的组件只需要根据媒体查询稍微改下样式即可,没有相似度的组件则需要重写一套,然后根据尺寸去展示对应的组件。相较于第一种方式来说,在代码层面会增加较多的文件,可以看做把pc和移动端的代码都合并在一个项目了,但业务逻辑相关的代码不会有太大的改动。

const Index = () => {
  const {isPhone} = useMediaScreen()
  return isPhone ? <Phone /> : <Pc />
}

rem适配方案

在pc端开发中,设计稿的元素尺寸是多少,那么开发中书写的尺寸就是多少,这个没有太大的问题。而在移动端中却不能这么做,一个长宽为100px的div在宽度为375px的设备和在一个宽度为320px的设备看起来的效果是完全不一样的,前者看起来正常,后者看起来就会有一种被放大,突兀的感觉。

移动端rem适配是目前比较常用的方案,通过在根标签设置字体大小作为基准值,当移动端页面尺寸发生变化的时候动态改变这一个值,在页面元素中根据基准值转换到正确的rem大小进行设置,达到等比例的缩放效果。 通过监听页面尺寸变化,动态改变根字体大小,这样就实现了rem适配

html{
  font-size:20px,
}

div{
  width:'100px';
  height:'100px';
}

/* 将上面的尺寸转化为rem */
div{
  width:5rem;
  height:5rem;
}

不过上面的写法也是很不方面,就是每次写尺寸的时候都需要手动去换算一遍rem,这里可以通过 px2rem 插件去转换。

rem进阶版

接入unocss如何实现跟上面第二种方案一致的效果呢。

当直接设置尺寸单位,如 w-20px,会被编译为 width:20px,而不加单位 w-20 就会被转化为 width:5rem

适配不同的屏幕尺寸,改变不同屏幕尺寸的根字体大小,可以得到对应屏幕的基准值

  function resizeRem() {
    let docDocument = document.documentElement
    let docWidth = docDocument.clientWidth
    if (docWidth <= 768) {
      docDocument.style.fontSize = docWidth / 10 + 'px'
    } else {
      docDocument.style.fontSize = ''
    }
  }
  resizeRem()
  window.addEventListener('resize', debounce(resizeRem, 0), false)

当宽度等于375的时候,根字体的大小为37.5rem,由于unocss的基准值默认是4px = 1rem,且没有修改的办法,所以这里可以从另外一个方面入手,将编译后有关尺寸的数值进行计算,就可以得到对应的参数

import { defineConfig, presetAttributify, presetUno, transformerVariantGroup } from 'unocss';

export default defineConfig({
  transformers: [transformerVariantGroup()],
  presets: [
    presetUno(),
    presetAttributify({
      prefix: 'uno-',
      prefixedOnly: true,
    }),
  ],
  postprocess: [
    (util) => {
      const remRE = /(-?[\.\d]+)rem/g;
      const designWidth = 422; // 设计稿宽度
      const defaultWidth = 384; // 默认浏览器窗口宽度
      const rootFontSize = 42.2; // 基准字体大小
      const unoDefaultFontSize = 4; // UnoCSS 默认字体大小
      const scale = defaultWidth / designWidth;

      util.entries.forEach((entry) => {
        const value = entry[1];
        if (value && typeof value === 'string' && remRE.test(value))
          entry[1] = value.replace(remRE, (_, p1) => {
            const computeRem = (rootFontSize / unoDefaultFontSize) * scale;
            return `${(p1 / computeRem) * scale}rem`;
          });
      });
    },
  ],
});

最后用一个header组件来展示效果,

pc设计稿为1920px,header宽度100%,高度80px

<header class="w-100% h-80px"></header>

移动端设计稿为375px,header宽度100%,高度56px

<header class="w-100% h-56"></header>

此时基准值不再是unocss默认的4px=1rem,而是 unocss默认基准值 / (37.5 / 4)

拿56为例,默认基准值为 (56 / 4) / (37.5 / 4) = 1.4933333333333334

此时根字体37.5px = 1rem,56px 就等于 1.4933333333333334rem