Electron入门

electron官网
electron-vite

一、Electron是什么?

一个构建桌面应用的框架。

二、构建Electron项目

Electron可以使用html+css+js进行原生开发。也可以通过electron-vite构建工具快速创建一个模板。本文是基于模板electron+vue模板进行介绍。

目录结构

创建项目:可以通过这行命令创建你想使用的模板。

sql 复制代码
pnpm create @quick-start/electron my-app --template vue/react/...

或者通过这行命令克隆一个Electron+vue3+Typescript项目模板。

bash 复制代码
npx degit alex8088/electron-vite-boilerplate electron-app

然后进入项目:cd 项目名,安装依赖:npm i ,运行:npm start

启动后会打开一个窗口。

目录结构

src目录:

electron分为主进程和渲染进程,主进程代码在main下,渲染进程也就是我们业务代码在renderer目录下

看下主进程入口文件的主要内容:

ts 复制代码
// src/main/index.ts 主进程入口文件
// 创建窗口
  const mainWindow = new BrowserWindow({
    width: 900,
    height: 670,
    show: false, // 窗口创建时是否显示
    resizable:false,//不能修改窗口大小
    autoHideMenuBar: true,//自动隐藏菜单栏,除非按了`Alt`键
    ...(process.platform === 'linux' ? { icon } : {}),
    webPreferences: {
      preload: join(__dirname, '../preload/index.js'),
      sandbox: false
    }
  })

打开开发者工具

ts 复制代码
  mainWindow.webContents.openDevTools();

或者直接按f12

显示窗口

ts 复制代码
  mainWindow.on('ready-to-show', () => {
    mainWindow.show()
  })

开发环境加载本地的地址,使用的loadURL,生产环境加载的对应磁盘的文件,使用是loadFile

arduino 复制代码
  // 判断开发环境,展示对应的窗口
   if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
     minWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
   } else {
     minWindow.loadFile(join(__dirname, '../renderer/index.html'))
   }

通信

主进程到渲染进程的通信

渲染进程:

arduino 复制代码
electron.ipcRenderer.invoke('event_name',{
   key:value
})

主进程:

javascript 复制代码
// 接收两个参数,第二个参数是渲染进程传过来的值
ipcMain.handle('event_name', ( event, data  )=>{
 
})

渲染进程到主进程的通

主进程:

php 复制代码
mainWindow.webContents.send('名称', {
    a:1,
    b:2
})

渲染进程:

javascript 复制代码
electron.ipcRenderer.on('名称',(e, msg)=>{ msg是参数 })

窗口操作

菜单、托盘

菜单

ts 复制代码
// src/main/index.ts
// 引入菜单
import { Menu } from 'electron'
const template = [
  {
    label: '菜单1',
    submenu: [
      {
        label: '选项1'
      },
      {
        label: '选项2'
      },
      {
        label: '选项3'
      }
    ]
  },
  {
    label: '菜单2',
    submenu: [
      {
        label: '劈里啪啦'
      }
    ]
  }
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)

修改主进程的内容要重启应用

还可以通过一些属性,给菜单设置快捷键、勾选、禁用等,更多参考官网

ts 复制代码
{
    label: '菜单1',
    submenu: [
      {
        label: '选项1',
        // 快捷键
        accelerator: 'ctrl+L' // macOS是command
      },
      // 分割线
      { type: 'separator' },
      {
        label: '选项2',
        // 是否勾选
        checked: true,
        type: 'checkbox',
        click(event) {
          // 点击勾选或取消勾选
          event.checked = false
        }
      },
      {
        label: '选项3',
        // 禁用
        enabled: false
      }
    ]
  },

效果如下:

它还有一些预定义的菜单,通过设置菜单的role为它指定的菜单名即可,如下:

ts 复制代码
  // 直接用它整个的预定义菜单,子菜单是固定的
  { role: 'editMenu' },
  // 或者根据自定义submenu选择自己需要的菜单
  {
    label: 'Edit',
    submenu: [
      { role: 'undo' },
      { role: 'redo' },
      { type: 'separator' },
      { role: 'cut' },
      { role: 'copy' },
      { role: 'paste' }
    ]
  },

