Electron:Electron的能力

一、前言

前面的课程已经讲述了什么是Electron,Electron的基本原理,Electron的工程化,如,怎么和React结合,怎么打包,怎么更新。Electron的基本原理,如主进程、子进程,进程间通信,Electron的窗口实现,Electron的基础结构等。
那么,今天,我们主要看看Electron的主要能力,也就是Electron能做些什么,同时对之前的课程进行加强和补充。

二、基本概念

2.1 什么是Electron

Electron是基于chromium的开源项目,和谷歌浏览器使用相同的内核,它的前身是Atom-shell,始于2013年,后来改名为Electron。
Electron 是一个能让你使用 JavaScript,HTML 和 CSS 来创建桌面应用程序的框架。 这些应用程序可以打包后在 macOS、Windows 和 Linux 上直接运行,或者通过 Mac App Store 或微软商店进行分发。
意思也就是,web前端开发者不仅仅可以依赖浏览器写代码,还可以将自己写的web代码打包成一个独立桌面应用,这是一个很大的进步。

2.2 Electron应用的基本结构

上面说到web前端开发者之前只能基于浏览器进行开发,写的代码都是运行在浏览器上,那么我们是否可以脱离浏览器,把自己写的web代码打包成独立的应用呢?Electron就实现了这个想法。
Electron是怎么做到的?
说的通俗一些,Electron就是将Chrome浏览器的内核抠出来(基于chromium),当做业务代码的运行环境,最后发版的时候连同浏览器内核一起打包成一个应用,分发给用户使用。
所以,Electron应用都不会太小,因为业务代码自带浏览器内核。通常来说,至少也有四五十兆。
如果说,仅仅只是把网页打包成一个应用,那么,这种应用似乎也没有多大意义。这就引入我们下面要说的。Electron不仅自带浏览器渲染内核,同时还提供了NodeJS运行环境和底层原生能力,扩展C++的能力,Electron内置的Native API,比如,你可以在应用中操作文件,访问打印驱动,访问蓝牙等等。这才是Electron的王炸之举。Web网页是运行在沙盒环境中的(沙盒:在计算机安全领域中是一种安全机制,为运行中的程序提供的隔离环境,沙盒通常严格控制其中的程序所能访问的资源),很多原生能力访问是不允许进行的,但Electron可以。
前面讲过,Electron和Chrome浏览器很类似,都是只有一个主进程,可以有多个子进程(我们这里只讨论渲染进程),比如Chrome中,每个tab页签就是一个渲染进程,简单看几张图,大致了解一下。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.3 为什么需要进程间通信

在Electron中,赋予主进程和渲染进程的能力范围是不同的,渲染进程能具备完整的Node能力,但是却不具备原生底层能力,比如访问打印机。而主进程不仅具备完整的Node能力,还具备原生底层能力。那么,我们在业务需求中需要访问原生能力的时候,只能让主进程去做,这个时候,需要渲染进程通知主进程,然后由主进程完成,主进程还需要把完成的结果传递给子进程。所以,进程间通信的必要性就出现了。
下面,我们通过几幅图来了解Electron的架构和能力分布。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.4 Electron的成功案例

到底有哪些我们耳熟能详的Electron案例呢?
在这里插入图片描述
在这里插入图片描述
另外,还有Atom编辑器、微信PC客户端、美团大象等等。

2.5 本章小结

通过上面的学习,我们已经知道,Electron使得web前端开发者可以开发出自己的应用,除了要要懂得纯web前端的知识,还需要学习NodeJS的知识,打包、更新等知识,另外,为了能够扩展更强的能力,有时候还需要写C++扩展,以提供业务调用。所以,对前端开发者的要求立马就提高了,所以,Electron的开发,是有一定门槛的。

三、Electron关键知识点

3.1 进程间通信

3.1.1 主进程

1,每个应用只有一个主进程,Electron项目中,在package.json中配置,如下图所示,main.js就是主进程文件,也是整个应用的入口;
2,创建渲染进程(子进程);
3,控制应用生命周期(app);
4,管理原生GUI,如BrowserWindow,Tray,Dock,Menu等等;
5,访问系统的能力;
在这里插入图片描述

3.1.2 渲染进程

