vue插件开发

1,002 阅读4分钟

插件开发的目的

目的:为了代码的复用,项目的快捷开发。 参考vue官方文档

开发插件的几种方式

  1. 添加全局方法或者 property。
  2. 添加全局资源:指令/过滤器/过渡等。
  3. 通过全局混入来添加一些组件选项。
  4. 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
  5. 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。

使用插件

通过全局方法 Vue.use() 使用插件。它需要在你调用 new Vue() 启动应用之前完成:

// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)
new Vue({ 
    // ...组件选项 
})

也可以传入一个可选的选项对象:

Vue.use(MyPlugin, { someOption: true })

Vue.use 会自动阻止多次注册相同插件,届时即使多次调用也只会注册一次该插件。

Vue.use 源码分析

地址:/src/core/global-api/use.js

/** 
* 定义 Vue.use,负责为 Vue 安装插件,做了以下两件事: 
* 1、判断插件是否已经被安装,如果安装则直接结束 
* 2、安装插件,执行插件的 install 方法 
* @param {*} plugin install 方法 或者 包含 install 方法的对象 
* @returns Vue 实例 
*/
import { toArray } from '../util/index'

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    // 已经安装过的插件列表
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
    // 判断 plugin 是否已经安装,保证不重复安装
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // 将 Vue 构造函数放到第一个参数位置,然后将这些参数传递给 install 方法
    const args = toArray(arguments, 1)
    args.unshift(this)
    if (typeof plugin.install === 'function') {
      // plugin 是一个对象,则执行其 install 方法安装插件
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      // 执行直接 plugin 方法安装插件
      plugin.apply(null, args)
    };
    // 在 插件列表中 添加新安装的插件
    installedPlugins.push(plugin)
    return this
  }
}

插件开发

Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象:

MyPlugin.install = function (Vue, options) { 
    // 1. 添加全局方法或 property 
    Vue.myGlobalMethod = function () { 
        // 逻辑... 
    } 
    // 2. 添加全局资源 
    Vue.directive('my-directive', {
        bind (el, binding, vnode, oldVnode) { 
            // 逻辑... 
        } 
        ... 
    }) 
    // 3. 注入组件选项 Vue.mixin({ 
        created: function () { 
            // 逻辑... 
        }
        ... 
    }) 
    // 4. 添加实例方法 Vue.prototype.$myMethod = function (methodOptions) { 
        // 逻辑... 
    } 
}

Vue.extend为主要技术点

Vue.extend源码分析

地址:/src/core/global-api/extend.js

/* @flow */

import { ASSET_TYPES } from 'shared/constants'
import { defineComputed, proxy } from '../instance/state'
import { extend, mergeOptions, validateComponentName } from '../util/index'

export function initExtend (Vue: GlobalAPI) {
  /**
   * 每个实例构造函数,包括 Vue,都有一个唯一的
   * cid. This enables us to create wrapped "child
   * constructors" for prototypal inheritance and cache them.
   */
  Vue.cid = 0
  let cid = 1

  /** 
  * 基于 Vue 去扩展子类,该子类同样支持进一步的扩展 
  * 扩展时可以传递一些默认配置,就像 Vue 也会有一些默认配置 
  * 默认配置如果和基类有冲突则会进行选项合并(mergeOptions) 
  */
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    /** 
    * 利用缓存,如果存在则直接返回缓存中的构造函数 
    * 什么情况下可以利用到这个缓存? 
    * 如果你在多次调用 Vue.extend 时使用了同一个配置项(extendOptions),这时就会启用该缓存 
    */
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name)
    }
    // 定义 Sub 构造函数,和 Vue 构造函数一样
    const Sub = function VueComponent (options) {
      // 初始化
      this._init(options)
    }
    // 通过原型继承的方式继承 Vue
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    // 选项合并,合并 Vue 的配置项到 自己的配置项上来
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    // 记录自己的基类
    Sub['super'] = Super

    // 初始化 props,将 props 配置代理到 Sub.prototype._props 对象上 
    // 在组件内通过 this._props 方式可以访问
    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // 定义 extend、mixin、use 这三个静态方法,允许在 Sub 基础上再进一步构造子类
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // 定义 component、filter、directive 三个静态方法
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // 递归组件的原理,如果组件设置了 name 属性,则将自己注册到自己的 components 选项中
    if (name) {
      Sub.options.components[name] = Sub
    }

    // 在扩展时保留对基类选项的引用。 
    // 稍后在实例化时,我们可以检查 Super 的选项是否具有更新
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor
    cachedCtors[SuperId] = Sub
    return Sub
  }
}

