从 postcss-pxtransform 源码到 Taro 跨端单位转换方案优化

配图源自 Freepik

目前,很多团队都选择 Taro 作为跨端跨框架解决方案,它可以使用 React、Vue 等语法,并支持编译为微信小程序、支付宝小程序、百度小程序以及 H5 等应用。

一、开始

# 全局安装 Taro
$ yarn global add @tarojs/cli

# 初始化项目
$ taro init simple-taro

# 启动/打包项目
$ yarn dev:weapp
$ yarn build:weapp

注意,@taro/cli@tarojs/taro 版本应保持一致,若版本升级应两者同步调整,以避免两者版本不一致导致的一些编译问题。

当我们在本地创建 Taro 项目之后,如果是 750px 的设计稿尺寸,通常会对 Taro 的编译配置调整为:

const config = {
  designWidth: 750,
  deviceRatio: { 750: 1 }
}

除此之外,目前 Taro 还支持 640px828px 两种尺寸的设计稿,更多请看设计稿及尺寸单位。

那么,我们在编写 CSS 样式的时候,只要使用 px 单位即可,Taro 打包时会使用插件将其转换为对应平台的单位(如 rpxrem)。

/* 编译前 */
.avatar {
  width: 50px;
}
 
/* 编译为小程序 */
.avatar {
  width: 50rpx;
}
 
/* 编译为 H5 */
.avatar {
  width: 1.0667rem;
}

如果是通过 JavaScript 等书写样式,Taro 无法在编译时对其进行单位的转换,那么 Taro 提供了 Taro.pxtransform API:

Taro.pxtransform(50) // 小程序:rpx,H5:rem

如果某些场景下,不想做单位的转换,那么只要将 px 单位,书写成 PxPX 即可。

.avatar {
  width: 50Px; /* 将会被忽略 */
}

若要忽略某个文件,则在文件顶部添加 /* postcss-pxtransform disable */ 注释即可。

但请注意,以上忽略规则,仅仅忽略了 Taro 编译时单位转换。如果项目中使用了 Prettier 或 Stylelint 等格式化工具(或编辑器中启用了某个格式化插件)的话,在保存文件时,由于自动格式化,可能会对样式文件进行 lowercase 处理,因此需要添加对应的 ignore 处理(比如 /* prettier-ignore */ 等)。

除了以上常用的功能之外,还提供了如下配置项:

postcss: {
  pxtransform: {
    enable: true,
    config: {
      onePxTransform: true, // 设置 1px 是否需要被转换
      unitPrecision: 5, // rem 单位允许的小数位
      propList: ['*'], // 允许转换的属性
      selectorBlackList: [], // 黑名单里的选择器将会被忽略,不做转换处理
      replace: true, // 直接替换而不是追加一条进行覆盖
      mediaQuery: false, // 允许媒体查询里的 px 单位转换
      minPixelValue: 0 // 设置一个可被转换的最小 px 值
    }
  }
}

二、为什么还要优化呢?

既然 Taro 已经提供了相对比较完备的解决方案,为什么还要优化呢?

本文将会以 750px 设计稿为例。

痛点在哪?请看前面的示例:

/* 编译前 */
.avatar {
  width: 50px;
}
 
/* 编译为小程序 */
.avatar {
  width: 50rpx;
}
 
/* 编译为 H5 */
.avatar {
  width: 1.0667rem;
}

从编译结果看,50px 转换为 H5 之后,对应大小是 1.0667rem如果我们在开发过程中需要使用 Chrome DevTools 对页面进行调试,当我们尝试对 width: 1.0667rem 进行修改,是不是很头痛?

假设编译结果如下,换算是不是没有负担了,即使在原来基础上添加 “2px” 进行调试,是不是改为 0.52rem 就好了。

/* 编译前 */
.avatar {
  width: 50px;
}

/* 编译为 H5 */
.avatar {
  width: 0.5rem;
}

但注意,小程序还是使用 Taro 默认的转换方案,下面会介绍 H5 端的实现。

三、转换原理

在实现以上设想之前,我们需要了解下 Taro 的实现原理(不难)。

我想,如果开发过 H5 项目,应该使用过 postcss-pxtorem 这个插件做单位转换。顾名思义,就是 px 转换为 rem。而 Taro 的单位转换插件 postcss-pxtransform 正是基于它二次开发而来的,在原来的基础拓展了对小程序的支持,即 px 转换为 rpx

我们知道 CSS 中的相对长度单位 rem 的参照物是根元素()的字体大小,当根元素的字体大小为 16px 时,1rem 表示 16px 的长度。

