目录
2.3 ipcMain.handle VS ipcMain.on
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/