1,展示web页面的进程成为渲染进程;
2,通过NodeJS、Electron提供的API直接或者间接和系统底层打交道;
3,一个Electron应用可以有多个渲染进程;
4,一个渲染进程对应一个窗口;

3.1.3 IPC:渲染进程通知主进程

1,callback写法:
- ipcRenderer.send
- ipcMain.on
2,Promise写法(Electron7.0之后,请求 + 响应):
- ipcRenderer.invoke
- ipcMain.handle

3.1.4 IPC:主进程通知渲染进程

- ipcRenderer.on
- xxWin.webContents.send

注:主进程通知渲染进程为啥不是ipcMain.send?这里要适当注意一下,因为主进程只有一个,而渲染进程可以有多个,所以主进程通知渲染进程时,由渲染进程所在的窗口发送,渲染进程窗口在主进程中被创建和管理。

3.1.5 IPC 渲染进程通知渲染进程

1, 通知事件
- 通过主进程转发(Electron5之前)
- ipcRenderer.sendTo(Electron5之后)
2,数据共享
- web技术(localStorage、sessionStorage、indexedDB)
- 使用remote(不推荐、不建议)

3.1.6 demo案例

我们写一个简单的例子,来实现上面进程间通信方式
在这里插入图片描述
主进程main.js

const {
    
     app, BrowserWindow, ipcMain, dialog, Notification, Menu, Tray, globalShortcut } = require('electron')
let windowA = null
let windowB = null
let tray = null
// Menu的用法
const template = [{
    
    
  label: app.name,
  submenu: [
    {
    
     role: 'about' },
    {
    
     type: 'separator' },
    {
    
     role: 'services' },
    {
    
     type: 'separator' },
    {
    
     role: 'hide' },
    {
    
     role: 'hideothers' },
    {
    
     role: 'unhide' },
    {
    
     type: 'separator' },
    {
    
     role: 'quit' }
  ]
}, {
    
    
  label: 'File',
  submenu: [{
    
     role: 'close' }]
}, {
    
    
  label: 'View',
  submenu: [
    {
    
     role: 'reload' },
    {
    
     role: 'forceReload' },
    {
    
     role: 'toggleDevTools' },
    {
    
     type: 'separator' },
    {
    
     role: 'resetZoom' },
    {
    
     role: 'zoomIn' },
    {
    
     role: 'zoomOut' },
    {
    
     type: 'separator' },
    {
    
     role: 'togglefullscreen' }
  ]
}, {
    
    
  role: 'help',
  submenu: [
    {
    
    
      label: 'Learn More',
      click: async () => {
    
    
        const {
    
     shell } = require('electron')
        await shell.openExternal('https://www.baidu.com/')
      }
    }
  ]
}, {
    
    
  label: '自定义',
  submenu: [
    {
    
    
      label: '弹框通知',
      click: () => {
    
    
        let x = new Notification({
    
     title: '我是TITLE', body: '我是BODY' })
        x.show()
      }
    }
  ]
}]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
class AppWindow extends BrowserWindow {
    
    
  constructor(config, htmlFile) {
    
    
    const baseConfig = {
    
    
      width: 1200,
      height: 800,
      webPreferences: {
    
    
        nodeIntegration: true,
        enableRemoteModule: true
      }
    }
    const finalConfig = {
    
    ...baseConfig, ...config}
    super(finalConfig)
    this.loadFile(htmlFile)
    this.webContents.openDevTools()
  }
}
// 初始化托盘Tray
const initTray = () => {
    
    
  tray = new Tray('./assets/icon.png')
  const contextMenu = Menu.buildFromTemplate([
    {
    
     label: 'Item1', type: 'radio' },
    {
    
     label: 'Item2', type: 'radio' },
    {
    
     label: 'Item3', type: 'radio', checked: true },
    {
    
     label: 'Item4', type: 'radio', click: () => {
    
     console.log('......点击了Item4.......') } }
  ])
  tray.setToolTip('This is my tray')
  tray.setContextMenu(contextMenu)
  // 事件
  tray.on('click', () => {
    
    
    let x = new Notification({
    
     body: 'Tray事件', title: '单击事件' })
    x.show()
  })
}

