Vue自定義render統一項目組彈框功能

往期推薦文章(但願各位看官有收穫)javascript

下文代碼篇幅較大部分只需關注帶*號部分,不會影響閱讀體驗html

1、本文收穫

  1. 爲何要取締常規彈框寫法;
  2. 深層參數合併;
  3. pick抽取保存數據;
  4. 如何統一Vue項目彈框及簡化調用

2、爲何要統一封裝彈框;要封裝成怎樣

經過舉例常規彈框的寫法。咱們能夠體會到,一般要彈出一個頁面,須要建立一個頁面normalDialog.vue包裹dialogBody.vue(彈框主體);須要parent.vue設置flag控制彈框顯示隱藏,normalDialog.vue關閉的時候設置parent.vue對應flag。缺點:流程繁雜、配置繁瑣、不靈活、樣式不統一和參數傳遞麻煩等。若是一個項目彈框較多的時候,弊端將會更明顯,大量的isXxxDialogShow,大量的vue文件。所以項目組急需一個能簡單配置就能彈出彈框的APIvue

1. 常規彈框寫法

  1. dialoBody.vue(彈框主體),此處採用Composition API的寫法。只作了簡單的頁面,包含校驗,抽取保存數據的常規邏輯。
<template>
  <div class="dialog-body">
    <div class="item">
      <div>名稱</div>
      <el-input v-model="name"></el-input>
    </div>
    <div class="item">
      <el-radio-group v-model="attention">
        <el-radio label="已關注"></el-radio>
        <el-radio label="等下關注"></el-radio>
      </el-radio-group>
    </div>
    <div class="item">
      <el-radio-group v-model="like">
        <el-radio label="已點贊"></el-radio>
        <el-radio label="等下點贊"></el-radio>
      </el-radio-group>
    </div>
  </div>
</template>

<script>
import { reactive, toRefs } from '@vue/composition-api'
import pick from 'lodash/pick'
import { Message } from 'element-ui'

export default {
  props: {
    defaultName: String,
  },
  setup(props, ctx) {
    const ATTENTIONED = '已關注'
    const LIKED = '已點贊'
    const state = reactive({
      name: props.defaultName, // 名稱
      attention: '已關注', // 關注
      like: '已點贊', // 點贊
    })
    /*************************************************************
     * 頁面綁定的事件
     * 建議寫法:
     * 1. 定義methods常量
     * 2. 處理相關業務邏輯的時候,須要綁定事件到頁面的時
     *    建議經過methods.onXxx = ()=>{ // 相關邏輯 }的形式定義
     *    好處1: onXxx定義的位置和相關業務邏輯代碼關聯一塊兒
     *    好處2: 能夠統一經過...methods的形式在setup統一解構
     *    好處3: 當頁面邏輯複雜,須要操做的數據關聯性強,不可拆解組件;
     *           可將相關業務的代碼在獨立模塊定義;
     *           獨立模塊暴露API handleXxx(methods,state),流水線加工methods;
     *           和Vue2源碼同樣,流水線加工的思想.
     */
    const methods = {}
    // 校驗名稱
    methods.onNameBlur = () => {}

    // ************************ 向外暴露的API ************************
    const apiMethods = {
      // 保存前校驗
      isCanSave() {
        if (state.attention !== ATTENTIONED || state.like !== LIKED) {
          Message.error('未關注或者點贊,不能關閉,嘻嘻')
          return false
        }
        return true
      },
      // 獲取保存數據
      getSaveData() {
        // ******* lodash pick 從對象中抽取數據
        return pick(state, ['name', 'attention', 'like'])
      },
    }
    return {
      ...toRefs(state),
      ...methods,
      apiMethods,
    }
  },
}
</script>

<style lang="less">
.dialog-body {
  width: 100%;
  height: 100px;
}
</style>
複製代碼
  1. normalDialog.vue包裹彈框主體dialoBody.vue
<template>
  <el-dialog 
    title="帥哥,美女,我是標題" 
    :visible.sync="isShow" 
    width="30%" 
    :before-close="onClose"
  >
    <dialog-body default-name="參數傳遞的名稱" ref="inner"></dialog-body>
    <span slot="footer" class="dialog-footer">
      <el-button @click="onClose">取 消</el-button>
      <el-button type="primary" @click="onOK">確 定</el-button>
    </span>
  </el-dialog>
</template>

