從零實現一個 VuePress 外掛

冴羽發表於2022-01-20

前言

《一篇帶你用 VuePress + Github Pages 搭建部落格》中,我們使用 VuePress 搭建了一個部落格,最終的效果檢視:TypeScript 中文文件

但在搭建 VuePress 部落格的過程中,也並不是所有的外掛都能滿足需求,所以本篇我們以實現一個程式碼複製外掛為例,教大家如何從零實現一個 VuePress 外掛。

本地開發

開發外掛第一個要解決的問題就是如何本地開發,我們檢視 VuePress 1.0 官方文件的「開發外掛」章節,並沒有找到解決方案,但在 VuePress 2.0 官方文件的「本地外掛」裡,卻有寫道:

推薦你直接將 配置檔案 作為外掛使用,因為幾乎所有的外掛 API 都可以在配置檔案中使用,這在絕大多數場景下都更為方便。

但是如果你在配置檔案中要做的事情太多了,最好還是將它們提取到單獨的外掛中,然後通過設定絕對路徑或者通過 require 來使用它們:

module.exports = {
  plugins: [
    path.resolve(__dirname, './path/to/your-plugin.js'),
    require('./another-plugin'),
  ],
}

那就讓我們開始吧!

初始化專案​

我們在 .vuepress 資料夾下新建一個 vuepress-plugin-code-copy 的資料夾,用於存放外掛相關的程式碼,然後命令列進入到該資料夾,執行 npm init,建立 package.json,此時檔案的目錄為:

.vuepress
├─ vuepress-plugin-code-copy 
│  └─ package.json
└─ config.js        

我們在 vuepress-plugin-code-copy下新建一個 index.js 檔案,參照官方文件外掛示例中的寫法,我們使用返回物件的函式形式,這個函式接受外掛的配置選項作為第一個引數、包含編譯期上下文的 ctx 物件作為第二個引數:

module.exports = (options, ctx) => {
   return {
      // ...
   }
}

再參照官方文件 Option API 中的 name,以及生命週期函式中的 ready 鉤子,我們寫一個初始的測試程式碼:

module.exports = (options, ctx) => {
    return {
        name: 'vuepress-plugin-code-copy',
        async ready() {
            console.log('Hello World!');
        }
    }
 }

此時我們執行下 yarn run docs:dev,可以在執行過程中看到我們的外掛名字和列印結果:

外掛設計

現在我們可以設想下我們的程式碼複製外掛的效果了,我想要實現的效果是:​

在程式碼塊的右下角有一個 Copy 文字按鈕,點選後文字變為 Copied!然後一秒後文字重新變為 Copy,而程式碼塊裡的程式碼則在點選的時候複製到剪下板中,期望的表現效果如下:

copy.gif

外掛開發

如果是在 Vue 元件中,我們很容易實現這個效果,在根元件 mounted 或者 updated的時候,使用 document.querySelector獲取所有的程式碼塊,插入一個按鈕元素,再在按鈕元素上繫結點選事件,當觸發點選事件的時候,程式碼複製到剪下板,然後修改文字,1s 後再修改下文字。

那 VuePress 外掛有方法可以控制根元件的生命週期嗎?我們查閱下 VuePress 官方文件的 Option API,可以發現 VuePress 提供了一個 clientRootMixin 方法:

指向 mixin 檔案的路徑,它讓你可以控制根元件的生命週期

看下示例程式碼:

// 外掛的入口
const path = require('path')

module.exports = {
  clientRootMixin: path.resolve(__dirname, 'mixin.js')
}
// mixin.js
export default {
  created () {},
  mounted () {}
}

這不就是我們需要的嗎?那我們動手吧,修改 index.js的內容為:

const path = require('path');

module.exports = (options, ctx) => {
    return {
        name: 'vuepress-plugin-code-copy',
        clientRootMixin: path.resolve(__dirname, 'clientRootMixin.js')
    }
 }

vuepress-plugin-code-copy下新建一個 clientRootMixin.js檔案,程式碼寫入:

