Skip to content
快速上手electron

官方文档: https://www.electronjs.org/zh/docs/latest/

搭建项目

新建文件夹并初始化项目

js
mkdir my-electron-app && cd my-electron-app
npm init -y

注意:新生成的package.json的author(作者)和description(描述)字段要填写补全,不然后期打包会打不了

package.json入口文件修改为main.js,修改如下:

js
{
  "name": "electron",
  "version": "1.0.0",
  "description": "这是我的第一个electron项目",
  "main": "main.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "萧寂",
  "license": "ISC"
}

安装electron

js
cnpm i --save-dev electron
// 或者
npm i --save-dev electron
// 或者
yarn add --dev electron

安装过程可能会很慢,等着就行了,除非报错

package.json下面修改脚本如下,新增一个start命令

js
{
  "name": "electron",
  "version": "1.0.0",
  "description": "这是我的第一个electron项目",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "keywords": [],
  "author": "萧寂",
  "license": "ISC",
  "devDependencies": {
    "electron": "^31.2.0"
  }
}

根目录下新建main.js

js
console.log('electron start')

执行yarn run start

控制台如果有打印上面那句话则代表electron项目已经启动了,目前还没创建窗体,至此,项目创建准备已经结束

在主进程创建窗口并显示外部链接页面

main.js

js
const { app, BrowserWindow } = require('electron')

// 当app准备好就执行创建一个窗口
app.on('ready', () => {
  // 创建窗口
  let win = new BrowserWindow({
    x: 100,
    y: 50, //窗体坐标
    show: true, // false为不展示窗体,默认true展示,相当于将窗口隐藏了
    width: 800,
    height: 600, //长宽
    // maxHeight: 600,
    // maxWidth: 1000, //最大宽高
    minHeight: 200,
    minWidth: 300, //最小宽高
    resizable: true, //是否允许缩放
    title: "萧寂", //标题(加上这个属性,在页面中就不要有title标签了)
    // icon: "./icon.png", //设置icon图标
    // frame: false, //只保留主体部分,不保留其他的选项卡窗口了,隐藏菜单栏
    // transparent: true, //将窗体完全透明化
    autoHideMenuBar: true, //只保留标题,不保留其他的选项卡部分,也是隐藏菜单栏意思
    alwaysOnTop:true, // 将窗口置顶
  })
  win.loadURL('https://xiaojiblog.netlify.app/') // 打开外部链接
})

运行yarn run start

效果图

在这里插入图片描述支持放大全屏,也可以在上面参数里面把最大宽度高度限制放开,不设置的话就默认可以全屏

更多配置项参考:https://www.electronjs.org/zh/docs/latest/api/base-window#class-basewindow

在主进程加载并显示本地页面

根目录下创建/pages/index.html和/pages/index.css

/pages/index.html内容如下

js
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="./index.css">
</head>

<body>
  <h1>第一个本地electron页面</h1>
</body>

</html>

/pages/index.css内容如下

css
h1{
  background-color: gray;
  color: orange;
}

修改主进程代码main.js如下

js
const { app, BrowserWindow } = require('electron')

// 当app准备好就执行创建一个窗口
app.on('ready', () => {
  // 创建窗口(下面的配置在上面讲过了,这里就删掉了,只留下三个)
  let win = new BrowserWindow({
    width: 800,
    height: 600, //长宽
    autoHideMenuBar: true, //只保留标题,不保留其他的选项卡部分,也是隐藏菜单栏意思
  })

  win.on('close', () => {
    // 从性能考虑,应该释放窗体这个变量,删除窗体引用
    win = null
  })
	
  win.loadFile('./pages/index.html') // loadFile就是加载本地页面的,loadURL加载的是在线链接
})

运行yarn run start

效果图如下:

在这里插入图片描述

electron生命周期事件

js
ready:app初始化完成  //重要
dom-ready:一个窗口中的文本加载完成  //重要
did-finsh-load:导航完成时触发   //重要
window-all-closed:所有窗口都被关闭时触发  //重要
before-quit:在关闭窗口之前触发
will-quit:在窗口关闭并且应用退出时触发
quit:当所有窗口被关闭时触发
close:当窗口关闭时触发,此时应删除窗口引用

main.js代码

js
const { app, BrowserWindow } = require("electron")
const createWindow = () => {
  // 创建窗口
  let win = new BrowserWindow({
    width: 800,
    height: 600,
  })
  //当前窗口显示的页面
  win.loadFile("index.html")

  // 这个webContents对象可以控制dom元素加载事件
  win.webContents.on('did-finish-load', () => {
    console.log('3333->did-finish-load')
  })
  win.webContents.on('dom-ready', () => {
    console.log('2222->dom-ready')
  })
  // 窗口关闭
  win.on('close', () => {
    console.log('8888->close')
    // 从性能考虑,应该释放窗体这个变量,删除窗体引用
    win = null
  })
}

// 生命周期
// 通过on监听事件
app.on('ready', () => {
  console.log("1111->ready")
  createWindow()
})

app.on("window-all-closed", () => {
  // 如果监听了window-all-closed这个事件,需要在事件里面主动退出应用,没有监听事件的话默认会直接退出应用
  // 但如果监听了此事件,但没有退出操作的话,后续的567生命周期也不会执行
  console.log("4444->window-all-closed")
  //退出应用
  app.quit()
})

app.on("before-quit", () => {
  console.log("5555->before-quit")
})

