1. 程式人生 > 程式設計 >詳解如何在vue+element-ui的專案中封裝dialog元件

詳解如何在vue+element-ui的專案中封裝dialog元件

1、問題起源

由於 Vue 基於元件化的設計,得益於這個思想,我們在 Vue 的專案中可以通過封裝元件提高程式碼的複用性。根據我目前的使用心得,知道 Vue 拆分元件至少有兩個優點:

1、程式碼複用。

2、程式碼拆分

在基於 element-ui 開發的專案中,可能我們要寫出一個類似的排程彈窗功能,很容易編寫出以下程式碼:

<template>
  <div>
    <el-dialog :visible.sync="cnMapVisible">我是中國地圖的彈窗</el-dialog>
    <el-dialog :visible.sync="usaMapVisible">我是美國地圖的彈窗</el-dialog>
    <el-dialog :visible.sync="ukMapVisible">我是英國地圖的彈窗</el-dialog>
    <el-button @click="openChina">開啟中國地圖</el-button>
    <el-button @click="openUSA">開啟美國地圖</el-button>
    <el-button @click="openUK">開啟英國地圖</el-button>
  </div>
</template>
<script>
export default {
  name: "View",data() {
    return {
      // 對百度地圖和谷歌地圖的一些業務處理程式碼 省略
      cnMapVisible: false,usaMapVisible: false,ukMapVisible: false,};
  },methods: {
    // 對百度地圖和谷歌地圖的一些業務處理程式碼 省略
    openChina() {},openUSA() {},openUK() {},},};
</script>

上述程式碼存在的問題非常多,首先當我們的彈窗越來越多的時候,我們會發現此時需要定義越來越多的變數去控制這個彈窗的顯示或者隱藏。

由於當我們的彈窗的內部還有業務邏輯需要處理,那麼此時會有相當多的業務處理程式碼夾雜在一起(比如我呼叫中國地圖我需要用高德地圖或者百度地圖,而呼叫美國、英國地圖我只能用谷歌地圖,這會使得兩套業務邏輯分別位於一個檔案,嚴重加大了業務的耦合度)

我們按照分離業務,降低耦合度的原則,將程式碼按以下思路進行拆分:

1、View.vue

<template>
  <div>
    <china-map-dialog ref="china"></china-map-dialog>
    <usa-map-dialog ref="usa"></usa-map-dialog>
    <uk-map-dialog ref="uk"></uk-map-dialog>
    <el-button @click="openChina">開啟中國地圖</el-button>
    <el-button @click="openUSA">開啟美國地圖</el-button>
    <el-button @click="openUK">開啟英國地圖</el-button>
  </div>
</template>
<script>
export default {
  name: "View",data() {
    return {
      /**
       將地圖的業務全部抽離到對應的dialog裡面去,View只存放排程業務程式碼
      */
    };
  },methods: {
    openChina() {
      this.$refs.china && this.$refs.china.openDialog();
    },openUSA() {
      this.$refs.usa && this.$refs.usa.openDialog();
    },openUK() {
      this.$refs.uk && this.$refs.uk.openDialog();
    },};
</script>

2、ChinaMapDialog.vue

<template>
  <div>
    <el-dialog :visible.sync="baiduMapVisible">我是中國地圖的彈窗</el-dialog>
  </div>
</template>
<script>
export default {
  name: "ChinaMapDialog",data() {
    return {
      // 對中國地圖業務邏輯的封裝處理 省略
      baiduMapVisible: false,methods: {
    // 對百度地圖和谷歌地圖的一些業務處理程式碼 省略
    openDialog() {
      this.baiduMapVisible = true;
    },closeDialog() {
      this.baiduMapVisible = false;
    },};
</script>

3、由於此處僅僅展示虛擬碼,且和 ChinaMapDialog.vue 表達的含義一致, 為避免篇幅過長 USAMapDialog.vue 和 UKMapDialog.vue 已省略

2、問題分析

我們通過對這幾個彈窗的分析,對剛才的設計進行抽象發現,這裡面都有一個共同的部分,那就是我們對 dialog 的操作程式碼都是可以重用的程式碼,如果我們能夠編寫出一個抽象的彈窗,
然後在恰當的時候將其和業務程式碼進行組合,就可以實現 1+1=2 的效果。

3、設計

由於 Vue 在不改變預設的 mixin 原則(預設也最好不要改變,可能會給後來的維護人員帶來困惑)的情況下,如果在混入過程中發生了命名衝突,預設會將方法合併(資料物件在內部會進行遞迴合併,並在發生衝突時以元件資料優先),因此,mixin 無法改寫本來的實現,而我們期望的是,父類提供一個比較抽象的實現,子類繼承父類,若子類需要改表這個行為,子類可以重寫父類的方法(多型的一種實現)。

因此我們決定使用 vue-class-component 這個庫,以類的形式來編寫這個抽象彈窗。

import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "AbstractDialog",})
export default class AbstractDialog extends Vue {}

3.1 事件處理

檢視 Element-UI 的官方網站,我們發現 ElDialog 對外丟擲 4 個事件,因此,我們需要預先接管這 4 個事件。
因此需要在我們的抽象彈窗裡預設這個 4 個事件的 handler(因為對於元件的行為的劃分,而對於彈窗的處理本來就應該從屬於彈窗本身,因此我並沒有通過$listeners 去穿透外部呼叫時的監聽方法)

import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "AbstractDialog",})
export default class AbstractDialog extends Vue {
  open() {
    console.log("彈窗開啟,我啥也不做");
  }

