iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 25
1
Modern Web

前端建置工具完全手冊系列 第 25

Day 25: 實作個 postcss plugin

在開始前先說一件事, postcss 的 plugin 設計其實有個不小的問題,而為了解決這個問題 postcss 8 大改了這部份的架構,然而大部份的 plugin 還是使用 postcss 7 的 plugin 結構,這邊會兩種都介紹,也會講一下 postcss 8 大改到底是為了解決什麼問題

postcss 7 以前

postcss 7 以前的 plugin 必須要用 postcss.plugin 包起來才行, plugin 的結構大概會長這樣:

const postcss = require('postcss')

module.exports = postcss.plugin('my-plugin', (options) => {
  return (css) => {
    // 處理 css
  }
})

跟 babel 很像的 css 是個像 path 一樣包含了各種函式,同時也紀錄了 AST tree 的物件,在 postcss 中的 AST 大致上只有五種節點:

  • Root: 根節點,就是整個檔案,上面的 css 就是 Root
  • Rule: 包含 selector 與其中的設定組合起來的一組規則,如 .example { user-select: none; }
  • Declaration: 一條樣式的設定,如 user-select: none;
  • AtRule: 由 @ 開頭的規則,像 media query 之類的
  • Comments: 就是註解

postcss 不會再去詳細 parse 如 selector 的每個部份,舉例來說 .a .b 就只會是存在 Ruleselector 的字串而已,頂多會 parse 用 , 分開的部份,如果想要詳細 parse selector 的內容的話,就要另外使用 postcss-selector-parser ,其它部份也是這樣的

知道了節點是怎麼樣後,其實再來就跟 babel 的 plugin 很像了,先去 AST Explorer 看 AST 長什麼樣,找出要處理的節點,直接修改 AST ,不過上面的 css 不是 Root 節點嗎?其它的節點要如何取得呢?這時後就要用 walk 系列的函式了:

const postcss = require('postcss')

module.exports = postcss.plugin('my-plugin', (options) => {
  return (css) => {
    css.walk(node => {
      // node 是一個節點
    })

    css.walkAtRules(rule => {
      // 這邊的 rule 就是一個 `AtRule`
    })

    css.walkAtRules('tailwind', rule => {
      // 這邊的 rule 是 `@tailwind` 的結點
    })
  }
})

除了 Root 節點外 (Root 只會有一個,根本不需要),其它種類的節點都有一個對應的 walk 的函式:

  • AtRule -> walkAtRules
  • Rule -> walkRules
  • Declaration -> walkDecls
  • Comments -> walkComments

可以用這些函式來找出自己要的節點,這邊就來一個移除掉 @tailwind 的 plugin :

const postcss = require('postcss')

module.exports = postcss.plugin('my-plugin', () => {
  return css => {
    css.walkAtRules('tailwind', rule => {
      rule.remove()
    })
  }
})

或者把不小心打成 @tailwindcss 變回 @tailwind 的 plugin:

const postcss = require('postcss')

module.exports = postcss.plugin('my-plugin', () => {
  return css => {
    css.walkAtRules('tailwindcss', rule => {
      // 直接改就行了
      rule.name = 'tailwind'
    })
  }
})

另外 postcss 也有提供建立 AST 節點跟插入節點的函式:

const postcss = require('postcss')

module.exports = postcss.plugin('my-plugin', () => {
  return css => {
    css.walkAtRules('tailwind', rule => {
      // 建立一個註解的節點
      const node = postcss.comment({ text: 'tailwind css' })
      // 插入到這個規則前
      rule.before(node)
    })
  }
})

可以用 plugin 的第二個參數產生 warning ,或是用在節點上的函式:

const postcss = require('postcss')

module.exports = postcss.plugin('my-plugin', () => {
  return (css, result) => {
    css.walkAtRules('tailwind', rule => {
      // 你可以選擇要不要傳入 node
      result.warn('foo', { node: rule })
      // 或是使用在 node 上的 `warn`
      rule.warn(result, 'foo')

      // error 只能用節點上的函式來拋出
      throw rule.error('foo')
    })
  }
})

另外 postcss 的 plugin 是可以 async 的

const postcss = require('postcss')

module.exports = postcss.plugin('my-plugin', () => {
  return (css, result) => {
    // 可以回傳 promise
    return Promise.resolve()
  }
})

對於 plugin 的介紹就先到這邊了,看到這,不知道你有沒有注意到 postcss 的問題了

postcss 8

postcss 的 plugin 有兩個很大的問題:

  • 直接相依於 postcss 本身,使得很多 plugin 直接把 postcss 做為一個 dependencies ,這可能使得使用者安裝到多個不同的 postcss
  • 每個 plugin 都要從根節點開始走訪,而不是像 babel 一樣,統一由 babel 來走訪,再去呼叫 plugin 對應的處理函式,這使得 plugin 相較之下較沒效率

所以 postcss 8 的 plugin 就變成了:

module.exports = (options) => {
  return {
    postcssPlugin: 'my-plugin',
    // 如果還是要取得 `Root` 可以在這邊取得
    Once(root) {},
    // 或是取得一個 `Rule`
    Rule(rule) {},
    AtRule: {
      // 如果要用像 postcss 7 那樣的過濾名字的功能的話
      tailwind(rule) {
      }
    }
  }
}
module.exports.postcss = true

下一篇來介紹 vite


上一篇
Day 24: postcss 的使用與設定
下一篇
Day 26 介紹 vite
系列文
前端建置工具完全手冊30

尚未有邦友留言

立即登入留言