前言
目前,很多团队都选择 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 还支持
640px
、828px
两种尺寸的设计稿,更多请看设计稿及尺寸单位。
那么,我们在编写 CSS 样式的时候,只要使用 px
单位即可,Taro 打包时会使用插件将其转换为对应平台的单位(如 rpx
或 rem
)。
/* 编译前 */ .avatar { width: 50px; } /* 编译为小程序 */ .avatar { width: 50rpx; } /* 编译为 H5 */ .avatar { width: 1.0667rem; }
如果是通过 JavaScript 等书写样式,Taro 无法在编译时对其进行单位的转换,那么 Taro 提供了 Taro.pxtransform
API:
Taro.pxtransform(50) // 小程序:rpx,H5:rem
如果某些场景下,不想做单位的转换,那么只要将 px
单位,书写成 Px
或 PX
即可。
.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
的参照物是根元素(<html>
)的字体大小,当根元素的字体大小为 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
配置项,它接受一个 Number
或 Function
参数,描述如下:
Represents the root element font size or returns the root element font size based on the
input
parameter.
简单来说,书写值/rootValue = 转换值
,比如源码中编写的是 50px
,rootValue
配置值为 16
,那么转换结果为 50/16 = 3.125rem
。
所以,我通常设置为
100
的原因就是:换算负担最小,等于「零」负担。这样的话50px
换算为0.5rem
、123px
换算为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 |
以上那么多分辨率、像素啥的,怎么区分呢:
- 设备分辨率:是用户比较关注的购机指标,哈哈。
- 设备像素:是设计师关注的指标,常说的 750px 设计稿尺寸,对应的 750 就是指设备像素的宽度。
- CSS 像素:是开发者需关注的指标,同时要理解设备像素与 CSS 像素的关系。
- 设备像素比:等于“设备像素 / CSS 像素”。
我们是不是经常看得到以下 <meta>
元素对 Viewport(视口)的声明:
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no" />
width=device-width
:定义 Viewport 宽度,由于各浏览器默认的 Viewport 宽度可能是不同的,加之移动设备的屏幕大小寸土寸金,因此通常会将设备宽度设置为 Viewport 宽度。initial-scale=1
:定义设备宽度与 Viewport 之间的缩放比例。minimum-scale=1
:定义缩放比例的最小值。maximum-scale=1
:定义缩放比例的最小值。user-scalable
:取值yes
或no
,其中no
表示用户将无法缩放当前页面。
那么,为什么通常会这样设置呢?
原来「设备像素比」是指在未缩放状态下,设备像素与 CSS 像素的初始比例关系。
当网页缩放比例设为 1
时,document.documentElement.clientWidth
的返回值等于该设备横向 CSS 像素宽度。
3.3 动态设置根元素字体大小
接下来,介绍如何动态地设置根元素 <html>
的字体大小。
需要知道的是,通过 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
完整实现如下:
<script> !(function (n, e) { var t = n.documentElement var i = 'orientationchange' in window ? 'orientationchange' : 'resize' var d = function () { var n = t.clientWidth if (n) { var e = 50 * (n / 375) e = e > 54 ? 54 : e t.style.fontSize = e + 'px' } } if (n.addEventListener) { e.addEventListener(i, d) n.addEventListener('DOMContentLoaded', d) } })(document, window) </script>
需监听下 DOMContentLoaded
和 orientationchange
事件,触发时重新设置根元素字体大小。
四、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()
方法来替换字符串而已。从这里,你也理解了 onePxTransform
和 minPixelValue
配置项的作用。
至于匹配 px
的正则表达式如下(源码在这里):
const pxRegex = /"[^"]+"|'[^']+'|url\([^\)]+\)|(\d*\.?\d+)px/g
五、Taro H5 转换优化
使用 Taro 初始化的项目中 index.html
是这样处理的:
<script> !(function (n) { function e() { var e = n.document.documentElement, t = e.getBoundingClientRect().width; e.style.fontSize = t >= 640 ? "40px" : t <= 320 ? "20px" : (t / 320) * 20 + "px"; } n.addEventListener("resize", function () { e(); }), e(); })(window); </script>
我们将其修改为前面动态设置根元素 font-size
的方法:
<script> !(function (n, e) { var t = n.documentElement var i = 'orientationchange' in window ? 'orientationchange' : 'resize' var d = function () { var n = t.clientWidth if (n) { var e = 50 * (n / 375) e = e > 54 ? 54 : e t.style.fontSize = e + 'px' } } if (n.addEventListener) { e.addEventListener(i, d) n.addEventListener('DOMContentLoaded', d) } })(document, window) </script>
通过源码,我们知道 H5 中 rootValue
的计算如下,由于我们的 designWidth
为 750
,而 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 设置 rootValue
为 100
的效果,编译前后就如预期所想:
/* 编译前 */ .avatar { width: 50px; } /* 编译为小程序 */ .avatar { width: 50rpx; } /* 编译为 H5 */ .avatar { width: 0.5rem; }
The end.