electron-vite 实现鼠标右键点击元素弹出菜单

376 阅读4分钟

最近想开发一个桌面应用,对比了一下,使用electron-vite框架.这个框架的资料并不多,但我觉得这个框架还是比较成熟的.文档写的还比较详细. 开发到文件目录时需要鼠标右键来修改元素的名字.查找之下,还并没有直接的案例. electron和electron-vite文档中找到了相关的内容 首先是electron里提到右键菜单功能Menu | Electron (electronjs.org)

// renderer
window.addEventListener('contextmenu', (e) => {
  e.preventDefault()
  ipcRenderer.send('show-context-menu')
})

ipcRenderer.on('context-menu-command', (e, command) => {
  // ...
})

// main
ipcMain.on('show-context-menu', (event) => {
  const template = [
    {
      label: 'Menu Item 1',
      click: () => { event.sender.send('context-menu-command', 'menu-item-1') }
    },
    { type: 'separator' },
    { label: 'Menu Item 2', type: 'checkbox', checked: true }
  ]
  const menu = Menu.buildFromTemplate(template)
  menu.popup({ window: BrowserWindow.fromWebContents(event.sender) })
})

可以看到,关键在于:在renderer模块里注册监听右击事件window.addEventListener('contextmenu',()=>{})并在里面调用ipcRenderer.send('show-context-menu')发送事件到main 然后在main模块里监听ipcMain.on('show-context-menu', (event) => {})方法中打开菜单.点击菜单后回调事件context-menu-command (renderer里面已经注册了监听context-menu-command) 我按这个逻辑写了一下.发现不行,首先main里面代码是没有问题.但是ipcRenderer在renderer里面无法识别和编译.会报错.因为代码提示就无法通过,所以我首先想到去electron-vite查找.开发 | electron-vite 其实之前我已经在这个文档里找了一圈,但当时我觉得这个和菜单功能对不上号.但其实这个文档想说的是:

  1. 创建一个预加载脚本并通过 contextBridge.exposeInMainWorld 将 方法 或 变量 暴露给渲染器。
import { contextBridge, ipcRenderer } from 'electron'

contextBridge.exposeInMainWorld('electron', {
  ping: () => ipcRenderer.invoke('ping')
})

这时候我明白了,文档想告诉你,为了代码和数据安全所有renderer里面调用ipcRenderer的事件都需要通过contextBridge来暴露给渲染器(renderer) 那么如何定义呢? 如果你和我一样,根据文档用命令生成项目.那么你应该在./src/preload 目录下找到index.ts找到以下代码

// Custom APIs for renderer
const api = {}
...
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
  try {
    contextBridge.exposeInMainWorld('electron', electronAPI)
    contextBridge.exposeInMainWorld('api', api)
  } catch (error) {
    console.error(error)
  }
}

一开始我没看说明,所以想直接添加一个暴露对象.但这是错的.总之最后正确的做法是修改api这个对象来暴露监听方法 具体修改如下

// ./src/preload/index.d.ts
import { ElectronAPI } from '@electron-toolkit/preload'

declare global {
  interface Window {
    electron: ElectronAPI
    api: ZExposeApi
  }
}
interface ZExposeApi {
  rightClick: (e: MouseEvent, itemId: number, oldName: string) => Promise<unknown>
  mouseRightCb: (callback: (event: IpcRendererEvent, trustObj:trustObj, itemId: number, oldName: string) => void) => Promise<unknown>
  menuClose: (callback: (event: IpcRendererEvent) => void) => Promise<unknown>
}
// ./src/preload/index.ts
const api = {
  rightClick: (e: MouseEvent, itemId: number, itemName: string) => ipcRenderer.invoke('rightClick', e, itemId, itemName),
  mouseRightCb: (
    callback: (event: IpcRendererEvent, trustObj: unknown, itemId: number, oldName: string) => void
  ) => ipcRenderer.on('mouseRightCb', callback),
  menuClose: (callback: (event: IpcRendererEvent) => void) => ipcRenderer.on('menuClose', callback)
}

这里有两个重点:

  • 1 因为菜单不单是打开,还是点击后实现操作.我的需求是修改页面的title.所以mouseRightCb传的参数是一个callback function
  • 2 记得要修改./src/preload/index.d.ts中的定义

当然,main里面的修改

// ./src/main/index.ts
...
app.whenReady().then(() => {
    ...
    //项目列表右键菜单
  ipcMain.handle('rightClick', (e, ...args) => {
    const menuTemplate = [
          { label: 'Save', click: async () => { console.log('--Mouse Right--Save', ...args) }},
      { label: 'Delete', click: async () => { console.log('--Mouse Right--Delete')}},
      { label: 'Rename', click: async () => { e.sender.send('mouseRightCb', ...args)}}
    ]
    const menu = Menu.buildFromTemplate(menuTemplate)
    Menu.setApplicationMenu(menu)
    menu.popup({ window: BrowserWindow.fromWebContents(e.sender) })
    menu.addListener('menu-will-close', () =>{
      e.sender.send('menuClose')
      return true
    })
  })
    ...
})

最后,在页面进行监听和调用鼠标右键事件.但我要的功能其实是当我右击一个item时才弹出菜单,如果你按electron里面的方案去监听,那么事实上是监听整个页面.但我要的是对应元素的事件.所以事实上代码是这样的

// ./src/renderer/layout/xxx/index.vue
const menuItemId = ref(0)
const renameInput = ref('')
const dialType = ref('rename')
const showRenameDial = ref(false)
const showMenu = ref(false)
const dialTitle = computed(() => {
  if (dialType.value === 'rename') {
    return 'Select the Folder Save Path'
  } else {
    return 'Add Package In root'
  }
})
onMounted(()=> {
  window.api.mouseRightCb((e: IpcRendererEvent, o: unknown, itemId: number, itemName: string) => {
    menuItemId.value = itemId
    renameInput.value = itemName
    dialType.value = 'rename'
    showRenameDial.value = true
  })
  window.api.menuClose((e: IpcRendererEvent) => {
    showMenu.value = false
  })
})
const saveToFolder = () => {
  projectStore.RenameElement(menuItemId.value, renameInput.value)
  menuItemId.value = 0
  renameInput.value = ''
}

// ./src/renderer/layout/xxx/menuItem.vue
const rightClick = async (e: MouseEvent, d: FolderPack) => {
  e.preventDefault()
  // console.log('packRightClick', d)
  await window.api.rightClick(e, d.id, d.name)
}
<template>
...
    <template v-for="element in props.routerList">
    <el-sub-menu
        ...
        @click.right.stop="rightClick($event, element)
      >
      </el-menu>
      ...
    </template>

</template>

这里也有几个重点:

  • 1 监听注册要放到onMounted里面
  • 2 @click.right.stop按键需要防止向上冒泡 最后一点.我自己还范一个错误,我用的是el-menu,我把这部分代码放到了el-sub-menu里.当我循环生成el-sub-menu时候.监听到了多听事件.查找之下才发现是多次注册了.请把注册监听的事件放到你需要生成一次的地方才不会重复我的错误.

这个框架资料比较少,希望对你们有用.