HelloChunWei / blog

個人部落格,紀錄自己的知識
MIT License
7 stars 0 forks source link

用Vue3 實做一個簡單的 modal hook 吧 (3) #13

Open HelloChunWei opened 2 years ago

HelloChunWei commented 2 years ago

前言

在第二篇中我們可以動態決定呼叫哪個 modal 以及關閉 modal,(前情提要)但現在還是遇到了兩個問題:

  1. 如果 modal 中有 props 的話該怎麼傳遞?
  2. 以及將close function 重構

那麼我們今天就來把這兩個問題給解決吧:

傳遞 props

我們可以利用 v-bind 的方式將 props 傳下去。那我們預計在 index.vue 中是這樣的:

<template>
    <!-- 這裡也用 v-if 決定是否顯示,然後利用 is 決定哪個 modal component -->
    <!-- 將closeModal 在傳入 component -->
    <component v-if="isShow" :is="componentName" v-bind="myProps" @closeModal="closeModal" />
</template>

<script lang="ts">
import { defineComponent, defineAsyncComponent, toRefs } from 'vue'

export default defineComponent({
  name: 'ModalIndex',
  props: ['isShow', 'component', 'closeModal', 'myProps'],
  setup(props) {
    const { component } = toRefs(props)
    const componentName = defineAsyncComponent(() => import(`./${component.value}.vue`))

    return {
      componentName
    }
  }
})
</script>

會在 index.vue 中多了一個 myProps 的參數,這代表這各個 modal 中自己定義的 props,然後利用 v-bind的方式傳下去。接下來修改一下 modal/index.ts:

import { createApp } from 'vue'
// 改用 index.vue
import Index from './index.vue'

type componentNameType = 'testModal'

export const useModal = () => {
  // 在呼叫openModal 的時候順便指定是要用哪一個 modal
  // component 利用 type 去指定,那目前只有 testModal 所以就先指定為 testModal
  const openModal = ({ component, myProps }: { component: componentNameType, myProps: any }) => {
    const container = document.createElement('div')
    // 直接remove
    const closeModal = () => {
      document.body.removeChild(container)
    }
    // 將 prop 傳入
    const vnode = createApp(Index, {
        isShow: true,
        component,
        closeModal,
        myProps
        // 將 closeModal 傳入
    })
    document.body.appendChild(container)
    vnode.mount(container)
  }
  return {
    openModal
  }
}

然後我們使用 openModal 的時候就是這樣:

openModal({ component: 'testModal', myProps: { user_id: 1 } })

那麼這樣我們就可以直接把 props 傳下去囉。所以這樣我們第一個問題就解決囉

重構close function

目前的 close function 是利用 props 的方式傳下去.但這樣的寫法是非常不好的,雖然目前是在 template.vue 中利用 slot 的方式將 close 傳遞出來,但萬一有其他 component 要使用的話還是得用 props的方式傳遞。所以我希望這邊可以把他抽離出來。在這我使用 provide/inject 去解決,我們 provide close function,然後在 compoment 去inject,這樣就不用用 props 的方式去傳遞了。

但是: 理想很豐滿,現實很骨感

provide/inject 只限於在 setup 中使用,目前我們的 index.ts 是利用

const vnode = createApp(Index, {
    isShow: true,
    component,
    closeModal,
    myProps
})

這種方式去建立 instance ,而這種方式我查了一下是無法使用 provide/inject 的,所以我們想要使用的話,index.ts 必須重構一番。

當初在這裡我想了大概兩天,都沒有想到一個方法去優化它,最後我在翻 element-ui 中的原始碼找到了靈感,他的 loading component 恰恰好解決我的問題: 原始碼

簡單來說就是在 mock 起一個 component 並利用 render function 的方式去掛載我們真正想要的 component,而 mock 的元件中就可以使用 setup 的 function, 既然知道原理了,我們就來修改一下吧:

export const useModal = () => {

  const openModal = ({ component, myProps }: { component: componentNameType, myProps: any }) => {
    const container = document.createElement('div')

    const _closeModal = function () {
      data.isShow = false
      container.parentNode!.removeChild(container)
    }

    const data = reactive({
      isShow: true,
      component,
      myProps,
      close: _closeModal
    })

    const mockModal = {
      name: 'mockModal',
      setup () {
        return {
          ...toRefs(data)
        }
      },
      render () {
        return h(Index, {
          isShow: data.isShow,
          component: data.component,
          myProps: data.myProps,
        })
      }
    }

    const vnode = createApp(mockModal)
    document.body.appendChild(container)
    vnode.mount(container)
  }
  return {
    openModal
  }
}

將所有的 data 用 reactive 包起來,並且宣告一個 mockModal,然後利用 render function 把真正要的 component 渲染,至於為什麼data 為何要用 reactive 包起來?這樣我們在 closeModal 的時候,就可以直接 data.isShow = false,讓 modal 關閉。這樣就可以將動畫效果套入到我們的 modal 中。

那麼接下來我們就可以用 provide/inject 將我們的 close function 傳遞下去囉。

從官方的文件中我們可以知道:可以利用 InjectionKey 去限制他的型態,所以我們先新增一個文件叫做 provideInject.ts 文件:

import { InjectionKey, inject } from 'vue';
// 利用 Symbol 把參數包起來
export const CLOSE_MODAL: InjectionKey<() => void> = Symbol('closeModal')
// 新增一個 injectStrict,會需要這個 function 的原因在於,inject時,必須還要給一個 default 值,不然 typescript 會認為他有可能是 undefined
export const injectStrict = <T>(key: InjectionKey<T>, fallback?: T)  => {
  const resolved = inject(key, fallback)
  if (!resolved) {
    throw new Error(`Could not resolve ${key}`)
  }
  return resolved;
}

接著我們修改 index.ts:

export const useModal = () => {

  const openModal = ({ component, myProps }: { component: componentNameType, myProps: any }) => {
    const container = document.createElement('div')

    const _closeModal = function () {
      data.isShow = false
      container.parentNode!.removeChild(container)
    }

    const data = reactive({
      isShow: true,
      component,
      myProps,
      close: _closeModal
    })

    const mockModal = {
      name: 'mockModal',
      setup () {
        // 將我們的 close function provide 進去
        provide(CLOSE_MODAL, data.close)
        return {
          ...toRefs(data)
        }
      },
      render () {
        return h(Index, {
          isShow: data.isShow,
          component: data.component,
          myProps: data.myProps,
        })
      }
    }

    const vnode = createApp(mockModal)
    document.body.appendChild(container)
    vnode.mount(container)
  }
  return {
    openModal
  }
}

這樣之後我們就可以直接在 component 中直接 inject 囉,修改一下 template.vue:

<template>
  <div class="modal-mask">
    <div ref="modalMask" class="modal-wrapper">
      <div class="modal-container">
        <div class="modal-header">
          <slot name="header">
          </slot>
        </div>
        <div class="modal-body" style="padding: 24px">
          <slot name="body">
          </slot>
        </div>
        <div class="modal-footer">
          <slot name="footer" :close="close">
            <button @click="close">
              取消
            </button>
          </slot>
        </div>
      </div>
    </div>
  </div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import { CLOSE_MODAL, injectStrict } from './provideInject'
export default defineComponent({
  setup() {
    const modalMask = ref<HTMLElement | null>(null)
    // inject
    const close = injectStrict(CLOSE_MODAL)
    return {
      close,
      modalMask
    }
  },
})
</script>

如果想要在其他 component 用的話,只要在該 component inject 就好囉,這邊就不在把code 寫出來了。

結論

到目前為止解決了我們第二篇所提到的問題,但在這架構中卻又發現了另一個問題:

我們雖然可以傳 props 下去到 component,但在使用的時候卻不知道該 component 到底吃哪些props,每次都要到那個component的檔案中去看他有什麼props,這樣來來回回後我自己覺得很惱人。 那這個問題,我們就等到下一篇在解決囉。