app.on("will-quit", () => {
  console.log("6666->will-quit")
})

app.on("quit", () => {
  console.log("7777->quit")
})

从打开窗体到关闭窗体打印结果如下

在这里插入图片描述

打开控制台调试工具

在当前窗口按下ctrl+shift+i

效果图:在这里插入图片描述

刷新页面

按下ctrl+r

启动项目遇到的两个警告

1.自动填充问题,在vscode终端打印的警告内容,官方未做处理,electron开发者回复不影响开发可以先忽略,警告内容如下:

js
[892:0714/014647.854:ERROR:CONSOLE(1)] "Request Autofill.enable failed. {"code":-32601,"message":"'Autofill.enable' wasn't found"}", source: devtools://devtools/bundled/core/protocol_client/protocol_client.js (1)
[892:0714/014647.854:ERROR:CONSOLE(1)] "Request Autofill.setAddresses failed. {"code":-32601,"message":"'Autofill.setAddresses' wasn't found"}", source: devtools://devtools/bundled/core/protocol_client/protocol_client.js (1)

2.在窗口的ctrl+shift+i的控制台,会有个warning的报错,是关于(Insecure Content-Security-Policy)的,是内容安全的警告,解决方式如下:

在html页面上新增一个meta标签,内容如下

js
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">

然后再次启动项目就会发现警告没了

完善窗口关闭行为

在Windows和Linux上,关闭所有窗口通常会完全退出一个应用程序。

但是在mac上,关闭所有窗口并不会完全退出程序,因此这里需要对mac电脑进行一个判断

官方介绍: https://www.electronjs.org/zh/docs/latest/tutorial/quick-start#%E7%AE%A1%E7%90%86%E7%AA%97%E5%8F%A3%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F

在主进程加上下面的事件和判断,修改main.js如下:

js
const { app, BrowserWindow } = require('electron')

// 创建窗口
function createWindow () {
  // 创建窗口
  let win = new BrowserWindow({
    width: 800,
    height: 600, //长宽
    autoHideMenuBar: true, //只保留标题,不保留其他的选项卡部分,也是隐藏菜单栏意思
  })

  win.on('close', () => {
    // 从性能考虑,应该释放窗体这个变量,删除窗体引用
    win = null
  })

  win.on('close', () => {
    // 从性能考虑,应该释放窗体这个变量,删除窗体引用
    win = null
  })
	
  win.loadFile('./pages/index.html')
}

