vue插件-(elementui-dropdown)开源包的开发和发布

使用vue开发过几个项目了,对vue感觉很好,不愧为开源世界华人的骄傲(顺便膜拜下尤大~~ )

Vue是一个数据驱动页面的一个框架,他的双向绑定原理使我们开发页面更简单

总结起来的几大特点:1.简洁2.轻量3.快速4.数据驱动5.模块友好6.组件化

在这整理一篇vue的丑陋版的下拉框组件(因为还没加更加详细的功能),用作学习vue的笔记

开发组件的设计原则:(摘自elementUI)

- 一致性 Consistency

与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;

在界面中一致:所有的元素和结构需保持一致,比如:设计样式、图标和文本、元素的位置等。

- 反馈 Feedback

控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;

页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。

- 效率 Efficiency

简化流程:设计简洁直观的操作流程;

清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;

帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。

- 可控 Controllability

用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;

结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。

不仅要遵循以上原则,还必须要禁用vue全家桶,组件要遵循低耦合性

以下为发开下拉框的几个步骤:

1. 安装vue脚手架,官网上有详细的说明,略过...

我没有用单元测试,直接将组件上到app.vue中了

2.按照elementui的组件写法写好APP.vue等待开发

<template>
  <div id="app">
    <div class="main">
      <my-dropdown @command="handleCommand">
        <my-button>
          {{smyectedValue}}<i class="my-icon-arrow-down"></i>
        </my-button>
        <my-dropdown-menu  slot="dropdown">
          <my-dropdown-item  v-for="(item,i) in dropdownData" :key="i" :command="item.id">{{item.value}}</my-dropdown-item>
        </my-dropdown-menu>
      </my-dropdown>
    </div>
  </div>
</template>

<script>
import MyButton from "./components/button";
import MyDropdown from "./components/dropdown";
import MyDropdownMenu from "./components/dropdown-menu";
import myDropdownItem from "./components/dropdown-item";

export default {
  name: "App",
  components: { MyButton, MyDropdown, MyDropdownMenu, myDropdownItem },
  data() {
    return {
      smyectedValue: "请选择",
      dropdownData: [
        { id: 1000, value: "我是选项一" },
        { id: 1001, value: "我是选项二" },
        { id: 1002, value: "我是选项三" },
        { id: 1003, value: "我是选项四" },
        { id: 1004, value: "我是选项五" }
      ]
    };
  },
  methods:{
    handleCommand(command){
      console.log(`我被点击了,command为${command}`);
      this.dropdownData.every(ele=>{
        if(command == ele.id){
          this.smyectedValue = ele.value
          return false
        }
        return true
      })
    }
  }
};
</script>

<style>
#app {
  font-family: "Avenir", Hmyvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
.main {}
</style>

复制代码

3.写入button.vue、dropdown.vue、dropdown-ment.vue、dropdown-item.vue初始元素架构,准备开发了

因文件太多,就不贴代码了,放上GitHub的开发日志链接

button.vue dropdown-item.vue dropdown-menu.vue dropdown.vue

现在基本已经有一个大致的内容了

4.在dropdown.vue中先给按钮加上点击事件,可以正确打印出显示隐藏的状态

dropdown.vue

    init() {
      this.buttonEl = this.$slots.default[0].elm; //组件按钮
      this.dropdownEl = this.$slots.dropdown[0].elm; //组件下拉框
    },
    initEvent() {
      let { buttonEl, dropdownEl } = this;
      buttonEl.addEventListener("click", this.handleClick); //设置按钮点击显示隐藏
    },
    handleClick() {
      this.visible ? this.hide() : this.show();
    },
    hide() {
      console.log("hide");
      this.visible = false;
    },
    show() {
      console.log("show");
      this.visible = true;
    }
复制代码

5.通过dropdown.vue将button与dropdown-item.vue连通起来,实现组件间的通信

具体做法为

1.在dropdown中为button绑定click事件,设置下拉组件的visible为true和false,通过Vue的watch钩子来监听visible的变化来向子组件dropdown-menu.vue来emit一个事件,在dropdown-menu.vue生命周期中只要事先绑定好了这个事件,就会接收到事件反馈和参数传递,通过参数的值来判断是否显示和隐藏menu,方法见(broadcast与dispatch)

dropdown.vue

//这是dropdown.vue
  methods: {
        //...
     /**
     * @description 递归遍历当前组件下面所有与组件名称相匹配的组件,并触发目标事件并传参
     * @param {Component} target 当前组件
     * @param {String} componentName 组件名称
     * @param {String} eventName 需要触发的事件名称
     * @param {any[]} params 需要传递的参数
     * @return {void}
     */
    broadcast(children, componentName, eventName, params) {
      let me = this;
      children.forEach(function(child) {
        var name = child.$options.componentName;
        if (name === componentName) {
          child.$emit.apply(child, [eventName].concat(params));
        } else {
          me.broadcast(child.$children, componentName, eventName, params);
        }
      });
    }
  },
  watch: {
    visible(val) {
      console.log(`即将为${val ? "显示" : "隐藏"}状态`);
      this.broadcast(this.$children, "MyDropdownMenu", "visible", val);
    }
  },
  
  //这是dropdown-menu.vue 
    methods:{
        //...
        initEvent(){
          this.$on('visible',val=>{
            console.log(`现在为${val?'显示':'隐藏'}状态`)
            this.showPopper = val
          })
        }
    }
