在过去的几个月里,我学到了很多关于 Vue 的知识。从构建 对 SEO 友好的 SPA 到制作 优秀的博客 或使用 过渡和动画,我彻底地尝试了这个框架。
但是,在我的学习过程中一直缺少一个部分:**插件**。
大多数使用 Vue 的人都依赖于插件作为工作流程的一部分,或者肯定会在未来的某个时间点遇到插件。无论哪种情况,它们都是一种利用现有代码的好方法,无需不断地从头开始编写。
你们中的许多人可能都使用过 jQuery,并且习惯于使用(或制作!)插件来创建任何东西,从 轮播图 和 模态框 到 响应式视频 和 文字。对于 Vue 插件,我们基本上是在谈论相同的事情。
所以,你想制作一个?我假设你正在点头,所以我们可以一起动手,一步一步地学习如何编写自定义 Vue 插件。
首先,一些背景知识……

插件不是 Vue 独有的东西,并且——就像 jQuery 一样——你会发现有很多不同类型的插件可以做很多不同的事情。根据定义,它们表示提供了一个接口来允许扩展。
简单来说:它们是将全局功能插入应用程序并扩展它们以供您使用的一种方式。
Vue 文档 中详细介绍了插件,并提供了一个插件通常属于的广泛类别的优秀列表
- 添加一些全局方法或属性。
- 添加一个或多个全局资源:指令/过滤器/过渡等。
- 通过全局混入添加一些组件选项。
- 通过将它们附加到 Vue.prototype 来添加一些 Vue 实例方法。
- 一个库,它提供自己的 API,同时注入上述组合中的某些内容。
好的,好的。开场白就到这里。让我们编写一些代码吧!
我们正在制作什么
在 Spektrum(Snipcart 的母公司),我们的设计会经过一个审批流程,我相信大多数其他工作室和公司也是如此。我们允许客户在他们审查设计时对其进行评论和提出建议,以便最终获得绿灯并开始构建它。
我们通常使用 InVision 来完成所有这些操作。评论系统是 InVision 的核心组件。它允许人们点击设计的任何部分,并在协作者可以直接理解的反馈位置留下评论。非常棒。

