Electron + Vue3 开发流程

目录

1. Electron安装

2. 主进程和渲染进程

2.1 创建主进程文件(electron.js)

2.2 创建渲染进程文件(preload.js)

2.3 ipcMain.handle VS ipcMain.on

2.3.1 ipcMain.handle

2.3.2 ipcMain.on

3. Electron打包配置


1. Electron安装

npm install --save-dev electron 

package.json配置文件中的scripts字段下增加一条start命令:

{
  "scripts": {
    "start": "electron ."
  }
}

运行electron项目,npm start  

2. 主进程和渲染进程

Electron是一个使用JavaScript, HTML和CSS构建跨平台桌面应用程序的库。在Electron中有两种进程类型:主进程和渲染进程。

主进程(Main Process)主进程是在Electron中运行package.json中指定的主脚本的Node.js进程。它负责管理渲染进程、与原生操作系统交互、管理应用程序的生命周期等。

渲染进程(Renderer Process):它是由主进程fork出来的,用于处理与DOM交互相关的所有任务。渲染进程可以通过Electron提供的ipc模块与主进程通信。

2.1 创建主进程文件(electron.js)

// 可以照搬,根据需要删减
const path = require('path')
const {
  app,
  protocol,
  Menu,
  BrowserWindow,
  ipcMain,
  shell,
  dialog,
} = require('electron')
const os = require('os')

// 判断开发环境还是生产环境
const isDev = process.env.IS_DEV == 'true' ? true : false
const menu = require('./menu')
// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([
  { scheme: 'app', privileges: { secure: true, standard: true } }
])

let mainWindow
function createWindow() {
  // Create the browser window.
  mainWindow = new BrowserWindow({
    minWidth: 400,
    minHeight: 500,
    minimizable: true,
    webPreferences: {
    // 指定预加载脚本,在执行脚本的路径 (在本例中,它指向你的项目的根文件夹)
      preload: path.join(__dirname, './preload.js'), 
      nodeIntegration: true // 允许在渲染进程中使用Node.js
    },
    // 配置窗口图标
    icon: path.join(__dirname, isDev ? '../public/logo.ico' : '../dist/logo.ico'),
    show: false
  })

  // and load the index.html of the app.
  // win.loadFile("index.html");

  // 加载页面
  mainWindow.loadURL(
    isDev ? 'http://localhost:3004' : `file://${path.join(__dirname, '../dist/index.html')}`
  )

  // 判断是否打开开发者工具
  if (isDev) {
  mainWindow.webContents.openDevTools()
  }

  mainWindow.on('close', (e) => {
    let choice = dialog.showMessageBoxSync(mainWindow, {
      type: 'info',
      buttons: ['最小化', '直接退出'],
      title: '提示',
      message: '确定要关闭吗?',
      defaultId: 0,
      cancelId: 3
    })

    let leave = choice === 0
    let quit = choice === 1
    let close = choice === 3
    if (leave) {
      e.preventDefault()
      mainWindow.minimize()
    } else if (quit) {
      let clear = {
        storages: ['localstorage']
      }
      mainWindow.webContents.session.clearStorageData(clear)
    } else if (close) {
      e.preventDefault()
    }
  })
    
  // 渲染进程用 ipcRenderer.send 发,主进程用 ipcMain.on接,不期待返回;
  // 渲染进程用 ipcRenderer.invoke 发,主进程用 ipcMain.handle接,期待返回;

  mainWindow.on('ready-to-show', function () {
    mainWindow.show()
  })

  ipcMain.handle('openPath', (event, name) => {
    shell.openPath(name)
    return name
  })

  ipcMain.handle('defaultFile', (event, name) => {
    return app.getPath('downloads')
  })

  ipcMain.handle('changSize', (event, type) => {
    if (type === 'logined') {
      mainWindow.setSize(1650, 850)
      mainWindow.setMinimumSize(1650, 850)
      mainWindow.center()
    } else if (type === 'sessionout') {
      mainWindow.unmaximize()
      mainWindow.setMinimumSize(400, 500)
      mainWindow.setSize(400, 500)
      mainWindow.center()
    }
    return true
  })

  ipcMain.handle('browseFile', (event, name) => {
    let file = dialog.showOpenDialogSync({ properties: ['openDirectory'] })
    return file
  })

  // 获取本地IP和MAC地址
  ipcMain.handle('getNetworkInfo', (event) => {
    const networkInterfaces = os.networkInterfaces()
    let ipv4Address
    let macAddress

    for (const iface of Object.values(networkInterfaces)) {
      for (const interfaceInfo of iface) {
        if (interfaceInfo.family === 'IPv4' && interfaceInfo.address !== '127.0.0.1') {
          ipv4Address = interfaceInfo.address
        }
        if (interfaceInfo.mac !== '00:00:00:00:00:00') {
          macAddress = interfaceInfo.mac
        }
      }
    }
    return { ipv4Address, macAddress }
  })

Menu.setApplicationMenu(menu)

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
  createWindow()
  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) createWindow()
  })
})

