充分理解 postcss
使用方法後,我們就能開始嘗試解決最初的目標:「在 css 屬性值寫函式呼叫,並將函式運算後,用結果替換函式呼叫的字串」。
我們先梳理一下應該做些什麼:
postcss
的 Declaration
拿到 css 屬性值。3.
)。postcss
處理。
Declaration
(回到 2.
)。
postcss
的特性,可參考「煉金工房的核心設施」的介紹。從梳理的流程中可知,我們需要從屬性值中挑出符合特定規則的字符,無可避免的必須使用正規表示式,但篇幅無法完整介紹正規表示式,我們只能換個方式:直接將正規表示式寫出來,儘量解釋其中每個部分的作用,如果看不懂也沒關係,只需知道這個正規表示式可以將「長得像函式呼叫的字串」與「函式參數」挑出來就行了!
const functionCallRegex = /(\w+)\(([^()]*)\)/g
const str = 'add(1,2) sub(3,add(4,5)) add()'
console.log(functionCallRegex.exec(str)) // ['add(1,2)', 'add', '1,2', index: 0]
console.log(functionCallRegex.exec(str)) // ['add(4,5)', 'add', '4,5', index: 15]
console.log(functionCallRegex.exec(str)) // ['add()', 'add', '', index: 25]
console.log(functionCallRegex.exec(str)) // null
正規表示式是用一堆具有特殊意義的字符去描述一套字符規則,用字符規則去匹配字串中是否有符合規則的部分。
在 js
中,/xxx/
為正規表示式的意思,而我以 /(\w+)\(([^()]*)\)/g
來描述函式呼叫的字符規則,並使用 exec
方法來匹配 'add(1,2) sub(3,add(4,5)) add()'
。以下我儘量解釋規則的每個部分含義:
(\w+)
\w
:
a-z
、A-Z
、0-9
、_
。+
:
+
表示匹配 1 個字符以上。
/\w/.exec('ab~c')
的匹配結果是 ['a']
:沒有任何個數的符號,所以只匹配 1 個字符 a
。/\w+/.exec('ab~c')
的匹配結果是 ['ab']
:+
表示匹配 1 個字符以上,在 ~
之前的字符都符合 \w
規則,所以匹配結果為 ab
。()
:
exec
輸出結果的 add
就是因為 (\w+)
分組而將 add(1,2)
、add(4,5)
、add()
前面的英數字提取出來所致。(\w+)
就是用來提取函式名稱,函式名需要:
\w
規則。+
。\(([^()]*)\)
\(
與 \)
:
(
與 )
在正規表示式中有特殊意義,但函式呼叫需要匹配到 ()
,所以就需要跳脫字符。
\
,告訴編譯器這個字符是單純的字符,沒有特殊意義。""
中使用 "
,也會需要跳脫字符(console.log("\"")
)。\(
與 \)
的意思是:
(
時,會匹配 \(
規則。)
時,會匹配 \)
規則。([^()]*)
:
[^()]
:
[]
是列舉的意思,例如 \w
就等於 [a-zA-Z0-9_]
。[()]
的意思是:我需要匹配 (
或 )
字符。[]
前面加 ^
表示不需要的意思,所以 [^()]
是匹配除了 ()
以外的字符。*
:
''
而非匹配失敗。
/\w+/.exec('~')
的結果是 null
,因為 +
至少要匹配 1 個字符,而 ~
不符合 \w
。/\w*/.exec('~')
的結果是 ['']
,因為 *
即使沒有字符匹配也算匹配成功而返回 ''
。(
與最後面的 )
:
exec
輸出結果的 '1,2'
、'4,5'
、''
就是因為這個分組而取得的。*
。g
global
的意思,表示每次匹配成功後,從上次匹配完的地方繼續往下匹配。
const g = /\w+/g
const str = 'ab~cd'
console.log(g.exec(str)) // [ 'ab', index: 0 ]
console.log(g.exec(str)) // [ 'cd', index: 3 ]
console.log(g.exec(str)) // null
const noG = /\w+/
console.log(noG.exec(str)) // [ 'ab', index: 0 ]
console.log(noG.exec(str)) // [ 'ab', index: 0 ]
console.log(noG.exec(str)) // [ 'ab', index: 0 ]
index
:表示這次結果是從第幾個字符開始匹配的。g
的 index
每次都是 0
,表示重頭匹配。sub
由於 sub
沒有匹配成功,我們以匹配 sub
的角度來理解整個匹配過程:
add(1,2)
後,下次 exec
從後面的空格開始找。\w+
的字符:找到 sub
後,遇到 (
而停下。\(
匹配到 sub
後面的 (
。([^()]*)
的字符:找到 3,add
後,遇到 (
而停下。([^()]*)
的下一個規則是 \)
,但是遇到的是 (
,所以整個匹配失敗。\w+
的字符。這其實是刻意設計的,為的是讓函式執行從內而外逐個執行後替換,例如:
sub(3,add(4,5))
的 add(4,5)
替換成 9
後。postcss
會拿 sub(3,9)
再來執行 Declaration
,此時 sub
也能被匹配成功而執行後替換。以上就是整個 /(\w+)\(([^()]*)\)/g
的解釋,正規表示式對於像我這種似懂非懂的人來說都很難了,所以我實在也不知道怎麼讓完全沒接觸過的人看懂 😦。如果你完全看不懂也真的沒關係,你只要知道一個結論:
/(\w+)\(([^()]*)\)/g
可以找到字串中所有最內層的函式呼叫,並拿到函式名與函式參數。
package.json
{
"type": "module",
"devDependencies": {
"postcss-load-config": "^6.0.1",
"vite": "^7.1.4"
}
}
index.html
<link rel="stylesheet" href="./normal.css">
normal.css
p {
width: calc(add(multiply(3, 3), 2)px + 3px);
padding: calc(multiply(add(multiply(1, 2), multiply(3, 4)), 5)px + 3px) multiply(3, 3)px;
}
有點複雜的函式呼叫,我們的目標是把 add
跟 multiply
給執行後替換!
postcss.config.js
const myPlugin = function (functions = {}) {
const functionCallRegex = /(\w+)\(([^()]*)\)/g
return {
postcssPlugin: 'my-plugin',
Declaration: (decl) => {
let newValue = decl.value
let match
console.log(`[ 處理 ${decl.prop}: ${decl.value} ]`)
while ((match = functionCallRegex.exec(decl.value)) !== null) {
const [fullMatch, functionName, argsString] = match
if (!functions[functionName]) {
console.log(`我沒有要攔截 ${functionName} 函式!`)
continue
}
try {
const args = argsString.trim() ? argsString.split(',').map(arg => arg.trim()) : []
const result = functions[functionName](...args)
newValue = newValue.replace(fullMatch, result)
console.log(`替換 ${fullMatch} 為 ${result}!`)
} catch (error) {
console.error(`Error executing function ${functionName}:`, error)
}
}
if (newValue !== decl.value) {
decl.value = newValue
}
},
}
}
myPlugin.postcss = true
/** @type {import('postcss-load-config').Config} */
export default {
plugins: [
myPlugin({
add: (a, b) => parseFloat(a) + parseFloat(b),
multiply: (a, b) => parseFloat(a) * parseFloat(b),
}),
],
}
add
與 multiply
。postcss
的 Declaration
拿到 css 屬性值。/(\w+)\(([^()]*)\)/g.exec
將長得像函式呼叫的字串挑出來。(\w+)
將函式名挑出來,並攔截 add
或 multiply
。([^()]*)
將函式參數挑出來。newValue = newValue.replace(fullMatch, result)
while
持續對同一個屬性值 exec
,查找並替換所有最內層的函式呼叫字串(回到 3.
)。postcss
處理,下一輪 Declaration
再更新替換後的 css 屬性值(回到 2.
)。
decl.value = newValue
結果
% npx vite build --minify false
[ 處理 width: calc(add(multiply(3, 3), 2)px + 3px) ]
替換 multiply(3, 3) 為 9!
[ 處理 padding: calc(multiply(add(multiply(1, 2), multiply(3, 4)), 5)px + 3px) multiply(3, 3)px ]
替換 multiply(1, 2) 為 2!
替換 multiply(3, 4) 為 12!
替換 multiply(3, 3) 為 9!
[ 處理 width: calc(add(9, 2)px + 3px) ]
替換 add(9, 2) 為 11!
[ 處理 padding: calc(multiply(add(2, 12), 5)px + 3px) 9px ]
替換 add(2, 12) 為 14!
[ 處理 width: calc(11px + 3px) ]
我沒有要攔截 calc 函式!
[ 處理 padding: calc(multiply(14, 5)px + 3px) 9px ]
替換 multiply(14, 5) 為 70!
[ 處理 padding: calc(70px + 3px) 9px ]
我沒有要攔截 calc 函式!
# ...
dist/assets/index-CkZrhj5N.css 0.07 kB │ gzip: 0.07 kB
% cat ./dist/assets/index-CkZrhj5N.css
p {
width: calc(11px + 3px);
padding: calc(70px + 3px) 9px;
}
我們直接觀察 width
的處理過程:
Declaration
拿到 calc(add(multiply(3, 3), 2)px + 3px)
multiply(3, 3)
匹配成功。9
(3 * 3 = 9)。Declaration
再次執行,拿到 calc(add(9, 2)px + 3px)
1.
將 multiply(3, 3)
替換成 9
了,所以 add(9, 2)
就能匹配成功。11
(9 + 2 = 11)。Declaration
再次執行,拿到 calc(11px + 3px)
。
calc
匹配成功,但我們沒有攔截 calc
。width
更新完成。以上就是整個處理過程!希望大家能理解我到底在幹嘛~如果無法理解也沒關係,這篇只是想讓大家清楚整個替換過程,要完美寫出沒有破綻的正規表示式是相當困難的,例如 /(\w+)\(([^()]*)\)/g
遇到 add("(")
會匹配失敗。
我在實際工作中都用別人寫好的工具,所以接下來將陸續分享好用的工具給大家,明天見囉~