那么,假设设计稿中某处长度为 123px 时,按照根元素字号 16px 来换算,对应就是 123 / 16 = 7.6875rem,这样的话换算负担非常大。

试想,如果换算的基础值是 100,那么无论你是 123px,还是 345px,那么换算为 rem 只要除以 100 就好,这样的话换算负担为「零」。

3.1 postcss-pxtorem 的使用

在非 Taro 项目中,我通常是这样使用 postcss-pxtorem 的:

// postcss.config.js
module.exports = {
  // ...
  plugins: [
    require('postcss-pxtorem')({
      propList: ['*'],
      rootValue: 100,
      minPixelValue: 2
    })
  ]
}

相应 Webpack 配置就不细说了,很简单你们都懂的。在说明为什么这样设置之前,我们先看下 postcss-pxtorem 的配置项的默认值:

{
  rootValue: 16,
  unitPrecision: 5,
  propList: ['font', 'font-size', 'line-height', 'letter-spacing'], // 这些 CSS 属性将会被转换
  selectorBlackList: [],
  replace: true,
  mediaQuery: false,
  minPixelValue: 0,
  exclude: /node_modules/i,
}

基本与前面提到的一致,这里仅介绍 rootValue 配置项,它接受一个 NumberFunction 参数,描述如下:

Represents the root element font size or returns the root element font size based on the input parameter.

简单来说,书写值/rootValue = 转换值,比如源码中编写的是 50pxrootValue 配置值为 16,那么转换结果为 50/16 = 3.125rem

所以,我通常设置为 100 的原因就是:换算负担最小,等于「零」负担。这样的话 50px 换算为 0.5rem123px 换算为 1.23rem

3.2 设备像素与 CSS 像素

在往下之前,先了解下这些内容:

设备 设备分辨率 设备像素 CSS 像素 设备像素比
iPhone 5/5s 640 × 1136 640 × 1136 320 × 568 2
iPhone 6/6s/7/8 750 × 1334 750 × 1334 375 × 667 2
iPhone 6/6s/7/8 Plus 1080 × 1920 1242 × 2208 414 × 736 3
iPhone X/XS 1125 × 2436 1125 × 2436 375 × 812 3
iPhone XR 828 × 1792 828 × 1792 414 × 896 2
iPhone 11 Pro 1125 × 2436 1125 × 2436 375 × 812 3
iPhone XS Max/11 Pro Max 1242 × 2688 1242 × 2688 414 × 896 3
iPhone 12 mini 1125 × 2436 1125 × 2436 375 × 812 2
iPhone 12/12 Pro 1170 × 2532 1170 × 2532 390 × 844 3
iPhone 12 Pro Max 1284 × 2778 1284 × 2778 428 × 926 3

这里,推荐有两个站点 YESVIZ 和 My Device,里面可以查看常见设备的各种参数。

以上那么多分辨率、像素啥的,怎么区分呢:

  • 设备分辨率:是用户比较关注的购机指标,哈哈。
  • 设备像素:是设计师关注的指标,常说的 750px 设计稿尺寸,对应的 750 就是指设备像素的宽度
  • CSS 像素:是开发者需关注的指标,同时要理解设备像素与 CSS 像素的关系。
  • 设备像素比:等于“设备像素 / CSS 像素”。

我们是不是经常看得到以下 元素对 Viewport(视口)的声明:


  • width=device-width:定义 Viewport 宽度,由于各浏览器默认的 Viewport 宽度可能是不同的,加之移动设备的屏幕大小寸土寸金,因此通常会将设备宽度设置为 Viewport 宽度。
  • initial-scale=1:定义设备宽度与 Viewport 之间的缩放比例。
  • minimum-scale=1:定义缩放比例的最小值。
  • maximum-scale=1:定义缩放比例的最小值。
  • user-scalable:取值 yesno,其中 no 表示用户将无法缩放当前页面。

那么,为什么通常会这样设置呢?

原来「设备像素比」是指在未缩放状态下,设备像素与 CSS 像素的初始比例关系。

当网页缩放比例设为 1 时,document.documentElement.clientWidth 的返回值等于该设备横向 CSS 像素宽度。

3.3 动态设置根元素字体大小

接下来,介绍如何动态地设置根元素 的字体大小。

需要知道的是,通过 JavaScript 脚本获取的某个元素的宽高等长度,对应的是 CSS 像素,而不是设备像素,更不是设备分辨率。比如:

