编写一份 PostCSS 插件【翻译】

177 阅读7分钟

编写一份 PostCSS 插件

原文:www.postcss.com.cn/docs/writin…

原文日期:Oct 6, 2022

原文作者:postcss

翻译日期:01/27/23 15:17:00 CST

Links

文档:

支持:

第一步:想到一个点子

许多领域中编写 PostCSS 会有助于您的工作:

  • 兼容性修复:如果你总是忘记添加浏览器兼容代码,你可以创建 PostCSS 插件来帮你自动插入补全。postcss-flexbugs-fixespostcss-flexbugs-fixes 是两个不错的例子;
  • 自动化常规操作:让计算机执行常规操作,解放我们自己去完成更有创造力的任务。例如,使用 RTLCSS 的 PostCSS 可以把页面转换为右对齐(right-to-left)语言(阿拉伯语或希伯来语),或者使用 postcss-dark-theme-class 来插入媒体查询作为夜晚/白天的主题开关;
  • 预防常见错误:“会第二次犯的错误,不会是最后一次。”PostCSS 插件检查你的代码的流行错误,帮你避免无意义的调试。最好的办法是写个心得 Stylelint 插件(Stylelint 内部使用 PostCSS);
  • 提高代码可维护性CSS Modulespostcss-autoreset 都是 PostCSS 通过隔离提高可维护性的好榜样;
  • ‌Polyfills:在 postcss-preset-env 里我们已经有很多 CSS 草案的 polyfills。如果你发现一个新草案,你可以创建一个新插件发送到这个 preset 里;
  • 全新的 CSS 语法:我们不推荐给 CSS 创建新语法。若你想添加新特性,通常最好是写个 CSS 草案提议,然后发给 CSSWG,然后自己再实现个 polyfill。这个提议postcss-easing-gradients 就是个好榜样。但也有很多情况是你不能提交草案的。例如,浏览器的解析性能大大限制了 CSSWG 的嵌套语法,你也许想要从 postcss-nested 获取非官方的类 Sass 语法。

第二步:创建一个项目

有两种方法编写一个插件:

  • 创建一个私有插件。当插件和项目强相关时,这样做。例如,你想自动化针对你的个人 UI 库的任务时;
  • 发布一个公共插件。我们推荐这样做。对于私有前端系统,即使在谷歌,也难以维护。另一方面,许多流行的插件又都是在闭源项目中产生的。

私有插件:

  • 创建插件文件;
  • 从我们的样板复制插件模版

公共插件:

  • 根据PostCSS plugin boilerplate中的指南,创建插件文件夹;
  • 在 GitHub 或 GitLab 上创建仓库;
  • 发布你的代码。

你也可以用 our Sharec config 来保持最新的最佳实践。当你每次更新配置后,开发配置和开发工具也会跟着更新。

module.exports = (opts = {}) => {
  // Plugin creator to check options or prepare caches
  return {
    postcssPlugin: 'PLUGIN NAME'
    // Plugin listeners
  }
}
module.exports.postcss = true

第三步:找到节点

大部分 PostCSS 插件做两件事:

  1. 在 CSS 里找某些东西(例如,即将修改的属性);
  2. 修改找到的元素(例如,在即将修改的地方前面添加transform: translateZ(0)来兼容旧浏览器)。

PostCSS 扫描 CSS 来构建节点树(我们称之为 AST)。树上大概是这些内容:

  • Root——根节点,代表了 CSS 文件;
  • AtRule——以 @ 开头的部分,例如 @charset "UTF-8"@media (screen) {}
  • Rule——带有内部声明的选择器,例如,inputbutton {}
  • Declaration——键值对,如 color: black;
  • Comment——单独的注释。selectorsat-rule 的参数以及 value 的注释在节点的 node 属性内。

你可以查看 AST Explorer 来学习 PostCSS 怎样转换 CSS 到 AST。

你可以通过给插件对象添加方法指定类型来查找所有节点:

module.exports = (opts = {}) => {
  return {
    postcssPlugin: 'PLUGIN NAME',
    Once (root) {
      //  每个文件执行一次,因为每个文件有一个 Root
    },
    Declaration (decl) {
      // 所有 declaration 节点
    }
  }
}
module.exports.postcss = true

如果你需要指定名称的 declaration 或 at-rule,你可以这样:

    Declaration: {
      color: decl => {
        // All `color` declarations
      }
      '*': decl => {
        // All declarations
      }
    },
    AtRule: {
      media: atRule => {
        // All @media at-rules
      }
    }

还有其它需求的话,就用正则或自定义扫描器:

其它用来分析 AST 的工具:

要注意正则和扫描器都是费性能的工具。你可以用在用这些工具前用 String#includes() 简单检查一下:

