前言
最近在看 AlloyTeam 团队的 Code Guide 代码书写习惯,其中一项是 CSS 属性编写顺序。
虽然 CSS 属性顺序先后并不影响我们的程序,那为什么要做这件事呢?我理解是可分类、有迹可循、有利于排查错误。
由于属性众多,那么人总是会犯错的,所以我们借助一个工具来处理,它就是 CSScomb,用于 CSS 格式化、且可排序。官方描述是:CSScomb is a coding style formatter for CSS.
本文旨在使得 CSScomb 支持格式化小程序样式
.wxss
。同时也支持支付宝小程序.acss
,而百度小程序样式文件的扩展名是.css
,所以它天生就可以直接使用了。
正文
一、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 <package> 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 '<filepath>' --ext <extension>" } }
我们借助 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 。