将CSScomb集成到微信小程序项目中

最近在看 AlloyTeam 团队的 Code Guide 代码书写习惯,其中一项是 CSS 属性编写顺序。

虽然 CSS 属性顺序先后并不影响我们的程序,那为什么要做这件事呢?我理解是可分类、有迹可循、有利于排查错误。

由于属性众多,那么人总是会犯错的,所以我们借助一个工具来处理,它就是 CSScomb,用于 CSS 格式化、且可排序。官方描述是:CSScomb is a coding style formatter for CSS.

本文旨在使得 CSScomb 支持格式化小程序样式 .wxss同时也支持支付宝小程序 .acss,而百度小程序样式文件的扩展名是 .css,所以它天生就可以直接使用了。

文章有两篇:

  1. 将 CSScomb 集成到微信小程序项目中(本文)
  2. 将 CSScomb 集成到 Git Hook 中

项目放在 GitHub 上 csscomb-mini 欢迎 Star 。

此前也写过一篇文章【使 Prettier 一键格式化 WXSS】是讲述使用 Prettier 来格式化小程序的,但它没有 CSScomb 的属性排序功能。(后续可能会考虑整合起来,这是后话了)

一、CSScomb 安装使用

创建一个微信小程序项目,因过于简单,忽略该步骤。

1. 本地安装
$ yarn add csscomb --dev
2. 添加配置文件

在项目根目录下,添加配置文件 .csscomb.json

若无配置文件,CSScomb 的默认配置请看 csscomb.js/config/csscomb.json。

而本文均采用 AlloyTeam 推荐的排序规则,而非默认。但由于 sort-order 很长,影响文章篇幅,部分配置没贴上去,完整配置请看看 csscomb-mini/.csscomb.json。

关于该配置文件是按我平常编写代码的习惯调整有所调整的,并非 CSScomb 默认配置。具体配置项所指可细看 CSScomb Configuration Options。注意 CSScomb 所能做的事情并非就只有属性排序哦,比如设置缩进、颜色值十六进制大小写、前导零也是可以的。

{
  "exclude": [".git/**", "node_modules/**", "bower_components/**"],
  "verbose": true,
  "always-semicolon": true,
  "block-indent": 2,
  "color-case": "lower",
  "color-shorthand": true,
  "element-case": "lower",
  "eof-newline": true,
  "leading-zero": false,
  "quotes": "single",
  "remove-empty-rulesets": false,
  "space-before-colon": 0,
  "space-after-colon": 1,
  "space-before-combinator": 1,
  "space-after-combinator": 1,
  "space-before-opening-brace": 1,
  "space-after-opening-brace": "\n",
  "space-before-closing-brace": "\n",
  "space-before-selector-delimiter": 0,
  "space-after-selector-delimiter": "\n",
  "space-between-declarations": "\n ",
  "strip-spaces": true,
  "unitless-zero": true,
  "vendor-prefix-align": false,
  "sort-order": []
}
3. 添加 NPM 脚本

为了测试效果,根目录下创建一个 app.css 文件。

{
  "scripts": {
    "csscomb": "csscomb app.css"
  }
}
4. 运行脚本
$ yarn run csscomb

✓ app.css
✨  Done in 0.45s.
5. 效果

按以上配置文件的排序规则,padding-right 属性在 padding-left 之前,所以可以看到修改前后的对比如下:

/* before */
view {
  padding-left: 30px;
  padding-right: 30px;
}

/* after */
view {
  padding-right: 30rpx;
  padding-left: 30rpx;
}

二、微信小程序如何使用 csscomb

由于 CSScomb 仅支持扩展名为 .css.sass.scss.less 的文件,那么怎么处理呢?

只能利用 Gulp 来曲线救国了。

大致思路,利用 Gulp 将 WXSS 文件临时转为 CSS 扩展名,使用 CSScomb 格式化之后,再将其转换回 WXSS 的扩展名,以达到曲线救国的目的。

插了一下,恰巧有一款 Gulp 插件 gulp-csscomb 可用,话不多说。

