无所畏惧地面对未知,并将其视为成长的机会
大纲
官网
中文官网:https://www.electronjs.org/zh
快速入门
1.安装node.js – 这里推荐用nvm管理
一.nvm安装node方式以及可能出现问题的解决方案
1.window下安装并使用nvm
2.nvm安装及安装后node不能使用
3.node.js安装后在命令行输入“node -v ” 查看版本提示:‘node‘ 不是内部或外部命令,也不是可运行的程序的解决方法
4.检查node/npm是否正确安装 node -v && npm -v
2.脚手架创建
mkdir my-electron-app && cd my-electron-app
npm init
//npm init 后 package.json
{
"name": "my-electron-app",
"version": "1.0.0",
"description": "Hello World!",
"main": "main.js", //!!!main代表主进程文件,需要根据配置的新建主进程文件名(这里init会存在为index.js情况,手动改为main.js)
"author": "Jane Doe",
"license": "MIT"
}
3.electron 包安装到应用的开发依赖
npm install --save-dev electron
//package.json 修改
{
"scripts": {
"start": "electron ."
}
}
//首次启动
//此时main.js主进程未创建,Electron应用在启动时找不到指定的入口文件,在package.json配置的main
npm start
4.创建主进程(main.js)并启动项目
1.创建页面
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
2.配置main.js
新建main.js文件 此时 npm start 不会抛出异常
const { app, BrowserWindow } = require('electron')
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
show: false, //ready-to-show 解决闪烁
autoHideMenuBar: true, // 隐藏顶部file menu
backgroundColor: '#fff', //对于一个复杂的应用,ready-to-show 可能发出的太晚,会让应用感觉缓慢。 在这种情况下,建议立刻显示窗口,并使用接近应用程序背景的 backgroundColor
})
//优雅地显示窗口 -- 解决窗口闪烁问题
win.once('ready-to-show', () => {
win.show()
win.webContents.openDevTools({ mode: 'detach' }) //开发者工具
})
win.loadFile('index.html') // 加载项目使用loadURL
win.maximize() //窗口最大化
}
//在 Electron 中,只有在 app 模块的 ready 事件被激发后才能创建浏览器窗口。 您可以通过使用 app.whenReady() API来监听此事件
app.whenReady().then(() => {
createWindow()
// 处理 macOS 特有的行为,提供一致的用户体验
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
//关闭所有窗口时退出应用 (Windows & Linux)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
3.启动项目 – 效果
进阶 – 基于项目场景功能使用
场景一:web交互打开文件系统
// main.js
const { app, BrowserWindow,dialog,ipcMain } = require('electron')
// ipcMain 从主进程到渲染进程的异步通信
// dialog 显示用于打开和保存文件、警报等的!!本机系统!!对话框
const path = require('path')
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
show: false,
backgroundColor: '#fff',
autoHideMenuBar: true,
webPreferences: {// 网页功能设置
preload: path.join(__dirname, 'preload.js')// 在页面运行其他脚本之前预先加载指定的脚本
}
})
win.once('ready-to-show', () => {
win.show()
win.webContents.openDevTools({ mode: 'detach' })
})
win.loadFile('index.html')
//ipcMain,用于在主进程中处理渲染进程(即前端页面)发送的异步消息。具体来说,这个方法的作用是监听名为 dialog:openProject 的事件(preload.js发送),并在事件触发时执行指定的回调函数。
ipcMain.handle('dialog:openProject', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog(win, {
properties: ['openFile'],
filters: [
{ name: 'Project Files', extensions: ['db'] },
]
})
if (canceled) {
return
} else {
return filePaths[0]
}
})
}
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
// preload.js
// contextBridge 和 ipcRenderer 结合使用的主要目的是在 Electron 应用程序中实现安全的进程间通信(Inter-Process Communication, IPC)。
// contextBridge 用于在渲染进程和主进程之间安全地传递数据和函数
// ipcRenderer 用于在渲染进程中与主进程进行通信
const {contextBridge, ipcRenderer} = require('electron')
//exposeInMainWorld(key,api) -- 将api注入到window,web通过window.myAPI.selectProject于主进程通信
contextBridge.exposeInMainWorld('myAPI', {
selectProject: () => ipcRenderer.invoke('dialog:openProject'),
})
场景二:区分开发和生产环境处理开发者工具
win.once('ready-to-show', () => {
win.show()
if (!app.isPackaged) {
console.log(`[main] open dev tools`)
mainWindow.webContents.openDevTools({ mode: 'detach' })
}
}
场景三:electron导入压缩包并解压到指定目录(后端需要解压资源进行后续处理
思路:
1.创建开发环境和生产环境都便于读取的资源存放目录
2.建立主进程和渲染进程的交互preload.js封装方法
3.主进程main.js使用Node.js tar模块实现解压到指定目录
//过程中针对于目录的管理进行了处理,main.js供参考
//渲染进程
window.myAPI && window.myAPI.getZip().then(res => {
console.log(res,'res');
})
//preload.js 详情参考场景一
getZip: () => ipcRenderer.invoke('dialog:getZip')
//main.js
// 安装 npm install tar
// 导入tar模块
const tar = require('tar');
//diagnosisDir 需要存放的目录位置,在这里单纯用来占位,后续处理
const diagnosisDir = path.join(__dirname, 'diagnosis')
//dialog:getZip 注册位置参考场景一
ipcMain.handle('dialog:getZip', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [
{ name: 'Compressed Files', extensions: ['tar.gz'] },// tar.gz 允许选择的格式
]
});
// canceled 检测用户是否取消了对话框
if (canceled) {
return;
} else {
const tarFilePath = filePaths[0];
// 确保目标目录存在
if (!fs.existsSync(diagnosisDir)) {
fs.mkdirSync(diagnosisDir, { recursive: true });
}
try {
// 解压 tar.gz 文件
await tar.extract({
file: tarFilePath,
cwd: diagnosisDir,
});
console.log('Extraction completed:', diagnosisDir);
return { tarFilePath, diagnosisDir }; // 返回解压后的目录路径
} catch (error) {
console.error('Error during extraction:', error);
return { error: 'Extraction failed' };
}
}
})
问题:一开始的设想是基于__dirname拼接diagnosis目录来统一存放开发和生产环境的压缩包资源,后续发现diagnosis生成路径与预期不符
问题具体原因:
1.通过配置package.json的config字段用来创建diagnosis目录,初衷是通过path.join(__dirname, ‘diagnosis’)都可以读取到
2.实际两种环境下,开发环境正常,生产环境下resources下成功生成了diagnosis目录但是具体目录是在resources/app/resources于预期不符,且层级嵌套过深后续不利于现场人员的使用和可读性
//这里参考下config中的配置 !!!后续优化不会使用 仅作参考
"config": {
"forge": {
"packagerConfig": {
"extraResource": [
"./diagnosis"
]
}
}
},
//优化版本 -- 此时不再使用config配置实现打包生成文件目录
//避免混淆 main.js全量代码 渲染进程和preload.js根据上面书写
const { app, BrowserWindow,dialog,ipcMain } = require('electron')
const path = require('path')
const fs = require('fs');
const tar = require('tar');
let diagnosisDir // 解压资源存放目录
// process.cwd() 返回当前工作目录的绝对路径。当前工作目录是指 Node.js 进程启动时的目录 .exe启动的目录
(function initDiagnosisDir() {
// 如果目录不存在,则创建目录
diagnosisDir = path.join(process.cwd(), 'diagnosis');
if (!fs.existsSync(diagnosisDir)) {
fs.mkdirSync(diagnosisDir, { recursive: true });
}
})();
const path = require('path')
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
show: false,
backgroundColor: '#fff',
autoHideMenuBar: true,
webPreferences: {// 网页功能设置
preload: path.join(__dirname, 'preload.js')// 在页面运行其他脚本之前预先加载指定的脚本
}
})
win.once('ready-to-show', () => {
win.show()
win.webContents.openDevTools({ mode: 'detach' })
})
win.loadFile('index.html')
ipcMain.handle('dialog:getZip', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [
{ name: 'Compressed Files', extensions: ['tar.gz'] },
]
});
if (canceled) {
return;
} else {
const tarFilePath = filePaths[0];
// 确保目标目录存在
if (!fs.existsSync(diagnosisDir)) {
fs.mkdirSync(diagnosisDir, { recursive: true });
}
try {
// 解压 tar.gz 文件
await tar.extract({
file: tarFilePath,
cwd: diagnosisDir,
});
console.log('Extraction completed:', diagnosisDir);
return { tarFilePath, diagnosisDir }; // 返回解压后的目录路径
} catch (error) {
console.error('Error during extraction:', error);
return { error: 'Extraction failed' };
}
}
})
}
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
场景四:文件下载,软件底部进度条
//基于场景三的dialog:getZip进行调整
const { PassThrough } = require('stream');
//
ipcMain.handle('dialog:getZip', async (event) => {
return new Promise((resolve, reject) => {
dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [
{ name: 'Compressed Files', extensions: ['tar.gz'] },
]
}).then(({ canceled, filePaths }) => {
if (canceled) {
resolve({ canceled: true });
} else {
const tarFilePath = filePaths[0];
// 确保目标目录存在
if (!fs.existsSync(diagnosisDir)) {
fs.mkdirSync(diagnosisDir, { recursive: true });
}
try {
// 获取 tar 文件的总大小
const fileStats = fs.statSync(tarFilePath);
const totalSize = fileStats.size;
let processedSize = 0;
// 创建一个可读流来读取 tar 文件
const readStream = fs.createReadStream(tarFilePath);
const passThrough = new PassThrough();
// 监听 data 事件来更新已处理的大小
passThrough.on('data', (chunk) => {
processedSize += chunk.length;
const progress = parseFloat((processedSize / totalSize).toFixed(2));
console.log(`Progress: ${progress}%`);
mainWindow.setProgressBar(progress);
});
// 使用 tar 解析流来解压文件到指定目录
const extractStream = tar.extract({
cwd: diagnosisDir, // 指定解压目录
});
// 监听错误事件
readStream.on('error', (err) => {
console.error('Error reading the archive:', err);
reject(err.message);
});
extractStream.on('finish', () => {
console.log('Extraction completed');
mainWindow.setProgressBar(-1); // Reset progress bar
resolve('success');
});
// 管道 tar 流到 passThrough,然后到 tar 解析流
readStream.pipe(passThrough).pipe(extractStream);
} catch (error) {
console.error('Error during extraction:', error);
reject(error.message);
}
}
}).catch((error) => {
console.error('Error showing dialog:', error);
reject(error.message);
});
});
});