function initProps (Comp) {
  const props = Comp.options.props
  for (const key in props) {
    proxy(Comp.prototype, `_props`, key)
  }
}

function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    defineComputed(Comp.prototype, key, computed[key])
  }
}

第一种: 添加全局方法和Vue 实例方法

示例:获取当前时间

// getNowDate.js
export default {
    install(Vue, options) {
        // 1.定义原型方法
        Vue.prototype.$getNowDate = function () {
            let oDate = new Date(),
                oYear = oDate.getFullYear(),
                oMonth = oDate.getMonth() + 1,
                oDay = oDate.getDate(),
                oTime = oYear + '-' + oMonth + '-' + oDay; //
            return oTime;
        };
        // 2.添加全局方法
        Vue.$getMonth = function () {
            let oDate = new Date(),
                oMonth = oDate.getMonth() + 1,
                oTime = oMonth; //
            return oTime;
        };
    }
}

vue项目中全局引入并使用 main.js

import getDateUtil from './getNowDate'
Vue.use(getDateUtil)

组件中的使用

原型方法:this.$getNowDate()
//需要引入vue
import Vue from 'vue'
全局方法:Vue.$getMonth()

理解js中的静态方法和实例方法 简单理解:静态方法是函数自己定义的,而实例方法是通过原型来定义。它们的区别是:静态方法是可以直接用类名.方法名去调用的,而实例是不可以调用静态方法的,实例方法必须要用实例才可以去调用(实例.实例方法),而本身无法调用示例方法。

//构造函数
function A(){}

//静态方法:也就是上面的全局方法
A.staticMethof =  function(){
    alert('静态方法');
}

//实例方法:也就是上面的原型方法
A.prototype.instaceMethod =  function(){
    alert('实例方法');
}

A.staticMethof();  //类A直接调用
A.instaceMethod();//Uncaught TypeError: A.instaceMethod is not a function
var instace =  new A();
instace.staticMethof();//Uncaught TypeError: instace.staticMethof is not a function
instace.instaceMethod();//A的实例对象instace调用

第二种:添加全局资源:指令/过滤器/过渡等

用全局指令模拟一个按钮loading插件

//btnLoading.js
export default {
    install(Vue, options) {
        let btnLoading = {
            template: '<div v-show="visible" class="com-v-loading-mask" style="display: flex;justify-content: center;align-items: center;position: absolute;left: 0; top: 0;background: rgba(255, 255, 255, 1);height: 100%;width: 100%;">' +
                '<p class="com-v-loading-text" style="color:red;">{{ text }}</p>' +
                '</div>',
            data: function () {
                return {
                    text: '加载中',
                    visible: false
                }
            }
        };
        // 使用 Vue.extend构造组件子类
        const LoadingContructor = Vue.extend(btnLoading);
        // 定义一个名为loading的指令
        Vue.directive('loading', {
            /**
             * 只调用一次,在指令第一次绑定到元素时调用,可以在这里做一些初始化的设置
             * @param {*} el 指令要绑定的元素
             * @param {*} binding 指令传入的信息,包括 {name:'指令名称', value: '指令绑定的值',arg: '指令参数 v-bind:text 对应 text'}
             */
            bind(el, binding) {
                const instance = new LoadingContructor({
                    el: document.createElement('div'),
                    data: {}
                })
                el.appendChild(instance.$el)
                el.instance = instance
                Vue.nextTick(() => {
                    el.instance.visible = binding.value
                })
            },
            /**
             * 所在组件的 VNode 更新时调用
             * @param {*} el
             * @param {*} binding
             */
            update(el, binding) {
                // 通过对比值的变化判断loading是否显示
                if (binding.oldValue !== binding.value) {
                    el.instance.visible = binding.value
                }
            },
            /**
             * 只调用一次,在 指令与元素解绑时调用
             * @param {*} el
             */
            unbind(el) {
                const mask = el.instance.$el
                if (mask.parentNode) {
                    mask.parentNode.removeChild(mask)
                }
                el.instance.$destroy()
                el.instance = undefined
            }
        });
    }
}