  close() {
    console.log("彈窗關閉,我啥也不做");
  }

  opened() {
    console.log("彈窗開啟,我啥也不做");
  }

  closed() {
    console.log("彈窗關閉,我啥也不做");
  }
}

3.2 屬性處理

dialog 有很多屬性,預設我們只需要關注的是 before-close 和 title 兩者,因為這兩個屬性從職責上劃分是從屬於彈窗本身的行為,所以我們會在抽象彈窗裡面處理開關和 title 的任務

import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "AbstractDialog",})
export default class AbstractDialog extends Vue {
  visible = false;

  t = "";

  loading = false;

  //定義這個屬性的目的是為了實現既可以外界通過傳入屬性改變dialog的屬性,也支援元件內部預設dialog的屬性
  attrs = {};

  get title() {
    return this.t;
  }

  setTitle(title) {
    this.t = title;
  }
}

3.3 slots 的處理

檢視 Element-UI 的官方網站,我們發現,ElDialog 有三個插槽,因此,我們需要接管這三個插槽

1、對 header 的處理

import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "AbstractDialog",})
class AbstractDialog extends Vue {
  /*
   構建彈窗的Header
   */
  _createHeader(h) {
    // 判斷在呼叫的時候,外界是否傳入header的插槽,若有的話,則以外界傳入的插槽為準
    var slotHeader = this.$scopedSlots["header"] || this.$slots["header"];
    if (typeof slotHeader === "function") {
      return slotHeader();
    }
    //若使用者沒有傳入插槽,則判斷使用者是否想改寫Header
    var renderHeader = this.renderHeader;
    if (typeof renderHeader === "function") {
      return <div slot="header">{renderHeader(h)}</div>;
    }
    //如果都沒有的話,返回undefined,則dialog會使用我們預設好的title
  }
}

2、對 body 的處理

import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "AbstractDialog",})
class AbstractDialog extends Vue {
  /**
   * 構建彈窗的Body部分
   */
  _createBody(h) {
    // 判斷在呼叫的時候,外界是否傳入default的插槽,若有的話,則以外界傳入的插槽為準
    var slotBody = this.$scopedSlots["default"] || this.$slots["default"];
    if (typeof slotBody === "function") {
      return slotBody();
    }
    //若使用者沒有傳入插槽,則判斷使用者想插入到body部分的內容
    var renderBody = this.renderBody;
    if (typeof renderBody === "function") {
      return renderBody(h);
    }
  }
}

3、對 footer 的處理

由於 dialog 的 footer 經常都有一些相似的業務,因此,我們需要把這些重複率高的程式碼封裝在此,若在某種時候,使用者需要改寫 footer 的時候,再重寫,否則使用預設行為

import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "BaseDialog",})
export default class BaseDialog extends Vue {
  showLoading() {
    this.loading = true;
  }

  closeLoading() {
    this.loading = false;
  }

  onSubmit() {
    this.closeDialog();
  }

  onClose() {
    this.closeDialog();
  }

  /**
   * 構建彈窗的Footer
   */
  _createFooter(h) {
    var footer = this.$scopedSlots.footer || this.$slots.footer;
    if (typeof footer == "function") {
      return footer();
    }
    var renderFooter = this.renderFooter;
    if (typeof renderFooter === "function") {
      return <div slot="footer">{renderFooter(h)}</div>;
    }

    return this.defaultFooter(h);
  }

  defaultFooter(h) {
    return (
      <div slot="footer">
        <el-button
          type="primary"
          loading={this.loading}
          on-click={() => {
            this.onSubmit();
          }}
        >
          儲存
        </el-button>
        <el-button
          on-click={() => {
            this.onClose();
          }}
        >
          取消
        </el-button>
      </div>
    );
  }
}

最後,我們再通過 JSX 將我們編寫的這些程式碼組織起來,就得到了我們最終想要的抽象彈窗
程式碼如下:

import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "AbstractDialog",})
export default class AbstractDialog extends Vue {
  visible = false;

  t = "";

  loading = false;

  attrs = {};

  get title() {
    return this.t;
  }

  setTitle(title) {
    this.t = title;
  }

  open() {
    console.log("彈窗開啟,我啥也不做");
  }

  close() {
    console.log("彈窗關閉,我啥也不做");
  }

  opened() {
    console.log("彈窗開啟,我啥也不做");
  }

  closed() {
    console.log("彈窗關閉,我啥也不做");
  }

  showLoading() {
    this.loading = true;
  }

  closeLoading() {
    this.loading = false;
  }

  openDialog() {
    this.visible = true;
  }

