从零搭建自己的Vue-ui插件库
- 前端
- ES2018
- 0
嗨,又出了一个新的前端框架,又出来一个UI框架,劳资学不动了;
项目中大家有没有遇到过这种情况,为了一个极小的功能,引入一个很大的库;为了使用一个confirm确认框在项目中引入了Layui, 虽然解决了产品的需求,但终究不是前端该有的正确姿势;一个项目中混杂着各种各样的库,如: jQuery\Bootstrap\LayerUI\elementUI\CustomUI….显得是百花齐放(鱼虾混杂),后端看到代码后忍不住称赞(吐槽)一声前端真牛逼(垃圾),。。。多么痛的领悟呀!
手写自己的Vue组件
高大上的话不多说了,拿来主义虽好,可不可贪恋(劲酒虽好,可不要贪杯)哦^_^, 知其然且知其所以然,方能在芸芸众生中来去自如; 借鉴ElementUI,沿着vue官网的脉络,尝试自己怎么手写一个很简单的vue组件;以下以最简单的alert\button\toast为例,看看如何编织自己的小鱼(麻雀虽小,五脏俱全);
先看下vue官网对于Vue插件的定义和描述:插件通常用来为 Vue 添加[全局]功能
插件的功能范围没有严格的限制——一般有下面几种:
- 添加全局方法或者属性
- 添加全局资源:指令/过滤器/过渡等
- 通过全局混入来添加一些组件选项
- 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现
- 一个库,提供自己的 API,同时提供上面提到的一个或多个功能
官网描述
- 通过全局方法 Vue.use() 使用插件,它需要在你调用 new Vue() 启动应用之前完成
// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)
new Vue({
// ...组件选项
}
也可以传入一个可选的选项对象:
Vue.use(MyPlugin, { someOption: true })
Vue.use 会自动阻止多次注册相同插件,届时即使多次调用也只会注册一次该插件
Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象
- 插件定义
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或属性
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-cli@3.x下开发演示的, 所有组件都放置在root/src/components下,以组件名命名的文件夹即组件目录; 因后面打包需要,对项目目录做一些简单的调整,主要结构如下:
project
|_ src
|_ packages // 新增package来保存我们编写的插件的组件包
|_alert
|_toast
|_button
|_index.js // 用于组合所有组件(后面会用到)
// ...其它的没变化
编写全局功能插件Alert
- 组件目录结构
packages
|_alert
|_alert.vue
|_index.js
- 编写插件HTML、CSS、js逻辑部分
// alert/alert.vue
<template>
<div v-show="visible">
<p>{{msg}}</p>
<a href="#" @click.prevent="close">×</a>
</div>
</template>
<script>
export default {
name: 'FyAlert', // 使用时可采用此2种方式: <fy-alert :msg="msg"></fy-alert> 或 <FyAlert :msg="msg"></FyAlert>
props: {
msg: { type: String, default: '' }
},
data(){
return {
visible: true
};
},
methods: {
close(){ this.visible = false; }
}
}
</script>
<style scoped>
*{ margin: 0; padding: 0; }
.simple-alert {
padding: 8px 16px;
margin: 10px 20px;
background-color: #67c23a;
color: #fff;
position: relative;
border-radius: 3px;
.simple-alert-text {
margin: 0;
font-size: 13px;
text-align: left;
}
.simple-alert-close {
font-size: 18px;
color: #fff;
line-height: 0;
opacity: 1;
position: absolute;
top: 16px;
right: 12px;
cursor: pointer;
}
}
</style>
- 注册为Vue插件
// alert/index.js
import Alert from './alert'
Alert.install = (Vue, options) => {
Vue.component(Alert.name, Alert);
};
export default Alert;
- 在项目中(main.js)导入插件
// 在文件中使用前需要提前导入
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
// 主要就下面两句啦
import Alert from '@/components/alert; // 或 import Alert from '@/components/alert/index.js'
Vue.use(Alert);
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
- 在示例组件示例中使用插件
// view/Home.vue 文件里面使用如下:
<template>
<div>
<fy-alert :msg="msg"></fy-alert>
</div>
</template>
<script>
export default {
name: 'home',
data(){
return {
msg: '成功提示的文案'
}
}
}
</script>
- 在未注册为全局插件的情况下, 当做局部组件使用
// 通过导入局部组件的形式使用;
<template>
<div>
<Alert :msg="msg"></Alert>
</div>
</template>
<script>
import Alert from '@/packages/alert/alert.vue';
export default {
name: 'home',
data(){
return {
msg: '成功提示的文案'
}
},
// 在这里注册为局部组件
components: {
Alert
}
}
</script>
综上可看出,全局组件无非就是将普通组件简单包装(批上一件明星的外衣);个人开发时普通组件完全可以胜任日常的需要; 不过官网说了"插件通常用来为 Vue 添加全局功能", 如果团队开发或者在大项目中使用,包装一套插件的价值就显而易见了; …..还是要学会做一个披着大神铠甲的搬码工(学会装X)
添加全局属性或方法
- 定义全局方法
export default {
install: (Vue, options) => {
Vue.rewrite = () => {
console.log('vue rewrite ~')
};
}
};
- 在项目中(main.js)导入插件
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import rewrite from '@/packages/rewrite'
Vue.use(rewrite)
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
- 在组件实例中使用全局方法
// 使用
<template>
<div></div>
</template>
<script>
import Vue from 'vue'; // 不要忘记这一行
export default {
name: 'home',
mounted(){
Vue.rewrite();
}
}
</script>
添加 Vue 实例方法, 如:Message@ElementUI 的 Vue.prototype.$message = Message
编写实例方法的一个核心点就是将方法挂载在Vue.prototype上面
- 组件目录结构
packages
|_toast
|_toast.vue
|_index.js
- 编写插件HTML、CSS、js逻辑部分
// toast/toast.vue
<template>
<transition name="fade" @after-leave="handleAfterLeave">
<div
:class="typeClass"
:style="positionStyle"
v-show="visible"
> {{msg}} </div>
</transition>
</template>
<script>
const typeMap = {
success: 'success',
info: 'info',
warning: 'warning',
error: 'error'
};
export default {
name: 'FyToast',
data(){
return {
duration: 3000,
visible: false,
msg: 'abc',
type: 'info', // 类型(成功、失败、警告)
offsetY: 30
};
},
methods: {
// 动画完成后再销毁组件
handleAfterLeave() {
this.$destroy(true);
this.$el.parentNode.removeChild(this.$el); // 从文档中移除组件的真实DOM
},
close(){
this.visible = false;
},
timeCountDown(){
this.timer = setTimeout(()=>{
this.close();
}, this.duration);
}
},
computed: {
typeClass(){
return typeMap[this.type]; // 根据状态返回不同皮肤的组件样式
},
positionStyle(){
return {
'top': `${this.offsetY}px`
};
}
},
mounted(){
this.timeCountDown(); // 组件生成后调用倒计时定时器
}
}
</script>
<style scoped>
.fy-toast {
min-width: 380px;
box-sizing: border-box;
border-radius: 4px;
border: 1px solid #ebeef5;
position: fixed;
left: 50%;
top: 20px;
transform: translateX(-50%);
background-color: #edf2fc;
transition: opacity .3s,transform .4s,top .4s;
overflow: hidden;
padding: 15px 15px 15px 20px;
display: flex;
align-items: center;
&.success { background-color: #67c23a; color: #fff; }
&.info { background-color: #f4f4f5; color: #909399; }
&.warning { background-color: #fdf6ec; color: #e6a23c; }
&.error { background-color: #fef0f0; color: #f56c6c; }
}
// 添加过渡动画
.fade-enter, .fade-leave-active {
opacity: 0;
transform: translate(-50%,-100%)
}
</style>
- 插件调用功能编写
// toast/index.js
import Vue from 'vue'; // 这一句很关键,勿忘
import Toast from './toast';
let ToastConstructor = Vue.extend(Toast); // 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象
// 格式化数据,对字符串做处理
let formatOptions = (options) => {
options = options || {};
if(typeof options === 'string') options = { 'msg': options };
return options;
};
// 添加Dom到文档
let ToastComponent = (options) => {
let toastInstance = new ToastConstructor({
data: formatOptions(options)
}).$mount();
document.body.appendChild(toastInstance.$el); // toastInstance.$el 为真实DOM,将其放在文档中
toastInstance.visible = true;
return toastInstance.vm;
};
// 添加不同状态的方法
['success', 'warn', 'error', 'info'].forEach((type) => {
ToastComponent[type] = (options) => {
options = formatOptions(options);
options['type'] = type;
return ToastComponent(options);
};
});
export default ToastComponent;
- 在项目中(main.js)导入插件
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
// 以下两句, 注意:跟全局组件有所不同
import Toast from '@/packages/toast'
Vue.prototype.$toast = Toast;
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
- 通过use的方式使用需要提前将组件挂载在Vue.prototype上
export default {
install: (Vue, options) => {
Vue.prototype.$toast = (options)=> {
// ...
};
}
};
- 在示例组件示例中使用插件
<template>
<div>
<button type="button" @click="showMessage">Success Click!</button>
</div>
</template>
<script>
export default {
name: 'home',
methods: {
showMessage(){
// this.$toast.success('Hello World!'); // 直接传递提示语句
// 传递对应参数
this.$toast.success({
msg: '测试信息',
duration: 2000
});
}
}
}
</script>
好啦好啦,添加 Vue 实例方法的插件就这样写好了,就问意不意外^_^; 尔等可能嗤之以鼻,你这(傻。。)泼猴又来忽悠老夫(老子), 这要你教啊…; 然而,这就是一个货真价实的Vue插件,还是实例方法;
看完上面的代码,有没有Get到了呢? 折腾了好久,说明还是对Vue文档不熟悉,来看看文档吧; 以下几点需要认真的去理解以下:
- vm.$el : Vue 实例使用的根 DOM 元素 [vm.$el](https://cn.vuejs.org/v2/api/#vm-el)
- vm.$mount( [elementOrSelector] ) [vm.$mount](https://cn.vuejs.org/v2/api/#vm-mount)
- Vue.extend( options ) [Vue.extend](https://cn.vuejs.org/v2/api/#Vue-extend)
- Vue.use( plugin ) [Vue.use( plugin )](https://cn.vuejs.org/v2/api/#Vue-use)
打包为插件库
- 整合所有的组件, 对外导出为一个完整的插件库
// 在packages目录下新增index.js, 内容如下
import Alert from './alert/index.js';
import Toast from './toast/index.js';
import Button from './button/index.js';
// 存储组件列表
const components = [
Alert,
Button
];
// 定义 install 方法,接收 Vue 作为参数。如果使用 use 注册插件,则所有的组件都将被注册
const install = function (Vue, options = {}) {
// 判断是否可以安装
if (install.installed) return
// 遍历注册全局组件
components.forEach(component => {
Vue.component(component.name, component)
});
Vue.prototype.$toast = Toast;
};
// 判断是否是直接引入文件
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
// 直接export对象,在使用时为按需加载...
export {
// 导出的对象必须具有 install,才能被 Vue.use() 方法安装
install,
// 以下是具体的组件列表
Alert,
Toast,
Button
};
- 新增打包执行命令
// 根目录下打开命令行窗口, 执行编译命令yarn dopack
, 命令执行完成后, 会发现目录下多出一个lib目录(即包文件)
- --target: 构建目标,默认为应用模式; 这里修改为 lib 启用库模式
- --dest : 输出目录,默认 dist,这里我们改成 lib
- --name 包里面的文件名
- [entry]: 最后一个参数为入口文件,默认为 src/App.vue; 这里我们指定编译 packages/ 组件库目录;
// 修改package.json文件, 新增lib命令
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
// ++++++新增下面这条, 打包时在命令行执行 yarn lib即可
"dopack": "vue-cli-service build --target lib --name feeyo --dest lib packages/index.js"
}
- 配置 package.json 文件中发布到 npm 的相关字段
name: 包名,该名字是唯一的。可在 npm 官网搜索名字,如果存在则需换个名字。
version: 版本号,每次发布至 npm 需要修改版本号,不能和历史版本号相同。
description: 描述。
main: 入口文件,该字段需指向我们最终编译后的包文件。
keyword:关键字,以空格分离希望用户最终搜索的词。
author:作者
private:是否私有,需要修改为 false 才能发布到 npm
license: 开源协议
// 其他的请自行查阅package.json配置项
- 较为完整的package.json 文件如下:
{
"name": "feeyo-ui",
"version": "0.1.5",
"author": "MinLi",
"private": false,
"description": "基于 Vue ui框架",
"main": "lib/feeyo.umd.min.js",
"keyword": "vue ui框架",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"dopack": "vue-cli-service build --target lib --name feeyo --dest lib packages/index.js"
},
"dependencies": {
"core-js": "^2.6.5",
"feeyo-ui": "^0.1.5",
"vue": "^2.6.10",
"vue-router": "^3.0.3",
"vuex": "^3.0.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.11.0",
"@vue/cli-plugin-eslint": "^3.11.0",
"@vue/cli-service": "^3.11.0",
"@vue/eslint-config-standard": "^4.0.0",
"babel-eslint": "^10.0.1",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"node-sass": "^4.9.0",
"sass-loader": "^7.1.0",
"vue-template-compiler": "^2.6.10"
},
"license": "ISC"
}
- 扩展 webpack 配置,使 packages 加入编译
vue-cli3 提供一个可选的 vue.config.js 配置文件。如果这个文件存在则他会被自动加载,所有的对项目和webpack的配置,都在这个文件中
module.exports = {
// 修改 src 为 examples
pages: {
index: {
entry: 'example/main.js',
template: 'public/index.html',
filename: 'index.html'
}
},
// 扩展 webpack 配置,使 packages 加入编译
chainWebpack: config => {
config.module
.rule('js')
.include
.add('/packages')
.end()
.use('babel')
.loader('babel-loader')
.tap(options => {
// 修改它的选项...
return options
})
}
}
- npm发布
- 添加 .npmignore 文件,设置忽略发布文件
npm 编译后端文件中,只有 libs 目录、package.json、README.md才是需要被发布的,因此在发布前我们将不需要提交的文件忽略
# 忽略目录
.DS_Store
node_modules/
example/
packages/
public/
# 忽略指定文件
vue.config.js
babel.config.js
*.map
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
- 接下来的操作您自行往下吧, 对于写插件的您而言,接下来的操作实在是太easy了
npm login
npm publish // 如果是重新发布,请注意修改版本号
- 验证
安装 npm install feeyo-ui 或 yarn add feeyo-ui
使用
在项目中(main.js)导入插件
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import { Alert, Button, Toast } from 'feeyo-ui';
import 'feeyo-ui/lib/feeyo.css';
Vue.use(Alert);
Vue.use(Button);
Vue.prototype.$toast = Toast;
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
在组件中使用
<template>
<div>
<img alt="Vue logo" src="../assets/logo.png">
<fy-alert msg="成功消息提示.."></fy-alert>
<fy-button type="warning" round @click="showMsg">警告按钮</fy-button>
<!-- <button type="button" @click="showMsg">Click</button> -->
</div>
</template>
<script>
export default {
name: 'home',
methods: {
showMsg(){
//this.$toast.success('Hello World!');
this.$toast.success({
msg: '测试信息',
duration: 2000
});
}
}
}
</script>
Vue-cli3搭建项目及使用从头走一波
vue create projectName
cd projectName
yarn serve (此时项目已经启动)
yarn add feeyo-ui-vue (安装ui)
// main.js 引用,此处贴上完整的main.js代码--------------------------
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import { Alert, Button, Toast } from 'feeyo-ui-vue';
import 'feeyo-ui-vue/lib/feeyo.css';
Vue.use(Alert);
Vue.use(Button);
Vue.prototype.$toast = Toast;
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
// 在src/components/HelloWorld.vue 添加测试(此处贴上完整的代码)
<template>
<div>
<h1>{{ msg }}</h1>
<fy-alert msg="成功消息提示.."></fy-alert>
<fy-button type="warning" round @click="showMsg">警告按钮</fy-button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
},
methods:{
showMsg(){
//this.$toast.success('Hello World!');
this.$toast.success({
msg: '测试信息',
duration: 2000
});
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
项目地址
- 码云地址 https://gitee.com/dymin/vue-plugin
- npm地址 https://www.npmjs.com/package/feeyo-ui
路漫漫其修远兮 吾将上下而求索
免责申明:本站发布的内容(图片、视频和文字)以转载和分享为主,文章观点不代表本站立场,如涉及侵权请联系站长邮箱:xbc-online@qq.com进行反馈,一经查实,将立刻删除涉嫌侵权内容。