// 当app准备好就执行创建一个窗口
app.on('ready', () => {
  createWindow()

  // 监听应用被激活
  app.on('activate', () => {
    // 当应用激活后,窗口数量为0时,重新创建一个窗口(mac使用,在windows和Linux窗口为0直接退出了)
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

// 监听所有窗口都被关闭事件
app.on('window-all-closed', () => {
  // 不是mac系统就执行退出
  if (process.platform !== 'darwin') app.quit()
})

配置热更新自动重启

安装插件

js
yarn add nodemon -D

修改package.json下面的script节点的start命令如下:

js
"start": "nodemon --exec electron ."

运行yarn run start

然后会发现.html后缀的文件内容更换了并不会自动重启项目,只有main.js主进程代码更换了才重启,为了解决这个问题,继续往下看

根目录下新建nodemon.json文件,内容如下:

js
{
  "ignore": [ // 忽略那些文件夹
    "node_modules",
    "dist"
  ],
  "restartable": "r", // 短命令,当文件没更改,在终端输入字母r可以自动重启
  "watch": ["*.*"], // 监视所有文件
  "ext": "js,json,html,css,vue" // 包含的文件名后缀
}

运行yarn run start再次重启项目就会发现一切正常,都可以实现自动刷新了

主进程与渲染进程进行通信(下面两种方式选其一即可)

简单来说就是渲染进程浏览器环境不能调用主进程的nodejs的相关api和方法,进行通信后就可以了,下面是两种方式, 第一种是:将渲染进程设置为可以访问nodejs方法,这时候渲染进程拥有了访问window环境和nodejs环境的方法和api调用 第二种是:使用预加载脚本,当有需求要操作nodejs的api调用时,渲染进程调用预加载脚本的对应方法,向主进程发起通信事件,主进程接收到对应的事件后就会执行操作

一:在渲染进程直接使用nodejs的所有api调用,本人常用方式(简单)

后面所有的案例都是使用这个方式在渲染进程里面做操作

需求: 用户输入文字后获取输入框内容并写入本地hello.txt文件内,点击第二个按钮可以将写入的文件内容信息弹出来

修改/pages/index.html内容如下:

html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="Content-Security-Policy"
    content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">
  <title>Document</title>
  <link rel="stylesheet" href="./index.css">
</head>

<body>
  <h1>第haha本地electron页面</h1>
  <input type="text" id="input">
  <br>
  <button id="btn1">向D盘写入文件hello.txt</button>
  <br>
  <button id="btn2">读取D盘hello.txt文件</button>
  <script src="./render.js"></script>
</body>

</html>

修改main.js如下

js
const { app, BrowserWindow } = require('electron')
const path = require('path')
// 创建窗口
function createWindow () {
  // 创建窗口
  let win = new BrowserWindow({
    width: 800,
    height: 600, //长宽
    autoHideMenuBar: true, //只保留标题,不保留其他的选项卡部分,也是隐藏菜单栏意思
    webPreferences: {
      nodeIntegration: true,  // 配置这三个选项就可以实现在渲染进程使用nodejs的api调用
      contextIsolation: false,
      enableRemoteModule: true
    }
  })

  win.on('close', () => {
    win = null
  })
	
  win.loadFile('./pages/index.html')
}

// 当app准备好就执行创建一个窗口
app.on('ready', () => {
  createWindow()
})

渲染进程render.js内容改为如下

js
const fs = require('fs')
const btn = document.getElementById('btn1')
const btn2 = document.getElementById('btn2')
const input = document.getElementById('input')
btn.addEventListener('click', () => {
  // 向D盘写入文件
  fs.writeFileSync('D:/hello.txt', input.value) // 直接调用nodejs写入文件的方法
})
btn2.addEventListener('click', async () => {
  // 向D盘读取文件
  alert(fs.readFileSync('D:/hello.txt').toString()) // 直接调用nodejs读取文件的方法
})

二:使用预加载脚本,本人不常用(复杂还麻烦)

主进程是在nodejs环境中的,可以使用很多nodejs的内置函数和变量,也可以操作文件和文件夹等,渲染进程是在window环境下,是浏览器环境,根目录下创建预加载脚本payload.js可以操作少量nodejs的api,但仅仅是少量,可以使用这个预加载脚本进行通信,如contextBridge,ipcRenderer在预加载脚本是支持的,可以利用它向主进程发送事件,主进程接收到对应事件可以在主进程nodejs环境进行各种处理,也可以把数据返回给预加载脚本,预加载本质是在渲染环境下的,打印语句也是在浏览器控制台可查看,因此在预加载脚本中的每个对象都是挂载到window上面的,可以直接在渲染进程使用

为了规范,渲染进程统一命名render.js

在根目录下创建/pages/render.js这个就是渲染进程

需求: 用户输入文字后获取输入框内容并写入本地hello.txt文件内,点击第二个按钮可以将写入的文件内容信息弹出来

html页面

/pages/index.html内容如下

js
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="Content-Security-Policy"
    content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">
  <title>Document</title>
  <link rel="stylesheet" href="./index.css">
</head>

<body>
  <h1>第haha本地electron页面</h1>
  <input type="text" id="input">
  <br>
  <button id="btn1">向D盘写入文件hello.txt</button>
  <br>
  <button id="btn2">读取D盘hello.txt文件</button>
  <script src="./render.js"></script>
</body>

</html>

主进程main.js

main.js代码如下

js
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const fs = require('fs')
// 创建窗口
function createWindow () {
  let win = new BrowserWindow({
    width: 800,
    height: 600,
    autoHideMenuBar: true,
    webPreferences: {
      preload: path.resolve(__dirname, './preload.js'), // 指向预加载脚本路径,这里必须使用根目录
    }
  })

  // 接收到预加载脚本传来的渲染进程的数据
  ipcMain.on('save-file', (event, data) => {
    // 参数二是传来的数据
    console.log('save-file', event, data)
    // 根据传来的数据向D盘写入文件
    fs.writeFileSync('D:/hello.txt', data)
  })

  // 接收到渲染进程的请求,向渲染进程进行数据通信
  ipcMain.handle('read-file', () => {
    // 根据传来的数据向D盘写入文件
    return fs.readFileSync('D:/hello.txt').toString()
  })

  win.on('close', () => {
    // 从性能考虑,应该释放窗体这个变量,删除窗体引用
    win = null
  })
	
  win.loadFile('./pages/index.html')
}

app.on('ready', () => {
  createWindow()
})

payload预加载脚本

payload.js内容如下

js
// 预加载脚本属于渲染进程,打印结果在浏览器控制台查看

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

contextBridge.exposeInMainWorld('myAPI', {
  saveFile: (data) => {
    // 渲染进程通知主进程进行通信
    ipcRenderer.send('save-file', data)
  },
  readFile: async () => {
    // 获取主进程传来的数据
    return ipcRenderer.invoke('read-file')
  }
})

渲染进程

/pages/render.js内容如下

js
const btn = document.getElementById('btn1')
const btn2 = document.getElementById('btn2')
const input = document.getElementById('input')
btn.addEventListener('click', () => {
  // 调用预加载脚本的方法,通过预加载脚本给主进程进行通信
  // 写入文件
  myAPI.saveFile(input.value)
})

btn2.addEventListener('click', async () => {
  let res = await myAPI.readFile()
  // 读取主进程读取出来的文件内容
  alert(res)
})

点击按钮创建子窗口两种方式(两种都可以推荐)

和上面的通信一样 方式一是直接在渲染进程使用remote模块进行窗口创建 方式二是渲染进程给主进程进行通信事件,主进程接收到对应事件后开始创建窗口

一:使用remote模块

较低版本的electron 在主进程中设置enableRemoteModule:true后,只需要从electron中引入remote即可

js
const remote = require('electron');

较高版本的electron(以我的19.0.8为例)

1、先安装模块@electron/remote

js
npm install --save @electron/remote

2.主进程中设置enableRemoteModule:true,同时在创建窗口实例之后引入模块

js
require('@electron/remote/main').initialize()
require("@electron/remote/main").enable(mainWin.webContents)

以下是修改的main.js主进程代码

js
const { app, BrowserWindow, ipcMain } = require('electron')
// 创建窗口
function createWindow () {
  let win = new BrowserWindow({
    width: 800,
    height: 600,
    autoHideMenuBar: true,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
      enableRemoteModule: true  // 这里要开启为true
    }
  })
  // 新增如下两行
  require('@electron/remote/main').initialize()
  require("@electron/remote/main").enable(win.webContents)

  win.on('close', () => {
    win = null
  })

  win.loadFile('./pages/index.html')
}

app.on('ready', () => {
  createWindow()
})

渲染进程/pages/render.js更改如下:

js
const btn1 = document.getElementById('btn1')
const remote = require("@electron/remote")
btn1.addEventListener('click', async () => {
  // 创建窗口
  let newWin = new remote.BrowserWindow({
    width: 800,
    height: 600,
    autoHideMenuBar: true,
  })

  newWin.on('close', () => {
    newWin = null
  })

  newWin.loadURL('https://xiaojiblog.netlify.app/')
})

这样就实现了点击按钮创建一个窗口了

二:使用通信在主进程创建窗口

由于我们这个博客案例都是基于上面的第一种通信方式直接在渲染进程可以使用nodejs脚本了,因此可以直接导入contextBridge, ipcRenderer向主进程进行通信了,无需借助预加载脚本,因此该案例改造如下

/pages/render.js内容如下:

js

const btn1 = document.getElementById('btn1')
const btn2 = document.getElementById('btn2')
const remote = require("@electron/remote")
const { ipcRenderer } = require("electron")
btn1.addEventListener('click', async () => {
  // 创建新窗口(向主进程发起请求,创建窗体,并显示pageFileName指定的页面)
  ipcRenderer.send('open-window', {
    width: 600,
    height: 400,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false
    },
    pageFileName: 'https://xiaojiblog.netlify.app/' // 确保传递了正确的页面文件名,list.html需要显示的页面
  })
})

btn2.addEventListener('click', async () => {
  // 创建新窗口(向主进程发起请求,创建窗体,并显示pageFileName指定的页面)
  ipcRenderer.send('open-window', {
    width: 200,
    height: 200,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false
    },
    pageFileName: 'https://jinweizhe.netlify.app/' // 确保传递了正确的页面文件名,list.html需要显示的页面
  })
})

主进程main.js改造如下:

js
const { app, BrowserWindow, ipcMain } = require('electron')
// 创建窗口
function createWindow () {
  let win = new BrowserWindow({
    width: 800,
    height: 600,
    autoHideMenuBar: true,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false
      enableRemoteModule: true,
    }
  })
  
   require('@electron/remote/main').initialize()
   require("@electron/remote/main").enable(win.webContents)

  win.on('close', () => {
    win = null
  })

  win.loadFile('./pages/index.html')
}

