vue外掛-(elementui-dropdown)開源包的開發和釋出

shuaiW發表於2019-02-28

使用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為元件的入口檔案

vue外掛-(elementui-dropdown)開源包的開發和釋出

第二步現在將四個元件通過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,歡迎圍觀

以上就是我的總結啦~~

相關文章