原生“跨組件”通信方式
現(xiàn)在已經(jīng)是“組件化”開發(fā)時代了。
相信大家平時在vue或者react中都碰到過“跨組件”通信的需求,通常我們需要將數(shù)據(jù)放在一個公共的父級上,然后用context之類的方式傳遞下去,或者借用pinia這樣的開源庫去更好的管理這些數(shù)據(jù)。
不過,大部分項目可能沒有那么復雜,可能只有極少部分需要“跨組件”通信或者全局傳遞的,專門去引入一個全局狀態(tài)管理庫還是有一定成本的,不僅僅是性能開銷,還有學習成本。
另外,還有一些舊項目,由于前期組件設計未考慮周全,或者由于后期需求迭代,導致需要跨組件通信,此時再引入狀態(tài)管理庫也有很大的改造成本。
框架用久了,可能有些都忘了,原生 web 并沒有組件化的概念,整個頁面都是開放的,所以,借助原生的一些行為和方式,也能很輕易的解決“跨組件”通信問題,下面列舉了幾個方式,一起看看吧~
一、label for
舉個例子,下面是之前做的一個錯別字糾錯功能,也就是自動檢測錯別字,然后提供替換和忽略一些功能,如下:
現(xiàn)在不用關注是怎么實現(xiàn)的,如果按照組件化拆分,右邊的糾錯面板肯定是單獨的組件。
很自然的,關于忽略的相關函數(shù)也是寫在這個組件,類似于這樣。
<!--糾錯組件-->
<script setup>
const ignore = () => {
// 相關操作
}
</script>
<template>
...
<button @click="ignore">忽略</button>
...
</template>
嗯,很好,組件化很不錯,邏輯也很清晰。
過了兩天,產(chǎn)品又需要在另外一個地方也要有“忽略”功能,當鼠標懸浮的時候,下拉選項中也有忽略選項,并且這兩個忽略的功能是完全一樣的,如下:
再單獨實現(xiàn)一遍肯定是不行的,把這個函數(shù)抽離出來?但是還有新的問題,整個錯別字的列表也是需要更新的,是不是也要把這個列表也獨立出來?
no,當然不需要這么麻煩,僅需要一個label標簽就可以輕松搞定,如下:
<!--寫作區(qū)域的下拉組件-->
<template>
...
<ui-dropdown>
<label for="AAA">忽略</label>
...
</ui-dropdown>
...
</template>
當然,右邊的忽略按鈕也需要加上一個相同的ID
(實際中是動態(tài)生成的)。
<!--糾錯組件-->
<template>
...
<button id="AAA" @click="ignore">忽略</button>
...
</template>
這樣,通過label+for就將這兩個元素關聯(lián)起來了,點擊這個label元素就相當于點擊了右邊的忽略按鈕,無需任何跨組件通信,是不是非常方便呢?
二、主動觸發(fā)事件
有時候,需要通信的事件可能并不是通過按鈕點擊的,比如這樣一個寫作頁面,按照頁面布局和功能,各個部分肯定是單獨的組件,如下:
其中,右上角有一個“保存”按鈕,可以保存內(nèi)容,假設是這樣。
<!--功能組件-->
<script setup>
const save = () => {
// 相關操作
}
</script>
<template>
...
<button @click="save">保存</button>
...
</template>
現(xiàn)在,產(chǎn)品又提出一個要求,希望在寫作時,按 Ctrl + S也能保存內(nèi)容。
那么,你會怎么處理呢?把保存方法封裝一下?全局通信?
其實,我們要做的事情很簡單,只需要主動去觸發(fā)一下保存按鈕的點擊事件就可以了,當然需要獲取到這個按鈕,所以要加個ID。
<!--功能組件-->
<template>
...
<button id="saveBtn" @click="save">保存</button>
...
</template>
然后在寫作組件中直接通過觸發(fā)click事件就好了。
<!--寫作組件-->
<script setup>
// Ctrl+S 保存
const save = () => {
document.getElementById('saveBtn').click()
}
</script>
當然,我更習慣于用dispatch的方式來觸發(fā)。
document.getElementById('saveBtn').dispatchEvent(new Event('click'))
這樣的好處是不限類型,任意事件都可以觸發(fā),而不僅僅是點擊事件。
dom.dispatchEvent(new Event('mouseover'))
關于dispatchEvent,下面還有更靈活的運用。
三、dispatchEvent 和 addEventListener
dispatchEvent不僅可以觸發(fā)常見的點擊事件,也能夠觸發(fā)任意自定義事件。
舉個例子,有兩個相互獨立的功能區(qū),有兩個按鈕,分別點擊后,會做一些操作,比如請求接口,最后會彈出同一個獎勵彈窗
如果按照組件話的思維來考慮,可能需要將獎勵彈窗的狀態(tài)放在一個公共的父級,然后依次回調(diào),或者用全局狀態(tài)管理庫區(qū)管理這些狀態(tài)。
但是,如果從原生 web 來考慮,其實不必那么麻煩,下面是一些偽代碼。
<!--功能組件A-->
<script setup>
const getPrize = () => {
// 領獎勵一系列邏輯
}
</script>
<template>
...
<button @click="getPrize">領獎勵A</button>
...
</template>
然后還有功能區(qū) B。
<!--功能組件B-->
<script setup>
const getPrize = () => {
// 領獎勵B一系列邏輯
}
</script>
<template>
...
<button @click="getPrize">領獎勵B</button>
...
</template>
然后還有獎勵彈窗。
<!--獎勵彈窗-->
<script>
const open = () => {
// 打開彈窗
}
</script>
<template>
...
<dialog>獎勵彈窗</dialog>
...
</template>
那么,怎么讓其他功能區(qū)的組件打開獎勵彈窗呢?
這就需要用自定義事件了,很簡單,直接觸發(fā)一個自定義事件,假設就叫做prize。
document.dispatchEvent(new CustomEvent('prize'))
放在業(yè)務組件中就是。
<!--功能組件A-->
<script setup>
const getPrize = () => {
// 領獎勵一系列邏輯
document.dispatchEvent(new CustomEvent('prize'))
}
</script>
<template>
...
<button @click="getPrize">領獎勵A</button>
...
</template>
然后,我們可以在頁面任意地方都監(jiān)聽到這個事件,直接通過addEventListener就可以了。
document.addEventListener('prize', () => {
// 監(jiān)聽到了prize
})
所以,我們可以把這個監(jiān)聽直接放在獎勵彈窗組件中。
<!--獎勵彈窗-->
<script>
const open = () => {
// 打開彈窗
}
onMounted(() => {
document.addEventListener('prize', () => {
// 監(jiān)聽到了prize,打開彈窗
open()
})
})
</script>
<template>
...
<dialog>獎勵彈窗</dialog>
...
</template>
這樣,無論組件是什么樣的嵌套關系,都可以隨心所欲地通信了。
四、最后總結一下
本文更多的還是提供一種思路,實際開發(fā)中可以自己權衡,什么方式比較合適,下面總結一下這三種方式
- label for比較適合于按鈕點擊事件的復用,可以將label元素與button元素綁定起來。
- 相比label for而言,主動觸發(fā)事件更靈活,可以觸發(fā)任意事件,無需button元素,也不限制點擊事件。
- dispatchEvent 和 addEventListener是最靈活的方式了,幾乎可以做到全局通信,通過dispatchEvent觸發(fā),然后通過addEventListener監(jiān)聽。
當然,這些方式也不能濫用,畢竟用多了就顯得邏輯混亂,如果全局通信的情況比較多,還是建議使用傳統(tǒng)的組件化方式就行管理。