app.on('ready', () => {
  createWindow()
})

// 下面代码就是创建渲染进程窗体代码
// 在主进程中监听渲染进程的请求
// open-window后面的回调函数,参数一默认是事件对象,参数二为渲染进程传递来的数据
// pageFileName为ipcRenderer.send()的第二个参数,ipcRenderer.send()由渲染进程发起,参数一为事件名,参数二为页面配置(大小,位置等等)
ipcMain.on('open-window', (event, winconfig) => {
  console.log('winconfig', winconfig)
  // 创建新窗口并设置相应的配置(配置由渲染进程提供)
  let newWindow = new BrowserWindow(winconfig)
  // 这里设置的是winconfig.pageFileName,所以渲染进程的请求的配置中必须pageFileName代表页面
  newWindow.loadURL(winconfig.pageFileName)
  // 监听创建的窗体关闭事件
  newWindow.on('close', () => {
    console.log('close')
    newWindow = null
  })
})

上面这个方式实现了点击两个按钮创建了两次窗体(要注意一点就是我这里使用的是loadURL,如果是打开本地页面则使用loadFile)

顶部菜单的创建和绑定事件(模块化创建)

创建菜单

main.js里面创建

完整代码

main.js

js
const { app, BrowserWindow, ipcMain, Menu } = require('electron')
// 创建窗口
function createWindow () {
  let win = new BrowserWindow({
    width: 800,
    height: 600,
    autoHideMenuBar: false,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false
      enableRemoteModule: true,
    }
  })
 
  // 新增如下两行
  require('@electron/remote/main').initialize()
  require("@electron/remote/main").enable(win.webContents)

  //  1.自定义菜单项
  let menutap = [
    { label: '菜单1', click: () => { console.log('菜单1') } }, // 菜单1
    { label: '菜单2', click: () => { console.log('菜单2') } }, // 菜单2
    {
      label: '菜单3', submenu: [
        { label: '菜单3-1', click: () => { console.log('菜单3-1') } },
        { label: '菜单3-2', click: () => { console.log('菜单3-2') } }
      ]
    }, // 菜单3
    {
      label: '角色',
      submenu: [
        { label: '复制', role: 'copy' },
        { label: '剪切', role: 'cut' },
        { label: '粘贴', role: 'paste' },
        { label: '最小化', role: 'minimize' },
      ]
    },
    {
      label: '类型',
      submenu: [
        { label: '多选一', type: 'checkbox' },
        { label: '多选二', type: 'checkbox' },
        { label: '多选三', type: 'checkbox' },
        { type: 'separator' },
        { label: '单选1', type: 'radio' },
        { label: '单选2', type: 'radio' },
        { label: '单选3', type: 'radio' },
        { type: 'separator' },
        { label: 'windows', type: 'submenu', role: 'windowMenu' } //这里两个属性必须同时给出
      ]
    },
    {
      label: '其他',
      submenu: [
        {
          label: '打开',
          icon: '',
          accelerator: 'ctrl + o', //定义执行点击事件的快捷键
          click () {
            console.log('打开操作执行了')
          }
        }
      ]
    }
  ]
  // 2.依据上述的数据创建一个menu
  let menu = Menu.buildFromTemplate(menutap)
  // 3.将上述菜单添加至app身上
  Menu.setApplicationMenu(menu)


  win.on('close', () => {
    win = null
  })

  win.loadFile('./pages/index.html')
}

