臺州網(wǎng)站建設(shè)優(yōu)化深圳seo推廣
前言: 最近在公司 PC 端的項(xiàng)目中使用到了右鍵出現(xiàn)菜單選項(xiàng)這樣的一個(gè)工作需求,并且自己現(xiàn)在也在實(shí)現(xiàn)一個(gè)偶然迸發(fā)的 idea( 想用前端實(shí)現(xiàn)一個(gè) windows 系統(tǒng)從開機(jī)到桌面的 UI),其中也要用到右鍵彈出菜單這樣的一個(gè)功能,個(gè)人覺得這個(gè)實(shí)現(xiàn)還不錯(cuò),特來分享🎁。
tips: 我個(gè)人是喜歡使用圖文來講解知識點(diǎn)的,相比于直接講概念,我個(gè)人更傾向于使用費(fèi)曼學(xué)習(xí)法來講解某一個(gè)功能的實(shí)現(xiàn)過程,因?yàn)槲乙彩莿倧囊恢徊锁B走過來,所以我更加清楚一個(gè)新手在去學(xué)習(xí)一個(gè)全新的知識的時(shí)候,他其實(shí)不是需要你給他講實(shí)現(xiàn)原理,而是你需要作為一個(gè) “引路人” 讓他先簡單知道這個(gè)知識是用來干什么的,后面隨著他自己一步一步的深入了解,他會自己慢慢領(lǐng)悟其中的原理。
一. 前期準(zhǔn)備
-
我們需要清楚的認(rèn)識到,這種用戶點(diǎn)擊右鍵然后彈出菜單的動(dòng)作行為是非常不適合將組件寫死在頁面上,然后通過使用
v-show
或者v-if
去控制它的出現(xiàn)和消失的,我們需要想辦法使用函數(shù)式去控制它的行為。 -
在此之前,你需要準(zhǔn)備兩個(gè)文件來和我一起實(shí)現(xiàn)這個(gè)右鍵菜單。
-
預(yù)覽圖:
二. 右鍵菜單的樣式
-
菜單樣式的書寫不是我們本文的重點(diǎn),你可以快速在 Menu.vue 里簡單書寫你自己喜歡的一個(gè)簡單 div 即可,我們的重點(diǎn)是在于如何右鍵彈出它。你也可以在下方的源碼標(biāo)題中直接復(fù)制我書寫的樣式,不過你需要使用
UnoCSS
來支持內(nèi)斂樣式屬性。 -
如果你不知道如何使用
Unocss
,你可以參考這篇文章的內(nèi)容 手把手教你實(shí)現(xiàn)一個(gè)代碼倉庫里面有詳細(xì)的過程來幫助你去完成代碼倉庫的構(gòu)建,其中包括了Unocss
如何引入和使用。)
三. h 函數(shù) 和 render 函數(shù)的使用
-
現(xiàn)在我們已經(jīng)完成了
Menu.vue
,文件的內(nèi)容,接下來我們需要轉(zhuǎn)頭去書寫index.ts
內(nèi)的內(nèi)容。 -
在此之前,我們需要引入兩個(gè)
vue
暴露給我們的,十分重要的函數(shù)。h,和 render
。
-
如果你之前讀過我另外三篇文章,我相信你對這兩個(gè)函數(shù)的使用一定不陌生,但是為了照顧之前沒有了解過的讀者,我還是會在接下來的內(nèi)容中簡單介紹一下。不過我還是建議你去看一看下面的實(shí)現(xiàn)方式,你一定會有不一樣的收獲。
- Vue3 如何實(shí)現(xiàn)一個(gè) Toast 小彈窗
- Vue3 如何實(shí)現(xiàn)一個(gè)全局搜索框
- Vue3 如何實(shí)現(xiàn)一個(gè)Dialog
-
接下來我簡單的介紹一下,這兩個(gè)函數(shù)的使用方式。你需要知道一個(gè)前提知識,我們在
template
標(biāo)簽里書寫的樣式,最終都會被轉(zhuǎn)變成虛擬dom
。
這里面書寫的div
其實(shí)是和我們在瀏覽器里看到的div
“并不是同一個(gè)”div
,只不過經(jīng)過vue
幫我們進(jìn)行了處理,讓它們的表現(xiàn)形式顯得一樣了。 -
那
template
是經(jīng)過了怎樣的處理呢?其實(shí)就是經(jīng)過了h
函數(shù)。然后h
函數(shù)會返回一個(gè)特殊的 JS 對象,這個(gè)特殊的對象就是我們所說的虛擬dom。 -
那我們在這個(gè)場景怎么使用呢?首先你需要在
index.ts
文件內(nèi)引入我們剛剛書寫的右鍵菜單的樣式。然后將這個(gè)組件作為h
函數(shù)的第一個(gè)參數(shù)放入,對,就是這么簡單。這個(gè)vnode
就是我們需要用到的虛擬 dom。
-
有了虛擬 dom 還不行,我們得告訴 vue 我們要把這個(gè)虛擬 dom 渲染到什么地方,這時(shí)候就需要用到
render
函數(shù)。render
函數(shù)要做的事情比較復(fù)雜,不過在這里你只需要簡單的知道。render
函數(shù)會將一個(gè) 虛擬dom 轉(zhuǎn)換成一個(gè)真實(shí)的 dom 節(jié)點(diǎn)。既然需要一個(gè)虛擬 dom,那我剛剛正好用h
函數(shù)轉(zhuǎn)換了得到了一個(gè),于是我們自然而然可以寫出下面的代碼。
-
怎么回事?怎么還報(bào)錯(cuò)了呢?
我們看一下報(bào)錯(cuò)信息,發(fā)現(xiàn)這個(gè)render
函數(shù)需要兩個(gè)參數(shù),我們只給了一個(gè)。那么第二個(gè)參數(shù)是什么呢?我們思考一下,現(xiàn)在這個(gè)dom
已經(jīng)被轉(zhuǎn)換成真實(shí)的 dom 節(jié)點(diǎn)了,但是目前它不知道自己應(yīng)該被渲染到哪里,什么意思呢?其實(shí)理解起來很簡單。
就好比你現(xiàn)在是一個(gè)外賣員,你到了餐廳取餐,餐廳人員說你去吧,你端著手上的一份外賣餐一臉茫然,我去哪啊?
就對應(yīng)著,vue 幫你處理好了這個(gè)虛擬節(jié)點(diǎn),但是你沒告訴它應(yīng)該在哪里去渲染。 -
知道原因就好辦了,我們直接創(chuàng)建一個(gè)空的
div
,先讓render
用著。
四. 右鍵彈出菜單的實(shí)現(xiàn)
-
在進(jìn)行下面的功能之前,你需要知道一個(gè)前提知識。
如上面的gif
所示,我們可以看到,瀏覽器本身是存在默認(rèn)的右鍵點(diǎn)擊事件的。在這里我們需要取消瀏覽器自身的右鍵彈出菜單事件。 -
我們再具體一點(diǎn)講,其實(shí)我們需要做的就是替換掉瀏覽器默認(rèn)的右鍵事件。通過查閱 MDN 我們可以得知,window 對象存在一個(gè)叫做
contextMenus
的事件。
-
那接下來就好辦了,我們直接替換這個(gè)事件為我們的自定義事件即可。(這里阻止默認(rèn)事件需要調(diào)用 e.preventDefault 方法。)
然后我們在隨便一個(gè)全屏的組件引入這個(gè)函數(shù),我們來測試一下,看看效果
-
嗯,現(xiàn)在已經(jīng)不會彈出瀏覽器默認(rèn)的菜單了。那么接下來要做的就是如何讓我們寫好的菜單呈現(xiàn)到頁面上。首先第一點(diǎn),我們需要明確告訴這個(gè)組件你的父元素是誰。
我們上面只是臨時(shí)創(chuàng)造了一個(gè)簡單的div
,但是目前我們還是沒告訴它應(yīng)該渲染到哪里。處理方法也很簡單,這里我提前創(chuàng)建好了一個(gè)很簡單的頁面,并且設(shè)置好了一個(gè)唯一 ID。
-
那么我們就可以非常輕松的獲得這個(gè)元素。
-
現(xiàn)在父元素也有了,只需要將我們的
containerEl
元素放入到scope
里即可。
不過你需要知道的是,我們這個(gè)元素是不應(yīng)該出現(xiàn)在正常的文檔流里的,因?yàn)樗奈恢檬遣还潭ǖ?#xff0c;所以我們在放進(jìn)去scope
元素之前,應(yīng)該給它處理成絕對定位類型的元素。
-
對了,這里需要注意,我們需要給
scope
設(shè)置一個(gè)relative
屬性,來告訴我們的containerEl
它要在誰的范圍內(nèi)是絕對定位。
-
接下來我們進(jìn)入到我們的
scope
組件內(nèi)引入這個(gè)函數(shù),調(diào)用一下看看效果。
ok,現(xiàn)在已經(jīng)實(shí)現(xiàn)我們的右鍵彈出菜單的基本功能了。
五. 菜單位置出現(xiàn)的位置
-
在這里我們需要用到
clientX,和 clientY
這兩個(gè)屬性。
-
如果你是第一次看到這個(gè)屬性,那么我簡單介紹一下。
假設(shè)我在屏幕的上點(diǎn)擊了一下(類比上圖的紅點(diǎn)出),那么此時(shí)這個(gè)點(diǎn)到屏幕最左邊的距離就是clientX
,同理到屏幕頂部的距離就是clientY
。 -
聰明的你一定想到了,那我此時(shí)將
containerEl
的top
和left
的值分別設(shè)置成這兩個(gè)屬性的值,不就恰好會讓菜單出現(xiàn)在我們的右邊嗎?我們試一下。
然后看看效果:
-
目前看起來一切正常,但是我們需要考慮一個(gè)邊界情況。
當(dāng)我們距離屏幕右側(cè)過近的時(shí)候,此時(shí)右鍵會導(dǎo)致有部分內(nèi)容被遮擋。所以我們要想辦法解決這個(gè)邊界情況。
六. 解決右側(cè)過近的問題
-
不要覺得很難,其實(shí)目前我們要做的事情很簡單。
-
如上圖,我們僅僅只需要去判斷
scope 的 clientWidth 的長度 - clientX 的長度= 是否大于containerEl 的 offsetWidth ?
如果大于,則調(diào)轉(zhuǎn)left
的方向?yàn)?right
,并設(shè)置right=0px
即可。 -
如果上面所說的
offsetWidth
和clientWidth
你還不了解。我強(qiáng)烈建議你請點(diǎn)擊這篇博文先去了解清楚這幾個(gè)width
屬性到底代表著什么意思,因?yàn)閷τ谇岸碎_發(fā)來說,這是極其重要的幾個(gè)屬性。如果你之后要接觸移動(dòng)端,那么這是你必須掌握的知識點(diǎn)。
你必須知道的 clientWdith,scrollWidth,offsetWidth -
既然知道了原理,那么代碼寫起來就非常簡單了,在此之前在這里我們需要調(diào)整一下
scope.appendChild
的執(zhí)行時(shí)機(jī)。
我們測試一下效果。
七. 增強(qiáng)該函數(shù)的健壯性
-
目前這個(gè)框我們無法確保它的唯一性,所以我們還需要改造一下這個(gè)函數(shù)。
-
增加一個(gè)變量
isShow
,我們需要知道當(dāng)前的Menu
菜單是否正在展示。
-
將
containerEl
由const
聲明變?yōu)?let
聲明。并將創(chuàng)造時(shí)機(jī)延遲到調(diào)用右鍵時(shí)再創(chuàng)建,這樣我們就能保證每次右鍵制造的這個(gè)Menu
組件是都是全新的。(不然就會出現(xiàn)沿用上一次 css 屬性,導(dǎo)致樣式錯(cuò)亂的 bug )
-
獲取
scope
元素的時(shí)機(jī)也推遲到用戶點(diǎn)擊右鍵的時(shí)候再獲取。(因?yàn)橄旅娴?close
函數(shù)也需要用到這個(gè)變量)
-
拆分兩個(gè)函數(shù),一個(gè)打開
openMenu
函數(shù),一個(gè)關(guān)閉函數(shù)closeMenu
。
-
最后在
window.oncontextmenu
的匿名函數(shù)里去調(diào)取這兩個(gè)函數(shù)。
-
然后我們將這三個(gè)變量暴露出去。
八. 右鍵菜單的使用方法
-
我們進(jìn)到
scope
的.vue
組件內(nèi),引入。
-
這樣我們既可以通過右鍵創(chuàng)建這個(gè)菜單欄,也可以自己在合適的時(shí)間去做一些邏輯判斷手動(dòng)打開。
-
效果如下
源碼
- Menu.vue 的源碼。
<script lang="ts" setup>
import { ref } from "vue"const menuItemsGroup = [{name: "查看(V)",arrow: true,action: () => {console.log("查看")},},{name: "排序方式(O)",arrow: false,action: () => {console.log("刷新")},},{name: "刷新(E)",arrow: false,action: () => {console.log("刷新")},},{name: "粘貼(P)",arrow: false,action: () => {console.log("刷新")},},{name: "粘貼快捷方式(S)",arrow: false,action: () => {console.log("刷新")},},{name: "新建(W)",arrow: false,action: () => {console.log("刷新")},},{name: "個(gè)性化(R)",arrow: false,action: () => {console.log("刷新")},},
]
</script>
<template><divclass="w-17rem bg-#ECECEC flex flex-col py-0.5rem shadow-[4px_4px_5px_2px_rgba(0,0,0,0.3)]"><divv-for="(item, i) in menuItemsGroup":key="i"@click="item.action"class="w-full h-2.5rem px-3rem text-1.5rem leading-2.5rem text-black hover:bg-white mb-0.3rem":class="[3, 5, 6].includes(i) ? `b-t-1px b-gray` : `static`"><span>{{ item.name }}</span></div></div>
</template>
- 這是 openContextMenus 的源碼。
import { h, render } from "vue"import Menu from "./Menu.vue"export function openContextMenus() {let isShow = falselet scope: HTMLElement | null // 拿到桌面元素let containerEl: HTMLDivElement // 創(chuàng)建一個(gè)容器元素,給 render 先用著window.oncontextmenu = function (e: MouseEvent) {e.preventDefault()if (isShow) closeMenu()openMenu(e)}//tips: open the menufunction openMenu(e: MouseEvent) {scope = document.getElementById("PCDesktop")containerEl = document.createElement("div")const vnode = h(Menu)render(vnode, containerEl) //將 vnode 傳遞給 render 函數(shù)containerEl.style.position = "absolute"scope?.appendChild(containerEl) // 1. 為了拿到 offsetWidth,因?yàn)橹挥谐霈F(xiàn)在瀏覽器才會產(chǎn)生 offsetWidth 屬性值,我們需要先渲染出真實(shí) domconst { offsetWidth } = containerEl //2 .取出 containerEl 的真實(shí)寬度const { clientWidth } = scope! //3. 獲取父元素的 clientWidth 準(zhǔn)備進(jìn)行計(jì)算const { clientX, clientY } = e //4. 取出 click 時(shí)鼠標(biāo)的坐標(biāo)const _X = clientWidth - clientX > offsetWidth ? "left" : "right" //調(diào)整方向const _X_offset = clientWidth - clientX // 如果是需要顯示在左邊,則需要獲取當(dāng)前的差值containerEl.style.top = `${clientY}px`containerEl.style[_X] = _X === "left" ? `${clientX}px` : `${_X_offset}px`isShow = true}//tips: close the menufunction closeMenu() {if (isShow) {render(null, containerEl)scope?.removeChild(containerEl)console.log("清楚")isShow = false}}return {isShow,openMenu,closeMenu,}
}
結(jié)語
最近在實(shí)現(xiàn)一個(gè) window
的全套 UI
,代碼開源到了 github
。
我會在之后一直更新類似的內(nèi)容,包括拖拽的實(shí)現(xiàn)。
如果你覺得本文對你有幫助,還希望點(diǎn)個(gè)贊
贈(zèng)人玫瑰,手有余香🌹