编写 PostCSS 插件(Writing a PostCSS Plugin)

¥Writing a PostCSS Plugin

目录

¥Table of Contents

¥Links

文档:

¥Documentation:

支持:

¥Support:

步骤 1:创造一个想法(Step 1: Create an idea)

¥Step 1: Create an idea

编写新的 PostCSS 插件将在许多字段对你的工作有所帮助:

¥There are many fields where writing new PostCSS plugin will help your work:

步骤 2:创建一个项目(Step 2: Create a project)

¥Step 2: Create a project

编写插件有两种方法:

¥There are two ways to write a plugin:

对于私有插件:

¥For private plugin:

  1. 使用你的插件名称在 postcss/ 文件夹中创建一个新文件。

    ¥Create a new file in postcss/ folder with the name of your plugin.

  2. 从我们的样板中复制 插件模板

    ¥Copy plugin template from our boilerplate.

对于公共插件:

¥For public plugins:

  1. 使用 PostCSS 插件样板 中的指南创建插件目录。

    ¥Use the guide in PostCSS plugin boilerplate to create a plugin directory.

  2. 在 GitHub 或 GitLab 上创建存储库。

    ¥Create a repository on GitHub or GitLab.

  3. 在那里发布你的代码。

    ¥Publish your code there.

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

步骤 3:查找节点(Step 3: Find nodes)

¥Step 3: Find nodes

大多数 PostCSS 插件都会做两件事:

¥Most of the PostCSS plugins do two things:

  1. 在 CSS 中查找某些内容(例如,will-change 属性)。

    ¥Find something in CSS (for instance, will-change property).

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

    ¥Change found elements (for instance, insert transform: translateZ(0) before will-change as a polyfill for old browsers).

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

¥PostCSS parses CSS to the tree of nodes (we call it AST). This tree may content:

你可以使用 AST 探索者 来了解 PostCSS 如何将不同的 CSS 转换为 AST。

¥You can use AST Explorer to learn how PostCSS convert different CSS to AST.

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

¥You can find all nodes with specific types by adding method to plugin object:

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

这是 插件的事件 的完整列表。

¥Here is the full list of plugin’s events.

如果你需要具有特定名称的声明或 at 规则,你可以使用快速搜索:

¥If you need declaration or at-rule with specific names, you can use quick search:

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

对于其他情况,你可以使用正则表达式或特定解析器:

¥For other cases, you can use regular expressions or specific parsers:

其他分析 AST 的工具:

¥Other tools to analyze AST:

不要忘记正则表达式和解析器是繁重的任务。你可以在使用重型工具检查节点之前使用 String#includes() 快速测试:

¥Don’t forget that regular expression and parsers are heavy tasks. You can use String#includes() quick test before check node with heavy tool:

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

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

¥There two types or listeners: enter and exit. Once, Root, AtRule, and Rule will be called before processing children. OnceExit, RootExit, AtRuleExit, and RuleExit after processing all children inside node.

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

¥You may want to re-use some data between listeners. You can do with runtime-defined listeners:

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() 动态生成监听器。例如,使用 浏览器列表 来获取声明属性。

¥You can use prepare() to generate listeners dynamically. For instance, to use Browserslist to get declaration properties.

步骤 4:更改节点(Step 4: Change nodes)

¥Step 4: Change nodes

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

¥When you find the right nodes, you will need to change them or to insert/delete other nodes around.

PostCSS 节点有一个类似 DOM 的 API 来转换 AST。看看我们的 API 文档。节点具有四处移动(如 Node#nextNode#parent)、查看子节点(如 Container#some)、删除节点或在内部添加新节点的方法。

¥PostCSS node has a DOM-like API to transform AST. Check out our API docs. Nodes has methods to travel around (like Node#next or Node#parent), look to children (like Container#some), remove a node or add a new node inside.

插件的方法将在第二个参数中接收节点创建者:

¥Plugin’s methods will receive node creators in second argument:

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

如果添加了新节点,复制 Node#source 以生成正确的源映射非常重要。

¥If you added new nodes, it is important to copy Node#source to generate correct source maps.

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

¥Plugins will re-visit all nodes, which you changed or added. If you will change any children, plugin will re-visit parent as well. Only Once and OnceExit will not be called again.

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

由于访问者将在任何更改时重新访问节点,因此仅添加子节点将导致无限循环。为了防止这种情况,你需要检查你是否已经处理了该节点:

¥Since visitors will re-visit node on any changes, just adding children will cause an infinite loop. To prevent it, you need to check that you already processed this node:

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

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

¥You can also use Symbol to mark processed nodes:

const processed = Symbol('processed')

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

第二个参数也有 result 对象来添加警告:

¥Second argument also have result object to add warnings:

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

如果你的插件依赖于另一个文件,你可以向 result 附加一条消息,向运行器(webpack、Gulp 等)表明,当该文件发生更改时,他们应该重建 CSS:

¥If your plugin depends on another file, you can attach a message to result to signify to runners (webpack, Gulp etc.) that they should rebuild the CSS when this file changes:

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

如果依赖是目录,你应该使用 dir-dependency 消息类型:

¥If the dependency is a directory you should use the dir-dependency message type instead:

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

如果发现语法错误(例如,未定义的自定义属性),你可以抛出特殊错误:

¥If you find an syntax error (for instance, undefined custom property), you can throw a special error:

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

步骤 5:与挫败感作斗争(Step 5: Fight with frustration)

¥Step 5: Fight with frustration

我讨厌编程
我讨厌编程
我讨厌编程
它有效!
我喜欢编程

¥I hate programming
I hate programming
I hate programming
It works!
I love programming

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

¥You will have bugs and a minimum of 10 minutes in debugging even a simple plugin. You may found that simple origin idea will not work in real-world and you need to change everything.

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

¥Don’t worry. Every bug is findable, and finding another solution may make your plugin even better.

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

¥Start from writing tests. Plugin boilerplate has a test template in index.test.js. Call npx jest to test your plugin.

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

¥Use Node.js debugger in your text editor or just console.log to debug the code.

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

¥PostCSS community can help you since we are all experiencing the same problems. Don’t afraid to ask in special channel.

步骤 6:公开(Step 6: Make it public)

¥Step 6: Make it public

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

¥When your plugin is ready, call npx clean-publish in your repository. clean-publish is a tool to remove development configs from the npm package. We added this tool to our plugin boilerplate.

写一篇关于你的新插件的推文(即使它很小)并提及 @postcss。或者在[我们的聊天]中讲述你的插件。我们将帮助你进行营销。

¥Write a tweet about your new plugin (even if it is a small one) with @postcss mention. Or tell about your plugin in [our chat]. We will help you with marketing.

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

¥Add your new plugin to PostCSS plugin catalog.