app.on('ready', () => {
  createWindow()
})

效果图:

在这里插入图片描述

拆分模块化管理

上面一个菜单的配置就要那么多代码,如果项目需求比较多则需要更多代码,不易于管理,因此将上面代码拆分成如下两个文件代码

在根目录下创建Menu.js内容如下:

js
const { Menu } = require('electron')

//  1.自定义菜单项
let menutap = [
  { label: '菜单1', click: () => { console.log('菜单1') } }, // 菜单1(可以在点击事件里加入操作,例如创建打开新窗口)
  { label: '菜单2', click: () => { console.log('菜单2') } }, // 菜单2
  {
    label: '菜单3', submenu: [
      { label: '菜单3-1', click: () => { console.log('菜单3-1') } },
      { label: '菜单3-2', click: () => { console.log('菜单3-2') } }
    ]
  }, // 菜单3
  {
    label: '角色',
    submenu: [
      { label: '复制', role: 'copy' },
      { label: '剪切', role: 'cut' },
      { label: '粘贴', role: 'paste' },
      { label: '最小化', role: 'minimize' },
    ]
  },
  {
    label: '类型',
    submenu: [
      { label: '多选一', type: 'checkbox' },
      { label: '多选二', type: 'checkbox' },
      { label: '多选三', type: 'checkbox' },
      { type: 'separator' },
      { label: '单选1', type: 'radio' },
      { label: '单选2', type: 'radio' },
      { label: '单选3', type: 'radio' },
      { type: 'separator' },
      { label: 'windows', type: 'submenu', role: 'windowMenu' } //这里两个属性必须同时给出
    ]
  },
  {
    label: '其他',
    submenu: [
      {
        label: '打开',
        icon: '',
        accelerator: 'ctrl + o', //定义执行点击事件的快捷键
        click () {
          console.log('打开操作执行了')
        }
      }
    ]
  }
]
// 2.依据上述的数据创建一个menu
let menu = Menu.buildFromTemplate(menutap)
// 3.将上述菜单添加至app身上
Menu.setApplicationMenu(menu)

main.js进行导入

js
const { app, BrowserWindow, ipcMain, Menu } = require('electron')
// 创建窗口
function createWindow () {
  let win = new BrowserWindow({
    width: 800,
    height: 600,
    autoHideMenuBar: false,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false
      enableRemoteModule: true,
    }
  })
  require('@electron/remote/main').initialize()
  require("@electron/remote/main").enable(win.webContents)

  // 导入动态菜单
  require('./Menu')

  win.on('close', () => {
    win = null
  })

  win.loadFile('./pages/index.html')
}

app.on('ready', () => {
  createWindow()
})

在渲染进程动态创建菜单(main.js不做更改)

  • 按钮1使用创建对象方式进行创建和追加
  • 按钮2使用输入的内容作为自定义菜单的下面的每一项
  • 按钮3使用数组的配置项进行创建

render.js

js
const remote = require("@electron/remote")
// 找到菜单和菜单项
const Menu = remote.Menu
const MenuItem = remote.MenuItem

window.addEventListener('DOMContentLoaded', () => {
  const btn1 = document.getElementById('btn1')
  const btn2 = document.getElementById('btn2')
  const btn3 = document.getElementById('btn3')
  const input = document.getElementById('input')
  // 自定义全局变量存放菜单项
  let menuItem = new Menu()

  btn1.addEventListener('click', async () => {
    // 使用对象方式创建菜单
    let menuFile = new MenuItem({ label: '文件', type: 'normal' })
    let menuEdit = new MenuItem({ label: '编辑', type: 'normal' })
    let customMenu = new MenuItem({ label: '自定义菜单项', submenu: menuItem })
    // 将创建好的菜单添加到menu
    let menu = new Menu()
    menu.append(menuFile)
    menu.append(menuEdit)
    menu.append(customMenu)
    // 将ment放置于app中显示
    Menu.setApplicationMenu(menu)
  })

  btn2.addEventListener('click', async () => {
    // 根据输入框内容动态创建(根据输入内容创建的菜单会在自定义菜单项下面)
    // 获取当前input输入框当中输入的内容
    let con = input.value.trim()
    if (con) {
      menuItem.append(new MenuItem({ label: con, type: 'normal' }))
      input.value = ''
    }
  })

  btn3.addEventListener('click', async () => {
    // 使用配置项创建菜单
    let menutap = [
      { label: '菜单1', click: () => { console.log('菜单1') } }, // 菜单1
      { label: '菜单2', click: () => { console.log('菜单2') } }, // 菜单2
      {
        label: '菜单3', submenu: [
          { label: '菜单3-1', click: () => { console.log('菜单3-1') } },
          { label: '菜单3-2', click: () => { console.log('菜单3-2') } }
        ]
      }, // 菜单3
      {
        label: '角色',
        submenu: [
          { label: '复制', role: 'copy' },
          { label: '剪切', role: 'cut' },
          { label: '粘贴', role: 'paste' },
          { label: '最小化', role: 'minimize' },
        ]
      },
    ]
    // 2.依据上述的数据创建一个menu
    let menu = Menu.buildFromTemplate(menutap)
    // 3.将上述菜单添加至app身上
    Menu.setApplicationMenu(menu)
  })
})