  closeDialog() {
    if (this.loading) {
      this.$message.warning("請等待操作完成!");
      return;
    }
    this.visible = false;
  }

  onSubmit() {
    this.closeDialog();
  }

  onClose() {
    this.closeDialog();
  }

  /*
   構建彈窗的Header
   */
  _createHeader(h) {
    var slotHeader = this.$scopedSlots["header"] || this.$slots["header"];
    if (typeof slotHeader === "function") {
      return slotHeader();
    }
    var renderHeader = this.renderHeader;
    if (typeof renderHeader === "function") {
      return <div slot="header">{renderHeader(h)}</div>;
    }
  }

  /**
   * 構建彈窗的Body部分
   */
  _createBody(h) {
    var slotBody = this.$scopedSlots["default"] || this.$slots["default"];
    if (typeof slotBody === "function") {
      return slotBody();
    }
    var renderBody = this.renderBody;
    if (typeof renderBody === "function") {
      return renderBody(h);
    }
  }

  /**
   * 構建彈窗的Footer
   */
  _createFooter(h) {
    var footer = this.$scopedSlots.footer || this.$slots.footer;
    if (typeof footer == "function") {
      return footer();
    }
    var renderFooter = this.renderFooter;
    if (typeof renderFooter === "function") {
      return <div slot="footer">{renderFooter(h)}</div>;
    }

    return this.defaultFooter(h);
  }

  defaultFooter(h) {
    return (
      <div slot="footer">
        <el-button
          type="primary"
          loading={this.loading}
          on-click={() => {
            this.onSubmit();
          }}
        >
          儲存
        </el-button>
        <el-button
          on-click={() => {
            this.onClose();
          }}
        >
          取消
        </el-button>
      </div>
    );
  }

  createContainer(h) {
    //防止外界誤傳引數影響彈窗本來的設計,因此,需要將某些引數過濾開來,有title beforeClose, visible
    var { title,beforeClose,visible,...rest } = Object.assign({},this.$attrs,this.attrs);
    return (
      <el-dialog
        {...{
          props: {
            ...rest,visible: this.visible,title: this.title || title || "彈窗",beforeClose: this.closeDialog,on: {
            close: this.close,closed: this.closed,opened: this.opened,open: this.open,}}
      >
        {/* 根據JSX的渲染規則 null、 undefined、 false、 '' 等內容將不會在頁面顯示,若createHeader返回undefined,將會使用預設的title */}
        {this._createHeader(h)}
        {this._createBody(h)}
        {this._createFooter(h)}
      </el-dialog>
    );
  }

  render(h) {
    return this.createContainer(h);
  }
}

4.應用

4.1元件呼叫

我們就以編寫 ChinaMapDialog.vue 為例,將其進行改寫

<script>
import Vue from "vue";
import AbstractDialog from "@/components/AbstractDialog.vue";
import Component from "vue-class-component";
@Component({
  name: "ChinaMapDialog",})
class ChinaMapDialog extends AbstractDialog {
  get title() {
    return "這是中國地圖";
  }
  
  attrs = {
   width: "600px",}

  //編寫一些中國地圖的處理業務邏輯程式碼

  //編寫彈窗的內容部分
  renderBody(h) {
    return <div>我是中國地圖,我講為你呈現華夏最壯麗的美</div>;
  }
}
</script>

4.2 使用 Composition API

由於我們是通過元件的例項呼叫元件的方法,因此我們每次都需要獲取當前元件的 refs 上面的屬性,這樣會使得我們的呼叫特別長,寫起來也特別麻煩。
我們可以通過使用 Composition API 來簡化這個寫法

<template>
  <div>
    <china-map-dialog ref="china"></china-map-dialog>
    <usa-map-dialog ref="usa"></usa-map-dialog>
    <uk-map-dialog ref="uk"></uk-map-dialog>
    <el-button @click="openChina">開啟中國地圖</el-button>
    <el-button @click="openUSA">開啟美國地圖</el-button>
    <el-button @click="openUK">開啟英國地圖</el-button>
  </div>
</template>
<script>
import { ref } from "@vue/composition-api";
export default {
  name: "View",setup() {
    const china = ref(null);
    const usa = ref(null);
    const uk = ref(null);
    return {
      china,usa,uk,methods: {
    // 對百度地圖和谷歌地圖的一些業務處理程式碼 省略
    openChina() {
      this.china && this.china.openDialog();
    },openUSA() {
      this.usa && this.usa.openDialog();
    },openUK() {
      this.uk && this.uk.openDialog();
    },};
</script>

總結

開發這個彈窗所用到的知識點:
1、面向物件設計在前端開發中的應用;
2、如何編寫基於類風格的元件(vue-class-component 或 vue-property-decorator);
3、JSX 在 vue 中的應用;
4、$attrs和$listeners 在開發高階元件(個人叫法)中的應用;
5、slots 插槽,以及插槽在 JSX 中的用法;
6、在 Vue2.x 中使用 Composition API;

到此這篇關於詳解如何在vue+element-ui的專案中封裝dialog元件的文章就介紹到這了,更多相關vue element封裝dialog內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!