app.on('ready', () => {
    
    
  initTray()
  // 初始化创建视窗A(渲染进程A)
  windowA = new AppWindow({
    
    }, './renderer/processA/a.html')
  // 快捷键用法
  globalShortcut.register('CommandOrControl+Q', () => {
    
    
    windowA.webContents.toggleDevTools()
  })
  ipcMain.on('create-B-window', () => {
    
    
    console.log('接到A进程的通知,我来创建进程B')
    windowB = new AppWindow({
    
    
      width: 800,
      height: 600,
      parent: windowA
    }, './renderer/processB/b.html')
  })

  global.sharedObject = {
    
    
    windowAWebContentsId: windowA.webContents.id
  }


  ipcMain.on('open-pic-file', () => {
    
    
    console.log('接到B进程的通知,我来打开系统视窗')
    dialog.showOpenDialog({
    
    
      properties: ['openFile', 'multiSelections'],
      filters: [{
    
     name: 'Images', extensions: ['jpg', 'jpeg', 'png', 'gif'] }]
    }).then(files => {
    
    
      console.log('files: ', files)
    })
  })
  // 获取打印机列表(老方法)
  ipcMain.on('b-main-pinterlist-old', () => {
    
    
    // 在主线程中获取打印机列表
    const list = windowB.webContents.getPrinters()
    console.log('list(old): ', list)
    // 通过webContents发送事件到渲染线程,同时将打印机列表页传过去
    windowB.webContents.send('main-b-printerlist-old', list)
  })
  // 获取打印机列表(新方法)
  ipcMain.handle('b-main-pinterlist-new', () => {
    
    
    return new Promise(resolve => {
    
    
      const list = windowB.webContents.getPrinters()
      console.log('list(new): ', list)
      resolve(list)
    })
  })
})})

看看渲染进程A:a.html/a.js

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Render-A</title>
    <link rel="stylesheet" href="../../node_modules/bootstrap/dist/css/bootstrap.min.css">
  </head>
  <body>
    <div class="container mt-4">
      <h1>我是渲染进程A</h1>
      <h2 id='myh2'></h2>
      <button
        type="button"
        class="btn btn-primary btn-lg btn-block mt-4"
        id="create-B-button"
      >点我通知主进程去创建B</button>
    </div>
    <script>
      // 网页中可以使用node
      require('./a.js')
    </script>
  </body>
</html>

const {
    
     ipcRenderer } = require('electron')
const {
    
     $ } = require('../../helper')
$('create-B-button').addEventListener('click', () => {
    
    
  console.log('通知主进程去创建B视窗')
  ipcRenderer.send('create-B-window')
})
ipcRenderer.on('b-a', (e, a) => {
    
    
  console.log('a ============== ', a)
  $('myh2').innerHTML = a
})

看看渲染进程B:b.html/b.js

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Render-B</title>
    <link rel="stylesheet" href="../../node_modules/bootstrap/dist/css/bootstrap.min.css">
  </head>
  <body>
    <div class="container mt-4">
      <h1>我是渲染进程B</h1>
      <button type="button" class="btn btn-outline-primary btn-lg btn-block mt-4" id="select-pic">请选择图片</button>
      <button type="button" class="btn btn-outline-primary btn-lg btn-block mt-4" id="fetch-printer-list-old">获取打印机列表-老</button>
      <button type="button" class="btn btn-outline-primary btn-lg btn-block mt-4" id="fetch-printer-list-new">获取打印机列表-新</button>
      <button type="button" class="btn btn-outline-primary btn-lg btn-block mt-4" id="a-render-b">渲染进程间通信sendTo</button>
    </div>
    <script>
      // 网页中可以使用node
      require('./b.js')
    </script>
  </body>
</html>
const {
    
     ipcRenderer, remote } = require('electron')
const {
    
     $ } = require('../../helper')
let obj1 = remote.getGlobal('sharedObject')
console.log('obj1: ', obj1)
$('select-pic').addEventListener('click', () => {
    
    
  console.log('通知主进程去打开操作系统视窗')
  ipcRenderer.send('open-pic-file')
})

$('fetch-printer-list-old').addEventListener('click', () => {
    
    
  console.log('通知主进程获取打印机列表--老方法')
  ipcRenderer.send('b-main-pinterlist-old')
  ipcRenderer.once('main-b-printerlist-old', (ev, data) => {
    
    
    console.log('data(old) ==== ', data)
  })
})
$('fetch-printer-list-new').addEventListener('click', async () => {
    
    
  console.log('通知主进程获取打印机列表')
  let res = await ipcRenderer.invoke('b-main-pinterlist-new')
  console.log('打印机列表res(新): ', res)
})