添加菜单后ctrl+shift+i打不开控制台解决

1.打开窗口就打开控制台

main.js里面的ready事件里面加入以下代码即可

本人不太推荐,不然控制台关闭还要重新起项目,我们调试不会让控制台一直出现,只是查看问题时才会打开

js
win.webContents.openDevTools();

2.在main.js里面直接创建的菜单

新增一个打开控制台的配置项

main.js

js
	{
      label: '开发者工具',
      accelerator: 'ctrl + shift + i', //定义执行点击事件的快捷键
      click() {
        win.webContents.openDevTools();
      }
    }

3.分模块时在Menu.js创建的菜单解决方式

由于这里面的创建,获取不到窗口的win,因此在导入main.js后在main.js进行追加配置项,main.js更改如下

main.js

js
const { app, BrowserWindow, ipcMain, Menu, MenuItem } = require('electron')
// 创建窗口
function createWindow () {
  let win = new BrowserWindow({
    width: 800,
    height: 600,
    autoHideMenuBar: false,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
      enableRemoteModule: true,
    }
  })
  require('@electron/remote/main').initialize()
  require("@electron/remote/main").enable(win.webContents)

  // 导入动态菜单
  require('./Menu')

  // 获取当前应用程序菜单
  let currentMenu = Menu.getApplicationMenu()

  // 如果当前菜单不存在,则创建一个新菜单
  if (!currentMenu) {
    currentMenu = new Menu()
  }

  // 追加一个自定义菜单项到当前菜单中
  currentMenu.append(new MenuItem({
    label: '打开控制台',
    accelerator: 'ctrl+shift+i', //定义执行点击事件的快捷键
    click () {
      win.webContents.openDevTools()
    }
  }))

  // 设置应用程序菜单
  Menu.setApplicationMenu(currentMenu)

  win.on('close', () => {
    win = null
  })

  win.loadFile('./pages/index.html')
}

app.on('ready', () => {
  createWindow()
})

渲染进程获取主进程的窗口win对象

js
const remote = require('@electron/remote')
const win = remote.getCurrentWindow() // 这个就相当于是主进程win对象,可以调用里面的方法

右键菜单的配置

右键菜单的配置项和自定义的菜单完全一致/pages/render.js

js
const remote = require('@electron/remote')
const Menu = remote.Menu

// 01 创建一个自定义菜单的内容
let contextTemp = [
  { label: '菜单1', click: () => { console.log('菜单1') } }, // 菜单1
  { label: '菜单2', click: () => { console.log('菜单2') } }, // 菜单2
  {
    label: '菜单3', submenu: [
      { label: '菜单3-1', click: () => { console.log('菜单3-1') } },
      { label: '菜单3-2', click: () => { console.log('菜单3-2') } }
    ]
  }, // 菜单3
  {
    label: '角色',
    submenu: [
      { label: '复制', role: 'copy' },
      { label: '剪切', role: 'cut' },
      { label: '粘贴', role: 'paste' },
      { label: '最小化', role: 'minimize' },
    ]
  },
  {
    label: '类型',
    submenu: [
      { label: '多选一', type: 'checkbox' },
      { label: '多选二', type: 'checkbox' },
      { label: '多选三', type: 'checkbox' },
      { type: 'separator' },  // 分割线
      { label: '单选1', type: 'radio' },
      { label: '单选2', type: 'radio' },
      { label: '单选3', type: 'radio' },
      { type: 'separator' }, // 分割线
      { label: 'windows', type: 'submenu', role: 'windowMenu' } //这里两个属性必须同时给出
    ]
  },
  {
    label: '其他',
    submenu: [
      {
        label: '打开',
        icon: '',
        accelerator: 'ctrl + o', //定义执行点击事件的快捷键
        click () {
          console.log('打开操作执行了')
        }
      }
    ]
  },
  {
    label: '打开控制台',
    accelerator: 'ctrl+shift+i', //定义执行点击事件的快捷键
    click () {
      remote.getCurrentWindow().webContents.openDevTools()
    }
  }
]


// 02 依据上述内容来创建menu
let menu = Menu.buildFromTemplate(contextTemp)

window.addEventListener('DOMContentLoaded', () => {
  // 03 在鼠标右击行为发生后显示出来
  window.addEventListener('contextmenu', (e) => {
    e.preventDefault() //阻止有些元素点击的默认行为
    menu.popup({ window: remote.getCurrentWindow() })  //将当前窗口对象作为popup参数,代表在当前窗口弹出
  }, false)
})

效果图

在这里插入图片描述

打开子窗口

方式一(可以监听子窗口传来的消息):

/pages/render.js

js
let btn = document.getElementById('btn1')

btn.addEventListener('click', () => {
  window.open('https://xiaojiblog.netlify.app/')
})

方式二(缺陷:不能监听子窗口传来的消息):

/pages/index.html

html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="Content-Security-Policy"
    content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">
  <title>Document</title>
  <link rel="stylesheet" href="./index.css">
</head>

<body>
  <!--如果设置 target="_blank" 会新创建一个窗口展示内容,并不会打开浏览器 -->
  <a target="_blank" href="https://xiaojiblog.netlify.app/">打开萧寂博客</a>
  <script src="./render.js"></script>
