Electron学习笔记

119 阅读10分钟

最近在学习Electron,想写一篇笔记总结并记录以加深印象。

Electron的概念

  • Electron是一个嵌入了chromium与node到自己的二进制包中的、可以利用js、html、css技术构建桌面应用的框架

从它的概念可以得知,Electron的桌面应用,可以利用前端技术来构建,同时需要结合浏览器与nodejs的特点-也就是它也可以运用浏览器的API并且可以编写nodejs代码。

知道了它的概念,我们就可以按照官方文档上的demo来进行逐步的学习

开始一个Electron程序

安装Electron的包

  • 可以选择全局安装 npm install --g elcectron
    • electron -v 出现版本号就算安装成功
    ele.jpg
  • 创建项目安装
    • mkdir electron-app
    • cd electron-app
    • npm install --save-dev electron
    • npx electron -v 出现版本号就算安装成功

编写一个主程序

进入到electron-app的项目目录下,此时是一个空的文件夹

  • npn init -y
  • 创建一个main.js(与package.json中的main属性的值命名一样,寓意为该程序以当前文件作为主入口)
  • 创建一个index.html,并在此文件夹下编写一些简单的页面,比如Hello World~ 我们在main.js中写入以下程序
const electron = require('electron')
const app = electron.app // 将这个app看成当前的整个应用
const BrowserWindow = electron.BrowserWindow // 顾名思义,浏览器的窗口
let mainWindow = null // 声明要打开的主窗口

// app的生命周期,在ready之后才可以使用BroswerWindow
//以下代码的含义就是应用准备好了之后,加载一个宽高为800px的窗口,这个窗口载入的是我们编写的index.html文件
app.on('ready', () => {
  mainWindow = new BrowserWindow({
    width: '800px',
    height: '800px',
  })
  // 加载页面
  mainWindow.loadFile('index.html')
  mainWindow.on('closed', () => {
    mainWindow = null
  })
})

现在我们可以尝试运行这个主程序啦~

在当前项目下执行electron . 如果运行成功就会直接弹出一个有菜单的浏览器窗口,如下:

微信图片_20220928172446.jpg

如果运行失败就会弹出一个错误提示,根据错误提示进行相应的代码调整,让然后再重新运行:

微信图片_20220928172349.jpg

ps:如果不想每次运行electron .,那么就配置package.json中的scripts属性"dev":"electron ."这样我们就可以使用npm run dev

增加一点交互

我们实现了Hello Word,就在此基础上添加一点交互---在页面上点击【引入文本】的按钮,引入指定文件内容

例如,我们在html中写下如下代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Electron</title>
  </head>
  <body>
    Hello World~
    <button id="btn">引入文本</button>
    <div id="text"></div>
    // 引入处理交互的JS文件
    <script src="./render/render1.js"></script>
  </body>
</html>

在项目目录下创建一个render目录,在render中创建一个render1.js。我们在该js文件下写下如下代码:

// 渲染进程与主进程是两个不用的进程,它们之间是独立的,若想在渲染进程中使用node的api,则需要配置
// 渲染进程暂且理解成我们这个renders.js文件,主进程理解成main.js
const fs = require('fs')

window.onload = function () {
  const btn = this.document.querySelector('#btn')
  const text = this.document.querySelector('#text')

  btn.onclick = function () {
    fs.readFile('text.txt', (err, data) => {
      console.log(err, data)
      if (!err) {
        text.innerHTML = data
      }
    })
  }
}

再创建一个text.txt文本文件,随意添加一些内容,此时我们的目录结构应该是这样的:

微信图片_20220928173834.jpg

运行electron .正常打开窗口,点击【引入文本】发现并没有将内容引进来,Ctrl+Shift+i打开调试器,发现如下错误:

微信图片_20220928174209.jpg 正如我们在代码注释中所说,在render.js中是不能够使用node的api的,所以require是没有用的(渲染进程与主进程是相互独立的,暂且记住,后面会详细记录)

解决这个问题,只需要告诉当前操作的这个窗口,允许它使用node的api就可以了。于是我们在创建窗口的时候:

app.on('ready', () => {
  mainWindow = new BrowserWindow({
    width: '800px',
    height: '800px',
    webPreferences: { nodeIntegration: true, contextIsolation: false }, // 这里
  })
  ...
})

这样我们再electron .点击【引入文本】,就可以实现这个功能了

实现一个超链接(打开一个新窗口)

我们已经可以在应用中使用html,css,js以及nodejs实现一些页面上的需求,接下来看看如何在页面中使用超链接跳转到新的网页。

第一种情况:a标签链接到外部

<a id="aHref" href="http://www.baidu.com" target="_blank">去百度</a>

