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、软件的自动更新。

相关推荐
编程猪猪侠1 天前
解决yarn install 报错 error \node_modules\electron: Command failed.
前端·javascript·electron
zooooooooy2 天前
Electron打包ARM环境deb包
后端·electron
red润3 天前
浏览器离屏渲染 vs. Electron离屏渲染——核心区别与应用场景
前端·electron·canvas
OpenIM4 天前
Electron Demo 的快速编译与启动
前端·javascript·electron
柚子a4 天前
Electron主进程渲染进程间通信的方式
electron
柚子a4 天前
electron使用remote报错
electron
DevUI团队5 天前
Electron 入门学习指南:快速搭建跨平台桌面应用
前端·javascript·electron
RedHood5 天前
鸿蒙投屏实现
electron·harmonyos
黑金IT6 天前
如何在 Electron 应用中安全地进行主进程与渲染器进程通信
服务器·安全·electron
培根芝士6 天前
Electron打包支持多语言
前端·javascript·electron