插件开发的目的
目的:为了代码的复用,项目的快捷开发。 参考vue官方文档
开发插件的几种方式
- 添加全局方法或者 property。
- 添加全局资源:指令/过滤器/过渡等。
- 通过全局混入来添加一些组件选项。
- 添加 Vue 实例方法,通过把它们添加到
Vue.prototype上实现。 - 一个库,提供自己的 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 配置讲解
- name :在包名称前加自己的 npm 账户名,采用 npm scope 的方式,包目录的组织方式和普通包不一样,而且可以有效的避免和他人的包名冲突。
- main: 指定了加载的入口文件。
- files:发布 npm 包时告诉发布程序只将 files 中指定的 文件 和 目录 上传到 npm 服务器
- repository:代码仓库地址,选项不强制,可以没有,不过一般都会提供,和他人共享
b、打包发布
npm run build
如果有npm账号,没有账号就去注册一个npm账号
npm login
npm publish
vue源码分析:参考博主连接