main.js全局引用

import btnLoading from "./plugin/btnLoading/index"
Vue.use(btnLoading);

组件中使用

<button @click="test" v-loading="inShow" style="position: relative;height: 40px;width: 120px;border: 1px solid #ccc;text-align: center;line-height: 40px;">测试loading</button>
data() {
    return {
        inShow: false
    {
}
methods: {
    test() {
        var _self = this;
        _self.inShow = true;
        setTimeout(() => {
            _self.inShow = false;
        }, 3000);
    }
}

第三种:通过混入mixins

Vue.mixin 源码解析

地址:/src/core/global-api/mixin.js

/** 
* 定义 Vue.mixin,负责全局混入选项,影响之后所有创建的 Vue 实例,这些实例会合并全局混入的选项 
* @param {*} mixin Vue 配置对象 
* @returns 返回 Vue 实例 
*/ 
Vue.mixin = function (mixin: Object) { 
    // 在 Vue 的默认配置项上合并 mixin 对象 
    this.options = mergeOptions(this.options, mixin) 
    return this 
}

源码分析 mergeOptions

地址:src/core/util/options.js

/** 
* 合并两个选项,出现相同配置项时,子选项会覆盖父选项的配置 
*/
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }
  // 标准化 props、inject、directive 选项,方便后续程序的处理
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // 处理原始 child 对象上的 extends 和 mixins,分别执行 mergeOptions,将这些继承而来的选项合并到 parent 
  // mergeOptions 处理过的对象会含有 _base 属性
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}
  let key
  // 遍历 父选项
  for (key in parent) {
    mergeField(key)
  }
  // 遍历 子选项,如果父选项不存在该配置,则合并,否则跳过,因为父子拥有同一个属性的情况在上面处理父选项时已经处理过了,用的子选项的值
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  // 合并选项,childVal 优先级高于 parentVal
  function mergeField (key) {
    // strat 是合并策略函数,如何 key 冲突,则 childVal 会 覆盖 parentVal
    const strat = strats[key] || defaultStrat
    // 值为如果 childVal 存在则优先使用 childVal,否则使用 parentVal
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

混合mixins开发全局过滤器插件

//comFilter.js
export default {
    install(Vue, options) {
        Vue.mixin({
            filters: {
                $_toFixed: (num) => {
                    return num.toFixed(2)
                }
            }
        });
    }
}

main.js全局引用

import comFilter from './comFilter.js';
Vue.use(comFilter);

组件使用

<div>{{ num | $_toFixed }}</div>

Toast插件开发 参考地址:githup

var Toast = {};
var showToast = false, // 存储toast显示状态
    showLoad = false, // 存储loading显示状态
    toastVM = null, // 存储toast vm
    loadNode = null; // 存储loading节点元素

Toast.install = function (Vue, options) {

    var opt = {
        defaultType: 'bottom',
        duration: '2500',
        wordWrap:false,
        width:''
    };
    for (var property in options) {
        opt[property] = options[property];
    }

    Vue.prototype.$testSui = function (tips, type) {
        //判断位置有无  没有就取默认值
        var curType = type ? type : opt.defaultType;
        //长度控制
        var wordWrap = opt.wordWrap ? 'lx-word-wrap' : '';
        var style = opt.width ? 'style="width: ' + opt.width + '"' : '';
        var tmp = '<div v-show="show" :class="type" class="lx-toast ' + wordWrap + '" ' + style + '>{{tip}}</div>';

        if (showToast) {
            // 如果toast还在,则不再执行
            return;
        }
        if (!toastVM) {
            var toastTpl = Vue.extend({
                data: function () {
                    return {
                        show: showToast,
                        tip: tips,
                        type: 'lx-toast-' + curType
                    }
                },
                template: tmp
            });
            toastVM = new toastTpl()
            var tpl = toastVM.$mount().$el;
            document.body.appendChild(tpl);
        }
        toastVM.type = 'lx-toast-' + curType;
        toastVM.tip = tips;
        toastVM.show = showToast = true;

        setTimeout(function () {
            toastVM.show = showToast = false;
        }, opt.duration)
    };
    ['bottom', 'center', 'top'].forEach(function (type) {
        Vue.prototype.$testSui[type] = function (tips) {
            return Vue.prototype.$testSui(tips, type)
        }
    });

    Vue.prototype.$loading = function (tips, type) {
        if (type == 'close') {
            loadNode.show = showLoad = false;
            document.querySelector('.lx-load-mark').remove();
        } else {
            if (showLoad) {
                // 如果loading还在,则不再执行
                return;
            }
            var loadTpl = Vue.extend({
                data: function () {
                    return {
                        show: showLoad
                    }
                },
                template: '<div v-show="show" class="lx-load-mark"><div class="lx-load-box"><div class="lx-loading"><div class="loading_leaf loading_leaf_0"></div><div class="loading_leaf loading_leaf_1"></div><div class="loading_leaf loading_leaf_2"></div><div class="loading_leaf loading_leaf_3"></div><div class="loading_leaf loading_leaf_4"></div><div class="loading_leaf loading_leaf_5"></div><div class="loading_leaf loading_leaf_6"></div><div class="loading_leaf loading_leaf_7"></div><div class="loading_leaf loading_leaf_8"></div><div class="loading_leaf loading_leaf_9"></div><div class="loading_leaf loading_leaf_10"></div><div class="loading_leaf loading_leaf_11"></div></div><div class="lx-load-content">' + tips + '</div></div></div>'
            });
            loadNode = new loadTpl();
            var tpl = loadNode.$mount().$el;

            document.body.appendChild(tpl);
            loadNode.show = showLoad = true;
        }
    };

    ['open', 'close'].forEach(function (type) {
        Vue.prototype.$loading[type] = function (tips) {
            return Vue.prototype.$loading(tips, type)
        }
    });
}
module.exports = Toast;

IE script标签引入vue.js的插件开发demo 地址:githup

构建并发布到npm

a.搭建项目

项目结构 参考githup

├── node_modules//依赖
├── packages // 组件列表
│   ├── hello//hello组件
│   │   └──index.js
│   └── log//log组件
│       └── index.js
├── src // 导入并导出组件
│   └── index.js
├── webpack.common.js // webpack 配置文件  入口,环境,输出配置
├── .editorconfig// 定义代码格式  
├── webpack.umd.js// UMD格式配置
├── package.json// 项目基本信息,包依赖信息等             
├── README.md// 项目说明

package.json 配置讲解

  1. name :在包名称前加自己的 npm 账户名,采用 npm scope 的方式,包目录的组织方式和普通包不一样,而且可以有效的避免和他人的包名冲突。
  2. main: 指定了加载的入口文件。
  3. files:发布 npm 包时告诉发布程序只将 files 中指定的 文件 和 目录 上传到 npm 服务器
  4. repository:代码仓库地址,选项不强制,可以没有,不过一般都会提供,和他人共享

b、打包发布

npm run build

如果有npm账号,没有账号就去注册一个npm账号

npm login
npm publish

vue源码分析:参考博主连接