编写 PostCSS 插件

文档

支持

步骤 1:创建创意

在很多领域,编写新的 PostCSS 插件将有助于你的工作

步骤 2:创建项目

编写插件有两种方法

对于私有插件

  1. postcss/ 文件夹中创建一个新文件,其名称为您的插件名称。
  2. 从我们的样板中复制 插件模板

对于公共插件

  1. 使用 PostCSS 插件样板 中的指南来创建插件目录。
  2. 在 GitHub 或 GitLab 上创建一个存储库。
  3. 将您的代码发布到那里。
module.exports = (opts = {}) => {
  // Plugin creator to check options or prepare caches
  return {
    postcssPlugin: 'PLUGIN NAME'
    // Plugin listeners
  }
}
module.exports.postcss = true

步骤 3:查找节点

大多数 PostCSS 插件做两件事

  1. 在 CSS 中查找内容(例如,will-change 属性)。
  2. 更改找到的元素(例如,在 will-change 之前插入 transform: translateZ(0) 作为旧浏览器的填充)。

PostCSS 将 CSS 解析为节点树(我们称之为 AST)。此树可能包含

您可以使用 AST Explorer 来了解 PostCSS 如何将不同的 CSS 转换为 AST。

您可以通过向插件对象添加方法来查找具有特定类型的节点

module.exports = (opts = {}) => {
  return {
    postcssPlugin: 'PLUGIN NAME',
    Once (root) {
      // Calls once per file, since every file has single Root
    },
    Declaration (decl) {
      // All declaration nodes
    }
  }
}
module.exports.postcss = true

以下是 插件事件 的完整列表。

如果您需要具有特定名称的声明或 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)
  …
}

有两种类型的侦听器:进入和退出。OnceRootAtRuleRule 将在处理子级之前调用。OnceExitRootExitAtRuleExitRuleExit 在处理节点内的所有子级之后调用。

你可能希望在侦听器之间重复使用一些数据。你可以使用运行时定义的侦听器

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 获取声明属性。

步骤 4:更改节点

当你找到正确的节点时,你需要更改它们或在周围插入/删除其他节点。

PostCSS 节点具有类似 DOM 的 API 来转换 AST。查看我们的 API 文档。节点具有遍历的方法(如 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 以生成正确的源映射非常重要。

插件将重新访问你更改或添加的所有节点。如果你要更改任何子级,插件也将重新访问父级。只有 OnceOnceExit 不会再次调用。

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 })
}

步骤 5:与挫折作斗争

我讨厌编程
我讨厌编程
我讨厌编程
它起作用了!
我喜欢编程

即使是一个简单的插件,你也会遇到错误,并且至少需要 10 分钟来调试。你可能会发现简单的原始想法在现实世界中行不通,你需要改变一切。

别担心。每个错误都是可以找到的,找到另一个解决方案可能会让你的插件变得更好。

从编写测试开始。插件样板在 index.test.js 中有一个测试模板。调用 npx jest 来测试你的插件。

在你的文本编辑器中使用 Node.js 调试器或 console.log 来调试代码。

PostCSS 社区可以帮助你,因为我们都遇到了相同的问题。不要害怕在 特殊频道 中提问。

步骤 6:公开

当你的插件准备就绪时,在你的存储库中调用 npx clean-publishclean-publish 是一个从 npm 包中删除开发配置的工具。我们已将此工具添加到我们的插件样板中。

@postcss 提及来发一条关于你的新插件(即使它是一个小插件)的推文。或者在 [我们的聊天室] 中介绍你的插件。我们会帮助你进行营销。

将你的新插件添加到 PostCSS 插件目录。