// 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()
  }
})

// 限制只可以打开一个应用
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
  app.quit()
} else {
  app.on('second-instance', (event, commandLine, workingDirectory) => {
    // 当运行第二个实例时,将会聚焦到mainWindow这个窗口
    if (mainWindow) {
      if (mainWindow.isMinimized()) mainWindow.restore()
      mainWindow.focus()
      mainWindow.show()
    }
  })
}

在pacakge.json中配置入口文件

2.2 创建渲染进程文件(preload.js)

要在渲染进程中使用Node.js模块,可以使用Electron的preload脚本。这是一个预加载脚本,在渲染进程被加载之前运行,可以在此脚本中引入Node.js模块供渲染进程使用。

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

const isDev = process.env.IS_DEV == 'true' ? true : false

const fs = require('fs')
const path = require('path')

//获取日期然后拼接目录
let Dir = path.join(__dirname, isDev ? `./log` : `../../document-filing/log`)

contextBridge.exposeInMainWorld('electron', {
  // 创建目录
  create_access: () => {
    fs.mkdirSync(Dir, {
      //是否使用递归创建目录
      recursive: true
    })
  },
  // 写入错误信息
  write_log(data, path) {
    let directory = Dir + '/' + path
    //先确认文件是否存在
    fs.exists(directory, (exist) => {
      if (exist) {
        // console.log('已经存在');

        //flag:'a'是追加的文件内容;追加之后记得换行
        fs.writeFile(directory, data, { flag: 'a' }, function (err, data) {
          if (err) {
            write_log(err + '\n', 'err.log')
          }
          // console.log('追加成功')
        })
      } else {
        fs.mkdirSync(Dir, {
          //递归创建目录
          recursive: true
        })

        fs.writeFile(directory, data, { flag: 'a' }, function (err, data) {
          if (err) {
            write_log(err + '\n', 'err.log')
          }
          // console.log('追加成功')
        })
      }
    })
  },
  // 删除文件
  deleteFile() {
    if (fs.existsSync(Dir)) {
      //先删除文件  Dir文件夹路径
      let files = fs.readdirSync(Dir)

      if (files.length > 3) {
        files = files.slice(0, -3)
        files.forEach(function (file, index) {
          let curPath = Dir + '/' + file
          // if (fs.statSync(curPath).isDirectory()) { // recurse
          //   fs.rmdirSync(Dir);
          //   console.log("文件夹");
          // } else { // delete file
          //   console.log("删除文件", file);
          //   fs.unlinkSync(curPath, function (err) {
          //     if (err) throw err;
          //   });
          // }

          fs.unlinkSync(curPath, function (err) {
            if (err) throw err
          })
        })
      }

      // fs.rmdirSync(pathImg);
    }
  },
  // 打开本地文件
  openPath: (path) => {
    return ipcRenderer.invoke('openPath', path)
  },
  defaultFile: () => {
    return ipcRenderer.invoke('defaultFile')
  },
  browseFile: () => {
    return ipcRenderer.invoke('browseFile')
  },
  downloadfile: (data) => {
    let obsClient = new ObsClient({
      access_key_id: data.ak,
      secret_access_key: data.sk,
      server: data.endPoint
    })

    return new Promise((res, rej) => {
      obsClient.getObject(
        {
          Bucket: data.bucketname,
          Key: data.objectname,
          SaveAsFile: data.downloadpath
        },
        (err, result) => {
          if (err) {
            // console.error('Error-->' + err);
            obsClient.close()
            rej(err)
          } else {
            // console.log('Status-->' + result.CommonMsg.Status);
            // console.log('result-->' + result.InterfaceResult.Content);
            obsClient.close()
            res(result)
          }
        }
      )
    })
  },
  changSize: (data) => {
    return ipcRenderer.invoke('changSize', data)
  },
  // 获取本地IP和MAC地址
  getNetworkInfo: (data) => {
    return ipcRenderer.invoke('getNetworkInfo', data)
  }
})