这时候点击a标签会新开一个当前app应用的窗口链接过去

如果想要实现,点击a标签就使用谷歌浏览器打开,需要用到shell模块,shell模块是在渲染进程与主进程中都可以直接使用的shell文档。我们在render1.js中添加如下代码:

const { shell } = require('electron')
const aHref = document.querySelector('#aHref')
aHref.onclick = function (e) {
  e.preventDefault()
  const href = this.getAttribute('href')
  shell.openExternal(href)
}

这时候再点击a标签,就会在谷歌浏览器中打开了(修改过后重新运行electron .)

第二种情况:点击一个元素,跳转到我们自己写的页面

  1. 首先我们先创建一个简单的页面red.html,可以先暂时放在项目目录下,将body的颜色设置成红色
  2. 我们需要在index.html中创建一个元素,然后为这个元素添加点击事件,新开窗口跳转到red.html
<button id="goNewWin">点击新开窗口</button>
  1. render1.js中编写该元素的点击逻辑
  const { BrowserWindow } = require('@electron/remote')
  
  window.onload = function () {
  
      ....
      
      // 先获取这个元素
      const goNewWin = this.document.querySelector('#goNewWin')
      goNewWin.onclick = function () {
      // 新开的窗口
        let newWin = new BrowserWindow({
          width: '500px',
          height: '500px',
        })
        // 载入刚才编写的红色页面
        newWin.loadFile('red.html')
        // 窗口注销的时候,将这个window设置为空
        newWin.on('closed', () => {
          newWin = null
        })
    }
  }

由于创建一个新的窗口是主线程才可以使用的api,我们不可以在render1.js这个渲染进程中使用,所以必须用到@electron/remote这个模块来帮助我们完成。使用的步骤如下:

  1. npm install --save @electron/remote文档
  2. 在主进程中进行初始化require('@electron/remote/main').initialize()
  3. 在渲染进程中引入使用

ps:在electron >= 14.0.0的时候,我们还需要使用enableapi指定具体的要渲染的webContents,这个webContents通常使用我们所创建的窗口的webContents获得-mainWindow.webContents

// main.js
 ...
require('@electron/remote/main').initialize()
...
app.on('ready', () => {
  ...
  require('@electron/remote/main').enable(mainWindow.webContents) // 这里
  ...
})

设置菜单

翻阅Electron文档就可以发现,在Main Process模块有一个Menu模块,我们要实现菜单的制定能够以配置,就需要借助它。

设置顶部菜单

我们在项目目录下新创建一个目录叫menu,在里面新创建一个menu.js文件,写入以下代码:

const { Menu, BrowserWindow } = require('electron')
// 菜单是以数组形式展示的
const template = [
  {
    label: '第一项',
    submenu: [
      {
        label: '子项1',
        accelerator: 'Ctrl+n', // 绑定一个快捷键
        click: () => {   // 实现一个子菜单的点击事件
        // 点击【子项1】新开一个页面
          let win = new BrowserWindow({
            width: 500,
            height: 500,
            webPreferences: { nodeIntegration: true, contextIsolation: false },
          })
          win.loadFile('red.html')
          win.on('closed', () => {
            win = null
          })
        },
      },
      {
        label: '子项2',
      },
    ],
  },
  {
    label: '第二项',
    submenu: [
      {
        label: '子项1',
      },
      {
        label: '子项2',
      },
    ],
  },
  {
    label: '第三项',
    submenu: [
      {
        label: '子项1',
      },
      {
        label: '子项2',
      },
    ],
  },
]

const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)

菜单是一个数组,里面包含的对象是菜单的每一项。每一个对象有一些特定的属性用于配置菜单,例如label-配置菜单的名称,submenu-配置菜单的子项等等。写好模板之后,我们需要使用Menu的api-buildFromTemplate,从定义好的模板中构建一个菜单,再使用Menu的setApplicationMenuapi将应用的菜单设置成我们构建好的菜单

这个独立的菜单文件编写好了之后,需要在main.js中引用,在main.js中引入添加以下代码:

// main.js
app.on('ready',()=>{ // 使用app.whenReady()也可以,并且更好,详见官方文档
....
  require('./menu/menu')
...
})

这时候我们在终端输入electron .,会发现顶部菜单变成了我们所设置的。

微信图片_20220929174718.jpg

设置右键菜单(contextmenu)方法一

我们平时在页面中或是在系统中点击右键,都会出现一个右键的菜单。从这个行为可知,点击行为是发生在渲染进程的,也就是说我们需要在渲染进程添加contextmenu的监听事件,如果发生了该点击行为,就创建出右键菜单。