效果:

role的属性值可参考官网

还可以通过判断当前系统是否是macOS来给不同系统添加不同菜单。

ts 复制代码
// 判断当前系统是否是macOS
const isMac = process.platform === 'darwin';
const template = [
// { role: 'appMenu' }
...(isMac
  ? [{
      label: app.name,
      submenu: [
        { role: 'about' },
        { type: 'separator' },
        { role: 'services' },
        { type: 'separator' },
        { role: 'hide' },
        { role: 'hideOthers' },
        { role: 'unhide' },
        { type: 'separator' },
        { role: 'quit' }
      ]
    }]
  : [])
]

托盘

托盘是啥呢?

就是这东西(windows系统)。

创建托盘

ts 复制代码
const { app, Menu, Tray } = require('electron')

let tray = null
app.whenReady().then(() => {
  tray = new Tray('/path/to/my/icon')
  const contextMenu = Menu.buildFromTemplate([
    { label: 'Item1', type: 'radio' },
    { label: 'Item2', type: 'radio' },
    { label: 'Item3', type: 'radio', checked: true },
    { label: 'Item4', type: 'radio' }
  ])
  tray.setToolTip('This is my application.')
  tray.setContextMenu(contextMenu)
})

隐藏标题栏

一个桌面应用中肯定会有很多窗口,很多窗口不需要显示标题栏,可以通过设置无边框窗口(frame:false),隐藏,但是会存在窗口无法拖动的问题,有两种解决方案,如下:

1、设置样式

ts 复制代码
// src/main/index.ts
  const mainWindow = new BrowserWindow({
    // frame 设置为 false 时可以创建一个无边框窗口 
    frame: false,
    width: 900,
    height: 670,
    show: false,
    ...(process.platform === 'linux' ? { icon } : {}),
    webPreferences: {
      preload: join(__dirname, '../preload/index.js'),
      sandbox: false
    }
  })

但是这种方法会导致窗口无法移动,可以在渲染进程里面添加样式:-webkit-app-region: drag,这样窗口就可以拖动了,但是这会导致设置可拖动的那个盒子的点击事件失效。如下:header可以拖动窗口,但是点击事件失效(我们可以把点击事件写在header外面,然后通过定位放到需要的位置),main部分点击事件有效但不可拖动。

vue 复制代码
<template>
 <div class="box">
   <header>
     <button @click="btn">头部按钮</button>
   </header>
   <main>
     <button @click="btn">按钮</button>
   </main>
 </div>
</template>
<script lang="ts" setup>
const btn = () => {
 console.log(1)
}
</script>
<style lang="less">
header {
 -webkit-app-region: drag;
}
main {
 // -webkit-app-region: no-drag;
}
</style>

2、自定义拖动事件

思路是:拖拽窗口的时候,在渲染进程记录鼠标的xy坐标,然后将坐标传给主进程,主进程通过setPosition(x,y)设置窗口位置。 渲染进程监听窗口窗口拖动:

vue 复制代码
<template>
 <div class="box" @mousedown="mousedown">
   <header></header>
   <main>
     <button @click="btn">按钮</button>
   </main>
 </div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const mouseDown = ref(false)
const disX = ref(0)
const disY = ref(0)
const mousedown = (e) => {
 mouseDown.value = true
 disX.value = e.x
 disY.value = e.y
 document.onmousemove = (ev) => {
   if (mouseDown.value) {
     const x = ev.screenX - disX.value
     const y = ev.screenY - disY.value
     // 向主进程通信,传入坐标
    electron.ipcRenderer.invoke('custom-adsorption', { x, y })
   }
 }
 document.onmouseup = (ev) => {
   mouseDown.value = false
 }
}
</script>

主进程接收渲染进程传过来的坐标,设置窗口坐标:

ts 复制代码
 ipcMain.handle('custom-adsorption', (event, res) => {
   if (窗口存在且窗口是显示的) {
     窗口.setPosition(res.x, res.y)
   }
 })

隐藏窗口标题栏的标题