1. 安装相关依赖包
$ yarn add --dev gulp gulp-debug gulp-csscomb gulp-rename
2. 创建 Gulp 任务

在项目根目录下创建 gulpfile.js 文件。而 gulp-csscomb 的使用方法很简单,于是我们很快可以写出:

const { src, dest } = require('gulp')
const rename = require('gulp-rename')
const debug = require('gulp-debug')
const csscomb = require('gulp-csscomb')

const wxssTask = cb => {
  return src('app.wxss')
    .pipe(debug()) // 打印一些调试信息
    .pipe(
      rename({
        extname: '.css'
      })
    )
    .pipe(csscomb()) // 格式化操作
    .pipe(
      rename({
        extname: '.wxss'
      })
    )
    .pipe(dest(file => file.base))
}

module.exports = {
  wxssTask
}

修改 NPM 脚本:

{
  "scripts": {
    "csscomb": "gulp wxssTask"
  }
}

执行脚本 yarn run csscomb 可以看到:

$ yarn run csscomb

yarn run v1.22.4
$ gulp wxssformat
[15:34:11] Using gulpfile ~/Desktop/Web/MyGitHub/csscomb-mini/gulpfile.js
[15:34:11] Starting 'wxssformat'...
[15:34:11] gulp-debug: app.wxss

Failed to configure "remove-empty-rulesets" option:
 Value must be one of the following: true.

Failed to configure "vendor-prefix-align" option:
 Value must be one of the following: true.
[15:34:11] gulp-debug: 1 item
[15:34:11] Finished 'wxssformat' after 98 ms
✨  Done in 1.05s.

此时 app.wxss 文件已经被格式化了,但很遗憾我们看到两行 Failed 信息:

Failed to configure "remove-empty-rulesets" option:
 Value must be one of the following: true.

Failed to configure "vendor-prefix-align" option:
 Value must be one of the following: true.

它不支持 remove-empty-rulesetsvendor-prefix-align 配置选项,估计是 gulp-csscomb “年久失修”了。当前安装 csscomb 是最新版本 4.3.0(本文编写时),而 gulp-csscomb 里面引用的 csscomb 版本还是 3.1.7,猜测是旧版本不支持该选项。

# 查看依赖包版本
$ npm view  versions

再次曲线救国一下,干脆自己实现一个 csscomb-gulp 插件的功能。

三、编写 Gulp 插件

顺便学习一下,如何写一个 Gulp 插件哦。

第一次写的同学,可简单看下这篇文章:Gulp 插件编写入门。

我看了 gulp-csscomb 和 CSScomb Core 的源码,发现自己实现一个插件不难,同时还发现可以少一步 wxsscss 来回切换的转换过程。

首先是 gulp-csscomb 的源码(gulp-csscomb),截取了一部分下来:

// 部分源码
var comb = new Comb(config || 'csscomb'); // 根据配置实例化对象
var syntax = options.syntax || file.path.split('.').pop(); // 获取 syntax,从参数或者扩展名获取,即 css、less、sass、scss

try {
  var output = comb.processString( // 关键就是这步,对我们的文件进行格式化
    file.contents.toString('utf8'), {
      syntax: syntax,
      filename: file.path
    });
  file.contents = new Buffer(output); // 将格式化后的字符串替换回文件中
} catch (err) {
  this.emit('error', new PluginError(PLUGIN_NAME, file.path + '\n' + err));
}

从以上源码可以看到,关键在于 comb.processString() 方法。于是找到 CSScomb 的核心源码(CSScomb Core),该方法的描述如下:

comb.processString(string, options)

Params:
  {String} Code to process
  {{context: String, filename: String, syntax: String}} Options (optional) where context is Gonzales PE rule, filename is a file's name that is used to display errors and syntax is syntax name with css being a default value.

Return: {Promise} Resolves with processed string.

syntax is syntax name with css being a default value.

所以只要在调用 comb.processString() 方法时,对微信小程序的样式文件,传值为 {syntax: 'css'} 即可。

