最近在看 AlloyTeam 团队的 Code Guide 代码书写习惯,其中一项是 CSS 属性编写顺序。
虽然 CSS 属性顺序先后并不影响我们的程序,那为什么要做这件事呢?我理解是可分类、有迹可循、有利于排查错误。
由于属性众多,那么人总是会犯错的,所以我们借助一个工具来处理,它就是 CSScomb,用于 CSS 格式化、且可排序。官方描述是:CSScomb is a coding style formatter for CSS.
本文旨在使得 CSScomb 支持格式化小程序样式
.wxss
。同时也支持支付宝小程序.acss
,而百度小程序样式文件的扩展名是.css
,所以它天生就可以直接使用了。文章有两篇:
- 将 CSScomb 集成到微信小程序项目中(本文)
- 将 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-rulesets
和 vendor-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 的源码,发现自己实现一个插件不难,同时还发现可以少一步 wxss
和 css
来回切换的转换过程。
首先是 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 源码笔记