ts 复制代码
const mainWindow = new BrowserWindow({
    titleBarStyle: 'hidden',//隐藏标题
    titleBarStyle: 'hidden', //隐藏标题
    titleBarOverlay: {
      color: 'orange',
      symbolColor: 'green'
    },
   // ...
})

只隐藏了标题,标题栏的窗口操作(放大、缩小、关闭)还存在。这个也存在不能拖动的问题,解决方式同上。

创建窗口、关闭窗口、锁定窗口

渲染进程:

vue 复制代码
<template>
  <div class="box">
    <main>
      <button @click="createWin">创建窗口</button>
      <button @click="close">关闭窗口</button>
      <button @click="kiosk">锁定窗口</button>
    </main>
  </div>
</template>
<script lang="ts" setup>
// 渲染进程点击创建窗口,向主进程通信在主进程中创建
const createWin = () => {
  electron.ipcRenderer.invoke('createWin')
}
// 渲染进程点击关闭窗口,向主进程通信在主进程中关闭
const close = () => {
  electron.ipcRenderer.invoke('closeWindow')
}
// 锁定窗口
const kiosk = () => {
  electron.ipcRenderer.invoke('kiosk', {
    isKiosk: true
  })
}
</script>

主进程:

ts 复制代码
  // 创建窗口
  ipcMain.handle('createWin', (event) => {
    new BrowserWindow({
      width: 300,
      height: 300,
      ...(process.platform === 'linux' ? { icon } : {}),
      webPreferences: {
        preload: join(__dirname, '../preload/index.js'),
        sandbox: false
      }
    })
  })
  //关闭窗口
  ipcMain.handle('closeWindow', (event, data) => {
    mainWindow.close()
    // 或使用destory,与close区别是close关闭可以监听close事件。mainWindow.on('close',()=>{})
    // mainWindow.destory()
  })
  //锁定窗口
  ipcMain.handle('kiosk', (event, data) => {
    mainWindow.setKiosk(true)
  })
ts 复制代码
// 关闭整个软件
// 可以监听berfore-quit事件
app.quit()
// 或者
// app.exit()

官方提供的锁定API是setKiosk,但是使用这个进行锁定,窗口会自动放大,所以不能用这个。锁定就是窗口不能移动,可以结合上面的自定义拖动窗口事件,在用户点击锁定的时候,让窗口不能移动,如下:

ts 复制代码
  let isKiosk = null;
  //锁定窗口
  ipcMain.handle('kiosk', ( event, data  )=>{
    isKiosk = data.isKiosk
  })
   // 拖动窗口
  ipcMain.handle('custom-adsorption',( event, res )=>{
    if( !isKiosk ){ // 锁住的时候不能拖动
      mainWindow.setPosition( res.x  , res.y );
    }
  })

举个例子

vue是单页面应用,通过vue-router将组件映射到路由上,切换路由渲染不同的视图。而在electron中,一个路由就代表一个窗口。切换路由的时候,需要向主进程通信,让主进程打开一个新的窗口。举个例子看下如何实现:

安装vue-routerpnpm add vue-router

渲染进程(renderer目录下),创建路由及对应的路由组件:

ts 复制代码
// renderer/src/router/index.ts
import { createRouter, createWebHashHistory } from 'vue-router'
import Home from '../pages/home.vue'
import About from '../pages/about.vue'
const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About }
]
const router = createRouter({
  history: createWebHashHistory(),
  routes
})
export default router;

挂载路由

ts 复制代码
// renderer/src/mian.ts
import { createApp } from 'vue'
import App from './App.vue'
// 挂载路由
import router from './router'
createApp(App).use(router).mount('#app')
vue 复制代码
// renderer/src/pages/home.vue
<template>
  <div class="container">
    <header>
      <h1>home</h1>
      <h1 @click="goAbout">about</h1>
    </header>
  </div>
</template>

<script setup>
// 点击about向主进程通信
const goAbout = () => {
  electron.ipcRenderer.invoke('aboutPage')
}
</script>
<style scoped>
header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 200px;
  font-weight: bold;
}
</style>

主进程接收事件后首先需要判断about窗口是否已经被创建了,没有被创建再去创建,防止点击多次创建多个窗口,如下:

ts 复制代码
// main/index.ts
// 声明一个变量用来保存about窗口信息
  const context = {
    isShow: false, //显示 & 隐藏
    isKiosk: false, //锁定
    aboutWindow: null //窗口
  }
  // 接收渲染进程事件
  ipcMain.handle('aboutPage', () => {
    if (context.aboutWindow === null) {
    // 窗口不存在,创建窗口
      createAbout()
    } else { // 窗口存在,判断是显示状态还是隐藏状态
      if (context.isShow) {//显示状态再次点击隐藏
        hideWindow()
      } else {//隐藏状态再次点击显示
        showWindow()
      }
    }
  })
  // 隐藏窗口
  const hideWindow = () => {
    if (context.aboutWindow && !context.aboutWindow.isDestroyed()) {
      context.isShow = false
      context.aboutWindow.hide()
    }
  }
  // 展示窗口
  const showWindow = () => {
    if (context.aboutWindow && !context.aboutWindow.isDestroyed()) {
      context.isShow = true
      context.aboutWindow.show()
    }
  }
ts 复制代码
  //创建子窗口
  const createAbout = () => {
    //创建
    context.aboutWindow = new BrowserWindow({
      width: 300,
      height: 300,
      show: false,
      frame: false, //无边框
      parent: mainWindow,//父子窗口,子窗口跟着父窗口一起移动
      transparent: true, // 透明窗口
      alwaysOnTop: true, //置顶
      hasShadow:false, //取消阴影
      autoHideMenuBar: true,
      ...(process.platform === 'linux' ? { icon } : {}),
      webPreferences: {
        preload: join(__dirname, '../preload/index.js'),
        sandbox: false
      }
    })

    //显示
    context.aboutWindow.on('ready-to-show', () => {
      context.aboutWindow.show()
    })
    context.isShow = true
    //显示的路由或页面(html)
    if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
      //开发阶段打开的文件路径
      context.aboutWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/#/about')
    } else {
      // 生产阶段打开的文件路径
      context.aboutWindow.loadFile(join(__dirname, '../renderer/index.html'), {
        hash: '/about'
      })
    }
    // 打开某个h5页面,比如刚打开软件上先展示广告页
    // context.listWindow.loadFile(join(__dirname, '../../resources/xxx/xxx.html'))
  }

主进程二次封装

一个软件会有很多个窗口(路由),窗口之间会频繁的进行通信,如果这些逻辑全部写在主进程的入口文件中,势必会很难维护,所以需要将其进行二次封装。

将主进程的功能分解成独立的模块,每一个模块负责一组相关的任务。

主要思路是将主进程的功能模块化,分为两个关键组件:窗口管理和事件路由。

具体思路:

1、新建windows/MianWindow.ts

创建一个 MainWindow 类,负责创建和管理主窗口

ts 复制代码
import {   BrowserWindow } from 'electron'
import * as path from 'path'
import {  is } from '@electron-toolkit/utils'
export default class MainWindow{
  #window:BrowserWindow  | null =  null;
  #width:number = 900;
  #height:number = 670;
  create():void{
    // 创建窗口
    this.#window = new BrowserWindow({
      width: this.#width,
      height: this.#height,
      show: false,
      autoHideMenuBar: true,
      ...(process.platform === 'linux'
        ? {
          icon: path.join(__dirname, '../../build/icon.png')
        }
        : {}),
      webPreferences: {
        preload: path.join(__dirname, '../preload/index.js'),
        sandbox: false
      }
    })
    // 显示窗口
    this.#window.on('ready-to-show', () => {
      this.#window?.show()
    })
    // Load the remote URL for development or the local html file for production.
    if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
      this.#window.loadURL(process.env['ELECTRON_RENDERER_URL'])
    } else {
      this.#window.loadFile(path.join(__dirname, '../renderer/index.html'))
    }
  }
  close(){
    this.#window?.close();
  }
  show(){
    this.#window?.show()
  }
  hide(){
    this.#window?.hide()
  }
}

2、新建router/EventRoute.ts

