用 Vue3 實做一個簡單的 modal hook 吧 (1)
HelloChunWei opened this issue · 0 comments
前言
一直以來modal的實作我都不是很滿意,大多數的modal實作方法都是在該 view 層引入 modal 的 component,再以一個 isShow 變數去控制 component 的顯示與否。
用這個方式一段時間後讓我覺得 view 層變得過於複雜,還要維護 modal 的業務邏輯以及顯示與否。剛好最近實作了一個目前都還算滿意的 modal 寫法,就打算來分享一下。
簡單又直覺的作法
最一開始學vue想要實作 modal 時我想大概都是類似這種用法:
這是在vue2 官方上的 modal 範例。那我們可以稍微改編一下變成這樣: 把他包成另個 compoment
src/comonents/modal1.vue
<template>
<transition name="modal">
<div class="modal-mask">
<div class="modal-wrapper">
<div class="modal-container">
<div class="modal-header">
header
</div>
<div class="modal-body">
body
</div>
<div class="modal-footer">
<button class="modal-default-button" @click="$emit('close')">
OK
</button>
</div>
</div>
</div>
</div>
</transition>
</template>
<script>
</script>
<style>
/* modal 的一些設定 */
.modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: table;
transition: opacity 0.3s ease;
}
.modal-wrapper {
display: table-cell;
vertical-align: middle;
}
.modal-container {
width: 300px;
margin: 0px auto;
padding: 20px 30px;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
font-family: Helvetica, Arial, sans-serif;
}
.modal-header h3 {
margin-top: 0;
color: #42b983;
}
.modal-body {
margin: 20px 0;
}
.modal-default-button {
float: right;
}
/*
* The following styles are auto-applied to elements with
* transition="modal" when their visibility is toggled
* by Vue.js.
*
* You can easily play with the modal transition by editing
* these styles.
*/
.modal-enter {
opacity: 0;
}
.modal-leave-active {
opacity: 0;
}
.modal-enter .modal-container,
.modal-leave-active .modal-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
</style>然後我們再需要用他的地方進行引入(vue3為例),例如:
<template>
<div>
<button @click="isShowModal = !isShowModal">open or close modal</button>
<modal1 v-if="isShowModal" @close="isShowModal = false" />
</div>
</template>
<script>
import { ref, defineComponent } from 'vue'
import modal1 from '@/components/modal1.vue'
export default defineComponent({
name: 'example',
components: {
modal1
}
setup () {
const isShowModal = ref(false)
return {
isShowModal
}
}
})
</script>會大概以這種方式做使用。但假如今天某一頁他可能需要三種 modal 去實現他的邏輯呢?
那可能會這樣寫:
<template>
<div>
<!-- 這裡面可能其他的 html tag 或者 v-for v-if
...
...
...
...
-->
<button @click="isShowModal = !isShowModal">open or close modal1</button>
<button @click="isShowModal2 = !isShowModal2">open or close modal1</button>
<button @click="isShowModal3 = !isShowModal3">open or close modal1</button>
<modal1 v-if="isShowModal" @close="isShowModal = false" />
<modal2 v-if="isShowModal2" @close="isShowModal2 = false" />
<modal3 v-if="isShowModal3" @close="isShowModal3 = false" />
</div>
</template>
<script>
import { ref, defineComponent } from 'vue'
import modal1 from '@/components/modal1.vue'
import modal2 from '@/components/modal2.vue'
import modal3 from '@/components/modal3.vue'
export default defineComponent({
name: 'example',
components: {
modal1,
modal2,
modal3
}
setup () {
/* 這中間可能又有許多關於這個 view 的業務邏輯在裡面 */
const isShowModal = ref(false)
const isShowModal2 = ref(false)
const isShowModal3 = ref(false)
return {
isShowModal,
isShowModal2,
isShowModal3
}
}
})
</script>在此模式使用一陣子之後發現有幾個問題:
- 在該頁面中必須要去處理 modal 的顯示與否,必須要宣告一個類似 isShow 的變數去控制
- 必須引入 modal 到此頁面
- 增加了該 page 閱讀理解上的困難度
假如業務邏輯的需要是有所謂的槽狀 modal 的話,那就會變得更難處理了,modal2 的顯示是由 modal1 中他的業務邏輯去控制要不要去顯示(可能是點擊一個 click 或者是某種狀態被觸發)
所以我是會需要在 modal1 中引入 modal2 嗎?還是我在外層component 同時處理 modal1 以及 modal2 的顯示呢?那這樣的話 modal 是不是要 emit 一個 function 去控制 modal2 的顯示?
諸如此類的問題不斷的浮現。直到最近決定想個辦法讓事情可以簡單化。
慢慢優化
我們先一步一步來慢慢優化吧,目前我覺得每一次都需要把 modal 的style 重新寫一遍覺得有點麻煩,所以先用 slot 把 modal 的架構給抽出來:
src/components/modal/template.vue
<template>
<transition name="modal">
<div class="modal-mask">
<div class="modal-wrapper">
<div class="modal-container">
<div class="modal-header">
<slot name="header">
default header
</slot>
</div>
<div class="modal-body">
<slot name="body">
default body
</slot>
</div>
<div class="modal-footer">
<slot name="footer">
default footer
<button class="modal-default-button" @click="$emit('close')">
OK
</button>
</slot>
</div>
</div>
</div>
</div>
</transition>
</template>
<script>
</script>
<style>
/* modal 的一些設定 */
.modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: table;
transition: opacity 0.3s ease;
}
.modal-wrapper {
display: table-cell;
vertical-align: middle;
}
.modal-container {
width: 300px;
margin: 0px auto;
padding: 20px 30px;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
font-family: Helvetica, Arial, sans-serif;
}
.modal-header h3 {
margin-top: 0;
color: #42b983;
}
.modal-body {
margin: 20px 0;
}
.modal-default-button {
float: right;
}
/*
* The following styles are auto-applied to elements with
* transition="modal" when their visibility is toggled
* by Vue.js.
*
* You can easily play with the modal transition by editing
* these styles.
*/
.modal-enter {
opacity: 0;
}
.modal-leave-active {
opacity: 0;
}
.modal-enter .modal-container,
.modal-leave-active .modal-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}那們這樣我們就可以運用這個 template 去比較快速的寫出一個 modal,以及萬一整個modal 的css 需要調整的話,我們就只需要調整這個 template.vue 就好了:
src/components/modal/newModal.vue
<template>
<modal>
<template #header>
這裡是header
</template>
<template #body>
這裡是body
</template>
<template #footer>
這裡是footer
</template>
</modal>
</template>
<script>
import { defineComponent } from 'vue'
import Modal from './template.vue'
export default defineComponent({
components: {
Modal
}
})
</script>該怎麼把 modal component 從 view 層中抽離?
雖然現在已經將 modal 的一些 css 設定獨立出來成一個 template.vue 。但是 modal component 還是會相依在 view 層中。例如我有一個頁面 view.vue:
<template>
<!-- 這裡有很多html tag -->
<new-modal v-if="isShow" @close="isSHow = false" />
</template>
<script lang="ts">
import { defineComponet, ref } from 'vue'
import NewModal from '../components/modal/newModal.vue'
export const defineComponent({
component: {
NewModal
},
setup () {
const isShow = ref(false)
return {
isShow
}
}
})
</script>之前的優化只有將 modal 的架構做優化而已,但是該怎麼使用 modal 的方式還是按照舊有的思路。
那們到底該怎麼優化才好?
試著將整個 modal 架構改寫成 hook 方式好了
vue3 後新增了 composition API 大大了增加了寫 code 的彈性。也受惠於這種彈性,對於寫前端的一些思路跟之前有些不太一樣。也因為這樣,就想試著將新學到的思路把他加進去到 modal hook。
先從固定的 modal 開始
總要有個想法吧?我最終的想要呈現的是什麼要先想出來,那我的想法會是:「有沒有一個方法,我呼叫某一個 function (openmModal 之類的)就可以把 modal 呼叫出來,那我可以點擊 modal 上的「關閉」就可以把 modal 取消。又或者呼叫一個 function (closeModal)就可以把它關閉」。
那現在我們有個想法了,就來一步一步構建我想要的hook吧。
src/hooks/modal/index.ts
// hook 雛形
export const useModal = () => {
const openModal = () ={}
return {
openModal
}
}雛形出來後,我希望使用的方式會希望是這樣:
import { defineComponent, onMounted } from 'vue'
import { useModal } from '@/hook/modal/'
<script lang="ts">
export default defineComponent({
setup () {
const { openModal } = useModal()
onMounted(() => {
// 開啟 modal
openModal()
})
}
})
</script>那我們該怎麼將 modal component 掛載在畫面上?當我在思考這個問題的時候發現 modal 有幾個特點:
- 在開啟的時候基本上是全螢幕的,會有一個遮罩,然後中間跳出 modal
- modal 的 z-index 應該是最大的
發現這兩個特點之後我想到:我可以利用 createApp 的方式將它掛載在 body 下就好了,不一定要將他們掛載在特定的dom上。
既然知道可以利用這種方式,那就試試看:
// hook 雛形
import { createApp } from 'vue'
export const useModal = () => {
// 準備動工
const openModal = () ={
// create new vue instance
const vnode = createApp()
// 掛載在某個dom 上
vnode.mount()
}
return {
openModal
}
}vnode.mount 這個比較好解決,就是掛載在我們要的位置上而已,所以我們先處理掉。
// hook 雛形
import { createApp } from 'vue'
export const useModal = () => {
const openModal = () ={
// 要掛載的地方
const container = document.createElement('div')
// create new vue instance
const vnode = createApp()
// 掛載在 container 上
vnode.mount(container)
// 記得要把他加在 body 下哦,不然即使 mount 後不 appendChild 畫面也不會有反應的
document.body.appendChild(container)
}
return {
openModal
}
}那接下來我們來處理 createApp 裡面究竟要帶什麼參數?從vue3的文件中可以知道,第一個參數是所謂的 compoment 第二個參數是, props。 知道了之後我們就來試試吧。
// hook 雛形
import { createApp } from 'vue'
import Modal from './modal.vue'
export const useModal = () => {
const openModal = () ={
// 要掛載的地方
const container = document.createElement('div')
// create new vue instance
const vnode = createApp(Modal, {
// 這裡傳輸 props
})
// 掛載在 container 上
vnode.mount(container)
// 記得要把他加在 body 下哦,不然即使 mount 後不 appendChild 畫面也不會有反應的
document.body.appendChild(container)
}
return {
openModal
}
}這樣我們就完成了一個很簡單的hook了,馬上來試試看:
最終的結果會長這樣:滿符合我的預期的,那把這次小成果的程式碼整理一下:
hooks/modal/template.vue
<template>
<div class="modal-mask">
<div 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">
<button>
取消
</button>
</slot>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({})
</script>
<style scoped>
.modal-mask {
overflow: scroll;
position: fixed;
z-index: 999;
padding-bottom: 20px;
top: 0;
left: 0;
right: 0;
bottom: 0;
height: 100vh;
width: 100%;
background-color: rgba(0, 0, 0, .7);
}
.modal-wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.modal-header {
display: -ms-flexbox;
display: flex;
-ms-flex-align: start;
align-items: flex-start;
-ms-flex-pack: justify;
justify-content: space-between;
padding: 1rem 1rem;
border-bottom: 1px solid #dee2e6;
border-top-left-radius: .3rem;
border-top-right-radius: .3rem;
}
.modal-header h3 {
margin-top: 0;
color: #313634;
}
.modal-container {
max-height: 100%;
max-width: 400px;
width: 95%;
margin: 0px auto;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 2px 8px rgba(0, 0, 0, .33);
transition: all .3s ease;
}
.modal-body {
position: relative;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
margin: 0px 0;
overflow-y:auto;
}
.modal-footer{
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
-ms-flex-pack: end;
justify-content: flex-end;
padding: 1rem;
border-top: 1px solid #dee2e6;
border-bottom-right-radius: .3rem;
border-bottom-left-radius: .3rem;
}
.font-set{
font-size: 20px;
}
</style>hooks/modal/testModal.vue
<template>
<modal>
<template #header>
我是頭
</template>
<template #body>
body
</template>
<template>
<button>
關閉
</button>
</template>
</modal>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Modal from './template.vue'
export default defineComponent({
name: 'test',
components: {
Modal
},
})
</script>hooks/modal/index.ts
import { createApp } from 'vue'
import TestModal from './testModal.vue'
export const useModal = () => {
const openModal = () => {
const container = document.createElement('div')
const vnode = createApp(TestModal)
document.body.appendChild(container)
vnode.mount(container)
}
return {
openModal
}
}呼叫以及使用
// 在你要呼叫的component引入
import { defineComponent, onMounted } from 'vue'
import { useModal } from '@hook/modal/index'
export const defineComponent({
setup () {
const { openModal } = useModal()
onMounted(() => {
openModal()
})
}
})結論
做到這裡,大致上已經完成了簡單的hook,可以將 modal 呼叫出來,但這裡面還有很多問題,例如:
- 不能決定呼叫哪個 modal
- 雖然少了 isShow 的參數,但目前不能關閉 modal
這些問題會在第二篇繼續完成。我原本以為可以在一篇文章中將我如何開發一個簡單的hook 的心路歷程給寫出來,但我發現這樣篇幅好像會太多XD 所以決定拆成幾篇做說明。那我們下次見
