整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          如何編寫屬于自己的 PostCSS 8 插件?

          者近期在將前端架構(gòu) webpack 升級到 5 時(shí),一些配套模塊也需要進(jìn)行升級,其中包括了 css 處理模塊 PostCSS。舊版本使用的是 PostCSS 7,在升級至 PostCSS 8 的過程中,筆者發(fā)現(xiàn)部分插件前置依賴還是停留在 7 版本,且年久失修,在 PostCSS 8 中出現(xiàn)各種各樣的問題,無奈只能研究源碼,將目前部分舊版本插件升級至新版本。這里,筆者將升級插件的過程進(jìn)行簡化和提煉,讓讀者自己也可以編寫一個(gè) PostCSS 8 插件。

          插件工作原理

          PostCSS 是一個(gè)允許使用 JS 插件轉(zhuǎn)換樣式的工具。開發(fā)者可以根據(jù)自己的實(shí)際需求,在編譯過程將指定 css 樣式進(jìn)行轉(zhuǎn)換和處理。目前 PostCSS 官方收錄插件有 200 多款,其中包括使用最廣泛的Autoprefixer自動(dòng)補(bǔ)全 css 前綴插件。

          PostCSS 和插件的工作原理其實(shí)很簡單,就是先將 css 源碼轉(zhuǎn)換為 AST,插件基于轉(zhuǎn)換后 AST 的信息進(jìn)行個(gè)性化處理,最后 PostCSS 再將處理后的 AST 信息轉(zhuǎn)換為 css 源碼,完成 css 樣式轉(zhuǎn)換,其流程可以歸結(jié)為下圖:

          下面我們通過實(shí)際例子看看 PostCSS 會將 css 源碼轉(zhuǎn)換成的 AST 格式:

          const postcss = require('postcss')
          postcss().process(`
          .demo {
           font-size: 14px; /*this is a comment*/
          }
          `).then(result => {
           console.log(result)
          })

          復(fù)制代碼

          代碼中直接引用 PostCSS,在不經(jīng)過任何插件的情況下將 css 源碼進(jìn)行轉(zhuǎn)換,AST 轉(zhuǎn)換結(jié)果如下:

          {
           "processor": {
           "version": "8.3.6",
           "plugins": []
           },
           "messages": [],
           "root": {
           "raws": {
           "semicolon": false,
           "after": "\n"
           },
           "type": "root",
           // ↓ nodes字段內(nèi)容重點(diǎn)關(guān)注
           "nodes": [
           {
           "raws": {
           "before": "\n",
           "between": " ",
           "semicolon": true,
           "after": "\n"
           },
           "type": "rule",
           "nodes": [
           {
           "raws": {
           "before": "\n ",
           "between": ": "
           },
           "type": "decl",
           "source": {
           "inputId": 0,
           "start": {
           "offset": 11,
           "line": 3,
           "column": 3
           },
           "end": {
           "offset": 26,
           "line": 3,
           "column": 18
           }
           },
           "prop": "font-size", // css屬性和值
           "value": "14px"
           },
           {
           "raws": {
           "before": " ",
           "left": "",
           "right": ""
           },
           "type": "comment", // 注釋類
           "source": {
           "inputId": 0,
           "start": {
           "offset": 28,
           "line": 3,
           "column": 20
           },
           "end": {
           "offset": 48,
           "line": 3,
           "column": 40
           }
           },
           "text": "this is a comment"
           }
           ],
           "source": {
           "inputId": 0,
           "start": {
           "offset": 1,
           "line": 2,
           "column": 1
           },
           "end": {
           "offset": 28,
           "line": 4,
           "column": 1
           }
           },
           "selector": ".demo", // 類名
           "lastEach": 1,
           "indexes": {}
           }
           ],
           "source": {
           "inputId": 0,
           "start": {
           "offset": 0,
           "line": 1,
           "column": 1
           }
           },
           "lastEach": 1,
           "indexes": {},
           "inputs": [
           {
           "hasBOM": false,
           "css": "\n.demo {\n font-size: 14px;\n}\n",
           "id": "<input css vi1Oew>"
           }
           ]
           },
           "opts": {},
           "css": "\n.demo {\n font-size: 14px;\n}\n"
          }

          復(fù)制代碼

          AST 對象中 nodes 字段里的內(nèi)容尤為重要,其中存儲了 css 源碼的關(guān)鍵字、注釋、源碼的起始、結(jié)束位置以及 css 的屬性和屬性值,類名使用selector存儲,每個(gè)類下又存儲一個(gè) nodes 數(shù)組,該數(shù)組下存放的就是該類的屬性(prop)和屬性值(value)。那么插件就可以基于 AST 字段對 css 屬性進(jìn)行修改,從而實(shí)現(xiàn) css 的轉(zhuǎn)換。

          PostCSS 插件格式規(guī)范及 API

          PostCSS 插件其實(shí)就是一個(gè) JS 對象,其基本形式和解析如下:

          module.exports = (opts = { }) => {
           // 此處可對插件配置opts進(jìn)行處理
           return {
           postcssPlugin: 'postcss-test', // 插件名字,以postcss-開頭
           
          Once (root, postcss) {
           // 此處root即為轉(zhuǎn)換后的AST,此方法轉(zhuǎn)換一次css將調(diào)用一次
           },
           
          Declaration (decl, postcss) {
           // postcss遍歷css樣式時(shí)調(diào)用,在這里可以快速獲得type為decl的節(jié)點(diǎn)(請參考第二節(jié)的AST對象)
           },
           
          Declaration: {
           color(decl, postcss) {
           // 可以進(jìn)一步獲得decl節(jié)點(diǎn)指定的屬性值,這里是獲得屬性為color的值
           }
           },
           
          Comment (comment, postcss) {
           // 可以快速訪問AST注釋節(jié)點(diǎn)(type為comment)
           },
           
          AtRule(atRule, postcss) {
           // 可以快速訪問css如@media,@import等@定義的節(jié)點(diǎn)(type為atRule)
           }
           
          }
          }
          module.exports.postcss = true

          復(fù)制代碼

          更多的 PostCSS 插件 API 可以詳細(xì)參考官方postcss8文檔,基本原理就是 PostCSS 會遍歷每一個(gè) css 樣式屬性值、注釋等節(jié)點(diǎn),之后開發(fā)者就可以針對個(gè)性需求對節(jié)點(diǎn)進(jìn)行處理即可。

          實(shí)際開發(fā)一個(gè) PostCSS 8 插件

          了解了 PostCSS 插件的格式和 API,我們將根據(jù)實(shí)際需求來開發(fā)一個(gè)簡易的插件,有如下 css:

          .demo {
           font-size: 14px; /*this is a comment*/
           color: #ffffff;
          }

          復(fù)制代碼

          需求如下:

          1. 刪除 css 內(nèi)注釋
          2. 將所有顏色為十六進(jìn)制的#ffffff轉(zhuǎn)為 css 內(nèi)置的顏色變量white

          根據(jù)第三節(jié)的插件格式,本次開發(fā)只需使用Comment和Declaration接口即可:

          // plugin.js
          module.exports = (opts = { }) => {
           return {
           postcssPlugin: 'postcss-test',
           
          Declaration (decl, postcss) {
           if (decl.value === '#ffffff') {
           decl.value = 'white'
           }
           },
           
          Comment(comment) {
           comment.text = ''
           }
           
          }
          }
          module.exports.postcss = true

          復(fù)制代碼

          在 PostCSS 中使用該插件:

          // index.js
          const plugin = require('./plugin.js')
          postcss([plugin]).process(`
          .demo {
           font-size: 14px; /*this is a comment*/
           color: #ffffff;
          }
          `).then(result => {
           console.log(result.css)
          })

          復(fù)制代碼

          運(yùn)行結(jié)果如下:

          .demo {
           font-size: 14px; /**/
           color: white;
          }

          復(fù)制代碼

          可以看到,字體顏色值已經(jīng)成功做了轉(zhuǎn)換,注釋內(nèi)容已經(jīng)刪掉,但注釋標(biāo)識符還依舊存在,這是因?yàn)樽⑨尮?jié)點(diǎn)是包含/**/內(nèi)容存在的,只要 AST 里注釋節(jié)點(diǎn)還存在,最后 PostCSS 還原 AST 時(shí)還是會把這段內(nèi)容還原,要做到徹底刪掉注釋,需要對 AST 的 nodes 字段進(jìn)行遍歷,將 type 為 comment 的節(jié)點(diǎn)進(jìn)行刪除,插件源碼修改如下:

          // plugin.js
          module.exports = (opts = { }) => {
           // Work with options here
           // https://postcss.org/api/#plugin
           return {
           postcssPlugin: 'postcss-test',
           
          Once (root, postcss) {
           // Transform CSS AST here
           root.nodes.forEach(node => {
           if (node.type === 'rule') {
           node.nodes.forEach((n, i) => {
           if (n.type === 'comment') {
           node.nodes.splice(i, 1)
           }
           })
           }
           })
           },
           
          
          Declaration (decl, postcss) {
           // The faster way to find Declaration node
           if (decl.value === '#ffffff') {
           decl.value = 'white'
           }
           }
           
          }
          }
          module.exports.postcss = true

          復(fù)制代碼

          重新執(zhí)行 PostCSS,結(jié)果如下,符合預(yù)期。

          .demo {
           font-size: 14px;
           color: white;
          }

          復(fù)制代碼

          插件開發(fā)注意事項(xiàng)

          通過實(shí)操開發(fā)可以看到,開發(fā)一個(gè) PostCSS 插件其實(shí)很簡單,但在實(shí)際的插件開發(fā)中,開發(fā)者需要注意以下事項(xiàng):

          1.盡量使插件簡單,使用者可以到手即用

          Build code that is short, simple, clear, and modular.

          盡量使你的插件和使用者代碼解耦,開放有限的 API,同時(shí)開發(fā)者在使用你的插件時(shí)從名字就可以知道插件的功能。這里推薦一個(gè)簡單而優(yōu)雅的 PostCSS 插件postcss-focus,讀者可以從這個(gè)插件的源碼中體會這個(gè)設(shè)計(jì)理念。

          2.開發(fā)插件前確認(rèn)是否有現(xiàn)成的輪子

          如果你對自己的項(xiàng)目有個(gè)新點(diǎn)子,想自己開發(fā)一個(gè)插件去實(shí)現(xiàn),在開始寫代碼前,可以先到 PostCSS 官方注冊的插件列表中查看是否有符合自己需求的插件,避免重復(fù)造輪子。不過截止目前(2021.8),大部分插件依舊停留在 PostCSS 8 以下,雖然 PostCSS 8 已經(jīng)對舊版本插件做了處理,但在 AST 的解析處理上還是有差異,從實(shí)際使用過程中我就發(fā)現(xiàn) PostCss8 使用低版本插件會導(dǎo)致 AST 內(nèi)的source map丟失,因此目前而言完全兼容 PostCSS 8 的插件還需各位開發(fā)者去升級。

          從低版本 PostCSS 遷移

          升級你的 PostCSS 插件具體可以參考官方給出的升級指引。這里只對部分關(guān)鍵部分做下解釋:

          1.升級 API

          • 將舊版module.exports = postcss.plugin(name, creator)替換為module.exports = creator;
          • 新版插件將直接返回一個(gè)對象,對象內(nèi)包含Once方法回調(diào);
          • 將原插件邏輯代碼轉(zhuǎn)移至Once方法內(nèi);
          • 插件源碼最后加上module.exports.postcss = true;

          具體示例如下。

          舊版插件:

          - module.exports = postcss.plugin('postcss-dark-theme-class', (opts = {}) => {
          - checkOpts(opts)
          - return (root, result) => {
           root.walkAtRules(atrule => { … })
          - }
          - })

          復(fù)制代碼

          升級后插件:

          + module.exports = (opts = {}) => {
          + checkOpts(opts)
          + return {
          + postcssPlugin: 'postcss-dark-theme-class',
          + Once (root, { result }) {
           root.walkAtRules(atrule => { … })
          + }
          + }
          + }
          + module.exports.postcss = true

          復(fù)制代碼

          2.提取邏輯代碼至新版 API

          把邏輯代碼都放在Once回調(diào)內(nèi)還不夠優(yōu)雅,PostCSS 8 已經(jīng)實(shí)現(xiàn)了單個(gè) css 的代碼掃描,提供了Declaration(), Rule(), AtRule(), Comment() 等方法,舊版插件類似root.walkAtRules的方法就可以分別進(jìn)行重構(gòu),插件效率也會得到提升:

          module.exports = {
           postcssPlugin: 'postcss-dark-theme-class',
          - Once (root) {
          - root.walkAtRules(atRule => {
          - // Slow
          - })
          - }
          + AtRule (atRule) {
          + // Faster
          + }
           }
           module.exports.postcss = true

          復(fù)制代碼

          總結(jié)

          通過本文的介紹,讀者可以了解 PostCSS 8 工作的基本原理,根據(jù)具體需求快速開發(fā)一個(gè) PostCSS 8 插件,并在最后引用官方示例中介紹了如何快速升級舊版 PostCSS 插件。目前 PostCSS 8 還有大量還沒進(jìn)行升級兼容的 PostCSS 插件,希望讀者可以在閱讀本文后可以獲得啟發(fā),對 PostCSS 8 的插件生態(tài)做出貢獻(xiàn)。

          信大家平時(shí)在電腦上逛掘金、知乎網(wǎng)站時(shí),肯定有看到過下面超級煩人的跳轉(zhuǎn)攔截確認(rèn)頁面

          雖然這種攔截的初衷是好的,但是我相信大家平時(shí)肯定不會因?yàn)橛辛诉@個(gè)攔截提醒頁面,就會對即將打開的網(wǎng)站安全性提高自己的警惕性,而是把它當(dāng)做用戶協(xié)議一樣無視并點(diǎn)擊“繼續(xù)訪問”。這種體驗(yàn)給人的感覺是十分難受的,特別是有時(shí)候看一些技術(shù)文章,文章里面會貼一些參考資料鏈接,有時(shí)我會習(xí)慣先右鍵新tab中打開,并且繼續(xù)往下閱讀,等看到剛打開的tab欄沒有加載圈圈時(shí)(說明頁面已經(jīng)加載完畢),再切過去看,結(jié)果被攔截了???

          上面的痛點(diǎn),其實(shí)很容易解決,就是通過開發(fā)一個(gè)瀏覽器插件實(shí)現(xiàn)。

          實(shí)現(xiàn)思路

          我們先打開控制臺看下這些網(wǎng)站跳轉(zhuǎn)鏈接長啥樣:

          掘金:

          知乎:

          可以看到,a標(biāo)簽的鏈接里面并不是直接放置我們要跳轉(zhuǎn)網(wǎng)站鏈接,而是把它放在了target參數(shù)里面。我們要做的就是通過插件,給頁面添加點(diǎn)擊監(jiān)聽事件,先攔截a標(biāo)簽的默認(rèn)跳轉(zhuǎn)行為,然后通過js提取到我們要跳轉(zhuǎn)的鏈接,通過window.open或者window.localtion打開即可。

          開始動(dòng)手開發(fā)插件

          新建manifest.json配置文件

          首先我們新建個(gè)項(xiàng)目文件夾,命名direct-link,在里面新建manifest.json配置文件,里面存放我們插件的配置信息。內(nèi)容如下:

          {
            "name": "direct link", // 插件名字
            "description": "跳過網(wǎng)站點(diǎn)擊跳轉(zhuǎn)詢問頁面!", // 插件描述
            "version": "0.0.1", // 版本號
            "manifest_version": 3, // 插件版本,目前大多插件還是2, 3是目前最新規(guī)范標(biāo)準(zhǔn)
            "permissions": ["storage", "tabs", "scripting"], // 插件需要用到的權(quán)限
            "background": {
              "service_worker": "./background.js" // 對應(yīng)background.js文件,相當(dāng)于程序運(yùn)行入口
            },
            "action": {
              "default_popup": "popup.html",
              "default_icon": {
                "16": "/images/logo16.png",
                "32": "/images/logo32.png",
                "48": "/images/logo48.png",
                "128": "/images/logo128.png"
              }
            },
            "icons": {
              "16": "/images/logo16.png",
              "32": "/images/logo32.png",
              "48": "/images/logo48.png",
              "128": "/images/logo128.png"
            }
          }

          新建images文件夾

          新建images文件夾,里面存放插件的圖標(biāo)。我們可以去iconfont網(wǎng)站查找下載圖片即可,尺寸需要下載多個(gè),從上面配置文件可以看到一共放了16,32,48及128四個(gè)分辨率的圖片。

          新建background.js

          在根目錄下新建background.js,該文件相當(dāng)于程序運(yùn)行入口。創(chuàng)建background.js文件之后,此時(shí)準(zhǔn)備的文件已經(jīng)可以在瀏覽器中運(yùn)行了。我們按如下圖方式打開瀏覽器插件頁面

          然后將右上角的開發(fā)者模式打開

          接著將direct link文件夾直接拖到當(dāng)前頁面即可看到插件成功安裝

          這里值得一提的是,上面manifest.json文件中在兩處地方配置了logo信息,上圖看到的插件圖標(biāo)對應(yīng)的是icon屬性,而action ->default_icon 對應(yīng)的是下圖中的圖標(biāo)顯示位置:

          編輯background.js

          上面有提到,background.js相當(dāng)于程序主入口,內(nèi)容如下:

          // 用戶首次安裝插件時(shí)執(zhí)行一次,后面不會再重新執(zhí)行。(除非用戶重新安裝插件)
          chrome.runtime.onInstalled.addListener(() => {
            // 插件功能安裝默認(rèn)啟用  
            chrome.storage.sync.set({
              linkOpen: true,
            });
          });
          
          // 監(jiān)聽tab頁面加載狀態(tài),添加處理事件
          chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
            // 設(shè)置判斷條件,頁面加載完成才添加事件,否則會導(dǎo)致事件重復(fù)添加觸發(fā)多次
            if (changeInfo.status === "complete" && /^http/.test(tab.url)) {
              chrome.scripting
                .executeScript({
                  target: { tabId: tabId },
                  files: ["./content-script.js"],
                })
                .then(() => {
                  console.log("INJECTED SCRIPT SUCC.");
                })
                .catch((err) => console.log(err));
            }
          });

          上面的代碼邏輯比較簡單,插件安裝初始化時(shí),在本地存儲一個(gè)變量linkOpen設(shè)為true,后面我們會新增一個(gè)選項(xiàng)切換是否啟用插件,需要用到這個(gè)變量判斷。

          接著在頁面初始化時(shí),添加執(zhí)行腳本代碼,這個(gè)腳本代碼叫content-script,里面執(zhí)行我們功能代碼邏輯。

          編輯content-script.js

          在根目錄新建content-script.js,編輯內(nèi)容如下:

          chrome.storage.sync.get("linkOpen", ({ linkOpen }) => {
            if (linkOpen) {
              document.body.addEventListener("click", function (event) {
                const target = event.target;
                // 判斷點(diǎn)擊的是否a標(biāo)簽
                if (target.nodeName.toLocaleLowerCase() === "a") {
                  const href = target.getAttribute("href");
                  if (href.indexOf("://link") > -1) {
                    // 禁止默認(rèn)的跳轉(zhuǎn)行為
                    event.preventDefault();
                    const link = href.split("target=")[1];
                    const url = decodeURIComponent(link);
                    // 處理完 a 標(biāo)簽的內(nèi)容,重新觸發(fā)跳轉(zhuǎn),根據(jù)原來 a 標(biāo)簽頁 target 來判斷是否需要新窗口打開
                    if (target.getAttribute("target") === "_blank") {
                      // 新窗口打開  
                      window.open(url);
                    } else {
                      // 當(dāng)前窗口打開  
                      window.location.href = url;
                    }
                  }
                }
              });
            }
          });

          插件主邏輯如上,對應(yīng)文章開頭提到的實(shí)現(xiàn)思路。

          添加是否啟用插件的功能開關(guān)

          在瀏覽器右上角插件點(diǎn)擊時(shí),通常會顯示一個(gè)功能菜單,如下圖

          下面我們也添加一個(gè)類似的功能,用來是否啟用插件。

          新建popup.js和popup.html

          popup.html對應(yīng)點(diǎn)擊時(shí)顯示的內(nèi)容,popup.js則是相關(guān)執(zhí)行邏輯。

          popup.html:

          <!DOCTYPE html>
          <html lang="en">
          <html>
          <meta charset="UTF-8">
          
          <head>
              <style>
                  .option{padding:30px 0;display:flex;align-items:center;justify-content:center;min-width:160px}.option .name{color:#333;font-size:18px;font-weight:bold}.switch{position:relative;display:inline-block;width:60px;height:34px}.switch input{opacity:0;width:0;height:0}.slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#ccc;-webkit-transition:.4s;transition:.4s}.slider:before{position:absolute;content:"";height:26px;width:26px;left:4px;bottom:4px;background-color:white;-webkit-transition:.4s;transition:.4s}input:checked+.slider{background-color:#45c7d8}input:focus+.slider{box-shadow:0 0 1px #45c7d8}input:checked+.slider:before{-webkit-transform:translateX(26px);-ms-transform:translateX(26px);transform:translateX(26px)}.slider.round{border-radius:34px}.slider.round:before{border-radius:50%}
              </style>
          </head>
          
          <body>
              <div class="option">
                  <span class="name">開啟:</span>
                  <label class="switch">
                      <input type="checkbox" id="switch">
                      <span class="slider round"></span>
                  </label>
              </div>
          
              <script src="popup.js"></script>
          </body>
          
          </html>

          顯示效果如下:

          popup.js

          const btn = document.querySelector("#switch");
          
          chrome.storage.sync.get("linkOpen", ({ linkOpen }) => {
            btn.checked = linkOpen;
          });
          
          btn.addEventListener("change", () => {
            if (btn.checked) {
              chrome.storage.sync.set({ linkOpen: true });
            } else {
              chrome.storage.sync.set({ linkOpen: false });
            }
            // 獲取當(dāng)前tab窗口
            chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
              chrome.scripting.executeScript({
                target: { tabId: tabs[0].id },
                func: refreshPage,
              });
            });
          });
          
          // 刷新頁面
          function refreshPage() {
            window.location.reload();
          }

          上面的邏輯也很簡單,就是監(jiān)聽swich按鈕,更新本地存儲變量,并且每次修改時(shí)刷新下頁面觸發(fā)content-script里面的邏輯

          至此,我們的一個(gè)插件就開發(fā)完成了。

          值得注意的事

          開發(fā)調(diào)試踩坑

          1. 每次修改代碼后,我們需要點(diǎn)擊如下圖的刷新按鈕,并且重新刷新頁面,否則可能出現(xiàn)代碼沒更新觸發(fā)的情況

          1. 如果出現(xiàn)報(bào)錯(cuò)信息,更新代碼后重新點(diǎn)擊刷新按鈕,錯(cuò)誤可能依然還在

          點(diǎn)擊“錯(cuò)誤”按鈕

          然后點(diǎn)擊右上角的全部清除,再重新刷新即可

          1. background.js文件代碼中的console.log不會在控制臺顯示

          我們在background.js文件中添加的打印代碼是不會在瀏覽器的控制臺打印出來的,因?yàn)樗袀€(gè)單獨(dú)的控制臺顯示。入口如下圖:

          點(diǎn)擊service worker會出現(xiàn)一個(gè)單獨(dú)的調(diào)試窗口,background.js里面添加打印代碼會在這個(gè)窗口的控制臺中顯示打印信息。

          插件訪問頁面權(quán)限問題

          如果你有按照上面內(nèi)容一步步實(shí)現(xiàn)的話,將鼠標(biāo)移動(dòng)到瀏覽器右上角插件圖標(biāo),你會發(fā)現(xiàn)如下圖所示:

          也就是說目前其實(shí)你的插件沒有訪問網(wǎng)站內(nèi)容的權(quán)限,此時(shí)你需要手動(dòng)點(diǎn)擊該插件圖標(biāo)才能成功獲得訪問網(wǎng)站的權(quán)限。那要如何配置默認(rèn)獲得訪問所有網(wǎng)站的權(quán)限呢?經(jīng)過漫長的查找,發(fā)現(xiàn)是需要在manifest.json文件中添加這么一個(gè)屬性

          "host_permissions": ["https://*/*"]

          添加該屬性之后,右鍵點(diǎn)擊圖標(biāo),可以看到默認(rèn)可讀取更改屬性是所有網(wǎng)站上

          發(fā)布到chrome應(yīng)用商店

          發(fā)布插件到應(yīng)用商店需要注冊開發(fā)者身份,如下圖所示

          額,需要5美元注冊費(fèi),本文結(jié)束。(感興趣的可以自己花錢注冊提交試試,哈哈)

          ntelliJ IDEA 是目前最好用的 JAVA 開發(fā) IDE,它本身的功能已經(jīng)非常強(qiáng)大了,但是每個(gè)人的需求不一樣,有些需求 IDEA 本身無法滿足,于是我們就需要自己開發(fā)插件來解決。工欲善其事,必先利其器,想要提高開發(fā)效率,我們可以借助 IDEA 提供的插件功能來滿足我們的需求。如果沒有我需要的功能怎么辦?很簡單,我們自己造一個(gè)!

          插件能做什么?

          IDEA 的插件幾乎可以做任何事情,因?yàn)樗?IDE 本身的能力都封裝好開放出來了。主要的插件功能包含以下四種:

          • 自定義語言支持:如果有 IDEA 暫時(shí)不支持的語言,你可以自己寫一個(gè)插件來支持,例如 Go 語言原來的支持就是通過插件做的,后來單獨(dú)做了一個(gè) Goland。官方有自定義語言插件支持的教程。
          • 框架支持:例如Struts 2 的框架支持
          • 工具集成:可以給 IDEA 的自帶功能進(jìn)行增強(qiáng),例如對 Git 的操作增加 CodeReview 的功能。參考Gerrit
          • 用戶界面:自定義的插件改變用戶界面。參考BackgroundImage

          我為了減少重復(fù)代碼的編寫,寫了一個(gè)代碼生成的插件IDEA代碼生成插件CodeMaker,支持自定義代碼生成的模板。

          Hello world 插件

          依照慣例,我們從 Hello world 開始。

          新建一個(gè) Gradle 的插件工程

          有些教程推薦用 IDEA 默認(rèn)的插件工程來開始,但是我比較推薦用 Gradle 來管理整個(gè)插件工程,后面的依賴管理會很方便,否則都得靠手動(dòng)管理。

          點(diǎn)擊新建工程,選擇 Gradle

          接下來填寫項(xiàng)目屬性

          配置 Gradle,用默認(rèn)配置就行

          新建完工程之后,IDEA 會自動(dòng)開始解析項(xiàng)目依賴,因?yàn)樗螺d一個(gè)幾百兆的 SDK 依賴包,所以會比較久,打開科學(xué)上網(wǎng)能快一點(diǎn)。

          Gradle 依賴解析完成之后,項(xiàng)目結(jié)構(gòu)如下圖,其中 plugin.xml 是插件的配置,build.gradle 是項(xiàng)目依賴的配置(類比 pom.xml)。

          下面就是默認(rèn)生成的 plugin.xml

          <idea-plugin>
           <!--插件id-->
           <id>com.xiaokai.test.demo</id>
           <!--插件名稱-->
           <name>Demo</name>
           <!--開發(fā)者信息-->
           <vendor email="support@yourcompany.com" url="http://www.yourcompany.com">YourCompany</vendor>
           <!--插件說明-->
           <description><![CDATA[
           Enter short description for your plugin here.<br>
           <em>most HTML tags may be used</em>
           ]]></description>
           <!-- please see http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/plugin_compatibility.html
           on how to target different products -->
           <!-- uncomment to enable plugin in all products
           <depends>com.intellij.modules.lang</depends>
           -->
           <!--依賴的其他插件能力-->
           <extensions defaultExtensionNs="com.intellij">
           <!-- Add your extensions here -->
           </extensions>
           <!--插件動(dòng)作-->
           <actions>
           <!-- Add your actions here -->
           </actions>
          </idea-plugin>
          

          創(chuàng)建一個(gè) Action

          Action 是 IDEA 中對事件響應(yīng)的處理器,它的 actionPerformed 就像是 JS 中的 onClick 方法。可以看出來,插件的開發(fā)本質(zhì)上跟 web、Android 的開發(fā)沒有什么不同,因?yàn)槎际鞘录?qū)動(dòng)的編程。

          我們可以直接使用 IDEA 提供的 Action 生成器

          點(diǎn)擊 OK 之后會在 src 生成類文件:

          package com.xiaokai.test;
          import com.intellij.openapi.actionSystem.AnAction;
          import com.intellij.openapi.actionSystem.AnActionEvent;
          public class HelloWorldAction extends AnAction {
           @Override
           public void actionPerformed(AnActionEvent e) {
           // TODO: insert action logic here
           }
          }
          

          同時(shí),動(dòng)作的信息也會注冊到 plugin.xml 中

           <!--插件動(dòng)作-->
           <actions>
           <!-- Add your actions here -->
           <action id="demo.hello.world" class="com.xiaokai.test.HelloWorldAction" text="HelloWorld"
           description="Say Hello World">
           <add-to-group group-id="GenerateGroup" anchor="last"/>
           </action>
           </actions>
          

          彈出對話框

          創(chuàng)建完 Action 之后我們就要開始往里面寫邏輯了,既然是 Hello World 教學(xué),那我們就來試一下最簡單的彈出對話框。

           @Override
           public void actionPerformed(AnActionEvent e) {
           //獲取當(dāng)前在操作的工程上下文
           Project project = e.getData(PlatformDataKeys.PROJECT);
           //獲取當(dāng)前操作的類文件
           PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE);
           //獲取當(dāng)前類文件的路徑
           String classPath = psiFile.getVirtualFile().getPath();
           String title = "Hello World!";
           //顯示對話框
           Messages.showMessageDialog(project, classPath, title, Messages.getInformationIcon());
           }
          

          代碼寫完之后,打開 Gradle 的界面,點(diǎn)擊 runIde 就會啟動(dòng)一個(gè)安裝了插件的 IDEA,然后就可以進(jìn)行測試。你還可以右鍵啟動(dòng) Debug 模式,這樣還能進(jìn)行斷點(diǎn)。

          運(yùn)行的效果如下圖:

          可以看到,我們右鍵打開 Generate 菜單之后,里面最后一項(xiàng)就是我們添加的 Action,

          進(jìn)階的教程

          如果想學(xué)習(xí)更多的原理和設(shè)計(jì)理念可以看IntelliJ Platform SDK的官方文檔。不過老實(shí)說,它的文檔寫的挺差的,基本上就是簡單講了一下概念和原理,沒有深入的分析。所以如果要深入研究還得靠自己。最靠譜的學(xué)習(xí)方式就是看別人寫的插件,舉個(gè)例子,你想知道怎么樣實(shí)現(xiàn)自動(dòng)生成代碼,你就去找支持這個(gè)功能的插件,看他的源碼是怎么寫的。

          我當(dāng)時(shí)寫CodeMaker的時(shí)候也是靠自己啃源碼之后寫出來的。下面我簡單介紹一下我用過的一些 API,這些 API 基本都沒有文檔說明,全靠代碼相傳。

          判斷當(dāng)前光標(biāo)選擇的元素是什么

           //獲取當(dāng)前事件觸發(fā)時(shí),光標(biāo)所在的元素
           PsiElement psiElement = anActionEvent.getData(LangDataKeys.PSI_ELEMENT);
           //如果光標(biāo)選擇的不是類,彈出對話框提醒
           if (psiElement == null || !(psiElement instanceof PsiClass)) {
           Messages.showMessageDialog(project, "Please focus on a class", "Generate Failed", null);
           return;
           }
          

          獲取當(dāng)前類文件的所有類對象

          一個(gè)類文件中可能會有內(nèi)部類,所以讀取的時(shí)候返回的是一個(gè)列表

           public static List<PsiClass> getClasses(PsiElement element) {
           List<PsiClass> elements = Lists.newArrayList();
           List<PsiClass> classElements = PsiTreeUtil.getChildrenOfTypeAsList(element, PsiClass.class);
           elements.addAll(classElements);
           for (PsiClass classElement : classElements) {
           //這里用了遞歸的方式獲取內(nèi)部類
           elements.addAll(getClasses(classElement));
           }
           return elements;
           }
          

          格式化代碼

           public static void reformatJavaFile(PsiElement theElement) {
           CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(theElement.getProject());
           try {
           codeStyleManager.reformat(theElement);
           } catch (Exception e) {
           LOGGER.error("reformat code failed", e);
           }
           }
          

          使用粘貼板

           CopyPasteManager.getInstance()
           .setContents(new SimpleTransferable(table.toString(), DataFlavor.allHtmlFlavor));
          

          更多

          更多的技巧可以參考我的項(xiàng)目CodeMaker,以及其他的開源插件。

          作者:風(fēng)馬蕭蕭


          主站蜘蛛池模板: 国产精品无码不卡一区二区三区| 无码日韩精品一区二区免费暖暖| 精品亚洲AV无码一区二区三区 | 国产在线精品一区二区在线看| 乱子伦一区二区三区| 波多野结衣一区视频在线| 日本在线视频一区二区| 免费国产在线精品一区| 国产亚洲福利精品一区二区 | 伊人久久精品一区二区三区| 国产精品综合一区二区三区| 免费人人潮人人爽一区二区| 亚洲av无码一区二区三区不卡| 亚洲一区在线免费观看| 理论亚洲区美一区二区三区| 一区二区三区国产| 日韩电影在线观看第一区| 亚洲视频一区在线播放| 欧洲精品一区二区三区在线观看| 91在线精品亚洲一区二区| 国产aⅴ精品一区二区三区久久| 国产一区二区精品久久凹凸 | 亚洲国产精品自在线一区二区| 国语对白一区二区三区| 国产亚洲日韩一区二区三区| 无码一区二区三区亚洲人妻| 人妻体内射精一区二区三区| 区三区激情福利综合中文字幕在线一区亚洲视频1 | 人妻免费一区二区三区最新| 精品香蕉一区二区三区| 亚洲AV本道一区二区三区四区| 国产精品亚洲一区二区三区久久 | 亚洲日韩精品一区二区三区无码 | 日本在线视频一区二区三区| 人妻夜夜爽天天爽爽一区| 久久综合一区二区无码| 鲁大师成人一区二区三区| 国产一区二区三区在线看片| 无码丰满熟妇浪潮一区二区AV| 国产午夜精品片一区二区三区| 中文激情在线一区二区|