前言

Vue 项目开发过程中,经常用到插件,比如原生插件 vue-routervuex,还有 element-ui 提供的 notifymessage 等等。这些插件让我们的开发变得更简单更高效。那么 Vue 插件是怎么开发的呢?如何自己开发一个 Vue 插件然后打包发布到npm?

本文涉及技术点:

  1. Vue 插件的本质
  2. Vue.extend() 全局方法
  3. 如何手动挂载 Vue 实例
  4. Vue.use() 的原理
  5. 如何打包成 umd 格式
  6. 发布前如何测试 npm

一、定义

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

“插件通常用来为 Vue 添加全局功能。”

“组件是可复用的 Vue 实例,且带有一个名字。”

—— Vue.js 官网

Emmmm,似乎好像有种朦胧美。。。

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

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

我们今天只聚焦 Vue 插件。

插件一般有下面几种:

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

    —— Vue.js 官网

二、插件的使用

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

下面以实现一个简易的提示框插件 toast 为例,给大家介绍怎么一步一步开发和发布一个 Vue 插件。

希望达到的效果:

在 main.js 中 use:

// src/main.js
import Vue from 'vue'
import toast from '@champyin/toast' Vue.use(toast)

在 App.vue 的生命周期 mounted 方法里调用 this.$toast():

// src/App.vue
<template>
<div>
<button @click='handleClick'>Toast</button>
</div>
</template>
<script>
export default {
name: 'demo',
methods: {
handleClick() {
this.$toast({
type: 'success',
msg: '成功',
duration: 3
})
}
}
}
</script>

运行后在页面上点击按钮,弹出 成功 的提示,然后3秒后消失。

在线体验地址:http://champyin.com/toast/

三、插件开发

1. 编写 toast 的本体。

在 Vue 项目(你可以使用 Vue-cli 快速生成一个 Vue 项目,也可以自己用 webpack 搭建一个)的 src 目录下创建 components/Toast/index.vue 文件。

// src/components/Toast/index.vue
<template>
<transition name='fade'>
<div class='uco-toast' v-if='isShow'>
<span :class='iconStyle'></span>
<span>{{msg}}</span>
</div>
</transition>
</template> <script>
export default {
data() {
return {
isShow: false,
type: 'success',
msg: '成功',
duration: 1,
};
},
computed: {
iconStyle() {
return `tfont icon-${this.type} toast-icon`;
},
},
mounted() {
this.isShow = true;
setTimeout(() => {
this.isShow = false;
}, this.duration * 1000);
},
};
</script> <style lang='less' scoped>
// 样式略
</style>

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

2. 手动挂载 toast 实例的 dom

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

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

// src/components/Toast/index.js
import Vue from 'vue';
import Toast from './index.vue'; // 使用 Vue.extend() 创建 Toast 的构造器
const ToastConstructor = Vue.extend(Toast); const toast = function(options = {}) {
// 创建 Toast 实例,通过构造函数传参,
// 并调用 Vue 实例上的 $mount() 手动挂载
const toastInstance = new ToastConstructor({
data: options
}).$mount(); // 手动把真实 dom 挂到 html 的 body 上
document.body.appendChild(toastInstance.$el); return toastInstance;
}; // 导出包装好的 toast 方法
export default toast;

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

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

—— Vue.js 官网

通过 Vue.js 源码也可以看出,Vue.use() 方法所做的事情就是调用插件或者组件的 install 方法,然后把全局 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 方法,在里面将 toast 实例放到 Vue.prototype 上作为 Vue 实例的方法暴露到全局。

// src/components/index.js
import toast from './Toast/index';
import '../icon/iconfont.css'; // 准备好 install 方法 给 Vue.use() 使用
const install = function (Vue) {
if (install.installed) return;
install.installed = true; // 将包装好的 toast 挂到Vue的原型上,作为 Vue 实例上的方法
Vue.prototype.$toast = toast;
} // 默认导出 install
export default {
install,
};

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

//在 main.js 中
import toast from src/components/index.js;
Vue.use(toast); //在 App.vue 中
handleClick(){
this.$toast();
}

四、发布到npm

为了方便其他人也可以使用到这个插件,我们可以把它发布到 npm 上去。发布的步骤很简单,但是发布之前,需要有一些小配置和一些注意的地方。

