当前位置:
首页
文章
前端
详情

从零搭建自己的Vue-ui插件库

嗨,又出了一个新的前端框架,又出来一个UI框架,劳资学不动了;

项目中大家有没有遇到过这种情况,为了一个极小的功能,引入一个很大的库;为了使用一个confirm确认框在项目中引入了Layui, 虽然解决了产品的需求,但终究不是前端该有的正确姿势;一个项目中混杂着各种各样的库,如: jQuery\Bootstrap\LayerUI\elementUI\CustomUI….显得是百花齐放(鱼虾混杂),后端看到代码后忍不住称赞(吐槽)一声前端真牛逼(垃圾),。。。多么痛的领悟呀!

手写自己的Vue组件

高大上的话不多说了,拿来主义虽好,可不可贪恋(劲酒虽好,可不要贪杯)哦^_^, 知其然且知其所以然,方能在芸芸众生中来去自如; 借鉴ElementUI,沿着vue官网的脉络,尝试自己怎么手写一个很简单的vue组件;以下以最简单的alert\button\toast为例,看看如何编织自己的小鱼(麻雀虽小,五脏俱全);

先看下vue官网对于Vue插件的定义和描述:插件通常用来为 Vue 添加[全局]功能插件的功能范围没有严格的限制——一般有下面几种:

  1. 添加全局方法或者属性
  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.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) =&gt; {
  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 =&gt; 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) =&gt; {
        Vue.rewrite = () =&gt; {
            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 =&gt; 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) =&gt; {
    options = options || {};
    if(typeof options === 'string') options = { 'msg': options };
    return options;
};

// 添加Dom到文档
let ToastComponent = (options) =&gt; {

    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) =&gt; {
    ToastComponent[type] = (options) =&gt; {
        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 =&gt; h(App)
}).$mount('#app')
  • 通过use的方式使用需要提前将组件挂载在Vue.prototype上
export default {
    install: (Vue, options) =&gt; {
        Vue.prototype.$toast = (options)=&gt; {
            // ...
        };
    }
};
  • 在示例组件示例中使用插件
<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)

打包为插件库

  1. 整合所有的组件, 对外导出为一个完整的插件库
    // 在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 =&gt; {
        Vue.component(component.name, component)
      });

      Vue.prototype.$toast = Toast;
    };

    // 判断是否是直接引入文件
    if (typeof window !== 'undefined' &amp;&amp; window.Vue) {
        install(window.Vue);
    }

    // 直接export对象,在使用时为按需加载...
    export {
      // 导出的对象必须具有 install,才能被 Vue.use() 方法安装
      install,

      // 以下是具体的组件列表
      Alert,
      Toast,
      Button  
    };
  • 新增打包执行命令
    // 根目录下打开命令行窗口, 执行编译命令 yarn dopack, 命令执行完成后, 会发现目录下多出一个lib目录(即包文件)
  1. --target: 构建目标,默认为应用模式; 这里修改为 lib 启用库模式
  2. --dest : 输出目录,默认 dist,这里我们改成 lib
  3. --name 包里面的文件名
  4. [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 =&gt; {
        config.module
          .rule('js')
          .include
            .add('/packages')
            .end()
          .use('babel')
            .loader('babel-loader')
            .tap(options =&gt; {
              // 修改它的选项...
              return options
            })
      }
    }
  1. 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 =&gt; 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进行反馈,一经查实,将立刻删除涉嫌侵权内容。