<script>
import dialogBody from './dialogBody.vue'
export default {
  components: {
    dialogBody,
  },
  data() {
    return {
      isShow: true,
    }
  },
  methods: {
    onClose() {
      // *********** 修改parent.vue ********
      this.$parent.isNormalDialogShow = false
    },
    // ******* 控制保存流程 ********
    onOK() {
      const inner = this.$refs.inner
      // 校驗是否能夠保存
      if (inner.apiMethods.isCanSave()) {
        // 獲取保存數據
        const postData = inner.apiMethods.getSaveData()
        console.log('>>>>> postData >>>>>', postData)
        // 保存成功後關閉彈框
        this.onClose()
      }
    },
  },
}
</script>
複製代碼
  1. parent.vue
// html 部分
<normal-dialog v-if="isNormalDialogShow" />

// Js部分
data(){
	isNormalDialogShow:false
}
methods:{
    onDialogShow(){ // ******控制彈框顯示*****
        this.isNormalDialogShow = true
    }
}
複製代碼

2. 要封裝成怎樣

2.1 API訴求:java

  • 調用簡單;
  • 不用設置isXxxDialogShow
  • 傳參簡便;
  • 能簡便控制el-dialog的樣式屬性;
  • 可控制彈框關閉流程。

2.2 理想API:react

import dialogBody from './dialogBody.vue'
const dialog = new JSDialog({
  comonent: dialogBody, 
  dialogOpts: { // 可擴展配置
    title: 'JSDialog設置的彈框標題',
    width: '400px'
  },
  props: {
    defaultName: 'JSDialog傳遞的參數',
  },
  onOK() {
    const inner = dialog.getInner() // 能取到dialogBody的引用
    // 控制流程
    if (inner.apiMethods.isCanSave()) {
      // 獲取保存數據
      const postData = inner.apiMethods.getSaveData()
      console.log('>>>>> postData >>>>>', postData)
      // 關閉彈框
      dialog.close()
    }
  },
  onCancel() {
    dialog.close() // 彈框關閉
  },
})
dialog.show() // 彈框顯示
複製代碼

3、如何封裝

動態控制顯示內容,腦海浮現的三個方案:卡槽、動態組件和重寫render。下面在動態彈框場景下簡單對比三個方案。vuex

  • slot(卡槽),和el-dialog原理相似,只是再封裝了一層,少定義了normalDialog.vue文件。缺點:調用複雜,不靈活;不容易控制關閉的流程;只能在template中定義
  • component(動態組件),建立commonDialog.vue,統一掛在App.vue下,利用<component :is="componentId"></component>動態切換彈框主體,commonDialog.vue監聽componentId變化來切換彈框主體。缺點:要提早將全部彈框主體組件註冊到commonDialog.vue頁面的components上;依賴於vuex,侵入性較強;純js文件經過vuex彈出彈框相對複雜,不靈活
  • 重寫renderrenderVue對造輪子開發者開放的後門。動態彈框可做爲獨立的功能模塊,內部經過new Vue,重寫render控制渲染內容。獨立Vue實例,可預先建立,可在任何位置控制彈框,靈活,清晰缺點:暫無

1. 總體代碼

先總體預覽一下代碼,下面再細分講解。vue-cli

import Vue from 'vue'
import merge from 'lodash/merge'
import orderBy from 'lodash/orderBy'

// 按鈕配置項構造器
function btnBuilder(options) {
  const defaultBtn = {
    text: '按鈕', // 顯示文本
    clickFn: null, // 點擊回調
    type: 'default', // 樣式
    isHide: false, // 是否隱藏
    order: 2 // 順序
  }
  return { ...defaultBtn, ...options }
}

