1. 创建electron项目
pnpm init
pnpm add -D electron
-
修改配置项
- package.json
{
"name": "electron-menu",
"version": "1.0.0",
"description": "",
"main": "main.js", // eletron入口
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev":"electron ." // 启动electron
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"electron": "^32.1.2"
}
}
2. 创建文件
- main.js 项目入口文件
const { BrowserWindow, app} = require("electron");
const path = require("path");
const createWindow = () => {
const win = new BrowserWindow({
width: 300,
height: 300,
webPreferences: {
// 引入预加载文件
preload: path.join(__dirname, "preload.js")
}
})
// 窗口的页面
win.loadFile(path.join(__dirname, "index.html"))
}
// 主进程启动完成后启动渲染进程
app.whenReady().then(() => {
createWindow()
})
- index.html 渲染进程的网页
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<!-- https://web.nodejs.cn/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<!-- 安全策略,它限制了页面可以加载哪些资源以及从哪里加载这些资源 -->
<meta
http-equiv="X-Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<script src="./index.js"></script>
<title>Hello from Electron renderer!</title>
</head>
<body>
<h1>Hello from Electron renderer!</h1>
<p>👋</p>
</body>
</html>
- index.js 网页的script部分
暂时无内容
- preload.js 预加载文件
暂时无内容
3. electron的进程
1. 主进程
- app:主进程。
- 主要作用创建和管理应用窗口BrowserWindow
- 主进程是运行在nodejs,可直接使用Node.js API对“操作系统”控制
const { app } = require("electron");
app.whenReady().then(() => {
// 主进程开启后,开启一个渲染进程
createWindow()
});
- 主进程的窗口
const createWindow = ()=>{
const win = new BrowserWindow({
// 视窗的宽/高
width: 200,
height: 200,
});
// 对窗口进行配置
}
- 窗口的部分方法
// 设置窗口的宽高比
// 1:1,在示例中就是正圆
win.setAspectRatio(1);
// 使用path模块,来设置路径,不同的操作系统,路径的表示方式是不一样的
// __dirname 表示当前文件所在的目录
// 加载文件
win.loadFile(path.join(__dirname,"index.html"));
// 加载链接
win.loadURL("https://www.baidu.com")
// 打开开发者工具
win.webContents.openDevTools();
2. 渲染进程
1. 预加载脚本: 负责进程间通信
- 主进程和渲染进程的通信通道
- 可使用部分nodejs的api,在渲染进程中执行
- 在预加载脚本中可以直接通过window的属性操作dom,但是最好放在渲染进程中操作dom
在窗口设置预加载脚本文件
在渲染进程渲染窗口前加载,是渲染进程与主进程通信的桥梁
const win = new BrowserWindow({
width: 200,
height: 200,
// 窗口功能的设置
webPreferences: {
// 预加载脚本
preload: path.join(__dirname,"preload.js"),
},
});
2. 负责html页面在窗口的渲染
- HTML 文件是渲染器进程的入口点。
- UI 样式是通过级联样式表 (CSS) 添加的。
- 可以通过 <script> 元素添加可执行的 JavaScript 代码。
3. 安全机制
1. 设置预加载脚本使用nodejs的全部api
- 不开启的话,预加载脚本没有权限做的事情,需要主进程去做
const win = new BrowserWindow({
width: 200,
height: 200,
// 网页功能的设置
webPreferences: {
// 预加载脚本
preload: path.join(__dirname,"preload.js"),
// 开启node环境
nodeIntegration: true,
//contextIsolation=true
},
});
2. 安全设置
node集成:nodeIntegration
默认值为false
上下文隔离:contextIsolation
默认值为true
contextIsolation
网页的scripit与预加载文件是否可以直接共享数据contextIsolation=true
:不共享数据contextIsolation=false
:共享数据
nodeIntegration
nodeIntegration:false
预加载文件不能直接使用nodejsnodeIntegration:true
预加载文件可以直接使用nodejs,如果同时contextIsolation=false
,网页与预加载文件共享数据,导致网页也可以直接使用nodejs
3. 预加载脚本和页面的数据共享
contextIsolation=true
- 在预加载脚本中和页面中的window是同一个对象,但是他俩定义的数据并没有共享
- 在预加载脚本中定义window.abc= 'aaabbb'
,在页面的window
无法访问abc
属性
4. 通信
预加载脚本最主要的功能:主进程与页面通信的桥梁
1. 主进程与预加载脚本订阅方法通信
- 主进程中的订阅和发送(与预加载脚本的通信)
// 订阅消息
ipcMain.on("load-data",(event, ...args)=>{
// 通过event.sender获取到发送者,向发送者返回信息
event.sender.send("response",...args)
})
- 预加载脚本的订阅和发送(与主进程通信)
- 发送消息
ipcRenderer.send("load-data",...args);
- 订阅消息
ipcRenderer.on("response",(event, ...args)=>{
// todo
});
2. 预加载脚本与html中的script通信
1. 预加载脚本
contextBridge.exposeInMainWorld
的key
与html中的script
共享
contextBridge.exposeInMainWorld("electronAPI", {
a:123,
b:function(callback){
// todo
ipcRenderer.send("load-data",...args1);
ipcRenderer.on("response",(event, ...args2)=>{
// 通过callback将参数传给 html中的scrip
// 这样就能将主进程的数据发往渲染进程
callback(args2)
});
}
});
2. html中的script
- 通过
window.[共享的key]
,访问预加载脚本中的数据
window.addEventListener("DOMContentLoaded", () => {
// 对dom的所有操作都放在这个文件
window.electronAPI.a
window.electronAPI.b(function(arg2){
// todo
})
})
3. 订阅通信方式示例: 自定义菜单
- 创建一个菜单文件
const { Menu } = require('electron')
const createMenu = (win) => {
const menu = [
{
label:'菜单',
submenu:[
{
label:'增加',
click(){
// 订阅事件发送
win.webContents.send("add-item")
}
}
]
}
]
Menu.setApplicationMenu(Menu.buildFromTemplate(menu))
}
module.exports = {
createMenu
}
- 在窗口中使用菜单
const createWindow = () => {
const win = new BrowserWindow({
width: 300,
height: 300,
webPreferences: {
preload: path.join(__dirname, "preload.js")
}
})
win.loadFile(path.join(__dirname, "index.html"))
// 菜单按钮
createMenu(win)
}
- 在预加载脚本中监听订阅事件
contextBridge.exposeInMainWorld('electronAPI', {
counter:(callback) => {
ipcRenderer.on('add-item',()=>{
callback()
})
}
})
- 页面的script
window.addEventListener("DOMContentLoaded", () => {
window.electronAPI.counter(()=>{
const dom = document.getElementById('counter')
dom.innerText = parseInt(dom.innerText) + 1
})
})
4. 订阅通信方式示例:在页面中显示版本号
- html
<div id="chrome"></div>
<div id="electron"></div>
<div id="node"></div>
- 预加载文件
- 不推荐在预加载文件中操作dom
- 预加载文件中使用了nodejs的api,process
window.addEventListener("DOMContentLoaded", () => {
for(let app of ['chrome','electron','node']){
const el = document.querySelector(`#${app}`)
el.innerHTML=`${app}:${process.versions[app]}`
}
})
5. 主进程和预加载脚本,invoke.handle,请求-响应模式
- 与on和send订阅的方式相比
- 在invoke和handle中使用return返回数据,更方便,适合有数据返回的情况
- html页面
<button id="upload_btn">上传图片</button>
<img id="img" src="" alt="">
- html页面的script
window.addEventListener("DOMContentLoaded", () => {
const btn = document.getElementById("upload_btn")
const img = document.getElementById("img")
btn.addEventListener("click", async () => {
img.src = await window.electronAPI.uploadImg()
})
})
- 预加载脚本
- 发送请求,并等待响应
- 使用on和send的订阅方式时,需要使用callback给页面传递数据,现在只要使用return就能把数据传回给html页面的script
- 一句代码即完成了给主进程发送消息的功能也完成了返回给页面数据的功能
contextBridge.exposeInMainWorld("electronAPI",{
uploadImg:()=>{
return ipcRenderer.invoke("upload-img")
}
})
- 主进程
- 接收请求并发送响应
- 与订阅相比,只需要return就可以把数据发送到预加载脚本中
ipcMain.handle("upload-img",async (event)=>{
// 使用dialog上传文件
const obj = await dialog.showOpenDialog()
return obj.filePaths[0]
})
5. Menu
- 运行在主进程中
- Menu的方法和属性官网链接
- 菜单配置项的官网链接
1. 顶部菜单项
- 创建menu.js文件
role
:electron
已经定义好的菜单.官网链接{type: "separator"},
分隔符accelerator: 'CommandOrControl+k'
定义快捷键
const { app, Menu, shell } = require("electron");
const isMac = process.platform === "darwin";
const mainMenu = (win) => {
const menu = [
...(isMac // 是否为mac系统,mac系统与win的菜单不一样
? []
: [
{
label: "File",
submenu: [
{
label: "开发者工具",
accelerator: 'CommandOrControl+k', // 定义快捷键
click: () => {
// 打开开发者工具
win.webContents.openDevTools({mode: "detach",activate:true,});
// 给渲染进程发送订阅事件
win.webContents.send("menu-subscribe");
},
},
{
label: "退出",
click: () => { // 自定义点击事件
app.quit();
},
},
{ type: "separator" }, // 分割线
{
label: "退出",
role: "quit", // electron已经定义好的菜单
},
],
},
]),
];
Menu.setApplicationMenu(Menu.buildFromTemplate(menu));
};
module.exports = mainMenu;
- 给menu传递窗口
const { app } = require('electron')
const mainWindow = require('./main-window.js')
const mainMenu = require('./menu')
app.whenReady().then(()=>{
const win = mainWindow()
// 传递窗口对象给菜单模块
mainMenu(win)
})
2. 右键菜单
- 鼠标的右键点击事件
const { ipcRenderer } = require("electron");
// 窗口右键点击事件
window.addEventListener("contextmenu", function (event) {
ipcRenderer.send("context-menu-event");
});
- 在主程序中订阅事件
const { ipcMain,Menu,BrowserWindow } = require("electron");
ipcMain.on("context-menu-event", (event, args) => {
// 右键菜单
const menu = [
{
label: "Menu Item 1",
click: () => {},
},
{ type: "separator" },
{ label: "Menu Item 2", type: "checkbox", checked: true },
];
Menu.buildFromTemplate(menu).popup(BrowserWindow.fromWebContents(event.sender));
});
6. dialog
- 运行在主进程中
- 官网链接
1. dialog.showMessageBox 显示提示信息
- 是electron的弹出框,不是网页的弹出框,所以要放在menu中使用
const { dialog, Menu } = require("electron");
const createMenu = (win) => {
const mainDialog = [
{
label: "文件",
submenu: [
{
label: "新建",
click: async () => {
const res = await dialog.showMessageBox(win,{ // 参数win可以省略,这里是为了当主窗口设置置顶后,dialog还可以出现在窗口的上面
title: "文件管理", // 标题
detail: "确定要新建文件吗?", // 提示信息
buttons: ["确定", "取消"], // 按钮的顺序
cancelId: 1, // 取消按钮的数组索引
});
console.log(res);
// 点击确定按钮:{ response: 0, checkboxChecked: false }
// 点击取消按钮:{ response: 1, checkboxChecked: false }
},
},
],
},
];
Menu.setApplicationMenu(Menu.buildFromTemplate(mainDialog));
};
module.exports = createMenu;
- 需要注意的配置项:
- 通过 Esc 键取消对话框,cancelId的值就是 Esc 键的目标
- 数组的顺序决定了按钮在对话框中位置
cancelId:1
,指明了取消按钮在数组中的索引为1
buttons: ["确定", "取消"], // 按钮的顺序
cancelId: 1, // 取消按钮的数组索引
cancelId
的默认值为0,则需要在数组中将取消按钮排到索引0的位置,这样就保证Esc键可以取消对话框
buttons: ["取消", "确定"], // 按钮的顺序
// cancelId: 1,
2. 复选框
{
label: "删除文件",
click: async () => {
const res = await dialog.showMessageBox(win, {
title: "文件管理", // 标题
detail: "确定要删除文件吗?", // 提示信息
buttons: ["取消", "确定"], // 按钮的顺序
checkboxLabel: "确定要删除文件?", // 复选框
checkboxChecked: false, // 复选框默认选中状态
});
console.log(res);
// 点击确定按钮:{ response: 0, checkboxChecked: false }
// 点击取消按钮:{ response: 1, checkboxChecked: false }
},
},
-
返回的结果:
通过对返回结果中两个属性值来判断下一步要做什么
response: 0
, 点击按钮对应的buttons
数组的索引checkboxChecked: false
点击确认或者取消后返回复选框的状态
3. dialog.showOpenDialog 打开文件
官网链接
- 参考4.5:通信的请求响应模式
const res = await dialog.showOpenDialog({
title: "选择图片文件",
properties: ["openFile", "multiSelections"], // multiSelections:打开多个文件
filters: [{ name: "image", extensions: ["jpg", "png"] }],
});
res的结果,如果是多选,路径组成数组filePaths
{
canceled: false,
filePaths: [ 'G:\\my doc\\图片\\测试用的图片\\c9b1eff4fc8e86abeab663cc7a9e7c30.jpg' ]
}
4. dialog.showSaveDialog 保存文件
- properties:保存文件时,可以新建文件夹
ipcMain.handle("save-file",async (event,value)=>{
const res = await dialog.showSaveDialog({properties:"createDirectory"})
// res:{canceled:false, filePath:'文件的保存地址'}
if(!res.canceled){ // 用户是否点击了取消
try{
fs.writeFileSync(res.filePath,value)
// fs方法没有返回值,保存失败会抛出错误信息
return true
}catch(error){
alert("文件保存失败,"+ error)
}
}
return false
})
7. webContents
1. 在浏览器打开<a>的新窗口
<a href="https://www.baidu.com" target="_blank">百度</a>
<--! target="_blank" 在新窗口打开 -->
点击a标签,结果会打开一个新的electron窗口,而不是在浏览器中打开
const createWindow = ()=>{
const win = new BrowserWindow({
width:800,
height:600,
alwaysOnTop:true,
webPreferences:{
preload: path.join(__dirname,'preload.js')
}
})
win.loadFile(path.join(__dirname,'index.html'))
win.webContents.openDevTools()
// 设置setWindowOpenHandler后就可以在浏览器打开新窗口
win.webContents.setWindowOpenHandler((detail)=>{
shell.openExternal(detail.url)
})
return win
}