手摸手教你开发一个postcss插件自动计算vw

1,960 阅读5分钟

因为平时工作中开发h5网页的时候,为了更好适配不同的尺寸, 经常会使用vw单位, 通常是通过 vw = 元素尺寸 / 设计图屏幕尺寸 , 比如一个元素在 37.5px的设计图下宽 100px, 那得到的 vw就是 37.5 / 375 * 100 = 10vw, 每次都需要这样去计算又特别麻烦, 所想把这个事情给自动化

postcss

postcss 会将 css转换为 ast, 并提供了运行插件的机制, 这么一看和babel 很像,只是处理的对象不同, 一个是js ,一个是css, 如果你对ast 或者 babel 的概念不清楚,推荐看看之前的这篇文章 保姆级教学!这次一定学会babel插件开发!

css 的 ast 在哪里查看呢, 到这个网站 astexplorer.net/, 将上面两个配置改成css和postcss即可

思路整理

写代码前思考环节是很重要的, 我们的目的有一下几点:

  1. 为了不影响老项目的px使用或者是一些必须px的地方, 我们不能暴力的针对所有px值进行转换
  2. 能够灵活设置屏幕尺寸, 保留小数点位数等等

那思考一下该如何一步步来做:

  1. 通过ast 访问,我们要匹配出例如 width: 300px,这样的值,取出 300 通过计算的出 vw的值, 并将px改为 vw
  2. 需要有一个标识符,我们好针对某些需要转换的px 进行处理, 其余的不进行处理, 这里我默认使用 width: '%300px' , 风格进行书写, 这里需要写成字符串(因为编辑器会报错)
  3. 允许动态传入参数
  4. 比如计算结果为 width: 80.00vw 的整数应该自动去掉小数点变为 width: 80vw , 减小体积

动手开发

因为公司使用的是postcss 7.x系列, 所以我这边使用7.x 系列的插件语法进行书写(8版本以上的插件注册有一点点不一样, 不过一通百通不影响)

先来看看如果我们什么逻辑都不写,最基本的一个插件应该如何去写

module.exports = postcss.plugin('plugin-name', option => { 
	return (root, result) => { 
	   // 在这里通过 root 操作 ast 
	}
})

知道了插件的基本模版, 那我们如何操作ast呢,postcss给我们定义了是这样几个类型

  • Root: PostCss处理过的Css,整个处理过程基本上都在围绕着Root,Commont,AtRule,Rule都是它的子节点。
  • AtRule: 为带@标识的部分,name为标识名称,params为标识参数。nodes为内部包含的其他子节点,可以是Commont,AtRule,Rule,这让我们可以自定义更多的规则
  • Rule: 选择器样式部分,一个选择器代表一个Rule,选择器对应的样式列表nodes为Declaration构造函数
  • Declaration: 为Css样式属性,prop为样式属性,value为样式值。可给Rule手动添加样式属性,也可以修改prop,value。

针对这些个类型, 文档中给我们提供了找到这些类型节点的方法如下, 具体可查看文档, 地址: postcss:

  • walk
  • walkAtRules
  • walkComments
  • walkDecls
  • walkRules

那我们应该用哪个来实现我们的功能呢, 这时候,astexplorer 网站的作用就体现出来了, 我们可以先将需要处理的css在上面生成ast进行查看, 可以看到,我们的 width: 300px 在一个 decl 节点里, 并且外层还有一个 rule , 这个 rule 就是叫.demo 的class

那我知道了, 接下来我是不是只需要处理 decl节点就行了。

来试一下

walkDecls 接收一个回调, 回调内会接收到 postcss给我们的 decl节点, 我们通过 正则将符合我们规则的value匹配出来, 计算后并将其改写

const postcss = require('postcss')
module.exports = postcss.plugin('PLUGIN_NAME', function (opts) {
  opts = opts || {}

  return function (root, result) {
    root.walkDecls(decl => {
	  // decl.value 就是上图ast内的 300px
      // 匹配出 '%300px' / '%-300px' 这样的值再进行处理,
      if( /\'%-?(\d+)px\'/.test(decl.value) ) {
        decl.value = decl.value.replace(/\'%-?(\d+)px\'/g, (matchStr) => {
          // 从 '%300px' 中提出去 300 
          const numberPx = matchStr.match(/-?\d+/g)
          // 设计图尺寸 375,先写死后面通过参数传入
          const toVw = (numberPx[0] / 375 * 100).toFixed(2)
          // 用+是为了 让 80.00 这种数字变成 80
          return +toVw + 'vw'
        })
      }
    })
  }
})

我们想办法运行一下我们书写的插件,这里我用的是vue-cli, 是在vue.config.js 中进行如下配置,

module.exports = {
 css: {
    loaderOptions: {
      postcss: {
        plugins: [
		  // 我们刚刚编写的插件文件名
          require('./test.postcss.js')({
			// 这里写参数
          })
        ]
      }
    }
  }
}
,

如果你的项目不是vue-cli 书写在 postcss.config.js也是同理, require 引入就行

查看一下执行结果

/* 处理前 */
.demo {
  width: '%300px';
  height: 400px;
  left: '%10px';
  border: '%-1px' solid black;
}

/* 处理后 */
.demo {
  width: 80vw;
  height: 400px;
  left: 2.67vw;
  border: -0.27vw solid black;
}

ok 成功了

然后我们想办法将其变得灵活可配置 定义三个参数

  • screen 设计图屏幕宽度, 默认375
  • toFixed 小数点保留位数, 默认 2
  • identifier 标识符 默认 '%'

我们来改造一下代码,如下:

const postcss = require('postcss')
module.exports = postcss.plugin('PLUGIN_NAME', function (opts) {
  opts = opts || {}
  const _screen = opts.screen || 375
  const _toFixed = opts.toFixed || 2
  const _identifier = opts.identifier || '%'

  const vwRgx = new RegExp(`\\'${_identifier}-?(\\d+)px\\'`, 'g')


  return function (root, result) {
    root.walkDecls(decl => {
      if( vwRgx.test(decl.value) ) {
        decl.value = decl.value.replace(vwRgx, (matchStr) => {
          const numberPx = matchStr.match(/-?\d+/g)
          const toVw = (numberPx[0] / _screen * 100).toFixed(_toFixed)
          return +toVw + 'vw'
        })
      }
    })
  }
})

测试一下:

插件参数如下

plugins: [
  require('./test.postcss.js')({
	screen: 750,
	toFixed: 4,
	identifier: 'vw'
  })
]

查看一下执行结果

/* 处理前 */
.demo {
  width: 'vw300px';
  height: 400px;
  left: 'vw10px';
  border: 'vw-1px' solid black;
}

/* 处理后 */
.demo {
  width: 40vw;
  height: 400px;
  left: 1.3333vw;
  border: -0.1333vw solid black;
}

这样我们的插件基本功能就实现完成了, 后面还有针对 @media 媒体查询单位的处理, 也是一样的就不过多阐述了,有兴趣的而已到github查看完整代码, 代码已上传 github , 欢迎star

插件的话也发布到了npm仓库 ,可以下载 postcss-px2vm 进行使用