export default {
    updated() {
        setTimeout(() => {
            document.querySelectorAll('div[class*="language-"] pre').forEach(el => {
                                console.log('one code block')
            })
        }, 100)
    }
}

重新整理下瀏覽器裡的頁面,然後檢視列印:


接下來就要思考如何寫入按鈕元素了。​

當然我們可以使用原生 JavaScript 一點點的建立元素,然後插入其中,但我們其實是在一個支援 Vue 語法的專案裡,其實我們完全可以建立一個 Vue 元件,然後將元件的例項掛載到元素上。那用什麼方法掛載呢?

我們可以在 Vue 的全域性 API 裡,找到 Vue.extendAPI,看一下使用示例:

// 要掛載的元素
<div id="mount-point"></div>
// 建立構造器
var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
})
// 建立 Profile 例項,並掛載到一個元素上。
new Profile().$mount('#mount-point')

結果如下:

// 結果為:
<p>Walter White aka Heisenberg</p>

那接下來,我們就建立一個 Vue 元件,然後通過 Vue.extend 方法,掛載到每個程式碼塊元素中。

vuepress-plugin-code-copy下新建一個 CodeCopy.vue 檔案,寫入程式碼如下:

<template>
    <span class="code-copy-btn" @click="copyToClipboard">{{ buttonText }}</span>
</template>

<script>
export default {
    data() {
        return {
            buttonText: 'Copy'
        }
    },
    methods: {
        copyToClipboard(el) {
            this.setClipboard(this.code, this.setText);
        },
        setClipboard(code, cb) {
            if (navigator.clipboard) {
                navigator.clipboard.writeText(code).then(
                    cb,
                    () => {}
                )
            } else {
                let copyelement = document.createElement('textarea')
                document.body.appendChild(copyelement)
                copyelement.value = code
                copyelement.select()
                document.execCommand('Copy')
                copyelement.remove()
                cb()
            }
        },
        setText() {
            this.buttonText = 'Copied!'

            setTimeout(() => {
                this.buttonText = 'Copy'
            }, 1000)
        }
    }
}
</script>

<style scoped>
.code-copy-btn {
    position: absolute;
    bottom: 10px;
    right: 7.5px;
    opacity: 0.75;
    cursor: pointer;
    font-size: 14px;
}

.code-copy-btn:hover {
    opacity: 1;
}
</style>

該元件實現了按鈕的樣式和點選時將程式碼寫入剪下版的效果,整體程式碼比較簡單,就不多敘述了。

我們修改一下 clientRootMixin.js

import CodeCopy from './CodeCopy.vue'
import Vue from 'vue'

export default {
    updated() {
        // 防止阻塞
        setTimeout(() => {
            document.querySelectorAll('div[class*="language-"] pre').forEach(el => {
                  // 防止重複寫入
                if (el.classList.contains('code-copy-added')) return
                let ComponentClass = Vue.extend(CodeCopy)
                let instance = new ComponentClass()
                instance.code = el.innerText
                instance.$mount()
                el.classList.add('code-copy-added')
                el.appendChild(instance.$el)
            })
        }, 100)
    }
}

這裡注意兩點,第一是我們通過 el.innerText 獲取要複製的程式碼內容,然後寫入到例項的 code 屬性,在元件中,我們是通過 this.code獲取的。

第二是我們沒有使用 $mount(element),直接傳入一個要掛載的節點元素,這是因為 $mount() 的掛載會清空目標元素,但是這裡我們需要新增到元素中,所以我們在執行 instance.$mount()後,通過 instance.$el獲取了例項元素,然後再將其 appendChild 到每個程式碼塊中。關於 $el的使用可以參考官方文件的 el 章節

此時,我們的檔案目錄如下:

.vuepress
├─ vuepress-plugin-code-copy 
│  ├─ CodeCopy.vue
│  ├─ clientRootMixin.js
│  ├─ index.js
│  └─ package.json
└─ config.js   