1. 打包配置

首先我们要把它打包成可以给浏览器解析的 UMD 格式的的模块,并且去掉对 Vue.js 的打包,这样别人在 Vue 项目中使用这个插件的时候就不会有两份 Vue 或者出现 Vue 版本冲突的问题,以保证可以更好被独立引用。

如果你是用 Vue-cli 生成的项目,那只需要在你的 npm 脚本中配置一下库的打包命令:

// package.json
"build:lib": "vue-cli-service build --target lib --name toast --dest lib src/components/index.js"

命令说明:

--target:构建的目标
targetType 有三个选项:lib | wc | wc-async
lib:库
wc:web component
wc-async:异步的 web component --name:库或组件的名字
当入口为单一文件时,name为库或组件的文件名
当入口为global表达式时,name为每个库或组件文件名字的前缀 [entry]:打包入口
可以是.vue文件,也可以是.js文件
当注册多个web component时,入口可以是一个global表达式,如 components/*.vue --dest:输出目录
默认为dist目录,也可以修改为自定义的目录

然后运行 npm run build:lib,即可在 lib 目录下生成如下文件:

toast.umd.js 一个直接给浏览器或者AMD loader 使用的 UMD 包
toast.umd.min.js 一个压缩版 UMD 构建版本
toast.common.js 一个给打包器用的CommonJS包

如果你是用 webpack 搭建的 Vue 项目,那就需要在 webpck 中配置一下 output.libraryTarget 等属性:

// build/webpack.lib.conf.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = {
mode: 'production',
entry: './src/components/index.js',
output: {
path: path.resolve(__dirname, '../lib'),
filename: 'toast.js',
library: 'toast',
libraryTarget: 'umd',
libraryExport: 'default',
umdNamedDefine: true,
globalObject: 'typeof self !== \'undefined\' ? self : this',
},
externals: {
vue: {
root: 'Vue',
commonjs: 'vue',
commonjs2: 'vue',
amd: 'vue',
},
},
plugins: [
new CleanWebpackPlugin(),
],
};

然后运行 npm run build:lib,即可在 lib 目录下生成如下文件:

toast.js 直接给浏览器或者AMD loader 使用的 UMD 包

2. 发布前的测试

发布前,我们需要配置一下 package.json 里的 namemain 字段:

name 的值是最终包的名字,installimport 的就是这个名字(请确保全网唯一)。

main 的值是包的入口文件路径(相对当前文件的路径),一定要填写正确,否则包无法被引用。

"name": "@champyin/toast",
"main": "lib/toast.js",

为了确保包的配置没有问题,我们可以利用 npm link 命令在本地测试一下包的使用情况。使用npm link测试包的使用估计很多人都会,就不赘述了。如果有需要可以看我的另一篇中文章npm link详解

这个时候,我们其实就可以发布了,但是为了防止把不必要的文件发布出去(比如测试用例和demo)浪费人家下载的流量,我们最好是建一个 .npmigore 文件,语法跟 .gitignore 相同。

3. 发布

发布的方法很简单(不过首先你要有个 npm 账号),在 package.json 所在的目录下执行这两句就可以了:

npm add user
npm publish

关于更详细的发布教程,我在另一篇文章有专门细说:如何发布一个npm模块

4. 安装测试

其实到了这一步一99.99%是不会出错了,安装一遍只是为了那 0.01% 的万一。

在另一个 Vue 项目里(注意不能在开发toast的项目里哈),从 npm 安装自己刚才发布的包:

npm i -D @champyin/toast

然后在项目中使用一下自己的插件,点击按钮就会弹出 toast 小提示了。

//在 main.js 中
import toast from '@champyin/toast';
Vue.use(toast); //在 App.vue 中
handleClick(){
this.$toast();
}

项目体验地址:http://champyin.com/toast/

npm 地址:https://www.npmjs.com/package/@champyin/toast

欢迎给我提 issue:https://github.com/yc111/toast/issues

Happy coding

如何开发和发布一个Vue插件的更多相关文章

  1. 用webpack发布一个vue插件包

    创建库 本来以为很简单,结果配置了webpack之后,运行build就报错了,似乎不认识es6语法,于是先后安装了几个包: @babel/core @babel/preset-env babel-lo ...

  2. 第一个Vue插件从封装到发布

    前言 这是我封装的第一个Vue插件,实现的功能是滑动选择省市区,虽然只是一个简单的插件,但还是挺开心的,记录一下步骤. 插件地址:https://github.com/leichangchun/vue ...

  3. 从零开始编写一个vue插件

    title: 从零开始编写一个vue插件 toc: true date: 2018-12-17 10:54:29 categories: Web tags: vue mathjax 写毕设的时候需要一 ...

  4. 自己编写并发布一个Vue组件

    自己编写并发布一个Vue组件 1. 几种开源协议的介绍 https://blog.csdn.net/techbirds_bao/article/details/8785413 2.开始编写组件 新建p ...

  5. 开发第一个VUE插件

    背景 项目中用到element-ui,里面用到了弹出组件,但是效果不太满意,于是自己就想写一个简单的弹出组件.目前已经发布到npm:可以通过npm i dialog-wxy -s 进行下载使用页面调用 ...

  6. 从0到1发布一个Vue Collapse组件

    需求背景 最近在项目中遇到了一个类似Collapse的交互需求,因此到github上找了一圈关于Vue Collapse的相关轮子,但是多少都有些问题.有的是实现问题,例如vue2-collapse, ...

  7. 学习如何写一个vue插件【入门篇】

    #### 疑答 1.市面上已经有那么多插件可用,为什么还要造轮子?学习.借鉴思想.应用到开发 2.能否在项目中使用?与网上插件使用相同   更新维护问题怎么解决? 自身动力,使用者反馈等 #### 准 ...

  8. 如何创建并发布一个 vue 组件

    步骤 创建 vue 的脚手架 npm install -g @vue/cli vue init webpack 绑定 git 项目 cd existing_folder git init git re ...

  9. 如何发布一个npm包(基于vue)

    前言:工作的时候总是使用别人的npm包,然而我有时心底会好奇自己如何发布一个npm包呢,什么时候自己的包能够被很多人喜欢并使用呢...今天我终于迈出了第一步. 前提:会使用 npm,有 vue 基础, ...

随机推荐

  1. day29-struct模块解决黏包问题

    #struct模块可以把一个数据类型,例如数字int,转化成固定长度(4个字节)的bytes.int转为4个bytes. #在大量传输数据之前先告诉接收端即将接收数据的大小,方可解决黏包问题: #利用 ...

  2. wait操作接口

    1.等待回收的两个函数wait()和waitpid()函数 1.1 wait(int *status)的用法:阻塞函数,等待任意一个子进程的返回.   *wait(NULL):对子进程的结束状态不关心 ...

  3. ckeditor深入挖掘吃透

  4. JsonPath入门教程

    有时候需要从json里面提取相关数据,必须得用到如何提取信息的知识,下面来写一下 语法格式 JsonPath 描述 $ 根节点 @ 当前节点 .or[] 子节点 .. 选择所有符合条件的节点 * 所有 ...

  5. [LC] 505. The Maze II

    There is a ball in a maze with empty spaces and walls. The ball can go through empty spaces by rolli ...

  6. mongodb use where and custom function to query mongodb存储过程

    function name regexObjSubObjKey function code function(proto,value) { var match=false; var reg = new ...

  7. SPSS|Data|Transfer|Analysis|Label|One sample test|Testval|Criables|

    生物统计与实验设计-使用SPSS Data用于整合:Transfer用于预处理:Analysis用于数据的二维呈现:Label是在报表中呈现的名字: 给离散值编码: 对于离散值做数学计算: 均值比较用 ...

  8. python3下scrapy爬虫(第一卷:安装问题)

    一般爬虫都是用urllib包,requests包 配合正则.beautifulsoup等包混合使用,达到爬虫效果,不过有框架谁还用原生啊,现在我们来谈谈SCRAPY框架爬虫, 现在python3的兼容 ...

  9. Nginx笔记总结十四: nginx反向代理,用内网域名转发

    user www www; worker_processes ; error_log logs/error.log; pid logs/nginx.pid; worker_rlimit_nofile ...

  10. 跨域带cookie失效的解决方案

    在webpack的tableproxy那儿配置完跨域以后,想给cookie添加domain以便请求的时候带上cookie domain为localhost,cookie不会失效,但是一但改成baidu ...