以 750px 设计稿为例,其课代表是 iPhone 7 等机型。

设备 设备像素 CSS 像素 设备像素比
iPhone 6/6s/7/8 750 × 1334 375 × 667 2

话句话说,750px 设计稿上的 100px 对应 iPhone 7 的 CSS 像素则为 50px,所以将根元素字体大小设为 50px,此时 1rem = 50 CSS 像素 = 100 设备像素

那么,如何适配其他设备呢,基于 iPhone 7 的 375px 横向宽度计算即可,如下:

rootFontSize = document.documentElement.clientWidth / 375 * 50

完整实现如下:


需监听下 DOMContentLoadedorientationchange 事件,触发时重新设置根元素字体大小。

四、Taro 转换原理

前面提到 Taro 团队对 postcss-pxtorem 进行了二次开发,以适配多端的单位转换。

其插件地址请看 postcss-pxtransform 。

其配置项与 postcss-pxtorem 是类似的,最大的区别在于 rootValue 上。尽管在自述文件中说明了 rootValue 是必填的,但其实是没用的。

从源码中看,rootValue 在不同端会有不同的换算规则。

  • 源文件 taro/packages/postcss-pxtransform/index.js。
  • 源文件中 rootValue 的关键逻辑,请看这里。

如果 Taro 的编译配置如下:

const config = {
  designWidth: 750,
  deviceRatio: { 750: 1 }
}

那么从源码中,可以知道调用 rootValue() 方法,将会得到什么值。以 50px 为例:

小程序端:

options.rootValue = input => 1 / options.deviceRatio[designWidth(input)]

// 根据配置,可知 designWidth(input) 结果为 750,
// 因此 options.deviceRatio[designWidth(input)] 即为 1
// 所以,小程序端转换,仅涉及单位的转换(px => rpx),数值是不变的,即转换结果为 50rpx。

H5端:

options.rootValue = input => baseFontSize * designWidth(input) / 640

// 其中 baseFontSize 是源码中写死的 40
// 其中 designWidth(input) 为 750,
// 因此该方法返回值将会是 46.875
// 所以 H5 端转换,50px 将会转换为 1.06666667 rem

到这里,你应该就明白其转换结果为什么会是这样的了。

/* 编译前 */
.avatar {
  width: 50px;
}
 
/* 编译为小程序 */
.avatar {
  width: 50rpx;
}
 
/* 编译为 H5 */
.avatar {
  width: 1.0667rem;
}

具体的转换过程,请看源码 createPxReplace 部分:

这个方法非常简单,理解前面内容之后,看完全没有难度,本质上就是通过 String.prototype.replace() 方法来替换字符串而已。从这里,你也理解了 onePxTransformminPixelValue 配置项的作用。

至于匹配 px 的正则表达式如下(源码在这里):

const pxRegex = /"[^"]+"|'[^']+'|url\([^\)]+\)|(\d*\.?\d+)px/g

五、Taro H5 转换优化

使用 Taro 初始化的项目中 index.html 是这样处理的:


我们将其修改为前面动态设置根元素 font-size 的方法:


通过源码,我们知道 H5 中 rootValue 的计算如下,由于我们的 designWidth750,而 baseFontSize 则是写死的 40

options.rootValue = input => baseFontSize * designWidth(input) / 640

那么如果要使得 rootValue() 方法的返回值为 100,意味着需要将 designWidth 设为 1600。但是小程序端的 designWidth 仍要设为 750,因此通过 process.env.TARO_ENV 变量来控制即可,如下:

const config = {
  designWidth: process.env.TARO_ENV === 'h5' ? 1600 : 750,
  deviceRatio: { 750: 1 },
  mini: {
    postcss: {
      pxtransform: {
        enable: true,
        config: {
          platform: 'weapp',
          minPixelValue: 2,
          onePxTransform: false
        }
      }
    }
  },
  h5: {
    postcss: {
      pxtransform: {
        enable: true,
        config: {
          platform: 'h5',
          minPixelValue: 2,
          onePxTransform: false,
        }
      }
    }
  }
}

至此,就能实现类似 postcss-pxtorem 设置 rootValue100 的效果,编译前后就如预期所想:

/* 编译前 */
.avatar {
  width: 50px;
}
 
/* 编译为小程序 */
.avatar {
  width: 50rpx;
}
 
/* 编译为 H5 */
.avatar {
  width: 0.5rem;
}

The end.

你可能感兴趣的:(从 postcss-pxtransform 源码到 Taro 跨端单位转换方案优化)