从零实现一个 VuePress 插件

冴羽 等级 575 1 1
标签:

前言

《一篇带你用 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,可以在运行过程中看到我们的插件名字和打印结果:

从零实现一个 VuePress 插件

插件设计

现在我们可以设想下我们的代码复制插件的效果了,我想要实现的效果是:​

在代码块的右下角有一个 Copy 文字按钮,点击后文字变为 Copied!然后一秒后文字重新变为 Copy,而代码块里的代码则在点击的时候复制到剪切板中,期望的表现效果如下:

从零实现一个 VuePress 插件

插件开发

如果是在 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)
    }
}

刷新下浏览器里的页面,然后查看打印:

从零实现一个 VuePress 插件 接下来就要思考如何写入按钮元素了。​

当然我们可以使用原生 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>
// ...

最终的效果如下:

从零实现一个 VuePress 插件

代码参考

完整的代码查看: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,对作者也是一种鼓励。

收藏
评论区

相关推荐

从零开始刷力扣(二)——495:提莫攻击
分类:数组的遍历 题目描述: 在《英雄联盟》的世界中,有一个叫 “提莫” 的英雄,他的攻击可以让敌方英雄艾希(编者注:寒冰射手)进入中毒状态。现在,给出提莫对艾希的攻击时间序列和提莫攻击的中毒持续时间,你需要输出艾希的中毒状态总时长。 你可以认为提莫在给定的时间点进行攻击,并立即使艾希处于中毒状态 示例1: 输入: 1,4, 2 输出: 4 原因:
从零搭建一款PC页面编辑器PC-Dooring
之前一直忙着调研lowcode平台和开发以下两个项目: H5编辑器 , 可视化大屏编辑器没有太多时间做PC端搭建化项目, 好在搭建平台很多原理都是通用的, 所以早在去年我就开发好了面向PC端的编辑器, 虽然在设计上还有些不足(在后面的内容中会提到), 但是基本模型已经实现, 接下来就和大家一起分享一下具体的实现.为了保证文章整体的逻辑性和条理性, 我将可
2020年数据库概念与MySQL的安装与配置
![file](https://oscimg.oschina.net/oscnet/a96560e7bfff79fd9c83a60eeb31e971700.jpg "file") 作者 | Jeskson 来源 | 达达前端小酒馆 从零基础入门MySQL数据库基础课 ================= ![file](https://oscimg.o
4K@60智能云台从零到一
![](https://oscimg.oschina.net/oscnet/378c3404-772f-4a7b-8968-a2fd109e61cb.png) 正文字数:6006  阅读时长:9分钟 > 近两年以来,短视频受到越来越多用户的喜爱和追捧,但短视频的制作成本却扼杀了大量内容创作者的热情和动力。LiveVideoStackCon2020北京
Elasticsearch入门之从零开始安装ik分词器
![](https://oscimg.oschina.net/oscnet/0197ba2bf7a6cba3bb1366fe28c985c0cba.jpg) 起因 -- 需要在ES中使用聚合进行统计分析,但是聚合字段值为中文,ES的默认分词器对于中文支持非常不友好:会把完整的中文词语拆分为一系列独立的汉字进行聚合,显然这并不是我的初衷。我们来看个实例:
Kubernetes实战
使用kind构建一个单层架构Node/Express网络应用程序 ================================ > Kubernetes实战-从零开始搭建微服务 1 ![k8s structure](https://d33wubrfki0l68.cloudfront.net/152c845f25df8e69dd24dd7b0836a2
OCRunner 第一篇:实现一个简单版热修复功能
> 作者: SilverFruity, https://github.com/SilverFruity > 小编寄语: > > > > 上一周我们发布了[《OCRunner 第零篇:从零教你写一个 iOS 热修复框架》](https://www.oschina.net/action/GoToLink?url=https%3A%2F%2Fmp.we
OCRunner 第零篇:从零教你写一个 iOS 热修复框架
> 作者: SilverFruity, https://github.com/SilverFruity 为什么要热修复 ------- 在软件开发过程中,很难避免 BUG 的存在,尤其是对于一些达到一定规模的 App 因为协作模式错综复杂,就很容易带着问题上线。 一旦问题上线之后,问题就麻烦了,不仅需要重新打包、测试,而且还需要重新提交审核,而这种修复
Python从零实现区块链仿真【含源码】
在区块链或数字货币领域,Python并不是主流的开发语言。但是如果你的目的是研究区块链技术的原理,或者需要在自己的笔记本上仿真一个区块链网络并进行一些研究性的实验,比如完成自己的毕业设计项目或科研课题,那么Python就是合适的。在这个教程里,我们将学习如何使用Python从零开发一个多节点的区块链网络,并基于这个仿真区块链网络,开发一个去中心化的数据分享应
React TypeScript 从零实现 Popup 组件发布到 npm
> 本文转载自掘金《从0到1发布一个Popup组件到npm》,作者「海秋」。 > 点击下方阅读原文去点个赞吧! 上篇文章\[1\]中介绍了如何从 0 到 1 搭建一个 React 组件库架子,但为了一两个组件去搭建组件库未免显得大材小用。 这次以移动端常见的一个组件 `Popup` 为例,以最方便快捷的形式发布一个流程完整的 npm 包。 *
Re:从零开始的鸿蒙开发教程
![](https://oscimg.oschina.net/oscnet/31d15e1c-2580-4ce6-87c1-cbc5879c70d7.jpg) **转载本文需注明出处:微信公众号EAWorld,违者必究。** 概述 **官网说:** HarmonyOS是一款“面向未来”、面向全场景(移动办公、运
Ros机械臂仿真建模基础:URDF模型进化版本xacro模型文件语法详细讲解及如何从零构建一个机械臂模型(一)
Ros机械臂仿真建模基础:URDF模型进化版本xacro模型文件语法基础及如何从零构建一个机械臂模型(一) ===================================================== * Ros机械臂仿真建模基础:URDF模型进化版本xacro模型文件语法基础及如何从零构建一个机械臂模型(一) * 一、机械臂仿真效
Spring Boot从零入门1_详述
本文属于原创,转载注明出处,欢迎关注微信小程序`小白AI博客` 和微信公众号`别打名名`或者网站 https://xiaobaiai.net 或者我的CSDN http://blog.csdn.net/freeape ![](https://oscimg.oschina.net/oscnet/f8db20c4-f396-4337-b76d-c956a3a8
VuePress 博客优化之 last updated 最后更新时间如何设置
前言在 中,我们使用 VuePress 搭建了一个博客,但是浏览最终搭建的站点:,我们会发现,在每篇文章的底部,并没有像 VuePress 官方文档那样,出现最后更新的时间:这篇我们来探究下如何实现最后更新时间。 官方自带查阅 VuePress 的,我们可以知道,VuePress 自带显示最后更新时间的插件,在默认主题下,无需安装本插件,因为 VuePre
从零实现一个 VuePress 插件
前言在 中,我们使用 VuePress 搭建了一个博客,最终的效果查看:。但在搭建 VuePress 博客的过程中,也并不是所有的插件都能满足需求,所以本篇我们以实现一个代码复制插件为例,教大家如何从零实现一个 VuePress 插件。 本地开发开发插件第一个要解决的问题就是如何本地开发,我们查看 VuePress 1.0 官方文档的「」章节,并没有找到解决