定义一个 EventRoute 类,用于描述一个事件路由的基本信息

ts 复制代码
export default class EventRoute {
  name: string;
  event: string;
  callback: (api: object, data: any) => void;

  constructor(name: string, event: string, callback: (api: object, data: any) => void) {
    this.name = name;
    this.event = event;
    this.callback = callback;
  }
}

3、新建router/Routes.ts

定义一个 Routes 对象,存储所有需要处理的事件路由

ts 复制代码
import EventRoute from './EventRoute';

const routers = {
   'close': new EventRoute('close', 'event', (api, data) => {
      console.log(api.mainWindow)
      api.mainWindow.close();
    }),
  'event_name':new EventRoute('event_name', 'event', () => {
    console.log('xxx');
  }),
};

export default routers;

4、新建router/EventRouter.ts

创建一个 EventRouter 类,负责管理和分发事件

ts 复制代码
interface Route {
  callback: (api: any, data: any) => void;
}

interface Routes {
  [key: string]: Route;
}

interface Api {
  [key: string]: any;
}

export default class EventRouter {
  #api: Api = {};
  routes: Routes = {};

  constructor() {}

  router(data: any): void {
    const route = this.routes[data.name];
    if (route && route.callback) {
      route.callback(this.#api, data);
    }
  }

  addApi(name: string, api: any): void {
    this.#api[name] = api;
  }

  addRoutes(routes: Routes): void {
    this.routes = { ...this.routes, ...routes };
  }
}

5、主进程入口文件main.ts

ts 复制代码
import { app,  BrowserWindow,dialog,ipcMain } from 'electron'
import { electronApp, optimizer, } from '@electron-toolkit/utils'
import MainWindow from './windows/MainWindow'
import EventRouter from './router/EventRouter'
import routers from './router/Routes'

app.whenReady().then(() => {
  // Set app user model id for windows
  electronApp.setAppUserModelId('com.electron')

  // Default open or close DevTools by F12 in development
  // and ignore CommandOrControl + R in production.
  // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
  app.on('browser-window-created', (_, window) => {
    optimizer.watchWindowShortcuts(window)
  })

// 创建eventRouter实例
  const eventRouter = new EventRouter();
//引入主窗口
  const mainWindow = new MainWindow();
  mainWindow.create();
// 将外部依赖添加到eventRouter
  eventRouter.addApi('mainWindow',mainWindow);
  eventRouter.addApi('dialog',dialog);
// 将所通信的事件数组添加到eventRouter里
  eventRouter.addRoutes(routers);

//主窗口接收:渲染进程通信内容
  ipcMain.handle('renderer-to-main',( e, data )=>{
    eventRouter.router( data );
  })

  app.on('activate', function () {
    // On macOS it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (BrowserWindow.getAllWindows().length === 0) {
      mainWindow.create();
      mainWindow.show();
    }
  })
})

// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

// In this file you can include the rest of your app"s specific main process
// code. You can also put them in separate files and require them here.

接下来就可以在渲染进程向主进程通信了

css 复制代码
 window.electron.ipcRenderer.invoke('renderer-to-main',{
    name:'close',
    data:{
      a:1
    }
  });

软件自动更新

打包:npm run build:win/mac

打包后dist文件夹下包含以下文件:

1:构建过程中使用的所有配置信息。

2:这个文件里面包含版本号。检查软件更新时,是拿本地的版本号(package.json里的版本号)跟这个文件里面的版本号做对比,如若不同则更新本地软件。

