【Vue3+Electron】桌面式应用实践,带你轻松掌握!

3,005 阅读10分钟

前言

这篇文章主要讲的是Electron中窗口的一些使用方法,所以在这里就不细说如何创建vue3+Electron的项目步骤了,可自行取官网查看,有详细介绍,网上也有许多详细操作教程。

下面提到的操作也都可以在Electron官网 查看到,有详细的操作教程和介绍,以下我大概总结了一部分。

正文

前提

以下操作都主要是在主进程文件中设置,在这里我将主进程文件命名为background.js,放在了根目录main下,先在这里放上主进程文件的基本代码,后续在此基础上增加或修改。

// main/background.js

const { app, BrowserWindow, ipcMain,Tray, Menu, nativeImage } = require("electron");
const { join } = require("path");
let exampleProcess;
const { spawn } = require("child_process");

let win,child
// 屏蔽安全警告
process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true";
const createWindow = () => {
  win = new BrowserWindow({
    frame: false,
    width: 520,
    height: 342,
    minWidth: 520,
    minHeight: 342,
    center: true,
    useContenRtSize: true,
    autoHideMenuBar: true,
    webPreferences: {
      webSecurity: false,
      nodeIntegration: true,
      enableRemoteModule: true,
      contextIsolation: false,
    },
    backgroundColor: "#2e2c29", 
  });

  // development模式
  if (process.env.VITE_DEV_SERVER_URL) {
    win.loadURL(process.env.VITE_DEV_SERVER_URL);
    // 开启调试台
    win.webContents.openDevTools();
  } else {
    win.setMenu(null);
    const appPath = app.getAppPath();
    process.chdir(appPath + "/../../");

    exampleProcess = spawn("remote.exe");
    win.loadFile(join(__dirname, "../dist/index.html"));
  }
};
app.whenReady().then(() => {
  createWindow();
  app.on("activate", () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});
app.on("window-all-closed", () => {
  if (process.platform !== "darwin") app.quit();
  try {
    if (exampleProcess) {
      process.kill(exampleProcess.pid);
    }
  } catch (error) {}
});

1. 自定义头部标题栏(无边框、可拖拽)

image.png

  • 无边框模式。首先需要去除electron自带的边框。在主文件background.js中设置如下:
const createWindow = () => {
  const win = new BrowserWindow({
    frame: false,//设置无边框
    width: 520,
    height: 342,
  });
  • 实现拖拽移动窗口。原本electron是可以手动拖拽移动的,但在设置frame: false后,这种效果就失效了,所以需要我们手动实现。

一般来说可拖动的范围是头部那部分,双击其他地方都是不能拖动的,所以我们需要把头部这一范围设置为可拖动状态;但这样头部的button按钮功能就失效点不动了,所以要额外设置button不可拖动状态,此外,头部右侧的菜单处也应不能拖拽,所以也要额外设置不可拖动状态。

<div class="header-title">
    <div class="logo">
      //头部左侧logo部分
    </div>
    <div class="title_menu">
     //头部右侧菜单功能部分
    </div>
  </div>
  
 <style lang="scss" scoped>
 button {
  -webkit-app-region: no-drag; //设置不可拖拽
}
 .header-title {
    -webkit-app-region: drag;//设置头部可拖拽
    -webkit-user-select:none;//禁用文本选择 防止在拖动范围内选中了文字
    display: flex;
    justify-content: space-between;
    align-items: center;
   .title_menu {
        display: flex;
        place-items: center center;
        height: 100%;
        -webkit-app-region: no-drag; //设置不可拖拽
    }
  }
 </style>

这样拖拽功能就实现了。细心的你们肯定也发现鼠标右击可拖拽范围的区域时,会弹出自带的菜单栏,这时我们并不希望它们出现。

  • 禁用右键菜单
// main/background.js

// 禁用右键菜单 这部分我从其他网站上搜索到的
  win.hookWindowMessage(278, function (e) {
    win.setEnabled(false); //窗口禁用
    setTimeout(() => {
      win.setEnabled(true);
    }, 100); //延时太快会立刻启动,太慢会妨碍窗口其他操作,可自行测试最佳时间
    return true;
  })

2. 自定义操作窗口的一些功能(最大、最小、全屏、关闭)

有以下有3种方法来实现

2.1 利用ipcMainipcRenderer(页面中直接暴露ipcRenderer)

  1. 最大化、最小化、全屏化、关闭窗口

首先在main/background.js 主文件下主进程IPC监听最大化、最小化、关闭窗口事件,并从electron中引入ipcMain,利用ipcMain.on()在主进程中进行监听,在需要触发事件的地方从electron中引入ipcRenderer并且利用ipcRenderer.send()暴露出来。

//background.js

const { app, BrowserWindow, ipcMain } = require("electron");

const createWindow = () => {
  const win = new BrowserWindow({
    frame: false,//实现无边框模式
    width: 520,
    height: 342,
    minWidth: 520,
    minHeight: 342,
    center: true,//设置初始位置在中间
    useContenRtSize: true,
    autoHideMenuBar: true,
    webPreferences: {
      nodeIntegration: true,
      enableRemoteModule: true,
      contextIsolation: false,//采用这种方法必须设置为false
    },
    backgroundColor: "#2e2c29", //背景色
  });
  //关闭窗口
  ipcMain.on("window-close", function () {
    app.exit();
  });
  //最大化
  ipcMain.on('window-max',()=>{
   if(win.isMaximized()){
    win.restore();//恢复窗口大小
   }else{
    win.maximize(); //最大化
   }
  })
  //最小化
  ipcMain.on('window-mini',()=>{
    win.minimize()
  })
  //全屏化
  ipcMain.on('window-fullscreen',()=>{
    win.setFullScreen(true)
  })
  //退出全屏化
  ipcMain.on('window-notFullscreen',()=>{
    if(win.isFullScreen()){
      win.setFullScreen(false)
    }
  })
 
};
//js
//其他要使用这些事件的文件,比如Header.vue文件

import { ipcRenderer } from "electron";

//关闭窗口 下面都是写在函数里在别的地方调用,核心是里面的
const close = () => {
  ipcRenderer.send("window-close");
};

//最大化
const maxWindow=()=>{
  ipcRenderer.send("window-max");
}

//最小化
const miniWindow=()=>{
  ipcRenderer.send("window-mini");
}

//全屏化
const fullscreenWindow=()=>{
  ipcRenderer.send("window-fullscreen")
}

//退出全屏化 比如要是返回上一个页面且上一个页面并不是全屏化状态这是就要设置退出全屏状态了
const exitFullscreen=()=>{
  ipcRenderer.send("window-notFullscreen");
}

注: 采用这种方法和下面2.3提到的remote模块方法时BrowserWindow创建的窗口中的contextIsolation必须设置为false

2.2 利用IPC通道(官网中写法,推荐)

这个方式和上一个提到的 2.1 方式类似,都是用到了ipcMainipcRenderer,不同之处在于在主进程background.js文件利用ipcMain.on监听接收消息,通过预加载脚本暴露 ipcRenderer.send来发送消息,然后在渲染器进程 (比如xxx.vue文件)中触发这个功能。

image.png

// main/background.js

let win
// 屏蔽安全警告
process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true";
const createWindow = () => {
  win = new BrowserWindow({
    frame: false,
    width: 520,
    height: 342,
    minWidth: 520,
    minHeight: 342,
    center: true,
    useContenRtSize: true,
    autoHideMenuBar: true,
    webPreferences: {
      preload: join(__dirname, 'preload.js'), //预加载脚本
      webSecurity: false,
      nodeIntegration: true,
      enableRemoteModule: true,
      contextIsolation: true, //必须设为true 默认就是true
    },
    backgroundColor: "#2e2c29", //'#2e2c29'
  });
  // 禁用右键菜单
  win.hookWindowMessage(278, function (e) {
    win.setEnabled(false); //窗口禁用
    setTimeout(() => {
      win.setEnabled(true);
    }, 100); //延时太快会立刻启动,太慢会妨碍窗口其他操作,可自行测试最佳时间
    return true;
  })

  //关闭窗口
  ipcMain.on("window-close", function (event) {
    app.exit();
  });
  //最大化
  ipcMain.on('window-max',()=>{
    if(win.isMaximized()){
      win.restore();//恢复窗口大小
     }else{
      win.maximize(); //最大化
     }
  })
  //最小化
  ipcMain.on('window-mini',()=>{
    win.minimize()
  })
  //全屏化
  ipcMain.on('window-fullscreen',()=>{
    win.setFullScreen(true)
  })
  //退出全屏化
  ipcMain.on('window-notFullscreen',()=>{
    if(win.isFullScreen()){
      win.setFullScreen(false)
    }
  })
  win.setMaximizable(false) // 禁止双击最大化
 
  win.setAspectRatio(1.6) //设置窗口宽高比

  // development模式
  if (process.env.VITE_DEV_SERVER_URL) {
    win.loadURL(process.env.VITE_DEV_SERVER_URL);
    // 开启调试台
    win.webContents.openDevTools();
  } else {
    win.setMenu(null);
    const appPath = app.getAppPath();
    process.chdir(appPath + "/../../");

    exampleProcess = spawn("remote.exe");
    win.loadFile(join(__dirname, "../dist/index.html"));
  }
};

app.whenReady().then(() => {
  createWindow();
  app.on("activate", () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});
app.on("window-all-closed", () => {
  if (process.platform !== "darwin") app.quit();
  try {
    if (exampleProcess) {
      process.kill(exampleProcess.pid);
    }
  } catch (error) {}
});

注: 要使用预加载脚本preload.js 前提必须将contextIsolation设置为true

//preload.js

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
  createWindows:(val)=>ipcRenderer.send('create-window',val),//创建子窗口
  setRightMenu:()=>ipcRenderer.send('show-context-menu'),//右键菜单
  maxSetWin:()=>ipcRenderer.send('window-max'),//窗口最大化
  minSetWin:()=>ipcRenderer.send('window-mini'),//窗口最小化
  fullSetWin:()=>ipcRenderer.send('window-fullscreen'),//窗口全屏化
  exitFull:()=>ipcRenderer.send('window-notFullscreen'),//退出全屏化
  exitWin:()=>ipcRenderer.send('window-close')//退出
})

这样就可以在vue页面中直接使用window.electronAPI的值进行调用函数执行相应功能,而不用直接暴露ipcRenderer

注: contextBridge.exposeInMainWorld(apiKey, api)apiKey是唯一key值,可以自定义取值

例如在Header.vue页面中使用了它们

//js
//关闭窗口 下面都是写在函数里在别的地方调用,核心是里面的
const close = () => {
  window.electronAPI.exitWin();
};

//最大化
const maxWindow=()=>{
  window.electronAPI.maxSetWin();
}

//最小化
const miniWindow=()=>{
  window.electronAPI.minSetWin();
}

//全屏化
const fullscreenWindow=()=>{
  window.electronAPI.fullSetWin()
}

//退出全屏化 比如要是返回上一个页面且上一个页面并不是全屏化状态这是就要设置退出全屏状态了
const exitFullscreen=()=>{
  window.electronAPI.exitFull()
}

2.3 利用remote模块的getCurrentWindow()

  1. 最大化、最小化、全屏化窗口

这个不需要在electron的主文件里编写相关代码,只要在需要使用的地方引入相关模块并且调用即可。

//比如在Header.vue文件里使用 

const remote = window.require("electron").remote;
const win = remote.getCurrentWindow();
//此时的win就相当于主文件中`new BrowserWindow()`创造出来的win对象,现在放到这里来写而已

//最大化和恢复原来大小 切换
if(win.isMaximized()){
    win.restore();
}else{
    win.maximize(); //最大化
}

//最小化
win.minimize()

//全屏化
win.setFullScreen(true)

//退出全屏化
win.setFullScreen(false)

所以一些通信啥的功能都可以利用这些方法实现(还有其他方法,可见于官网),后续要加的一些功能都可以类似这样写。

3. 禁止双击最大化

在主文件中使用frame: false实现无边框模式的窗口,并且实现拖动效果后,会发现双击所能拖动的范围内出现窗口最大化的问题,采用以下方法就能解决。

// main/background.js

 win.setMaximizable(false) // 禁止双击最大化

4. 窗口等比缩放

有时候我们希望一张图片的大小能随窗口大小的变化而变化,能够保证图片的大小按等比例缩放。

// main/background.js

 win.setAspectRatio(1.6) //设置宽高比,也可以填其他值

5. 托盘

创建并打开窗口的同时在电脑屏幕的右下角添加托盘,关闭窗口时并不是真正的关闭应用,而是将此窗口隐藏掉并从任务栏中移除掉,在点击托盘后可以再次打开此窗口;此外还给托盘右击添加菜单,选项包括退出应用功能。

image.png image.png

image.png

image.png

//main/background.js

app.whenReady().then(() => {
//还有其他操作 比如创建主窗口

  //托盘
  const tray = new Tray('public/bg.jpg')
  const contextMenu = Menu.buildFromTemplate([ //托盘下的菜单
    { label: 'Item1', type: 'radio' },
    { label: 'Item2', type: 'radio' },
    { label: 'Item3', type: 'radio', checked: true },
    { label: 'Item4', type: 'radio' },
    {
      label: "帮助",
      click: () => {
      }
    },
    {
      label: "退出",
      click: () => {
        win.destroy();
      }
    }
  ])

tray.on("click", () => {
  //点击通知区图标实现打开应用的功能
    win.show() //显示并聚焦窗口
    win.setSkipTaskbar(false) //窗口显示在任务栏中
});

在点击关闭按钮后从任务栏中移除,但在点击托盘后又显示出来

//Header.vue

//这里我采用remote方式通信,也可以用上面我提到的那几种方式也行

//关闭按钮
const close = () => {
  const remote = window.require("electron").remote;
  const win = remote.getCurrentWindow();

  win.hide();
  win.setSkipTaskbar(true);// 将应用从任务栏移出
};

注: 这里我采用的通信方式是上面提到的2.3 remote方式 ,也可以使用其他2种方式实现,都可行,这里就不一一介绍了

6. 自定义右键菜单

image.png

有时候想要鼠标按下右键后展示自定义的菜单出来

// main/background.js

//监听右键菜单
ipcMain.on('show-context-menu', (event) => {
  const template = [
    {
      label: '菜单1',
      click: () => {  }
    },
    { type: 'separator' },//分隔符
    { label: '菜单2', type: 'checkbox', checked: true }
  ]
  const menu = Menu.buildFromTemplate(template)
  menu.popup({ window: BrowserWindow.fromWebContents(event.sender) })
})

想实现在主窗口的任何地方右键都能打开菜单,我在App.vue主文件里监听了右键菜单事件,触发这个事件时发送打开自定义菜单的请求。

//App.vue

<template>
  <div>
    <router-view></router-view>
  </div>
</template>

<script setup>
import { ipcRenderer } from "electron";

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

注: 这里我采用的通信方式是上面提到的2.1 ipcMain ipcRenderer方式 ,也可以使用其他2种方式实现,都可行

7. 多窗口

这里我直接采用ipcMain监听,ipcRenderer发送请求的方式,按照上面提到的另外2种方法也是一样的。如下图点击设置后打开子窗口,子窗口的设置和主窗口一样,下面就简单设置,不做过多样式修改。

image.png

// main/background.js

let child
const openOtherWindow=(val)=>{ //创建子窗口 样式配置
  child = new BrowserWindow({
    parent: win,
    width: 300,
    height: 200,
    title:'test',
     //frame: false,
     useContentSize: false,
     resizable: false,
     skipTaskbar: true,
     transparent: false,
     webPreferences: {
       nodeIntegration: true,
       webSecurity: false
     },
     backgroundColor: "#F5F5F5",
   })
   child.loadURL(val); //加载地址 指定路由地址 就可以和平常一样修改里面的页面
   child.webContents.openDevTools() //打开调试器

    //监听子窗口关闭时销毁窗口并设置child为空
   child.on('close',(e)=>{
    e.preventDefault()
    child.destroy()
    child=null
   })
}

//监听创建子窗口
ipcMain.on('create-window', (event, val) => {
  if(!child){ //为了一个操作只打开一个窗口
    openOtherWindow(val)
  }
})
//Header.vue

import { ipcRenderer } from "electron";
 let t=process.env.VITE_DEV_SERVER_URL 

//点击设置按钮触发的函数
const toset=()=>{
  ipcRenderer.send('create-window',t+'#/setting') //以hash模式的地址
}

注: 要想设置子窗口的页面,指定路由的方式,那么创建路由的方式必须是哈希模式创建---createWebHashHistory

image.png

8. 单应用启动实现

在成功打包Electron项目后,会生成一个exe应用。但有时候我们只想打开一个应用,多次点击应用都是同一个,这时候就用到了请求单一实例锁

const additionalData = { myKey: 'myValue' } //配置项 唯一key
const gotTheLock = app.requestSingleInstanceLock(additionalData)

if (!gotTheLock) {
// 已经有一个实例在运行,退出当前实例
  app.quit()
} else {
// 监听第二个实例被运行时
  app.on('second-instance', (event, commandLine, workingDirectory, additionalData) => {
    // 输出从第二个实例中接收到的数据
    console.log(additionalData)

    // 试图运行第二个实例,我们应该关注我们的窗口
    if (win) {
      if (win.isMinimized()) win.restore()
      win.focus()
      if (!win.isVisible()) { 
        win.show(); 
       win.setSkipTaskbar(true); 
      }
    }
  })
}

结束语

以上的一些功能都能在Electron官网找到相关操作,也有详细的API介绍。

本篇文章就到此为止啦,由于本人经验水平有限,难免会有纰漏,对此欢迎指正。如觉得本文对你有帮助的话,欢迎点赞收藏❤❤❤。要是您觉得有更好的方法,欢迎评论,提出建议!