尽管 InVision 很棒,但我认为我们可以使用一些 Vue 魔法自己做同样的事情,并开发出一个任何人都可以使用的插件。
好消息是它们并不那么令人生畏。只需掌握 Vue 的基本知识,就可以立即开始使用插件。
步骤 1. 准备代码库
Vue 插件应该包含一个install
方法,该方法接受两个参数
- 全局
Vue
对象 - 一个包含用户定义选项的对象
感谢 Vue CLI 3,启动 Vue 项目非常简单。安装完成后,在命令行中运行以下命令
$ vue create vue-comments-overlay
# Answer the few questions
$ cd vue-comments-overlay
$ npm run serve
这为我们提供了启动所需的经典“Hello World”示例,我们可以使用它来创建测试应用程序,并将我们的插件投入使用。
步骤 2. 创建插件目录
我们的插件必须存在于项目的某个地方,因此让我们创建一个目录,以便我们可以在其中存放所有工作,然后将我们的命令行导航到新目录
$ mkdir src/plugins
$ mkdir src/plugins/CommentsOverlay
$ cd src/plugins/CommentsOverlay
步骤 3:连接基本线路
Vue 插件基本上是一个带有install
函数的对象,每当使用它的应用程序使用Vue.use()
包含它时,该函数就会被执行。
install
函数接收全局Vue
对象作为参数以及一个选项对象
// src/plugins/CommentsOverlay/index.js
//
export default {
install(vue, opts){
console.log('Installing the CommentsOverlay plugin!')
// Fun will happen here
}
}
现在,让我们将其插入我们的“Hello World”测试应用程序
// src/main.js
import Vue from 'vue'
import App from './App.vue'
import CommentsOverlay from './plugins/CommentsOverlay' // import the plugin
Vue.use(CommentsOverlay) // put the plugin to use!
Vue.config.productionTip = false
new Vue({ render: createElement => createElement(App)}).$mount('#app')
步骤 4:提供对选项的支持
我们希望插件可配置。这将允许任何在其自己的应用程序中使用它的人调整设置。它还使我们的插件更加通用。
我们将选项作为install
函数的第二个参数。让我们创建默认选项,这些选项将表示插件的基本行为,即在没有指定自定义选项时插件的操作方式
// src/plugins/CommentsOverlay/index.js
const optionsDefaults = {
// Retrieves the current logged in user that is posting a comment
commenterSelector() {
return {
id: null,
fullName: 'Anonymous',
initials: '--',
email: null
}
},
data: {
// Hash object of all elements that can be commented on
targets: {},
onCreate(created) {
this.targets[created.targetId].comments.push(created)
},
onEdit(editted) {
// This is obviously not necessary
// It's there to illustrate what could be done in the callback of a remote call
let comments = this.targets[editted.targetId].comments
comments.splice(comments.indexOf(editted), 1, editted);
},
onRemove(removed) {
let comments = this.targets[removed.targetId].comments
comments.splice(comments.indexOf(removed), 1);
}
}
}
然后,我们可以将传递给install
函数的选项合并到这些默认选项之上
// src/plugins/CommentsOverlay/index.js
export default {
install(vue, opts){
// Merge options argument into options defaults
const options = { ...optionsDefaults, ...opts }
// ...
}
}
步骤 5:为评论层创建实例
使用此插件时,您要避免的一件事是让其 DOM 和样式干扰安装它的应用程序。为了最大程度地降低发生这种情况的可能性,一种方法是在另一个根 Vue 实例中使插件处于活动状态,位于主应用程序的组件树之外。
将以下内容添加到install
函数中
// src/plugins/CommentsOverlay/index.js
export default {
install(vue, opts){
...
// Create plugin's root Vue instance
const root = new Vue({
data: { targets: options.data.targets },
render: createElement => createElement(CommentsRootContainer)
})
// Mount root Vue instance on new div element added to body
root.$mount(document.body.appendChild(document.createElement('div')))
// Register data mutation handlers on root instance
root.$on('create', options.data.onCreate)
root.$on('edit', options.data.onEdit)
root.$on('remove', options.data.onRemove)
// Make the root instance available in all components
vue.prototype.$commentsOverlay = root
...
}
}
上面代码片段中的重要部分
- 应用程序位于
body
末尾的新div
中。 - 在
options
对象中定义的事件处理程序已挂接到根实例上的匹配事件。到本教程结束时,这将变得有意义,保证。 - 添加到 Vue 原型的
$commentsOverlay
属性将根实例公开给应用程序中的所有 Vue 组件。
步骤 6:制作自定义指令
最后,我们需要一种方法让使用该插件的应用程序告诉它哪个元素将启用评论功能。这需要一个自定义 Vue 指令。由于插件可以访问全局Vue
对象,因此它们可以定义新的指令。
我们的指令将命名为comments-enabled
,如下所示
// src/plugins/CommentsOverlay/index.js
export default {
install(vue, opts){
...
// Register custom directive tha enables commenting on any element
vue.directive('comments-enabled', {
bind(el, binding) {
// Add this target entry in root instance's data
root.$set(
root.targets,
binding.value,
{
id: binding.value,
comments: [],
getRect: () => el.getBoundingClientRect(),
});
el.addEventListener('click', (evt) => {
root.$emit(`commentTargetClicked__${binding.value}`, {
id: uuid(),
commenter: options.commenterSelector(),
clientX: evt.clientX,
clientY: evt.clientY
})
})
}
})
}
}
指令执行两件事
- 它将目标添加到根实例的数据中。为其定义的键是
binding.value
。它使用户能够为目标元素指定自己的 ID,例如:<img v-comments-enabled="imgFromDb.id" src="imgFromDb.src" />
。 - 它在目标元素上注册一个
click
事件处理程序,该处理程序依次为此特定目标在根实例上发出事件。稍后我们将回到如何处理它。
install
函数现在已完成!现在我们可以继续进行评论功能和要渲染的组件了。
步骤 7:建立“评论根容器”组件
我们将创建一个CommentsRootContainer
并将其用作插件 UI 的根组件。让我们看看它
<!--
src/plugins/CommentsOverlay/CommentsRootContainer.vue -->
<template>
<div>
<comments-overlay
v-for="target in targets"
:target="target"
:key="target.id">
</comments-overlay>
</div>
</template>
<script>
import CommentsOverlay from "./CommentsOverlay";
export default {
components: { CommentsOverlay },
computed: {
targets() {
return this.$root.targets;
}
}
};
</script>
这是什么作用?我们基本上创建了一个包装器,它包含了另一个我们尚未创建的组件:CommentsOverlay
。您可以在脚本中看到该组件的导入位置以及包装器模板中请求的值(target
和 target.id
)。请注意,target
计算属性是如何从根组件的数据中派生的。
现在,覆盖组件是所有魔法发生的地方。让我们开始吧!
步骤 8:使用“评论覆盖”组件创造魔法
好的,我即将向您展示很多代码,但我们会确保逐步讲解。
<!-- src/plugins/CommentsOverlay/CommentsRootContainer.vue -->
<template>
<div class="comments-overlay">
<div class="comments-overlay__container" v-for="comment in target.comments" :key="comment.id" :style="getCommentPostition(comment)">
<button class="comments-overlay__indicator" v-if="editing != comment" @click="onIndicatorClick(comment)">
{{ comment.commenter.initials }}
</button>
<div v-else class="comments-overlay__form">
<p>{{ getCommentMetaString(comment) }}</p>
<textarea ref="text" v-model="text" />
<button @click="edit" :disabled="!text">Save</button>
<button @click="cancel">Cancel</button>
<button @click="remove">Remove</button>
</div>
</div>
<div class="comments-overlay__form" v-if="this.creating" :style="getCommentPostition(this.creating)">
<textarea ref="text" v-model="text" />
<button @click="create" :disabled="!text">Save</button>
<button @click="cancel">Cancel</button>
</div>
</div>
</template>
<script>
export default {
props: ['target'],
data() {
return {
text: null,
editing: null,
creating: null
};
},
methods: {
onTargetClick(payload) {
this._resetState();
const rect = this.target.getRect();
this.creating = {
id: payload.id,
targetId: this.target.id,
commenter: payload.commenter,
ratioX: (payload.clientX - rect.left) / rect.width,
ratioY: (payload.clientY - rect.top) / rect.height
};
},
onIndicatorClick(comment) {
this._resetState();
this.text = comment.text;
this.editing = comment;
},
getCommentPostition(comment) {
const rect = this.target.getRect();
const x = comment.ratioX <em> rect.width + rect.left;
const y = comment.ratioY <em> rect.height + rect.top;
return { left: `${x}px`>, top: `${y}px` };
},
getCommentMetaString(comment) {
return `${
comment.commenter.fullName
} - ${comment.timestamp.getMonth()}/${comment.timestamp.getDate()}/${comment.timestamp.getFullYear()}`;
},
edit() {
this.editing.text = this.text;
this.editing.timestamp = new Date();
this._emit("edit", this.editing);
this._resetState();
},
create() {
this.creating.text = this.text;
this.creating.timestamp = new Date();
this._emit("create", this.creating);
this._resetState();
},
cancel() {
this._resetState();
},
remove() {
this._emit("remove", this.editing);
this._resetState();
},
_emit(evt, data) {
this.$root.$emit(evt, data);
},
_resetState() {
this.text = null;
this.editing = null;
this.creating = null;
}
},
mounted() {
this.$root.$on(`commentTargetClicked__${this.target.id}`, this.onTargetClick
);
},
beforeDestroy() {
this.$root.$off(`commentTargetClicked__${this.target.id}`, this.onTargetClick
);
}
};
</script>
我知道,我知道。有点让人望而生畏。但它基本上只做了一些关键的事情。
首先,<template>
标签中包含的整个第一部分建立了评论弹出窗口的标记,该弹出窗口将显示在屏幕上,并带有一个提交评论的表单。换句话说,这是呈现我们评论的 HTML 标记。
接下来,我们编写脚本,为评论的行为提供动力。组件接收完整的 target
对象作为 prop
。这是存储评论数组和定位信息的地方。
然后,就是魔法了。我们定义了一些方法,在触发时执行重要操作。
- 监听点击事件
- 渲染评论框,并将其定位在执行点击操作的位置
- 捕获用户提交的数据,包括用户的姓名和评论
- 提供创建、编辑、删除和取消评论的功能
最后,我们之前看到的 commentTargetClicked
事件的处理程序在 mounted
和 beforeDestroy
钩子中进行管理。
值得注意的是,根实例用作事件总线。即使这种方法通常不被鼓励,但我认为在这种情况下它是合理的,因为这些组件不会公开暴露,可以被视为一个整体单元。
就这样,我们完成了!经过一些样式设置(我不会详细介绍我可疑的 CSS 技能),我们的插件就可以接收目标元素上的用户评论了!
演示时间!

了解更多 Vue 插件
我们在这篇文章的大部分时间里都在创建 Vue 插件,但我想将它完整地联系到我们使用插件的原因。我整理了一个非常流行的 Vue 插件的简短列表,以展示使用插件时可以获得的所有美好事物。
- Vue-router – 如果你正在构建单页应用程序,你毫无疑问需要 Vue-router。作为 Vue 的官方路由器,它与 Vue 的核心深度集成,以完成映射组件和嵌套路由等任务。
- Vuex – Vuex 充当应用程序中所有组件的集中式存储,如果你希望构建易于维护的大型应用程序,它是一个明智的选择。
- Vee-validate – 在构建典型的业务线应用程序时,如果没有谨慎处理,表单验证可能会很快变得难以管理。Vee-validate 以优雅的方式处理所有这些问题。它使用指令,并且在设计时考虑了本地化。
我将限制在这些插件上,但要知道还有很多其他的插件在等待帮助像你一样的 Vue 开发人员!
而且,嘿!如果你找不到满足你特定需求的插件,你现在已经具备了创建自定义插件的实践经验。😀