3:这个文件就是我们要安装到本地的软件,在检查更新后,如若需要更新,则从线上下载最新的这个文件(mac是.dmg

打包前要修改electron-builder.yml,这个文件是 electron-builder 的配置文件,用于定义如何打包和构建你的 Electron 应用。它包括很多方面的配置,比如打包格式、目标平台、图标、构建脚本、签名证书等信息。其中,关于自动更新的部分,主要是定义了发布配置,即你的应用更新包(更新服务器)的位置。如下:

yml 复制代码
# dev-app-update.yml  这个文件用于开发环境下测试自动更新
provider: generic
# 这个url要换成你的存放更新文件的服务器地址。electron-updater会从这个URL下载最新的更新。
url: https://example.com/auto-updates
updaterCacheDirName: my-app-updater

检查软件更新操作:

在打开软件时,先打开检查软件更新的窗口,如果当前版本跟线上版本不一致,则更新本地版本,更新完成打开窗口。

下载electron-updater:npm i electron-updater

引入:import { autoUpdater } from 'electron-updater'

ts 复制代码
 // 软件自动更新操作
function update() {
  const updateWin = new BrowserWindow({
    width: 300,
    height: 300,
    show: false,
    autoHideMenuBar: true,
    ...(process.platform === 'linux' ? { icon } : {}),
    webPreferences: {
      preload: join(__dirname, '../preload/index.js'),
      sandbox: false
    }
  })
  // 显示窗口
  updateWin.on('ready-to-show', () => {
    updateWin.show()
  })
  // app.isPackaged 应该是否被打包,开发阶段为false,在开发阶段测试自动更新时,需要改为true来模拟打包后的环境
  if (is.dev) {
    Object.defineProperty(app, 'isPackaged', {
      get() {
        return true
      }
    })
    //开发阶段测试自动更新的配置文件
    autoUpdater.updateConfigPath = join(__dirname, '../../dev-app-update.yml')
  }
  //检查软件更新事件
  autoUpdater.on('checking-for-update', () => {})

  //有新版本==》下载
  autoUpdater.on('update-available', () => {})

  //更新中错误
  autoUpdater.on('error', (e) => {
    console.log('error', e)
  })

  //当前版本已经是最新版本,无需更新
  autoUpdater.on('update-not-available', () => {
    updateWin.destroy()
    mainWindow.show()
  })

  //下载
  autoUpdater.on('download-progress', (progressInfo) => {
    console.log('进度', progressInfo.percent)
  })

  //下载完成,准备安装
  autoUpdater.on('update-downloaded', () => {
    dialog
      .showMessageBox(updateWin, {
        title: '安装新版本',
        message: '新版本已下载完成,是否立即安装?',
        type: 'info',
        buttons: ['安装','取消']
      })
      .then(() => {
        // 退出开始直接安装
        autoUpdater.quitAndInstall()
      })
  })

  //检查是否有新版本
  if (is.dev) {
    autoUpdater.checkForUpdates()
  } else {
    autoUpdater.checkForUpdatesAndNotify()
  }

  //打开更新页面
  updateWin.loadFile(join(__dirname, '../../resources/update.html'))
}

注:对于mac os系统,要实现软件自动更新的功能需要将软件上架到app store,自动更新才会生效。window系统不需要。

小结

1、窗口的操作:创建窗口、创建菜单、托盘。无标题窗口的实现方式。

2、通信:主进程==>渲染进程 渲染进程==>主进程

3、主进程二次封装。

4、软件的自动更新。

相关推荐
樊南3 天前
npm安装electron依赖时卡顿,下载不下来
前端·electron·npm
web前端进阶者3 天前
electron-vite_15打包报错proxyconnect
前端·javascript·electron
407指导员3 天前
electron 顶部的元素点不中,点击事件不生效
前端·javascript·electron
努力学前端Hang3 天前
electron-vite打包后图标不生效问题
前端·javascript·electron
朝阳393 天前
electron-vite【实战】自定义标题栏【组件封装】(含异形标题栏,指定区域拖拽,窗口置顶,窗口最小化,窗口最大化,取消最大化,隐藏窗口到托盘等)
electron
朝阳393 天前
electron-vite【实战】登录/注册页
electron
他在时间门外3 天前
使用Electron获取用户信息,监听程序打开,用户退出连接关闭程序【全代码,有图】
前端·javascript·electron
407指导员3 天前
electron opacity 百分比设置不生效 变成1% 问题
前端·javascript·electron
森叶3 天前
【附源码】Electron Windows桌面壁纸开发中的 CommonJS 和 ES Module 引入问题以及 Webpack 如何处理这种兼容
webpack·electron
乐容5 天前
electron窗口锁定、解锁、解决阴影问题
前端·javascript·electron