至此,其實我們就已經實現了程式碼複製的功能。

外掛選項

有的時候,為了增加外掛的可擴充性,會允許配置可選項,就比如我們不希望按鈕的文字是 Copy,而是中文的「複製」,複製完後,文字變為 「已複製!」,該如何實現呢?

前面講到,我們的 index.js匯出的函式,第一個引數就是 options 引數:

const path = require('path');

module.exports = (options, ctx) => {
    return {
        name: 'vuepress-plugin-code-copy',
        clientRootMixin: path.resolve(__dirname, 'clientRootMixin.js')
    }
 }

我們在 config.js先寫入需要用到的選項:

module.exports = {
    plugins: [
      [
        require('./vuepress-plugin-code-copy'),
        {
          'copybuttonText': '複製',
          'copiedButtonText': '已複製!'
        }
      ]
    ]
}

我們 index.js中通過 options引數可以接收到我們在 config.js 寫入的選項,但我們怎麼把這些引數傳入 CodeCopy.vue 檔案呢?

我們再翻下 VuePress 提供的 Option API,可以發現有一個 define API,其實這個 define 屬性就是定義我們外掛內部使用的全域性變數。我們修改下 index.js

const path = require('path');

module.exports = (options, ctx) => {
    return {
        name: 'vuepress-plugin-code-copy',
        define: {
            copybuttonText: options.copybuttonText || 'copy',
            copiedButtonText: options.copiedButtonText || "copied!"
        },
        clientRootMixin: path.resolve(__dirname, 'clientRootMixin.js')
    }
 }

現在我們已經寫入了兩個全域性變數,元件裡怎麼使用呢?答案是直接使用!

我們修改下 CodeCopy.vue 的程式碼:

// ...
<script>
export default {
    data() {
        return {
            buttonText: copybuttonText
        }
    },
    methods: {
        copyToClipboard(el) {
            this.setClipboard(this.code, this.setText);
        },
        setClipboard(code, cb) {
            if (navigator.clipboard) {
                navigator.clipboard.writeText(code).then(
                    cb,
                    () => {}
                )
            } else {
                let copyelement = document.createElement('textarea')
                document.body.appendChild(copyelement)
                copyelement.value = code
                copyelement.select()
                document.execCommand('Copy')
                copyelement.remove()
                cb()
            }
        },
        setText() {
            this.buttonText = copiedButtonText

            setTimeout(() => {
                this.buttonText = copybuttonText
            }, 1000)
        }
    }
}
</script>
// ...

最終的效果如下:

1234.gif

程式碼參考

完整的程式碼檢視:https://github.com/mqyqingfeng/Blog/tree/master/demos/VuePress/vuepress-plugin-code-copy

其實本篇程式碼是參考了 Vuepress Code Copy Plugin這個外掛的程式碼,點選檢視原始碼地址

系列文章

部落格搭建系列是我至今寫的唯一一個偏實戰的系列教程,講解如何使用 VuePress 搭建部落格,並部署到 GitHub、Gitee、個人伺服器等平臺。

  1. 一篇帶你用 VuePress + GitHub Pages 搭建部落格
  2. 一篇教你程式碼同步 GitHub 和 Gitee
  3. 還不會用 GitHub Actions ?看看這篇
  4. Gitee 如何自動部署 Pages?還是用 GitHub Actions!
  5. 一份前端夠用的 Linux 命令
  6. 一份簡單夠用的 Nginx Location 配置講解
  7. 一篇從購買伺服器到部署部落格程式碼的詳細教程
  8. 一篇域名從購買到備案到解析的詳細教程
  9. VuePress 部落格優化之 last updated 最後更新時間如何設定
  10. VuePress 部落格優化之新增資料統計功能
  11. VuePress 部落格優化之開啟 HTTPS
  12. VuePress 部落格優化之開啟 Gzip 壓縮

微信:「mqyqingfeng」,加我進冴羽唯一的讀者群。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者 有所啟發,歡迎 star,對作者也是一種鼓勵。

相關文章