The framework CSS used in the work has the following problems, need to use postCSS to do some automatic handling.
- The latter will override the former:
.a{color: #fff}
.a{background: #fff}
, the latter takes effect - A maximum of two layers can be nested:
.a .b .c {}
Don’t take effect
Learn what PostCSS is and how to do some work with it.
Introduction to the
Postcss: PostCSS is a tool for transforming styles with JS plugins. These plugins can lint your CSS, Support variables and mixins, transpile Future CSS syntax, inline images, and more. postcss are tools that convert styles using JS plug-ins. These plug-ins can validate CSS, support variables and mixins, compile future CSS syntax, inline images, and more.
Its name, PostCSS, suggests its early use as a post-processor. That is, CSS compiled by less/sass. The most commonly used plug-in is Autoprefixer, which adds compatible prefixes based on the browser version.
Postcss, like Babel, converts style to AST, goes through a series of plug-in transformations, and then converts as T to generate a new style. As things have evolved, postcss is no longer an appropriate word for postprocessor. It is currently possible to convert less/sass code using postCSs-sass/postCSs-less (to convert less/sass to less/sass instead of directly to CSS), Precss can also be used instead of SASS (which feels immature).
Therefore, it is recommended to use postCSS and less/ sASS together. In webPack configuration, postCSs-loader must be written before sas-loader /less-loader.
module.exports = {
module: {
rules: [{test: /\.(css|less)$/i,
use: ['style-loader'.'css-loader'.'postcss-loader'.'less-loader'],},]}}Copy the code
For more information on the uses of PostCSS, see github.com/postcss/pos…
The working process
General steps:
- Make Tokens of CSS strings
- Pass Tokens through rules to generate an AST tree
- Pass the AST tree to the plug-in for processing
- Generate new CSS resources from the processed AST tree (CSS strings, sourceMap, etc.)
CSS Input → Tokenizer → Parser → AST → Plugins → Stringifier Give 🌰 :
@import url('./default.css');
body {
padding: 0;
/* margin: 0; * /
}
Copy the code
1. input
'@import url('./default.css'); \nbody {\n padding: 0; \n /* margin: 0; */\n}\n'Copy the code
2. tokenizer
Tokenizer includes the following methods:
- Back: The back method sets the return value of the next call to nextToken.
- NextToken: obtains the nextToken.
- EndOfFile: Determines whether the file is finished.
- Position: Obtains the current token position.
// The nextToken method of tokenize.js simplifies code
function nextToken(opts) {
If the back method was called before, the next call to nextToken will return the token set by the back method
if (returned.length) return returned.pop()
code = css.charCodeAt(pos)
// Judge each character
switch (code) {
case xxx:
break;
}
pos++
return currentToken
}
Copy the code
The nextToken method determines each character and generates tokens of the following type:
space:
- \ n: a newline
- : the blank space
- \ f: page breaks
- \ r: press enter
- \ T: horizontal TAB character
// Spaces, newlines, tabs, carriage returns, etc., are all treated as space tokens
case NEWLINE:
case SPACE:
{
next = pos
// loop, taking successive Spaces, newlines, carriage returns, etc., as a token
do {
next += 1
code = css.charCodeAt(next)
} while (
/ / if it is
)
// Intercepts the token value
currentToken = ['space', css.slice(pos, next)]
pos = next - 1
break
}
Copy the code
string:
- ‘: single quotation mark
- “: double quotation marks
// Single quotation marks. The content between double quotation marks is regarded as a string token
case SINGLE_QUOTE:
case DOUBLE_QUOTE: {
quote = code === SINGLE_QUOTE ? "'" : '"'
next = pos
do {
next = css.indexOf(quote, next + 1)
if (next === -1) {
if (ignore || ignoreUnclosed) {
next = pos + 1
break
} else {
unclosed('string')}}}while (escaped)
currentToken = ['string', css.slice(pos, next + 1), pos, next]
pos = next
break
}
Copy the code
At-word: @ and the characters following it are regarded as at-word token @* : AT
case AT: {
currentToken = ['at-word', css.slice(pos, next + 1), pos, next]
pos = next
break
}
Copy the code
[and] : brackets
) : Close parenthesis
{and} : curly braces
; : a semicolon
: : the colon
Both are independent token types.
/ / [] {} :) Etc are independent token types
case OPEN_SQUARE:
case CLOSE_SQUARE:
case OPEN_CURLY:
case CLOSE_CURLY:
case COLON:
case SEMICOLON:
case CLOSE_PARENTHESES: {
let controlChar = String.fromCharCode(code)
currentToken = [controlChar, controlChar, pos]
break
}
Copy the code
(and brackets
- Url () : The value of URL () that is not enclosed by single or double quotation marks is considered as the brackets type token
- Url (“) : if there is no close parenthesis), or matches the re as (type token, such as URL (“)).
- Var (–main-color) : otherwise use brackets as tokens, such as var(–main-color).
// Special handling of the left parenthesis
case OPEN_PARENTHESES: {
prev = buffer.length ? buffer.pop()[1] : ' '
n = css.charCodeAt(pos + 1)
// Attach brackets with extra brackets. // Attach brackets
if (
prev === 'url'&& n ! == SINGLE_QUOTE && n ! == DOUBLE_QUOTE && ) { next = posdo {
escaped = false
next = css.indexOf(') ', next + 1)
if (next === -1) {
if (ignore || ignoreUnclosed) {
next = pos
break
} else {
unclosed('bracket')}}}while (escaped)
currentToken = ['brackets', css.slice(pos, next + 1), pos, next]
pos = next
} else {
next = css.indexOf(') ', pos + 1)
content = css.slice(pos, next + 1)
// If there is no close parenthesis), or matches the re as (type token, such as url(")).
if (next === -1 || RE_BAD_BRACKET.test(content)) {
currentToken = ['('.'(', pos]
} else {
// Use brackets as extra brackets, like var(--main-color).
currentToken = ['brackets', content, pos, next]
pos = next
}
}
break
}
Copy the code
Comment: The token of the comment type and Word type is used by default
- / : slash
- * : wildcard
word
:
- \ : Backslash
default: {
if (code === SLASH && css.charCodeAt(pos + 1) === ASTERISK) {
next = css.indexOf('* /', pos + 2) + 1
if (next === 0) {
if (ignore || ignoreUnclosed) {
next = css.length
} else {
unclosed('comment')
}
}
currentToken = ['comment', css.slice(pos, next + 1), pos, next]
pos = next
} else {
RE_WORD_END.lastIndex = pos + 1
RE_WORD_END.test(css)
if (RE_WORD_END.lastIndex === 0) {
next = css.length - 1
} else {
next = RE_WORD_END.lastIndex - 2
}
currentToken = ['word', css.slice(pos, next + 1), pos, next]
buffer.push(currentToken)
pos = next
}
break
}
Copy the code
Tokenizer creates the following tokens:
[ 'at-word', '@import', 0, 6 ]
[ 'space', ' ' ]
[ 'word', 'url', 8, 10 ]
[ '(', '(', 11 ]
[ 'string', "'./default.css'", 12, 26 ]
[ ')', ')', 27 ]
[ ';', ';', 28 ]
[ 'space', '\n' ]
[ 'word', 'body', 30, 33 ]
[ 'space', ' ' ]
[ '{', '{', 35 ]
[ 'space', '\n ' ]
[ 'word', 'padding', 39, 45 ]
[ ':', ':', 46 ]
[ 'space', ' ' ]
[ 'word', '0', 48, 48 ]
[ ';', ';', 49 ]
[ 'space', '\n ' ]
[ 'comment', '/* margin: 0; */', 53, 68 ]
[ 'space', '\n' ]
[ '}', '}', 70 ]
[ 'space', '\n' ]
Copy the code
As you can see, the token is an array. Take the first token as an example. The data structure is as follows
[' the at - word ', / / type '@ import', / / the value of 0, / / starting position 6 / / terminate position]Copy the code
3. parser
The Parser loop calls tokenizer’s nextToken method until the file ends. Some algorithms and conditional judgments are used during the loop to create nodes and then build the AST. The above example generates the following AST:
3.1 the node
The base class of Node and Container nodes, where Container inherits from Node. The AST consists of the following nodes
- Root: inherits from Container. The root node of the AST represents the entire CSS file
- AtRule: inherits from Container. Statements that start with @ and have the core property params, for example: @import URL (‘./default.css’), @keyframes shaking {}. Params for the url (‘. / default. CSS ‘)
- Rule: Inherited from Container. A selector with a declared core property of selector, for example: body {}, selector body
- Declaration: Inherited from Node. Declaration, is a key-value pair with core properties prop, value, for example: padding: 0, prop is padding, value is 0
- Comment: Inherited from Node. Margin: 0; margin: 0;
The node contains some general properties
-
Type: indicates the node type
-
Parent: indicates the parent node
-
Source: indicates the resource information of a storage node. Calculate sourcemap
- Input: input
- Start: indicates the start position of a node
- End: indicates the end position of a node
-
Raws: Additional symbols for storage nodes, semicolons, Spaces, comments, etc., which are concatenated in the Stringify procedure
General:
- Before: The space symbols before The node. It also stores
*
and_
symbols before the declaration (IE hack).
On the Rule:
- After: The space symbols after The last child of The node to The end of The node. The space symbol between the last child node and the end of the node
- Between: The symbols between The selector and
{
For rules. Symbol between selector and { - Semicolon: the Contains
true
if the last child has an (optional) semicolon. The last child node has; It is true - ownSemicolon: Contains
true
if there is semicolon after rule. If rule is followed by; It is true
Acting on the Comment
- Left: The space symbols between
/ *
And the comment’s text. /* The space symbol between the comment content - Right: The space symbols between The comment’s text. */ And The space symbols between The comment content are used on The Declaration
- Important: The content of The important statement. Is it important
- value: Declaration value with comments. Declared values with comments.
- Before: The space symbols before The node. It also stores
Each node has its own API. For details, see the PostCSS API
3.2 Generation Process
class Parser {
parse() {
let token
while (!this.tokenizer.endOfFile()) {
token = this.tokenizer.nextToken()
switch (token[0]) {
case 'space':
this.spaces += token[1]
break
case '; ':
this.freeSemicolon(token)
break
case '} ':
this.end(token)
break
case 'comment':
this.comment(token)
break
case 'at-word':
this.atrule(token)
break
case '{':
this.emptyRule(token)
break
default:
this.other(token)
break}}this.endFile()
}
}
Copy the code
Create a root node and set current to root. Use the tokens variable to store tokens that have been traversed but not yet used.
- encounter
at-rule
Token to create an atRule node - encounter
{
Token to create a rule node- Generate the selector property of the rule from the tokens stored in tokens
- Set current to the rule node
- Push the rule node to the current nodes
- encounter
;
Token to create a DECL node. There is a special case: declaration is; If it is the last rule, you can leave it out. , such as. A {color: blue},- Push decL nodes into current Nodes.
- encounter
comment
Token to create a comment node - encounter
}
Token thinks when the rule ends- Set current to current-.parent (the parent of the current node)
Specific process can see the source: github.com/postcss/pos…
Algorithms used:
- Depth-first traversal
- Matching parentheses
4. plugins
Plugins are then called to modify the AST tree derived from the Parser. Plugins are executed in lazy-result.js.
class LazyResult {
get [Symbol.toStringTag]() {
return 'LazyResult'
}
get processor() {
return this.result.processor
}
get opts() {
return this.result.opts
}
/ / get the CSS
get css() {
return this.stringify().css
}
get content() {
return this.stringify().content
}
get map() {
return this.stringify().map
}
/ / to get root
get root() {
return this.sync().root
}
get messages() {
return this.sync().messages
}
}
Copy the code
Result. CSS, result.map, and result.root are all executed when they are accessed.
stringify() {
if (this.error) throw this.error
if (this.stringified) return this.result
this.stringified = true
// Execute plugins synchronously
this.sync()
let opts = this.result.opts
let str = stringify
if (opts.syntax) str = opts.syntax.stringify
if (opts.stringifier) str = opts.stringifier
if (str.stringify) str = str.stringify
// Generate map and CSS
let map = new MapGenerator(str, this.result.root, this.result.opts)
let data = map.generate()
this.result.css = data[0]
this.result.map = data[1]
return this.result
}
Copy the code
When accessing result.css, the plug-in is executed synchronously, and then the processed AST is used to generate CSS and sourcemap
sync() {
if (this.error) throw this.error
if (this.processed) return this.result
this.processed = true
if (this.processing) {
throw this.getAsyncError()
}
for (let plugin of this.plugins) {
let promise = this.runOnRoot(plugin)
if (isPromise(promise)) {
throw this.getAsyncError()
}
}
Collect accessors first
this.prepareVisitors()
if (this.hasListener) {
let root = this.result.root
// If root is dirty, re-execute the plug-in on root
while(! root[isClean]) { root[isClean] =true
this.walkSync(root)
}
if (this.listeners.OnceExit) {
this.visitSync(this.listeners.OnceExit, root)
}
}
return this.result
}
Copy the code
The new plug-in supports accessors of two types: Enter and Exit. Once, Root, AtRule, Rule, Declaration, Comment, etc. Call OnceExit, RootExit, AtRuleExit… Will be called after all child nodes have finished processing. Declaration and AtRule support listening on attributes, such as:
module.exports = (opts = {}) = > {
Declaration: {
color: decl= > {}
The '*': decl= >{}},AtRule: {
media: atRule= >{}}Rule(){}}Copy the code
PrepareVisitors methods collect these listeners and add them to listeners. For example, the code above adds declaration-color, Declaration*, atrule-media, and Rule.
prepareVisitors() {
this.listeners = {}
let add = (plugin, type, cb) = > {
if (!this.listeners[type]) this.listeners[type] = []
this.listeners[type].push([plugin, cb])
}
for (let plugin of this.plugins) {
if (typeof plugin === 'object') {
for (let event in plugin) {
if(! NOT_VISITORS[event]) {if (typeof plugin[event] === 'object') {
for (let filter in plugin[event]) {
if (filter === The '*') {
add(plugin, event, plugin[event][filter])
} else {
add(
plugin,
event + The '-' + filter.toLowerCase(),
plugin[event][filter]
)
}
}
} else if (typeof plugin[event] === 'function') {
add(plugin, event, plugin[event])
}
}
}
}
}
this.hasListener = Object.keys(this.listeners).length > 0
}
Copy the code
Then, during walkSync, it determines the type of accessors that the node can have and recursively calls the CHILDREN if it is CHILDREN, or executes the accessors if it is other executable accessors such as Rule.
walkSync(node) {
node[isClean] = true
let events = getEvents(node)
for (let event of events) {
if (event === CHILDREN) {
if (node.nodes) {
node.each(child= > {
if(! child[isClean])this.walkSync(child)
})
}
} else {
let visitors = this.listeners[event]
if (visitors) {
if (this.visitSync(visitors, node.toProxy())) return}}}}Copy the code
If you perform some operations on a node that have side effects, such as append, prepend, remove, insertBefore, insertAfter, etc., the side effect node[isClean] = false is cyclically marked upwards. Until root[isClean] = false. This can cause the plug-in to execute again, or even cause an endless loop.
5. stringifier
The stringifier traverses the AST tree in a hierarchy starting from root and concatenates node data into strings based on node types.
// stringifier. Js simplifies code
class Stringifier {
constructor(builder) {
this.builder = builder
}
stringify(node, semicolon) {
// Invoke the corresponding type node
this[node.type](node, semicolon)
}
// Root node processing
root(node) {
this.body(node)
if (node.raws.after) this.builder(node.raws.after)
}
// root node processing
body(node) {
let last = node.nodes.length - 1
while (last > 0) {
if(node.nodes[last].type ! = ='comment') break
last -= 1
}
let semicolon = this.raw(node, 'semicolon')
for (let i = 0; i < node.nodes.length; i++) {
let child = node.nodes[i]
let before = this.raw(child, 'before')
if (before) this.builder(before)
this.stringify(child, last ! == i || semicolon) } }// Comment type node stitching
comment(node) {}
// DecL type node splicing
decl(node, semicolon) {}
// Node splicing of rule type
rule(node) {}
// Splicing at-rule nodes
atrule(node, semicolon) {}
// Block node processing, rule, at-rule(@media, etc.)
block(node, start){}
// Raw information processing
raw(){}}Copy the code
Root, body, comment, DECl, rule, atrule, block, and RAW are string splicing functions of different types of nodes and information. The whole process starts from root, does the sequence traversal, root→body→rule/atrule/comment/decl, and then concatenates the string through Builder. Builder is a splicing function:
const builder = (str, node, type) = > {
this.css += str
}
Copy the code
Plugins plugins
1. The old way
const postcss = require('postcss');
module.exports = postcss.plugin('postcss-plugin-old'.function (opts) {
return function (root) {
root.walkRules((rule) = > {
if (rule.selector === 'body') {
rule.append(postcss.decl({ prop: 'margin'.value: '0' }));
rule.append(postcss.decl({ prop: 'font-size'.value: '14px'})); }}); }; });Copy the code
The old writing method needs to introduce PostCSS, so the plug-in needs to set postCSS to peerDependence, and then use the API of PostCSS to operate AST.
2. The new way
// Use symbol to mark the processed nodes
const processd = Symbol('processed');
module.exports = (opts = {}) = > {
return {
postcssPlugin: 'postcss-plugin-new'.Once() {},
OnceExit(root) {
root.walkDecls((decl) = > {
// Delete a node
if (decl.prop === 'color') {
decl.value = '#ee3'; }}); },Rule(rule, { Declaration }) {
if(! rule[processd]) {if (rule.selector === 'body') {
rule.append(new Declaration({ prop: 'color'.value: '# 333' }));
}
rule[processd] = true; }},Declaration: {
padding: (decl) = > {},
margin: (decl) = > {
if (decl.value === '0') {
decl.value = '10px'; }}},DeclarationExit() {},
prepare(result){
const variables = {};
return {
Declaration(){}
OnceExit(){}}}}; };module.exports.postcss = true;
Copy the code
The new version eliminates the need to introduce PostCSS and adds a Visitor.
- There are two types of accessors: Enter and Exit. For example, Declaration is executed when a DECL node is accessed, and DeclarationExit is processed after all Declaration accessors are processed.
- You can use prepare() to dynamically generate accessors.
- The first argument to the accessor is node, the node to be accessed, which can be operated on directly by calling node’s methods.
- The accessor’s second argument is {… Postcss, result: this.result, postcss}, convenient to call methods on postcss.
For more information, please refer to the official document write-a-plugin
Grammatical syntax
Postcss-less and postCSs-SCSS are syntax. They recognize this syntax and convert it. CSS is not compiled
The internal implementations also inherit from Tokenizer and Parser classes and override some of the internal methods.
Syntax does not support // comments. If syntax is not specified as postcss-scss, postCSS will report CssSyntaxError: Unknown word.
- First, tokenizer needs to recognize // as a comment token
function nextToken(){
// ...
if(){
} else if (code === SLASH && n === SLASH) {
RE_NEW_LINE.lastIndex = pos + 1
RE_NEW_LINE.test(css)
if (RE_NEW_LINE.lastIndex === 0) {
next = css.length - 1
} else {
next = RE_NEW_LINE.lastIndex - 2
}
content = css.slice(pos, next + 1)
// inline means a // comment
currentToken = ['comment', content, pos, next, 'inline']
pos = next
}
}
Copy the code
- Parser then needs to build it into a Node and store source, RAWS, etc.
class ProParser extends Parser{
comment (token) {
if (token[4= = ='inline') {
let node = new Comment()
this.init(node, token[2])
node.raws.inline = true
let pos = this.input.fromOffset(token[3])
node.source.end = { offset: token[3].line: pos.line, column: pos.col }
let text = token[1].slice(2)
if (/^\s*$/.test(text)) {
node.text = ' '
node.raws.left = text
node.raws.right = ' '
} else {
let match = text.match(/^(\s*)([^]*\S)(\s*)$/)
let fixed = match[2].replace(/(\*\/|\/\*)/g.'* / / *)
node.text = fixed
node.raws.left = match[1]
node.raws.right = match[3]
node.raws.text = match[2]}}else {
super.comment(token)
}
}
}
Copy the code
- Finally, the stringifier needs to concatenate it into a string
class ProStringifier extends Stringifier {
comment (node) {
let left = this.raw(node, 'left'.'commentLeft')
let right = this.raw(node, 'right'.'commentRight')
if (node.raws.inline) {
let text = node.raws.text || node.text
this.builder('/ /' + left + text + right, node)
} else {
this.builder('/ *' + left + node.text + right + '* /', node)
}
}
}
Copy the code
To solve the opening
For the opening scene, the idea is as follows:
- Split by selector, for example
.a .b{}
Split into, a {}, {b}
And combine the declaration of the rule with the same selector. - The selector
split(' ')
.length>2
For cutting processing
module.exports = (options = {}) = > {
return {
postcssPlugin: 'postcss-plugin-crop-css',
Once (root, { postcss }) {
const selectorRuleMap = new Map()
root.walkRules((rule) = > {
const { selector } = rule
const selectorUnits = selector.split(', ')
for (let selectorUnit of selectorUnits) {
let selectorUnitArr = selectorUnit.split(' ')
// An error is reported when the selector exceeds two layers
if (selectorUnitArr.length > 2) {
throw rule.error('no more than two nested levels')}const selectorCrop = selectorUnitArr.join(' ').replace('\n'.' ')
const existSelectorRule = selectorRuleMap.get(selectorCrop)
const nodes = existSelectorRule ? [existSelectorRule.nodes, rule.nodes] : rule.nodes
const newRule = new postcss.Rule({
selector: selectorCrop,
source: rule.source,
nodes
})
selectorRuleMap.set(selectorCrop, newRule)
}
rule.remove()
})
selectorRuleMap.forEach(selectorRule= > {
root.append(selectorRule)
})
}
}
}
module.exports.postcss = true
Copy the code
reference
- postcss