前言
在 《一篇帶你用 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,而程式碼塊裡的程式碼則在點選的時候複製到剪下板中,期望的表現效果如下:
外掛開發
如果是在 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.extend
API,看一下使用示例:
// 要掛載的元素
<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>
// ...
最終的效果如下:
程式碼參考
完整的程式碼檢視:https://github.com/mqyqingfeng/Blog/tree/master/demos/VuePress/vuepress-plugin-code-copy
其實本篇程式碼是參考了 Vuepress Code Copy Plugin
這個外掛的程式碼,點選檢視原始碼地址。
系列文章
部落格搭建系列是我至今寫的唯一一個偏實戰的系列教程,講解如何使用 VuePress 搭建部落格,並部署到 GitHub、Gitee、個人伺服器等平臺。
- 一篇帶你用 VuePress + GitHub Pages 搭建部落格
- 一篇教你程式碼同步 GitHub 和 Gitee
- 還不會用 GitHub Actions ?看看這篇
- Gitee 如何自動部署 Pages?還是用 GitHub Actions!
- 一份前端夠用的 Linux 命令
- 一份簡單夠用的 Nginx Location 配置講解
- 一篇從購買伺服器到部署部落格程式碼的詳細教程
- 一篇域名從購買到備案到解析的詳細教程
- VuePress 部落格優化之 last updated 最後更新時間如何設定
- VuePress 部落格優化之新增資料統計功能
- VuePress 部落格優化之開啟 HTTPS
- VuePress 部落格優化之開啟 Gzip 壓縮
微信:「mqyqingfeng」,加我進冴羽唯一的讀者群。
如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者 有所啟發,歡迎 star,對作者也是一種鼓勵。