编写 PostCSS 插件(Writing a PostCSS Plugin)
¥Writing a PostCSS Plugin
目录
¥Table of Contents
链接(Links)
¥Links
文档:
¥Documentation:
支持:
¥Support:
-
PostCSS 推特 有最新更新。
¥PostCSS twitter with latest updates.
步骤 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:
-
兼容性修复:如果你总是忘记添加 hack 以实现浏览器兼容性,你可以创建 PostCSS 插件来自动为你插入此 hack。
postcss-flexbugs-fixes
和postcss-100vh-fix
就是很好的例子。¥Compatibility fixes: if you always forget to add hack for browser compatibility, you can create PostCSS plugin to automatically insert this hack for you.
postcss-flexbugs-fixes
andpostcss-100vh-fix
are good examples. -
自动化日常操作:让计算机执行日常操作,释放自己去执行创造性任务。例如,带有 RTLCSS 的 PostCSS 可以自动将设计转换为从右到左的语言(如阿拉伯语或希伯来语),或者带有 [postcss-深色主题-类
][postcss-dark-theme-class
] 的 PostCSS 可以插入暗/亮主题切换器的媒体查询。¥Automate routine operations: let’s computer do routine operations, free yourself for creative tasks. For instance, PostCSS with RTLCSS can automatically convert a design to right-to-left languages (like Arabic or Hebrew) or with postcss-dark-theme-class` can insert media queries for dark/light theme switcher.
-
防止常见错误:“如果一个错误发生两次,那么它还会再次发生。” PostCSS 插件可以检查你的源代码是否存在常见错误,并节省你不必要的调试时间。最好的方法是 编写新的 Stylelint 插件(Stylelint 内部使用 PostCSS)。
¥Preventing popular mistakes: “if an error happened twice, it will happen again.” PostCSS plugin can check your source code for popular mistakes and save your time for unnecessary debugging. The best way to do it is to write new Stylelint plugin (Stylelint uses PostCSS inside).
-
提高代码的可维护性:CSS 模块 或
postcss-autoreset
是 PostCSS 如何通过隔离来提高代码可维护性的很好例子。¥Increasing code maintainability: CSS Modules or
postcss-autoreset
are great example how PostCSS can increase code maintainability by isolation. -
Polyfills:我们已经在
postcss-preset-env
中为 CSS 草稿提供了很多 Polyfill。如果你发现新草稿,可以添加新插件并将其发送到此预设。¥Polyfills: we already have a lot polyfills for CSS drafts in
postcss-preset-env
. If you find a new draft, you can add a new plugin and send it to this preset. -
新的 CSS 语法:我们建议避免向 CSS 添加新语法。如果你想添加一个新功能,最好写一个 CSS 草案提案,将其发送到 CSSWG,然后实现 polyfill。
postcss-easing-gradients
和 这个提议 就是一个很好的例子。但是,在很多情况下你无法发送提案。例如,浏览器的解析器性能很大程度上限制了 CSSWG 嵌套语法,你可能希望使用 [postcss 嵌套][
postcss-nested] 中的非官方类似 Sass 的语法。¥New CSS syntax: we recommend avoiding adding new syntax to CSS. If you want to add a new feature, it is always better to write a CSS draft proposal, send it to CSSWG and then implement polyfill.
postcss-easing-gradients
with this proposal is a good example. However, there are a lot of cases when you can’t send a proposal. For instance, browser’s parser performance limited CSSWG nested syntax a lot and you may want to have non-official Sass-like syntax from `postcss-nested.
步骤 2:创建一个项目(Step 2: Create a project)
¥Step 2: Create a project
编写插件有两种方法:
¥There are two ways to write a plugin:
-
创建一个私有插件。仅当插件与项目的特定事物相关时才使用这种方式。例如,你希望为你独特的 UI 库自动执行特定任务。
¥Create a private plugin. Use this way only if the plugin is related to specific things of projects. For instance, you want to automate a specific task for your unique UI library.
-
发布公共插件。这始终是推荐的方式。请记住,即使在 Google,私有前端系统也经常变得无人维护。另一方面,许多流行的插件是在闭源项目的工作过程中创建的。
¥Publish a public plugin. It is always the recommended way. Remember that private front-end systems, even in Google, often became unmaintained. On the other hand, many popular plugins were created during the work on a closed source project.
对于私有插件:
¥For private plugin:
-
使用你的插件名称在
postcss/
文件夹中创建一个新文件。¥Create a new file in
postcss/
folder with the name of your plugin. -
从我们的样板中复制 插件模板。
¥Copy plugin template from our boilerplate.
对于公共插件:
¥For public plugins:
-
使用 PostCSS 插件样板 中的指南创建插件目录。
¥Use the guide in PostCSS plugin boilerplate to create a plugin directory.
-
在 GitHub 或 GitLab 上创建存储库。
¥Create a repository on GitHub or GitLab.
-
在那里发布你的代码。
¥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:
-
在 CSS 中查找某些内容(例如,
will-change
属性)。¥Find something in CSS (for instance,
will-change
property). -
更改找到的元素(例如,在
will-change
之前插入transform: translateZ(0)
作为旧浏览器的填充)。¥Change found elements (for instance, insert
transform: translateZ(0)
beforewill-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:
-
Root
:树顶部的节点,代表 CSS 文件。¥
Root
: node of the top of the tree, which represent CSS file. -
AtRule
:语句以@
开头,如@charset "UTF-8"
或@media (screen) {}
。¥
AtRule
: statements begin with@
like@charset "UTF-8"
or@media (screen) {}
. -
Rule
:内部带有声明的选择器。例如input, button {}
。¥
Rule
: selector with declaration inside. For instanceinput, button {}
. -
Declaration
:键值对,如color: black
;¥
Declaration
: key-value pair likecolor: black
; -
Comment
:独立注释。选择器内的注释、规则参数和值存储在节点的raws
属性中。¥
Comment
: stand-alone comment. Comments inside selectors, at-rule parameters and values are stored in node’sraws
property.
你可以使用 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:
-
维度解析器 对应
number
、length
和percentage
。¥Dimension parser for
number
,length
andpercentage
. -
侧面解析器 代表
margin
、padding
和border
属性。¥Sides parser for
margin
,padding
andborder
properties.
其他分析 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)
…
}
有两种类型或监听器:进入和退出。Once
、Root
、AtRule
和 Rule
将在处理子项之前被调用。处理节点内的所有子节点后的 OnceExit
、RootExit
、AtRuleExit
和 RuleExit
。
¥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#next
或 Node#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.
插件将重新访问你更改或添加的所有节点。如果你要更改任何子项,插件也会重新访问父项。只有 Once
和 OnceExit
不会再被调用。
¥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-publish
。clean-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.