充分理解 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("(") 會匹配失敗。
我在實際工作中都用別人寫好的工具,所以接下來將陸續分享好用的工具給大家,明天見囉~