2.3 ipcMain.handle VS ipcMain.on

都是用于处理主进程和渲染进程之间的通信,但它们的使用方式和场景有所不同。

2.3.1 ipcMain.handle

  • 用途:用于处理从渲染进程发送的异步请求。适合用于需要返回结果的情况。
  • 异步行为:返回一个 Promise,可以通过 ipcRenderer.invoke 在渲染进程中获取结果。

示例:获取本地MAC和IP地址

// 主进程(electron.js)
const { ipcMain } = require('electron')
const os = require('os')

ipcMain.handle('getNetworkInfo', (event) => {
    const networkInterfaces = os.networkInterfaces()
    let ipv4Address
    let macAddress

    for (const iface of Object.values(networkInterfaces)) {
      for (const interfaceInfo of iface) {
        if (interfaceInfo.family === 'IPv4' && interfaceInfo.address !== '127.0.0.1') {
          ipv4Address = interfaceInfo.address
        }
        if (interfaceInfo.mac !== '00:00:00:00:00:00') {
          macAddress = interfaceInfo.mac
        }
      }
    }
    return { ipv4Address, macAddress }
  })
// 渲染进程(preload.js)
getNetworkInfo: (data) => {
    return ipcRenderer.invoke('getNetworkInfo', data)
}
// vue文件中
async function getNetworkInfo() {
  const { ipv4Address, macAddress } = await window.electron?.getNetworkInfo()
  console.log(macAddress, 'macAddress')
  console.log(ipv4Address, 'ipv4Address')
}

2.3.2 ipcMain.on

  • 用途:用于监听从渲染进程发送的消息。适合用于处理事件驱动的情况。
  • 异步行为:可以使用回调函数来处理消息。

示例:shell.openPath() 打开本地文件

// 主进程(electron.js)
ipcMain.on('open-path', (event, filePath) => {
    // 检查路径是否存在
    if (fs.existsSync(filePath)) {
      // 如果路径存在,使用 shell.openPath 打开
      shell
        .openPath(filePath)
        .then(() => {
          // 路径存在,发送成功消息给渲染进程
          event.reply('path-opened', 'fileExist')
        })
        .catch((error) => {
          event.reply('path-error', `Error opening path: ${error.message}`)
        })
    } else {
      // 路径不存在,发送错误信息给渲染进程
      event.reply('path-error', 'notExist')
    }
  })
// 渲染进程(preload.js)
contextBridge.exposeInMainWorld('electron', {
  // 打开本地文件
  openPath: (filePath) => {
    ipcRenderer.send('open-path', filePath)
    return new Promise((resolve, reject) => {
      ipcRenderer.on('path-opened', (event, message) => {
        // console.log(message,'打印信息message');
        resolve(message)
      })
      // 监听错误消息
      ipcRenderer.on('path-error', (event, errorMessage) => {
        // console.log(errorMessage,'打印信息errorMessage');
        resolve(errorMessage)
        // 这里可以显示错误消息到用户界面
      })
    })
  }
})
// vue文件中
function openFile() {
  const filePath = '/path/documents' // 文件所在的路径
  window.electron.openPath(filePath)
    .then((res) => {
      if (res == 'notExist') {
        // 进行文件不存在的操作
      }else if(res == 'fileExist'){
        // 进行文件存在的操作
      }
    })
    .catch((err) => {
      console.log(err, '打印信息err')
    })
}

3. Electron打包配置

"build": {
    "appId": "reviewtools-site",
    "mac": {
      "category": "public.app-category.utilities"
    },
    "nsis": {
      "oneClick": false,
      "allowToChangeInstallationDirectory": true,
      "perMachine": true
    },
    "win": {
      "target": "nsis"
    },
    "files": [
      "dist/**/*",
      "electron/**/*"
    ],
    "directories": {
      "buildResources": "assets",
      "output": "dist_electron"
    }
  }

开发工具: npm i -D electron-devtools-installer

打包工具: npm i -D vite-plugin-electron-builder

Electron官网: https://www.electronjs.org/zh/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值