上代码,框架大致是这样:

先安装必要依赖

$ yarn add --dev gulp-csscomb through2
// gulpfile.js
const { src, dest } = require('gulp')
const rename = require('gulp-rename')
const debug = require('gulp-debug')
const csscomb = require('gulp-csscomb')
const through = require('through2')

// csscomb 插件
const csscombPlugin = () => {
  return through.obj(function (file, enc, cb) {
    // 暂时什么都没做...
    return cb()
  })
}

// Gulp 任务
const csscombTask = cb => {
  try {
    return src('app.wxss')
      .pipe(debug())
      .pipe(csscombPlugin())
      .pipe(dest(file => file.base))
  } catch (e) {
    console.warn(e)
  }
}

module.exports = {
  csscombTask
}

创建新脚本,并运行 yarn run csscomb:mini

{
  "scripts": {
    "csscomb:mini": "gulp csscombTask"
  }
}

看样子是可以正常跑起来的,接下来就实现 csscomb 转化过程。

frankie@iMac csscomb-mini %  yarn run csscomb:mini

yarn run v1.22.10
$ gulp csscombTask
[20:29:25] Using gulpfile ~/Desktop/Web/Git/csscomb-mini/gulpfile.js
[20:29:25] Starting 'csscombTask'...
[20:29:25] gulp-debug: app.wxss
[20:29:25] gulp-debug: 1 item
[20:29:25] Finished 'csscombTask' after 14 ms
✨  Done in 0.51s.

如对 Gulp 和 through2 不了解,可去官网了解一下。through2 是快速创建一个 transform stream 的工具函数。

编写 csscombPlugin 实现

安装必要依赖

$ yarn add --dev plugin-error
const fs = require('fs')
const path = require('path')
const { src, dest } = require('gulp')
const rename = require('gulp-rename')
const debug = require('gulp-debug')
const Comb = require('csscomb')
const csscomb = require('gulp-csscomb')
const through = require('through2')
const PluginError = require('plugin-error') // 用于处理异常


// csscomb 插件
const csscombPlugin = () => {
  // 默认支持扩类型
  const defaultExts = ['.css', '.sass', '.scss', '.less']
  // 扩展类型,假设以后兼容字节跳动小程序,可加上 .ttss
  const expandExts = ['.wxss', '.acss']
  const supportExts = defaultExts.concat(expandExts)

  return through.obj(async function (file, enc, cb) {
    let syntax = 'css'
    const filePath = file.path
    const extname = path.extname(filePath)
    // 获取 csscomb 配置
    const combConfigPath = Comb.getCustomConfigPath(path.resolve(__dirname, '.csscomb.json'))
    const combConfig = Comb.getCustomConfig(combConfigPath)

    if (file.isNull()) {
      // 文件为空不做处理,直接返回进入下一个 pipe()
      return cb()
    } else if (file.isStream()) {
      // 不支持对流(Stream)进行操作,抛出异常
      this.emit('error', new PluginError('csscombPlugin', 'Streams are not supported!'))
      return cb()
    } else if (file.isBuffer() && supportExts.includes(extname)) {
      // 默认支持类型,通过扩展名获取。其余当做 css
      if (defaultExts.includes(extname)) {
        syntax = extname.split('.').pop()
      }

      // 找不到配置文件
      if (combConfigPath && !fs.existsSync(combConfigPath)) {
        this.emit('error', new PluginError('csscombPlugin', 'Configuration file not found: ' + configPath))
        return cb()
      }

      try {
        // 实例化
        const comb = new Comb(combConfig)
        // 对所要转换的文件进行格式化
        const output = await comb.processString(
          file.contents.toString('utf8'),
          {
            syntax,
            filename: filePath
          }
        )
        // 创建 Buffer,并将格式化后的字符串替换原本的 contents
        file.contents = Buffer.from(output, 'utf-8')
        this.push(file)
        return cb()
      } catch (e) {
        this.emit('error', new PluginError('csscombPlugin', filePath + '\n' + e))
      }

    } else {
      // 其余情况,直接返回。例如 file 为 JS 文件等
      return cb()
    }
  })
}