// render1.js
const { BrowserWindow, Menu, getCurrentWindow } = require('@electron/remote')

// 第一步先创建一个右键菜单模板contextMenuTemplate
const contextMenuTemplate=[
  { label: '复制'},
  { label: '粘贴'},
]

// 第二步,使用模板生成一个Menu
const m = Menu.buildFromTemplate(contextMenuTemplate)

window.addEventListener('contextmenu',(e)=>{
   e.preventDefault() // 阻止默认行为
   // 弹出右键菜单
   m.popup({
     window:getCurrentWindow()
   })
})

这里需要注意的是,men、getCurrentWindow等API都是Main Process中的,如果我们要在渲染进程中使用它,就要从@electron/remote中引入

设置右键菜单(contextmenu)方法二

ipcRendereripcMain

  • ipcRenderer是一个 EventEmitter 的实例,可以使用它提供的一些方法从渲染进程 (web 页面) 发送同步或异步的消息到主进程。 也可以接收主进程回复的消息。
  • ipcMain是一个EventEmitter实例,当在主进程中使用时,它处理从渲染器进程(网页)发送出来的异步和同步信息。 从渲染器进程发送的消息将被发送到该模块。
  • ipcRender在渲染进程中引入并直接使用,ipcMain在主进程中引入并直接使用
// render1.js
window.addEventListener('contextmenu', e => {
  e.preventDefault()
  // 向主进程发送一个名为‘show-context--menu’事件
  ipcRenderer.send('show-context-menu')
})

// main.js
// 处理渲染进程发送过来的事件
  ipcMain.on('show-context-menu', event => {
    const template = [
      {
        label: 'Menu Item 1',
        click: () => {
         // ...
        },
      },
      { type: 'separator' },
      { label: 'Menu Item 2', type: 'checkbox', checked: true },
    ]
    const menu = Menu.buildFromTemplate(template)
    // 在发送的位置弹出
    menu.popup(BrowserWindow.fromWebContents(event.sender))
  })

使用这个方法需要注意的是,渲染进程只向主进程发送消息,弹出菜单的事件需要主进程来处理(毕竟相关api都是在主进程中使用的)。主进程接收到“弹出菜单的消息”后,就在回调函数中处理弹出的逻辑。

打开一个子窗口并与它通信

创建一个子窗口内容sub.html与处理该子窗口内容的render2.js

// sub.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    我是子窗口
    <button id="sendMessage">点击向父窗口发送信息</button>
  </body>

  <script src="./render/render2.js"></script>
</html>
//render2.js
window.onload = function () {
  const sendMessage = document.querySelector('#sendMessage')
  sendMessage.onclick = function () {
    // 点击button,从子窗口向父窗口发送消息
    window.opener.postMessage('从子窗口发送过来的消息')
  }
}
// render1.js   
...
// 监听子窗口发送过来的数据,监听window的message事件
window.addEventListener('message', msg => {
  // msg是获取到的MessageEvent对象,收到消息后对其做后续的处理
  const subMessage = document.querySelector('#subMessage')
  subMessage.innerHTML = msg.data
})
...

在子窗口使用postMessage与在父窗口监听message事件,从而完成父子窗口之间的通信

应用断网功能提醒

实现这个其实非常简单,我们只需要监听onlineoffline的事件就可以实现

window.addEventListener('online', e => {
// 做恢复网络逻辑处理
  console.log(e, 'online')
})
window.addEventListener('offline', e => {
// 做断网逻辑处理
  console.log(e, 'offline')
})

应用底部消息通知

方法一

直接使用window.Notification(title,options)的api

方法二

使用electron中主线程的Notification模块,示例:

// 在渲染进程中使用
const { Notification} = require('@electron/remote')
...
new Notification({
   title: '我是electron的notification',
   body: '我是通知的消息主体,消息主体',
}).show()
...

注册全局的快捷键

  // 注册一个全局的快捷键,返回一个布尔值,true-注册成功,false-注册失败
  const ret = globalShortcut.register('Ctrl+d', () => {
    // 处理按下Ctrl+d的逻辑
  })

  if (!ret) {
    // 注册失败的处理逻辑
  }

  // 检查快捷键是否注册成功,true-成功  false-失败
  const isRegister = globalShortcut.isRegistered(ret)

  // 注销快捷键
  app.on('will-quit', () => {
    // 单个注销
    globalShortcut.unregister('Ctrl+d')
    // 注销所有
    globalShortcut.unregisterAll()
  })

至此,已经可以搭建起一个简单的electron项目并进行编写相关的内容。还有一部分本文未提到的知识,例如:dialog clipboard等等,官方文档其实很详细,加油学习吧~