
px2rem/px2vw响应式方案探究
px2rem/px2vw响应式方案探究
响应式是一个很基础的概念,大家写前端的一定都很熟了,就不再过多赘述。这里主要是想整理一下如何实现从设计稿的 px 自动转换到 rem/vw 这种单位。
引例
这是一段简单的实现响应式的 demo,非常清晰的展示了如何根据设计稿上的元素单位,然后计算出一个最终的 rem 单位。我们要做的就是如何把这一部分自动化。
1 | // entry-task-1/src/index.tsx |
入口文件调用方法。
1 | // entry-task-1/src/helpers/index.ts |
然后通过 JS 获取到当前设备的宽度,除以设计稿宽度 320,乘了一个 100。这样设置html
的font-size
主要是为了后面写 CSS 的时候,直接根据设计稿的元素尺寸除以 100 就可以了。
1 | // entry-task-1/src/container/List/Styled/Header.tsx |
比如这里的padding: 0 0.16rem
就是还原了设计稿中的padding: 0 16px
。如果在不同的设备上展示,自然会设置不同的根元素font-size
,然后根据 rem 的特性自然能实现一个简单的响应式。
rem 和 vw
rem 我们大家都比较熟悉了,是指相对于根元素的字体大小的单位,但是似乎有一个问题。举个例子:如果窗口 resize,你还要做额外的逻辑处理来监听这部分,究其原因看起来本身就不太适合做响应式这件事情,因为中间需要根元素的font-size
转一层才能最终取到结果,
rem 方案应该是前几年手淘的 winter 推广的lib-flexible
弹性布局方案,里面用的 rem 来做弹性布局这件事推起来的。前几年手淘的lib-flexible
是一些历史兼容原因产生的,目前viewport
单位的兼容性已经很好了,在我看来已经可以替换掉了。那么我们可以用vw
来替换掉 rem 单位。
实现自动转换
那么上面阐述完响应式的基本原理之后,可以发现手动计算的过程还是太过于繁琐。我么可以尝试将这一部分自动化,自动计算出对应的结果。我们最终的目标是实现下面的这一个过程。

自动转换的方案还是有挺多的,可以使用 gulp/webpack 等打包工具,引入一些 px2vw/px2rem 等方案即可实现这一部分的转换。里面一般都是使用 PostCSS 来对 CSS 文件内容进行解析,vue 也提供了vue-loader
来针对单文件提供 CSS 输出。PostCSS 的最终产物都是一个 AST 结构,然后就可以根据这个 AST 结构做进一步处理。
但是针对使用 styled-components 技术栈的项目会有所不同,因为根据编译出的最终产物来看,styled-components 编译出的最终产物是 js 后缀的文件,这部分往往不能使用一些现有的库来进行处理,在 github 上找了一下。搜了一个babel-plugin-styled-components-px2vw 的 babel 插件,star 数量很少只有 9 个。
我们来看一下这个 babel 插件的具体的实现思路。
主流程

主流程如上图所示,我们现在只是先看一下大致的实现流程,后面会分析四个部分的实现逻辑。
babel 部分
babel 解析
对于 babel,我们大家肯定都不陌生,babel 的用途非常的广泛,现在的每一个项目几乎都离不开 babel 的处理。最常见的比如 ES6 -> ES5 等等。babel 提供的能力也非常强大,能把提供的代码解析生成 AST 结构的语法树。然后我们就可以根据遍历出来的这棵语法树修改对应的节点。其实就是大家非常熟悉的编译原理的部分,词法分析,语法分析,生成代码等等的一个过程,编译原理还是比较复杂的一个领域,这里了解不深就不做展开。
1 | export const StyledButton = styled.button` |
我们先来看上面这一段代码转换成 AST 之后的结果。

是一棵很长的树,可以看到最关键的红框部分,就是我们想要去处理的 CSS 部分逻辑。styled-components 利用了 ES6 的 标签模板字符串,因此这里解析出来的type
就是taggedTemplateExpression
,那么我们的 babel 插件就可以使用 babel 提供的遍历整个树的方法去拿到对应的节点,并且取到对应的数据。我们来看一看对应的代码实现。
babel 插件代码分析
1 | export default declare((api: ConfigAPI, options?: IConfiguration) => { |
因为针对 styled-components 并不只是我们上面的 demo 那么简单的写法,还有一些比如传入 props 等等的函数写法,因此还需要对应的处理逻辑。我们只看其中的核心部分,也就是上面 demo 的处理逻辑。首先可以看到一个 babel 插件的定义结构,babel 整体的设计思想才用了访问者模式,将操作部分独立拆分出来。然后使用了 babel 提供的traverse
遍历树的方法去查找TaggedTemplateExpression
这个节点。拿到节点之后呢使用了自己封装的replace
,我们来看一下对应的实现。
1 | function replaceWithRecord(cssText: string): string { |
上面的正则去匹配成对的结构。对于匹配到的内容会在外面包一层,即:
1 | width: 120px; |
转换成下面的这种结构,是为了后面的 PostCSS 能够方便处理。
1 | styled-fake-wrapper/* start of styled-fake-wrapper */ { |
对于处理后的逻辑我们可以看到是用了 PostCSS 里面的postcss-px-to-viewport
这个插件来做的。
1 | import px2vw from "postcss-px-to-viewport"; //TODO: upgrade to 8 version wait postcss-px-to-viewport support |
那么到这里 babel 的部分就分析完了,我们来看一下后面的 postcss-px-to-viewport
实现。
PostCSS 部分
PostCSS 和 babel 很像,只不过 PostCSS 是针对 css 内容处理的,因此根据上面 babel 处理后的 css 内容,我们这里就可以让一些成熟的方案来接管后面的处理了。
postcss-px-to-viewport 插件代码分析
1 | module.exports = postcss.plugin("postcss-px-to-viewport", function (options) { |
PostCSS 提供的遍历方法,会遍历每一条 css 规则,可以看到这里依然是用正则来匹配对应的规则,然后匹配到的规则用createPxReplace(opts, unit, size)
这个方法来替换。看一下这个方法的实现。
1 | function createPxReplace(opts, viewportUnit, viewportSize) { |
最关键的就是这一行var parsedVal = toFixed((pixels / viewportSize * 100), opts.unitPrecision);
其实就是根据书写的 css px 规则,来根据传入的配置做一个计算,来达到最终的 vw 单位。
那么到这里整个的过程就分析完了。你可以再返回主流程来看一下整个流程。核心思路就是使用 babel 处理 styled-components,来让处理后的结果能让目前比较广泛使用的库(比如 postcss-px-to-viewport)来处理。
最后
上面的方案虽然未必是最优解,但是提供了一个很好的思路,随着对 react 技术栈的深入探究,可以探索一下有没有更好地解决方案。
- 感谢你的欣赏!