export default class JSDialog {
  constructor(originOptions) {
    this.options = {}
    this.vm = null
    this._mergeOptions(originOptions)
    this._initVm()
  }
  // 參數合併
  _mergeOptions(originOptions) {
    const defaultOptions = {
      component: '', // 彈框主體vue頁面
      // 可擴展el-dialog官方api全部配置項,小駝峯aaaBbbCcc
      dialogOpts: {
        width: '40%',
        title: '默認標題'
      },
      // 傳入彈框主體vue組件的參數
      props: {},
      // 點擊肯定回調
      onOK: () => {
        console.log('JSDialog default OK'), this.close()
      },
      // 點擊取消回調
      onCancel: () => {
        console.log('JSDialog default cancel'), this.close()
      },
      footer: {
        ok: btnBuilder({
          text: '肯定',
          type: 'primary',
          order: 0
        }),
        cancel: btnBuilder({
          text: '取消',
          order: 1
        })
      }
    }
    // 參數合併到this.options
    merge(this.options, defaultOptions, originOptions)
    const footer = this.options.footer
    Object.entries(footer).forEach(([key, btnOptions]) => {
      // 肯定和取消默認按鈕
      if (['ok', 'cancel'].includes(key)) {
        const clickFn = key === 'ok' ? this.options.onOK : this.options.onCancel
        // 默認按鈕回調優先級: footer配置的clickFn > options配置的onOK和onCancel
        btnOptions.clickFn = btnOptions.clickFn || clickFn
      } else {
        // 新增按鈕
        // 完善配置
        footer[key] = btnBuilder(btnOptions)
      }
    })
  }
  _initVm() {
    const options = this.options
    const beforeClose = this.options.footer.cancel.clickFn // 彈框右上角關閉按鈕回調
    this.vm = new Vue({
      data() {
        return {
          // 須要響應式的數據
          footer: options.footer, // 底部按鈕
          visible: false // 彈框顯示及關閉
        }
      },
      methods: {
        show() {
          // 彈框顯示
          this.visible = true
        },
        close() {
          // 彈框關閉
          this.visible = false
        },
        clearVm() {
          // 清除vm實例
          this.$destroy()
        }
      },
      mounted() {
        // 掛載到body上
        document.body.appendChild(this.$el)
      },
      destroyed() {
        // 從body上移除
        document.body.removeChild(this.$el)
      },
      render(createElement) {
        // 彈框主體
        const inner = createElement(options.component, {
          props: options.props, // 傳遞參數
          ref: 'inner' // 引用
        })
        // 控制按鈕顯示隱藏
        const showBtns = Object.values(this.footer).filter(btn => !btn.isHide)
        // 控制按鈕順序
        const sortBtns = orderBy(showBtns, ['order'], ['desc'])
        // 底部按鈕 jsx 寫法
        const footer = (
          <div slot="footer"> {sortBtns.map(btn => ( <el-button type={btn.type} onClick={btn.clickFn}> {btn.text} </el-button> ))} </div> ) // 彈框主體 const elDialog = createElement( 'el-dialog', { // el-dialog 配置項 props: { ...options.dialogOpts, visible: this.visible, beforeClose }, // **** 看這裏,visible置爲false後,el-dialog銷燬後回調 ***** on: { closed: this.clearVm }, ref: 'elDialog' }, // 彈框內容:彈框主體和按鈕 [inner, footer] ) return elDialog } }).$mount() } // 封裝API // 關閉彈框 close() { this.vm.close() } // 顯示彈框 show() { this.vm.show() } // 獲取彈框主體實例,可訪問實例上的方法 getInner() { return this.vm.$refs.inner } } 複製代碼

2. 參數合併

​ 要作到API訴求中的:調用簡單、傳參簡便和可擴展控制彈框樣式。參數合併即是成本最小的實現方案,配合TS效果更佳。定義默認參數,經過lodashmerge,合併深層屬性。經過參數合併還能作到自定義footer按鈕,控制文本,樣式,順序和執行回調。編程

// 參數合併
_mergeOptions(originOptions) {
  const defaultOptions = {
    component: '', // 彈框主體vue頁面
    // 可擴展el-dialog官方api全部配置項,小駝峯aaaBbbCcc
    dialogOpts: {
      width: '40%',
      title: '默認標題'
    },
    // 傳入彈框主體vue組件的參數
    props: {},
    // 點擊肯定回調
    onOK: () => {
      console.log('JSDialog default OK'), this.close()
    },
    // 點擊取消回調
    onCancel: () => {
      console.log('JSDialog default cancel'), this.close()
    },
    footer: {
      ok: btnBuilder({
        text: '肯定',
        type: 'primary',
        order: 0
      }),
      cancel: btnBuilder({
        text: '取消',
        order: 1
      })
    }
  }
  // 參數合併到this.options
  merge(this.options, defaultOptions, originOptions)
  const footer = this.options.footer
  Object.entries(footer).forEach(([key, btnOptions]) => {
    // 肯定和取消默認按鈕
    if (['ok', 'cancel'].includes(key)) {
      const clickFn = key === 'ok' ? this.options.onOK : this.options.onCancel
      // 默認按鈕回調優先級: footer配置的clickFn > options配置的onOK和onCancel
      btnOptions.clickFn = btnOptions.clickFn || clickFn
    } else { // 新增按鈕
      // 完善配置
      footer[key] = btnBuilder(btnOptions)
    }
  })
}
複製代碼

3. render函數

​ 摘取一段渲染函數 & JSX官方文檔關於render的描述: Vue 推薦在絕大多數狀況下使用模板來建立你的 HTML。然而在一些場景中,你真的須要 JavaScript 的徹底編程的能力。這時你能夠用渲染函數,它比模板更接近編譯器。 ​ ​ 官方文檔對渲染函數的寫法,參數,對應JSX寫法介紹已經很詳細,這裏就再也不贅述。下面代碼是在最新vue-cli建立項目上運行的,嘗試了JS參數建立元素和JSX建立元素兩種寫法。element-ui

render(createElement) {
  // 彈框主體
  const inner = createElement(options.component, {
    props: options.props, // 傳遞參數
    ref: 'inner' // 引用
  })
  // 控制按鈕顯示隱藏
  const showBtns = Object.values(this.footer).filter(btn => !btn.isHide)
  // 控制按鈕順序
  const sortBtns = orderBy(showBtns, ['order'], ['desc'])
  // 底部按鈕 jsx 寫法
  const footer = (
    <div slot="footer"> {sortBtns.map(btn => ( <el-button type={btn.type} onClick={btn.clickFn}> {btn.text} </el-button> ))} </div> ) // 彈框主體 const elDialog = createElement( 'el-dialog', { // el-dialog 配置項 props: { ...options.dialogOpts, visible: this.visible }, on: { closed: this.clearVm }, ref: 'elDialog' }, // 彈框內容:彈框主體和按鈕 [inner, footer] ) return elDialog } 複製代碼

4. 封裝API

​ 暫時只封裝了三個API,可根據不一樣的場景擴展API,好比彈框不銷燬隱藏,彈框刷新等。api

  1. show(),彈框顯示

顯示主要是修改el-dialogvisibletrue,控制掛載到body上的彈框顯示。

show() {
  this.vm.show()
}
複製代碼
  1. close(),彈框關閉

關閉處理流程:修改el-dialogvisiblefalse;觸發el-dialogclosed事件;執行clearVm;執行vm$destroy()destroyed()回調中將$elbody中移除。

close() {
  this.vm.close()
}
複製代碼
  1. getInner(),獲取彈框主體實例,可用於訪問實例上的方法,控制按鈕流程
getInner() {
  return this.vm.$refs.inner
}
複製代碼

4、如何使用

1. 最簡單場景,只配置頁面

按鈕事件回調採用默認的回調,肯定和取消按鈕均可關閉彈框

import dialogBody from './renderJsx/dialogBody'
const dialog = new JSDialog({
  component: dialogBody,
})
dialog.show() // 彈框顯示
複製代碼

效果以下:

2. 控制彈框樣式及肯定流程

可自定義el-dialog支持的配置項,見Dialog 對話框;好比:title、 customClass 。經過customClass可統一控制項目內彈框的風格;可控制肯定取消按鈕代碼回調。

import dialogBody from './renderJsx/dialogBody'
const dialog = new JSDialog({
  component: dialogBody,
  dialogOpts: {
    title: '靚仔,美女歐嗨呦',
    customClass:'js-dialog'
  },
  props: {
    defaultName: 'JSDialog傳遞的參數'
  },
  onOK() {
    const inner = dialog.getInner() // 能取到dialogBody的引用
    // 控制流程
    if (inner.apiMethods.isCanSave()) {
      // 獲取保存數據
      const postData = inner.apiMethods.getSaveData()
      console.log('>>>>> postData >>>>>', postData)
      // 關閉彈框
      dialog.close()
    }
  },
  onCancel() {
    dialog.close() // 彈框關閉
  }
})
複製代碼

效果以下:

3. 自定義footer

自定義按鈕可控制執行回調,樣式,順序,顯示與隱藏

import dialogBody from './renderJsx/dialogBody'
const dialog = new JSDialog({
  component: dialogBody,
  footer: {
    ok: { // 修改默認按鈕
      text: '新增'
    },
    cancel: { // 隱藏默認按鈕
      isHide: true
    },
    add: { // 新增按鈕
      text: '另存爲',
      clickFn() {
        dialog.close()
      },
      order: -1 // 控制按鈕順序,order小的顯示在右邊
    },
    add2: {
      text: '新增按鈕2',
      clickFn() {
        dialog.close()
      },
      order: 3
    }
  }
})
dialog.show() // 彈框顯示
複製代碼

效果以下:

相關文章
相關標籤/搜索