$('a-render-b').addEventListener('click', async () => {
    
    
  console.log('渲染进程b通知渲染进程a')
  let obj = await remote.getGlobal('sharedObject')
  console.log('obj: ', obj)
  let windowAWebContentsId = obj.windowAWebContentsId
  ipcRenderer.sendTo(windowAWebContentsId, 'b-a', '从b窗口带来的数据')
})

helper/index.js

exports.$ = id => {
    
    
  return document.getElementById(id)
}

3.2 Electron常用的自身能力

3.2.1 主进程/渲染进程都可以直接使用Node

我们甚至可以在Electron的控制台上直接写代码:

const fs = require('fs')
const path = require('path')

const fileContent = fs.readFileSync(path.join(__dirname, 'a.js'), 'utf-8')

console.log(fileContent)

在这里插入图片描述

3.2.2 菜单Menu

直接上图吧,这就是menu
在这里插入图片描述
如何给我们自己的应用添加Menu呢?主进程中添加如下代码!

const {
    
     app, BrowserWindow, ipcMain, Menu, Tray} = require('electron')
// Menu的用法
const template = [{
    
    
  label: app.name,
  submenu: [
    {
    
     role: 'about' },
    {
    
     type: 'separator' },
    {
    
     role: 'services' },
    {
    
     type: 'separator' },
    {
    
     role: 'hide' },
    {
    
     role: 'hideothers' },
    {
    
     role: 'unhide' },
    {
    
     type: 'separator' },
    {
    
     role: 'quit' }
  ]
}, {
    
    
  label: 'File',
  submenu: [{
    
     role: 'close' }]
}, {
    
    
  label: 'View',
  submenu: [
    {
    
     role: 'reload' },
    {
    
     role: 'forceReload' },
    {
    
     role: 'toggleDevTools' },
    {
    
     type: 'separator' },
    {
    
     role: 'resetZoom' },
    {
    
     role: 'zoomIn' },
    {
    
     role: 'zoomOut' },
    {
    
     type: 'separator' },
    {
    
     role: 'togglefullscreen' }
  ]
}, {
    
    
  role: 'help',
  submenu: [
    {
    
    
      label: 'Learn More',
      click: async () => {
    
    
        const {
    
     shell } = require('electron')
        await shell.openExternal('https://www.baidu.com/')
      }
    }
  ]
}, {
    
    
  label: '自定义',
  submenu: [
    {
    
    
      label: '弹框通知',
      click: () => {
    
    
        let x = new Notification({
    
     title: '我是TITLE', body: '我是BODY' })
        x.show()
      }
    }
  ]
}]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)

3.2.3 托盘Tray

什么叫托盘?直接看图吧!
在这里插入图片描述

const initTray = () => {
    
    
  tray = new Tray('./assets/icon.png')
  const contextMenu = Menu.buildFromTemplate([
    {
    
     label: 'Item1', type: 'radio' },
    {
    
     label: 'Item2', type: 'radio' },
    {
    
     label: 'Item3', type: 'radio', checked: true },
    {
    
     label: 'Item4', type: 'radio', click: () => {
    
     console.log('......点击了Item4.......') } }
  ])
  tray.setToolTip('This is my tray')
  tray.setContextMenu(contextMenu)
  // 事件
  tray.on('click', () => {
    
    
    let x = new Notification({
    
     body: 'Tray事件', title: '单击事件' })
    x.show()
  })
}

在这里插入图片描述
在这里插入图片描述

3.2.4 快捷键用法

需求:我们要用 “Command + Q” 来 打开/关闭 应用的控制台

const {
    
     globalShortcut } = require('electron')  
// 快捷键用法
globalShortcut.register('CommandOrControl+Q', () => {
    
    
  windowA.webContents.toggleDevTools()
})

3.2.5 访问打印机

比如,获取电脑连接的打印机列表

ipcMain.handle('b-main-pinterlist-new', () => {
    
    
  return new Promise(resolve => {
    
    
    const list = windowB.webContents.getPrinters()
    console.log('list(new): ', list)
    resolve(list)
  })
})

猜你喜欢

转载自blog.csdn.net/GY_U_YG/article/details/122363127