</body>

</html>

子窗口向父窗口传递信息

上面的方式二不适用

创建一个子窗口页面

/pages/index2.html

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <h2>我是弹出子窗口</h2>
  <button id="btn">向父窗口传递消息</button>
  <script>
    let btn = document.getElementById('btn')
    btn.onclick = function () {
      // 向父窗口传递消息
      window.opener.postMessage('我是子窗口向父窗口传递的消息', '*')
    }
  </script>
</body>
</html>

/pages/render.js

js
let btn = document.getElementById('btn1')

btn.addEventListener('click', () => {
  window.open('./index2.html')
})

window.addEventListener('message', (e) => {
  console.log(e.data) // e.data就是子窗口传递来的消息
})

只有通过window.open打开的子窗口才能监听到子窗口给父窗口返回的数据,a标签直接打开的子窗口监听听不到

a标签通过浏览器打开链接进行查看

在electron里面,默认打开链接会在当前窗口展示,如果有需求需要点击后使用浏览器进行查看怎么解决呢???往下看即可

/pages/index.html

html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="Content-Security-Policy"
    content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">
  <title>Document</title>
  <link rel="stylesheet" href="./index.css">
</head>

<body>
  <a id="ahref" href="https://xiaojiblog.netlify.app/">打开萧寂博客</a>
  <script src="./render.js"></script>
</body>

</html>

/pages/render.js

js
const { shell } = require('electron')
let a = document.getElementById('ahref')

a.addEventListener('click', (e) => {
  e.preventDefault() // 阻止在窗口内打开
  shell.openExternal(a.href) // 使用shell在浏览器打开链接
})

嵌入网页

main.js

js
const { app, BrowserWindow, ipcMain, Menu, MenuItem, BrowserView } = require('electron')
// 创建窗口
function createWindow () {
  let win = new BrowserWindow({
    width: 800,
    height: 600,
    autoHideMenuBar: false,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
      enableRemoteModule: true,
    }
  })
  require('@electron/remote/main').initialize()
  require("@electron/remote/main").enable(win.webContents)

  // 下面四行实现窗口内嵌套网页
  let view = new BrowserView()
  win.setBrowserView(view)
  view.setBounds({ x: 0, y: 120, width: 800, height: 600 })
  view.webContents.loadURL('https://xiaojiblog.netlify.app/')

  win.on('close', () => {
    win = null
  })

  win.loadFile('./pages/index.html')
}

app.on('ready', () => {
  createWindow()
})

效果图:

在这里插入图片描述

对话框的使用

选择文件对话框的使用

/pages/index.html

html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="Content-Security-Policy"
    content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">
  <title>Document</title>
  <link rel="stylesheet" href="./index.css">
</head>

<body>
  <button id="btn1">打开图片</button>
  <img id="image" style="width: 100%;height: 200px;" alt="">
  <script src="./render.js"></script>
</body>

</html>

/pages/render.js

js
const remote = require('@electron/remote')
const dialog = remote.dialog
let btn = document.getElementById('btn1')
let img = document.getElementById('image')
btn.addEventListener('click', () => {
  dialog.showOpenDialog({
    title: '请选择文件', // 标题  
    defaultPath: 'hahaha.png', // 默认显示的文件名字
    filters: [ // 选择的类型
      { name: 'Images', extensions: ['jpg', 'png', 'gif'] },
      { name: 'Movies', extensions: ['mkv', 'avi', 'mp4'] },
      { name: 'Custom File Type', extensions: ['as'] },
    ],
    buttonLabel: '打开图片', // 确定按钮的文字
  }).then(result => {
    // 给图片赋值
    img.src = result.filePaths[0]
  }).catch(err => {
    console.log('err', err)
  })
})

保存对话框使用

/pages/index.html

html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="Content-Security-Policy"
    content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">
  <title>Document</title>
  <link rel="stylesheet" href="./index.css">
</head>

<body>
  <button id="btn1">保存文件</button>
  <script src="./render.js"></script>
</body>

</html>

/pages/render.js

js
const remote = require('@electron/remote')
const fs = require('fs')
const dialog = remote.dialog
let btn = document.getElementById('btn1')


btn.addEventListener('click', () => {
  dialog.showSaveDialog({
    title: '保存文件', // 标题
    defaultPath:'hello.txt', // 默认保存的文件名
    buttonLabel:"保存文件" // 确认保存文件按钮
  }).then(result => {
    if(result.canceled) return; // 点击了取消保存
    fs.writeFile(result.filePath, 'hello world', err => {
      if (err) {
        console.log('err', err)
      } else {
        console.log('保存成功')
      }
    })
  }).catch(err => {
    console.log('err', err)
  })
})

消息对话框使用

对话框相关api

  • type: String类型 可选 有none info error questionwarning
  • title String类型 弹出框标题
  • message: String类型 必选 message box 的内容,这个必须要写的
  • buttons: 数组类型 返回值是下标

/pages/index.html

html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="Content-Security-Policy"
    content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">
  <title>Document</title>
  <link rel="stylesheet" href="./index.css">
</head>

<body>
  <button id="btn1">弹出对话框</button>
  <script src="./render.js"></script>
</body>

</html>

/pages/render.js

js
const remote = require('@electron/remote')
const dialog = remote.dialog
let btn = document.getElementById('btn1')


