toFrankie / blog

种一棵树,最好的时间是十年前。其次,是现在。
20 stars 1 forks source link

文件保存时 CSS 属性自动排序 #315

Open toFrankie opened 1 year ago

toFrankie commented 1 year ago

配图源自 Freepik

背景

在使用 VS Code 编写代码时,通常会开启 editor.formatOnSave 选项以在保存时格式化文件。

{
  "editor.formatOnSave": true
}

一般来说,对于大多数语言来说,VS Code 都有对应的内置格式化程序(formatter)。它们通常只会处理缩进、折行、前导尾随空格等操作。

如果内置的不够用,可以通过安装第三方插件以实现更多的可能。比如,将颜色值转为小写、改进 JavaScript 写法等都可以在保存文件时自动处理,具体取决于插件实现。

由于语言众多,格式化插件也很多,还可以支持配置默认的格式化程序。这个可以在文件编辑区右键选择「使用 ... 格式化」进行配置。比如:

{
  "[html]": {
    "editor.defaultFormatter": "vscode.html-language-features"
  },
  "[css]": {
    "editor.defaultFormatter": "stylelint.vscode-stylelint"
  },
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  }
}

现在很多工程化项目都会配置格式化处理,也可在 Git 的 pre-commit 勾子中跑一遍处理脚本,以确保提交到仓库的代码格式是统一的,尽管大家使用的编辑器可能有差异。

虽说如此,仍然有必要我们的编辑器配置一些格式化程序,以便于在编写一些 Snippet、Demo 等的时候,也有着较为规范、格式统一的代码。尤其是像我这种强迫症患者。

个人用得最多的是 Prettier,主流编辑器都有对应插件,支持 JavaScript、HTML、CSS 以及对应变体等 Web 前端相关的语言。

Prettier 是固执己见的(opinionated),提供的选项很少,且在可见的未来可能不会添加更多新功能。原因请看 Option Philosophy

诉求

目前我想要的是:在保存文件时,按一定的规则自动对 CSS 属性进行排序。此时 Prettier 就无能为力了。

对于 CSS 属性声明顺序,最初是因为看了 Code Guide by @AlloyTeam 的规范,目前里面提及的属性顺序已过时,最后维护时间已经是 2015 年了。有一句话我很喜欢(源自):

虽然这些细节是小事,不会有体验或者性能上的优化,但是却体现了一个 coder 和团队的专业程度。

排序的好处无非就是快速看得出元素盒子所占的空间,是否有重复声明的属性等。

目前,本人所参考的是 Code Guide @mdo 规范,对应的 Stylelint 插件是 Bootstrap 在用的 stylelint-config-recess-order,维护较为积极,也能紧跟最新的 CSS 属性,它们会按照以下分组排序(详见):

与 Declaration order 相关的插件还有以下几个,有些已经好几年没更新了,仅供参考。

以上这些集成到项目里面是非常好用的,但对于单个文件或者工程化程度不高的项目,它们没办法施展才华。原因是它们都是基于 Stylelint 的插件 stylelint-order 实现的,而对应 VS Code 插件并不支持配置插件。

因此,本文会借助一个名为 CSScomb.js 的格式化程序,它的主要功能便是「属性排序」,对应的 VS Code 插件是 vscode-csscomb

CSScomb.js 最后更新已经是 2019 年,看起来似乎不再维护更新了,对应插件也因此不再更新。

尽管如此,它目前仍然足够好用。

配置

其中 vscode-csscomb 插件配置项有以下几个:

此处不展开说明,具体可看文档:

我的 VS Code 配置如下:

{
  "csscomb.formatOnSave": true,
  "csscomb.syntaxAssociations": {
    "*.wxss": "css",
    "*.acss": "css"
  },
  "csscomb.ignoreFilesOnSave": ["node_modules/**"],
  "csscomb.preset": "~/Library/Mobile Documents/com~apple~CloudDocs/Terminal/csscomb/preset-custom.json",
}

其中,对于像小程序等 WXSS 样式文件,可以在 csscomb.syntaxAssociations 进行关联,使其当成 CSS 一样去处理。

由于 csscomb.preset 配置项非常地长(sort-order 造成的),也便于多设备同步,我选择将其存放到 iCloud Drive 目录。如下:

{
  "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": true,
  "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": []
}

以上都是一些比较常规的配置,其中一些也是为了跟 Prettier 保持一致而设置的。另外,为了避免影响篇幅以上 sort-order 配置省略了,可参考这里

生成 sort-order

由于 CSS 在不断的发展,为了让 sort-order 跟上最新标准,这里使用 stylelint-config-recess-order 来生成。

脚本很简单,仅供参考:

const path = require('path')
const fs = require('fs')
const propertyGroups = require('stylelint-config-recess-order/groups')

const CSSCOMB_SORT_ORDER_FILE_PATH = path.resolve(__dirname, '../csscomb/sort-order.json')
const CSSCOMB_PRESET_BASE_FILE_PATH = path.resolve(__dirname, '../csscomb/preset-base.json')
const CSSCOMB_PRESET_CUSTOM_FILE_PATH = path.resolve(__dirname, '../csscomb/preset-custom.json')

;(function main() {
  // generate sort-order
  const sortOrder = []
  propertyGroups.forEach(group => {
    if (!group?.properties?.length) return
    sortOrder.push(...group.properties)
  })

  // write sort-order.json
  let preset = { 'sort-order': sortOrder }
  const content = JSON.stringify(preset, null, 2)
  fs.writeFileSync(CSSCOMB_SORT_ORDER_FILE_PATH, content, 'utf8')

  // read preset-base.json
  const presetBaseContent = fs.readFileSync(CSSCOMB_PRESET_BASE_FILE_PATH, 'utf8')
  const presetBase = JSON.parse(presetBaseContent)

  // write preset.json
  preset = {
    ...presetBase,
    ...preset,
  }
  const presetContent = JSON.stringify(preset, null, 2)
  fs.writeFileSync(CSSCOMB_PRESET_CUSTOM_FILE_PATH, presetContent, 'utf8')
})()

其中 preset-base.json 就是上一章节 csscomb.preset 所贴出来的内容,除了 sort-order 选项之外,其他基本上是不变的。其中 sort-order.json 只是本地的一份存档,可要可不要。最后生成的 preset-custom.json 文件才是供 vscode-csscomb 使用的配置。

这个脚本生成的过程,我是将它存放在 iCloud Drive 中的。比如,今天写下文章的时候我发现 stylelint-config-recess-order 可更新,只要更新下依赖,然后执行 NPM Script 就 OK 了。

关于 node_modules 同步问题

以上方式,势必在本地产生一个 node_modules 目录,但我们不希望里面成千上万的文件在 iCloud Drive 中同步。

如果希望 iCloud Drive 上某个目录不同步,需在目录名称后加上 .nosync

但是,如果将 node_modules 修改为 node_modules.nosync,那么在执行脚本的时候,就会因为找不到 node_modules 而出错。这里可参考 nosync-icloud 的解决方案。

原理大致是:创建一个名为 node_modules 的软链接至 node_modules.nosync 目录,而软链接文件内容只是存放一个文件地址,文件大小可忽略,所以它同步到 iCloud Drive 也问题不大。当脚本执行,在查找 node_modules 模块时,会指向真实的目录,所以也没问题。

References