if (decl.value.includes('gradient(')) {
  let value = valueParser(decl.value)
  // …
}

有两种类型或监听器:enter 和 exit。Once、Root、AtRule 和 Rule 会在处理前调用;OnceExit、RootExit、AtRuleExit 和 RuleExit 会在处理结束被调用。

你也许会在不同监听器中重复使用某些数据。你可以用“进行时定义”监听器:

module.exports = (opts = {}) => {
  return {
    postcssPlugin: 'vars-collector',
    prepare (result) {
      const variables = {}
      return {
        Declaration (node) {
          if (node.variable) {
            variables[node.prop] = node.value
          }
        },
        OnceExit () {
          console.log(variables)
        }
      }
    }
  }
}

你可以用 prepare() 来动态生成监听器。例如,使用 Browserslist 来获取 declaration 属性。

第四步:修改节点

找到节点后,你可能修改或在节点周围插入/删除。

PostCSS 节点有类 DOM 的 API 来改变 AST。查看我们的 API docs。有方法用来遍历(如 Node#nextNode#parent)、查看子元素(如 Container#some)、移除节点或添加新节点。

插件的方法会在第二个入参中接受节点创造器:

    Declaration (node, { Rule }) {
      let newRule = new Rule({ selector: 'a', source: node.source })
      node.root().append(newRule)
      newRule.append(node)
    }

如果你添加了新节点,复制 Node#source 来生成正确的 source maps 很重要。

当你做了改变或添加操作后,插件会重新访问所有节点。如果你想改变某些子节点,插件同样会重新访问父节点。只有 Once 和 OnceExit 不会重新调用。

const plugin = () => {
  return {
    postcssPlugin: 'to-red',
    Rule (rule) {
      console.log(rule.toString())
    },
    Declaration (decl) {
      console.log(decl.toString())
      decl.value = 'red'
    }
  }
}
plugin.postcss = true

await postcss([plugin]).process('a { color: black }', { from })
// => a { color: black }
// => color: black
// => a { color: red }
// => color: red

由于任何变化导致重新访问,仅添加子节点将导致无限循环。所以我们要检查该节点是否已处理,来避免无限循环。

    Declaration: {
      'will-change': decl => {
        if (decl.parent.some(decl => decl.prop === 'transform')) {
          decl.cloneBefore({ prop: 'transform', value: 'translate3d(0, 0, 0)' })
        }
      }
    }

你也可以用 Symbol 来标记已处理的节点:

const processed = Symbol('processed')

const plugin = () => {
  return {
    postcssPlugin: 'example',
    Rule (rule) {
      if (!rule[processed]) {
        process(rule)
        rule[processed] = true
      }
    }
  }
}
plugin.postcss = true

第二个入参也包含 result 对象,内容是警告信息:

    Declaration: {
      bad: (decl, { result }) {
        decl.warn(result, 'Deprecated property bad')
      }
    }

若是你的插件依赖其它文件,你可以给 result 附加信息,告诉运行程序(webpack、Gulp 等等),当某个文件发生变化后应该重新构建 CSS。

    AtRule: {
      import: (atRule, { result }) {
        const importedFile = parseImport(atRule)
        result.messages.push({
          type: 'dependency',
          plugin: 'postcss-import',
          file: importedFile,
          parent: result.opts.from
        })
      }
    }

如果依赖项是文件夹,你可以用 dir-dependency 来代替:

result.messages.push({
  type: 'dir-dependency',
  plugin: 'postcss-import',
  dir: importedDir,
  parent: result.opts.from
})

如果你发现了语法错误,你可以抛出错误:

if (!variables[name]) {
  throw decl.error(`Unknown variable ${name}`, { word: name })
}

第五步:战胜挫折

我恨编程

我恨编程

我恨编程

能跑了!

我爱编程

即使是最简单的插件,你也会碰到 bug,也会需要至少 10 分钟的调试时间。你会发现最初的想法在真实情况里行不通并做出改变。

别担心。每个 bug 都会被发现,然后你会发现另一个解决方案,这就会让你的插件变得更好更优秀。

从编写测试案例开始。插件模版里有个测试模版 index.test.js,它会调用 npx jest 来测试你的插件。

在编辑器里使用 Node.js 来调试,或者就干脆用 console.log

PostCSS 社区会帮助你的,因为我们都碰到过相同的问题。请大胆在 special Gitter channel 里提问。

发布

当你的插件准备就绪,唤起 npx clean-publishclean-publish 是个移除测试环境配置的工具。我们为插件模版添加了这个工具。

为你的新插件发个推,带上@postcss。或者在聊天室告诉我们。我们会帮你推广。

把你的新插件添加到 PostCSS plugin catalog