btn.addEventListener('click', () => {
  dialog.showMessageBox({ // 弹出对话框
    type: 'info',
    title: '提示',
    message: '这是一个对话框',
    buttons: ['确定', '取消']
  }).then(result => {
    console.log(result.response) // 返回值是上面的buttons的下标,代表点击了哪个按钮,根据按钮的下标做相应的操作
  })
})

断网提醒功能制作

这个功能就是当电脑网络断开会弹出对话框提醒

/pages/render.js

js
window.addEventListener('online', () => {
  alert('网络已连接')
})
window.addEventListener('offline', () => {
  alert('网络已断开')
})

底部通知消息制作

/pages/index.html

html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="Content-Security-Policy"
    content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">
  <title>Document</title>
  <link rel="stylesheet" href="./index.css">
</head>

<body>
  <button id="btn1">发送通知消息</button>
  <script src="./render.js"></script>
</body>

</html>

/pages/render.js

js
let btn = document.getElementById('btn1')

let option = {
  body: '您收到一条消息,请查收', // 发送的内容
  icon: './image.png' // 带头像
}
btn.addEventListener('click', () => {
  new window.Notification('消息来了', option)
})

不要开通知勿扰,切记,本人找了半小时错误发现开了勿扰,giao

注册全局快捷键

建议在主进程注册,不建议在单窗口注册

在主进程注册

main.js

js
const { app, BrowserWindow, globalShortcut } = require('electron')
// 创建窗口
function createWindow () {
  let win = new BrowserWindow({
    width: 800,
    height: 600,
    autoHideMenuBar: false,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
      enableRemoteModule: true,
    }
  })
  require('@electron/remote/main').initialize()
  require("@electron/remote/main").enable(win.webContents)


  // 注册全局快捷键
  globalShortcut.register('ctrl + o', () => {
    console.log('ctrl + o')
    win.loadURL('https://xiaojiblog.netlify.app/')
  })
  globalShortcut.register('ctrl + i', () => {
    console.log('ctrl + i')
  })
  globalShortcut.register('ctrl + a', () => {
    console.log('ctrl + a')
  })

  let isRegister = globalShortcut.isRegistered('ctrl + o') // 判断这个快捷键是否被绑定,返回true或者false

  win.on('close', () => {
    win = null
  })

  win.loadFile('./pages/index.html')
}

app.on('ready', () => {
  createWindow()
})

app.on('will-quit', () => {
  // 将要退出时时注销全局快捷键
  globalShortcut.unregisterAll() // 注销所有
  // globalShortcut.unregister('ctrl+o') // 注销指定快捷键
})

在渲染进程注册

但是也要在main.js监听一下will-quit事件,将注册的事件全部注销

/pages/render.js

js
const remote = require('@electron/remote')
const globalShortcut = remote.globalShortcut
// 注册全局快捷键
globalShortcut.register('ctrl + o', () => {
  console.log('ctrl + o')
})
globalShortcut.register('ctrl + i', () => {
  console.log('ctrl + i')
})
globalShortcut.register('ctrl + a', () => {
  console.log('ctrl + a')
})

剪切板功能使用

/pages/index.html

html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="Content-Security-Policy"
    content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">
  <title>Document</title>
  <link rel="stylesheet" href="./index.css">
</head>

<body>
  <div>
    激活码: <span id="code">sdcbksdkcbskcbknjnxas</span>
    <br>
    <button id="btn1">复制激活码</button>
  </div>
  <script src="./render.js"></script>
</body>

</html>

/pages/render.js

js
const remote = require('@electron/remote')
let btn = document.getElementById('btn1')
let code = document.getElementById('code')
const clipboard = remote.clipboard
btn.addEventListener('click',()=>{
  clipboard.writeText(code.innerHTML)
  alert('复制成功')
})

打包

安装插件

js
yarn add electron-builder -D

修改package.json,内容如下:

js
{
  "name": "electron",
  "version": "1.0.0",
  "description": "这是我的第一个electron项目",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "nodemon": "nodemon --exec electron .",
    "build": "electron-builder" // 使用electron-builder打包
  },
  "build": {
    "appId": "com.xiaoji.electron", // 应用程序唯一标识符
    "productName": "萧寂electron", // 应用程序名称
    "copyright": "xiaoji © 2021 <your-name>", // 版权信息
    "directories": {
      "output": "dist_electron" // 打包位置,会新建到项目根目录
    },
    // 打包windows平台安装包的具体配置
    "win": {
      "icon": "icon.ico", // 应用图标
      "target": [
        {
          "target": "nsis", // 使用nsis作为安装程序格式
          "arch": ["x64"] // 生成64位安装包
        }
      ]
    },
    "nsis": {
      "oneClick": false, // 设置false使安装程序显示安装向导页面,而不是一键安装
      "perMachine": true, // 允许每台机器安装一次,而不是每个用户都安装
      "allowToChangeInstallationDirectory": true, // 允许用户更改安装目录
      "createDesktopShortcut": true, // 创建桌面图标
      "createStartMenuShortcut": true, // 创建开始菜单图标
      "shortcutName": "xiaoji" // 图标名称
    }
  },
  "keywords": [],
  "author": "萧寂",
  "license": "ISC",
  "devDependencies": {
    "electron": "^31.2.0",
    "electron-builder": "^24.13.3",
    "nodemon": "^3.1.4"
  }
}

记得把我上面的注释都删了,然后执行yarn run build打包安装应用程序