复制代码

2.在dropdown.vue中绑定一个item中的自定义点击事件,当触发该方法是隐藏整个menu 这两个方法的关键点在父子组件之间的通信),整个下拉组件四个子组件的通信实现

dropdown-item.vue

//dropdown-item.vue
    methods: {
        handleClick() {
          this.dispatch(this.$parent, "MyDropdown", "menu-item-click", [
            this.command,
            this
          ]);
        },
        /**
         * @description 逆向寻找当前组件的父组件,然后emit事件并传递参数
         * @param {Component} target 当前组件
         * @param {String} componentName 组件名称
         * @param {String} eventName 需要触发的事件名称
         * @param {any[]} params 需要传递的参数
         * @return {void}
         */
        dispatch(target, componentName, eventName, params) {
          let name;
          if( !(name = parent.$options.componentName))return
          if (name === componentName) {
            target.$emit.apply(target, [eventName].concat(params));
          } else {
            this.dispatch(target.$parent, componentName, eventName, params);
        }
    }
//dropdown.vue
methods: {
     //...
    initEvent() {
        //...
      this.$on('menu-item-click', this.handleMenuItemClick); //注册下拉菜单点击事件
    },
  },

复制代码

6.因为下拉框受父元素的高度和overflow影响,不能作为button的兄弟组件存在,现在将menu渲染到body中,并给出美观的css(copy自elementUI),然后根据button的位置绝对定位到具体位置

具体做法为

1.设置menu的position:absolute;

2.获取button的位置信息--使用getBoundingClientRect方法可以获取元素在窗口的相对位置和元素的宽高(但不是在文档流中的位置,所以绝对定位时要加上window的scrollTop和scrollLeft)

3.如果元素的display为none时,getBoundingClientRect方法是无法获取物理尺寸的,所以,实现menu与button的右对齐还需要算出menu的宽度,所以用jQuery的方法为将元素设置visible为hidden,然后将其脱离文档流获取尺寸后在还原回去

dropdown-menu.vue

//dropdown-menu.vue
methods: {
    init() {
      this.buttonEl = this.$parent.$children[0].$el; //获取按钮
      this.dropdownEl = this.$el; //下拉框组件
      document.body.appendChild(this.$el); //将组件挂载到body中去
    },
    initEvent() {
      this.$on("visible", val => {
        console.log(`现在为${val ? "显示" : "隐藏"}状态`);
        this.showPopper = val;
        val && this.$emit("update", val);
      });
      this.$on("update", val => {
        val && this.update(this.buttonEl, this.dropdownEl);
      });
    },
    /**
     * @description 根据目标元素更新挂载元素的位置
     * @param {Element} $el 目标元素
     * @param {Element} $target 挂载元素
     * @return {Void}
     */
    update($el, $target) {
      if (!$el || !$target) return;
      let {
        bottom: $elBottom,
        height: $elHeight,
        left: $elLeft,
        right: $elRight,
        top: $elTop,
        width: $elWidth
      } = this.getBoundingClientRect($el);
      let {
        bottom: $targetBottom,
        height: $targetHeight,
        left: $targetLeft,
        right: $targetRight,
        top: $targetTop,
        width: $targetWidth
      } = this.getBoundingClientRect($target);
      let scrollTop = window.scrollY;
      let scrollLeft = window.scrollX;
      $target.style.top = `${$elTop + $elHeight + scrollTop}px`;
      $target.style.left = `${$elRight - $targetWidth + scrollLeft}px`;
    },
    /**
     * @description 获取元素的bottom、height、left、right、top、width属性
     * @param {Element} $el  目标元素
     * @return {ClientRect}
     */
    getBoundingClientRect($el) {
      let style = $el.style;
      if (style.display === "none") {
        let _addCss = {
          display: "",
          position: "absolute",
          visibility: "hidden"
        };
        let _oldCss = {};
        for (let i in _addCss) {
          _oldCss[i] = style[i];
          style[i] = _addCss[i];
        }
        let clientRect = $el.getBoundingClientRect();
        for (let i in _oldCss) {
          style[i] = _oldCss[i];
        }
        return clientRect;
      }
      return $el.getBoundingClientRect();
    }
 }
复制代码

第七步现在大致的定位和样式以及交互都完成了,现在如果button的父组件或祖父组件可以拉滚动条的话,在滚动时需要实时跟新menu的位置

具体做法为:

由button向上递归获取父元素,并为他们绑定滚动监听,如果下拉框是显示的状态。则需要不断的更新下拉框的位置 dropdown-menu.vue

//dropdown-menu.vue
methods: {
        //...
    initEvent() {
        //...
      //实现滑动滚动条时,下拉框能跟随按钮一起滑动定位
      this.bindUpdate(this.buttonEl, () => {
        this.showPopper && this.update(this.buttonEl, this.dropdownEl);
      });
    },
    /**
     * @description 逆向寻找当前组件的父组件,然后emit事件并传递参数
     * @param {Component} $el 当前组件
     * @param {Component} $target 组件名称
     * @param {($el,$target)=>void} callback 回调函数
     * @return {void}
     */
    bindUpdate($el, callback) {
      let target;
      if ((target = $el.parentElement)) {
        target.addEventListener("scroll", callback);
        this.bindUpdate(target, callback);
      }
    }
}
复制代码

第八步为将一些可以重用的方法提出到util中,作为组件的混合使用

mixins.js

第九步添加一些tab键聚焦和按键操作,整个流程走完了,组件基本功能算是完善了 dropdown.vue

methods: {
    init() {
      this.buttonEl = this.$slots.default[0].elm; //组件按钮
      this.dropdownEl = this.$slots.dropdown[0].elm; //组件下拉框
      this.dropdownItem = this.dropdownEl.querySelectorAll("li")
    },
    initEvent() {
      let {
        buttonEl,
        dropdownEl,
        dropdownItem,
        hide,
        handleClick,
        handleKeyDown,
        handleMenuKeyDown
      } = this
      buttonEl.addEventListener("click", handleClick); //按钮点击设置显示隐藏
      buttonEl.addEventListener("keydown", handleKeyDown); //按钮键盘事件监听
      dropdownEl.addEventListener("keydown", handleMenuKeyDown); //下拉菜单键盘事件监听
      document.addEventListener("click", event => {
        if (event.target != buttonEl) {
          this.visible && hide()
        }
      }) //点击整个窗口下拉框消失
      this.$on("menu-item-click", this.handleMenuItemClick) //注册下拉菜单点击事件
    },
    handleClick() {
      this.visible ? this.hide() : this.show()
    },
    handleKeyDown(event) {
      let keyCode = event.keyCode
      if (keyCode == 38 || keyCode == 40) {
        // up|down
        this.resetIndex(0)
        this.dropdownItem[0].focus()
        event.preventDefault()
        event.stopPropagation()
      }
      return;
    },
    handleMenuKeyDown(event) {
      let keyCode = event.keyCode
      let currentIndex = [].indexOf.call(this.dropdownItem, event.target)
      let max = this.dropdownItem.length - 1
      let nextIndex

      if (keyCode == 38 || keyCode == 40) {
        // up|down
        if (keyCode === 38) {
          // up
          nextIndex = currentIndex !== 0 ? currentIndex - 1 : 0
        } else {
          // down
          nextIndex = currentIndex < max ? currentIndex + 1 : max
        }
        this.removeIndex()
        this.resetIndex(nextIndex)
        this.dropdownItem[nextIndex].focus()
        event.preventDefault()
        event.stopPropagation()
      } else if (keyCode === 13) {
        //enter选中
        event.target.click()
      }
      return;
    },
    removeIndex() {
      this.dropdownItem.forEach(ele => {
        ele.setAttribute("tabindex", "-1")
      })
    },
    resetIndex(index) {
      this.dropdownItem[index].setAttribute("tabindex", "0")
    },
    hide() {
      console.log("hide")
      this.visible = false
      this.removeIndex()
    },
    show() {
      console.log("show")
      this.visible = true
    },
    handleMenuItemClick(command, instance) {
      this.hide()
      this.$emit("command", command, instance)
    }
 },
复制代码

将下拉框插件化,并发布到npm上

第一步构建这样的目录结构,根目录的index.js为组件的入口文件

第二步现在将四个组件通过index.js文件export出去,因为vue.use方法会调用组件export中暴露的install方法,所以在index.js中添加install方法,用来将插件组件化

index.js

import MyButton from './packages/button'
import MyDropdownItem from './packages/dropdown-item'
import MyDropdownMenu from './packages/dropdown-menu'
import MyDropdown from './packages/dropdown'

const components = [
    MyButton, MyDropdownItem, MyDropdownMenu, MyDropdown
]
const install = function(Vue, opts = {}) {
    components.map(component => {
        Vue.component(component.name, component)
    })
}

if (typeof window !== 'undefined' && window.Vue) {
    install(window.Vue)
}
export default {
    MyButton,
    MyDropdownItem,
    MyDropdownMenu,
    MyDropdown,
    install
}
复制代码

第三步可以将这个插件发布到npm上去啦 (暂时还没有加types~~)

具体做法为在组件组件根目录执行$ npm init后执行$ npm publish

项目安装:

$ npm install my-dropdown --save-dev
复制代码

项目GitHub地址:vue-dropdown,欢迎围观

以上就是我的总结啦~~

转载于:https://juejin.im/post/5abbabc2f265da23986753e2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值