前言

vue 项目开发过程中,经常用到插件,比如原生插件 vue-router、vuex,还有 element-ui 提供的 notify、message 等等。这些插件让我们的开发变得更简单高效。那么 vue 插件是怎么开发的呢?

需要涉及的技术点:

  • vue 插件的本质
  • Vue.extend() 全局方法
  • 如何手动挂载 Vue 实例
  • Vue.use() 的原理
  • 如何打包成 umd 格式

一、什么是vue插件?

什么是 Vue插件 ?它和 Vue组件 有什么区别?我们来看一下官网的解释:

  • 插件” 通常用来为 Vue 添加全局功能。
  • 组件” 是可复用的 Vue 实例,且带有一个名字。

其实, Vue 插件 和 Vue组件 只是在 Vue.js 中包装的两个概念而已,不管是插件还是组件,最终目的都是为了实现 逻辑复用 。它们的本质都是对代码逻辑的封装,只是封装方式不同而已。在必要时,组件也可以封装成插件,插件也可以改写成组件,就看实际哪种封装更方便使用了。

除此之外,插件是全局的,组件既可以全局注册也可以局部注册。

我们今天只聚焦 Vue 插件。

插件一般有下面几种:

  • 添加全局方法或者属性,如: vue-custom-element;
  • 添加全局资源:指令/过滤器/过渡等,如 vue-touch;
  • 通过全局混入来添加一些组件选项,如 vue-router;
  • 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现;
  • 一个库,提供自己的 API,同时提供上面提到的一个或多个功能,如 vue-router;

二、vue插件的使用

插件需要通过 Vue.use() 方法注册到全局,并且需要在调用 new Vue() 启动应用之前完成。之后在其他 vue 实例里面就可以通过 this.$xxx 来调用插件中提供的 API 了。

下面以实现一个简易的 弹框插件 dialog 为例,给大家讲解下怎么一步一步地开发并发布一个 Vue 插件。

希望达到的效果:
在 main.js 中 引入:

// src/main.js
import Vue from 'vue'
import dialog from '@bluehe/dialog'

Vue.use(dialog)

在 App.vue或其他组件 的 方法里调用 this.$dialog():

// src/App.vue
<template>
 <div>
   <button @click='handleClick'>click</button>
 </div>
</template>
<script>
export default {
    name: 'App',
    methods: {
      handleClick() {
        this.$dialog({
          header: '我的弹窗',
          content: 'This is a dialog'
        })
      }
    }
}
</script>

运行后在页面上点击按钮,弹出弹框,当点击关闭按钮时,弹框消失:
在这里插入图片描述

三、vue插件的开发

1. 编写 dialog 的本体

在 src 目录下创建 components/Dialog/index.vue 文件:

// src/components/Dialog/index.vue
<template>
  <transition name="fade">
    <div id="dialog" class="back" v-if="isShow">
      <div id="div1" class="content">
        <div id="close">
          <span id="close-button" @click="close">×</span>
          <!--  弹窗标题  -->
          <h2>{{title}}</h2>
        </div>
        <div id="div2">
          <!--  弹窗内容  -->
          <p>{{content}}</p>
        </div>
      </div>
    </div>
  </transition>
</template>

<script>
export default {
  name: 'index',
  data () {
    return {
      isShow: false,
      title: '标题',
      content: ''
    }
  },
  mounted () {
    this.isShow = true
  },
  methods: {
    close () {
      this.isShow = false
    }
  }
}
</script>

<style scoped>
#open_btn {
  background: #ddd;
}

#dialog {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0,0,0,0.5);
}

#div1 {
  background:#eeeeee;
  width: 700px;
  z-index: 1;
  margin: 12% auto;
  overflow: auto;
}

span {
  padding-top: 12px;
  cursor: pointer;
  padding-right: 15px;
}

#div2 {
  background:#eeeeee;
  margin: auto;
  height: 300px;
  padding: 0 20px;
}

#close {
  padding: 5px;
  background: #ddd;
}

#close-button {
  float: right;
  font-size: 30px;
}

h2 {
  margin: 10px 0;
  padding-left: 15px;
}
</style>

现在 dialog 本体完成了,但是它里面的数据目前没法改变,因为我没有给它定义 props 属性。这不是 bug,而是,插件并不是通过 props 来传值的。

2. 手动挂载 dialog 实例的 dom

为了给插件传值,可以利用基础 vue 构造器 Vue.extend() 创建一个“子类”。这个子类相当于一个继承了 Vue 的 Dialog 构造器。然后在 new 这个构造函数的时候,给 Dialog 的 data 属性传值,然后手动调用这个实例的 $mount() 方法手动挂载,最后使用原生 js 的 appendChild 将真实 DOM (通过实例上的 $el 属性获取)添加到 body 上。

在 src 目录下新建 components/Dialog/index.js 文件:

// src/components/Dialog/index.js
import Vue from 'vue';
import Dialog from './index.vue';

// 使用 Vue.extend() 创建 Dialog 的构造器
const DialogConstructor = Vue.extend(Dialog);

const dialog = function(options = {}) {
    // 创建 dialog 实例,通过构造函数传参,
    // 并调用 Vue 实例上的 $mount() 手动挂载
    const dialogInstance = new DialogConstructor({
        data: options
    }).$mount();

    // 手动把真实 dom 挂到 html 的 body 上
    document.body.appendChild(dialogInstance.$el);

    return dialogInstance;
};

// 导出包装好的 dialog 方法
export default dialog;

3. 暴露 install 方法给 Vue.use() 使用

为了支持 Vue.use(),vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象。

通过 vue.js 源码也可以看出,Vue.use() 方法所做的事情就是调用插件或者组件的 install 方法,然后把全局 Vue 传进去供插件和组件使用。

vue 源码:

// https://github.com/vuejs/vue/blob/dev/src/core/global-api/use.js
/* @flow */

import { toArray } from '../util/index'

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    const args = toArray(arguments, 1)
    args.unshift(this)
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }
}

在 src 目录下新建 components/index.js 文件,定义一个 install 方法,在里面将 dialog 实例 放到 Vue.prototype 上作为 Vue 实例的方法暴露到全局:

// src/components/index.js
import dialog from './Dialog/index';

// 准备好 install 方法 给 Vue.use() 使用
const install = function (Vue) {
  if (install.installed) return;
  install.installed = true;

  // 将包装好的 dialog 挂到Vue的原型上,作为 Vue 实例上的方法
  Vue.prototype.$dialog = dialog;
}

// 默认导出 install
export default {
  install
};

现在插件就开发完成了,我们可以在当前项目中本地引用这个插件了。

4. 引用插件

在 main.js 中引入插件:

// main.js
import dialog from src/components/index.js;
Vue.use(dialog);

5. 使用插件

在App.vue中 使用插件:

// App.vue 
<template>
  <div>
    <button @click="handleClick">click</button>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    handleClick () {
      this.$dialog({
        title: '我的弹窗',
        content: 'This is a dialog'
      })
    }
  }
}
</script>
Logo

基于 Vue 的企业级 UI 组件库和中后台系统解决方案,为数万开发者服务。

更多推荐