// Gulp 任务
const csscombTask = cb => {
  try {
    return src('app.wxss')
      .pipe(debug())
      .pipe(csscombPlugin())
      .pipe(dest(file => file.base))
  } catch (e) {
    console.warn(e)
  }
}


module.exports = {
  csscombTask
}

我们再次运行脚本 yarn run csscomb:mini,可以看到 app.wxss 文件的变化:

/* before */
.container {
  height: 100%;
  padding: 200rpx 0;
  display: flex;
  align-items: center;
  flex-direction: column;
  justify-content: space-between;
  box-sizing: border-box;
}

/* after */
.container {
  display: flex;

  box-sizing: border-box;
  padding: 200rpx 0;
  height: 100%;

  align-items: center;
  flex-direction: column;
  justify-content: space-between;
}

至此,我们的工作完成了 90%... 还可以继续优化。

你注意到我们 csscombTask 方法里面是针对一个相对固定的路径或者文件了吗?假设我每次格式化其他目录的文件,都需要修改此方法,显然是不合理的。

我们可在 NPM 脚本进行传参,然后通过 process.argv 来获取,我们处理下再传入 gulp.src() 方法即可。

四、优化

先约定好传参规则:

  • --path 表示符合 glob 文件匹配模式的路径,多个路径是用 , 隔开,并用单引号 ' 括起来,还有我限制了仅支持项目下的文件。
  • --ext 表示扩展名,如 .css.wxss 等。(此选项目前没什么用,保留下来后续优化用)

若对 Glob 模式不了解,可看 Glob 详解。其中 gulp.src(globs[, options]) 第一个参数就是这种模式的。

{
  "scripts": {
    "csscomb:mini": "gulp csscombTask --path '' --ext "
  }
}

我们借助 minimist 来获取 NPM 参数,通过

安装必要依赖,

$ yarn add --dev minimist
// gulp.js
// 这里省略其他部分,仅修改了 csscombTask 方法
const minimist = require('minimist')

// Gulp 任务
const csscombTask = cb => {
  try {
    // 获取参数,如 { _: [ 'csscomb:mini' ], path: 'xxx', ext: 'xxx' }
    const options = minimist(process.argv.slice(2))

    // 过滤掉非项目下的路径
    const paths = options.path.split(',').filter(item => {
      const re1 = /^\//
      const re2 = /^\.\.\//
      return item && !re2.test(item) && (!re1.test(item) || (re1.test(item) && item.includes(__dirname)))
    })

    // 去重
    const newPaths = Array.from(new Set(paths))

    if (!newPaths.length) {
      return cb()
    }

    // allowEmpty 选项是为了避免在没有找到匹配的文件时抛出错误
    // Error: File not found with singular glob: xxx (if this was purposeful, use `allowEmpty` option)
    return src(newPaths, { allowEmpty: true })
      .pipe(debug())
      .pipe(csscombPlugin())
      .pipe(dest(file => file.base))
  } catch (e) {
    console.warn(e)
  }
}

修改 NPM 脚本,并创建一个支付宝小程序的样式文件 app.acss 测试一下:

{
  "scripts": {
    "csscomb:mini": "gulp csscombTask --path './**/*.wxss,app.acss'"
  }
}

运行脚本,发现可以一键格式化了,至此基本完成了。

五、GitHub

项目放在 GitHub 上 csscomb-mini 欢迎 Star 。

下一篇介绍:将 CSScomb 集成到 Git Hook 中。

六、参考

  • Code Guide by @AlloyTeam
  • CSScomb Core
  • CSScomb
  • gulp-csscomb
  • glob-promise
  • Gulp.js
  • Gulp:插件编写入门
  • Gulp 从命令行传递参数
  • Gulp 系列之 through2 源码笔记

你可能感兴趣的:(将CSScomb集成到微信小程序项目中)