electron 研究

发布于 2020-11-08  381 次阅读


综述

vscode atom 都是基于 electron 开发,就目前使用情况来看,表现不错
使用 JavaScript,HTML 和 CSS 构建跨平台的桌面应用程序
其内核为 chrorium 与 node.js

详细内容

安装

electron 是 node.js 中的一个框架,需要先安装 node.js
安装 node.js 后然后安装 electron 包

npm install electron

由于 electron 外网被屏蔽,此时可以使用 cnpm 且将源定位淘宝源

npm install cnpm -g  --registry=https://registry.npm.taobao.org
cnpm install electron

创建 electron app

1 创建 应用文件夹

mkdir test_app_001

2 初始化包管理 .json

cd test_app_001
cnpm init
cnpm install --save-dev electron

3 创建 设置的 入口文件 -- main.js

4 创建所需的 html

5 运行

npm start

6 打包

安装打包工具

npm install electron-packager -g

打包

electron-packager . 'test_app_002' --platform=win32 --arch=x64 --icon=icon.ico --out=./out --asar --app-version=0.0.1

electron doc

electron 由一下三部分组成:

  1. chromium - 浏览器引擎 - 用于显示
  2. node.js - 操作文件系统与调用系统接口
  3. custom apis - 常用的 操作系统原生接口

electron 有两类进程:

  1. main - 主进程 - 每个 BrowserWindow 实例都有一个进程
  2. renderer - 渲染进程 - 每一个 BrowserWindow 实例在其 renderer 进程渲染显示内容

进程间通过 Inter-Process Communication(IPC) 模块进行通信:

  1. ipcMain
  2. ipcRenderer
const {BrowserWindow,ipcMain,ipcRenderer} = require('electron')

const win = new BrowserWindow()

ipcMain.handle('perform-action',(event,...args) => {
    // do actions on behalf of the renderer
})

ipcRenderer.invoke('perform-action',...args)

electron 可以使用完整的 node.js 的接口与模块,使用时需要安装到本地作为依赖

cnpm install --save aws-sdk

notifications

renderer 的通知可以直接通过使用 html5 notification api 完成
main process 通知需要在 程序逻辑 js 内添加

最新版的 win10 支持带图片的高级通知,此时可以使用 electron-windows-notifications 进行通知

recent documents

最近打开的文档,类似于 vscode 菜单栏的最近打开的文档
相关的接口为 app.addRecentDocument,通过此类接口将指定文档添加到 recent documents 列表

仅限 windows,mac
windows 上如果想要支持这个特性,需要将应用注册为文档类应用

progress bar in taskbar(windows macos unity)

通过 BrowserWindow.setProgressBar() 接口

macos dock

通过接口 app.dock.setMenu 接口设置,仅限mac

windows taskbar

const { app } = require('electron')

app.setUserTasks([
  {
    program: process.execPath,
    arguments: '--new-window',
    iconPath: process.execPath,
    iconIndex: 0,
    title: 'New Window',
    description: 'Create a new window'
  }
])

win.setThumbarButtons([
  {
    tooltip: 'button1',
    icon: path.join(__dirname, 'button1.png'),
    click () { console.log('button1 clicked') }
  }, {
    tooltip: 'button2',
    icon: path.join(__dirname, 'button2.png'),
    flags: ['enabled', 'dismissonclick'],
    click () { console.log('button2 clicked.') }
  }
])

shortcuts

  1. local shortcuts
  2. global shortcuts
const { Menu, MenuItem, globalShortcut } = require('electron')

const menu = new Menu()
menu.append(new MenuItem({
  label: 'Electron',
  submenu: [{
    role: 'help',
    accelerator: process.platform === 'darwin' ? 'Alt+Cmd+I' : 'Alt+Shift+I',
    click: () => { console.log('Electron rocks!') }
  }]
}))

Menu.setApplicationMenu(menu)

app.whenReady().then(() => {
  globalShortcut.register('Alt+CommandOrControl+I', () => {
    console.log('Electron loves global shortcuts!')
  })
}).then(createWindow)

若想在 BrowserWindow 中处理键盘快捷键,可以在 renderer process 中监听 DOM events 中的 keyup 和 keydown 事件

window.addEventListener('keyup',doSomething,true)

在 main process 处理

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

app.whenReady().then(() => {
  const win = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true } })

  win.loadFile('index.html')
  win.webContents.on('before-input-event', (event, input) => {
    if (input.control && input.key.toLowerCase() === 'i') {
      console.log('Pressed Control+I')
      event.preventDefault()
    }
  })
})

也可以使用第三方库来进行 shortcut 快捷键的解析, electron 的文档推荐了 mousetrap 并提供了 在 renderer process 进行处理的实例代码。

Mousetrap.bind('4', () => { console.log('4') })
Mousetrap.bind('?', () => { console.log('show shortcuts!') })
Mousetrap.bind('esc', () => { console.log('escape') }, 'keyup')

// combinations
Mousetrap.bind('command+shift+k', () => { console.log('command shift k') })

// map multiple combinations to the same callback
Mousetrap.bind(['command+k', 'ctrl+k'], () => {
  console.log('command k or control k')

  // return false to prevent default behavior and stop event from bubbling
  return false
})

// gmail style sequences
Mousetrap.bind('g i', () => { console.log('go to inbox') })
Mousetrap.bind('* a', () => { console.log('select all') })

// konami code!
Mousetrap.bind('up up down down left right left right b a enter', () => {
  console.log('konami code')
})

online/offline 时间监测

online/offline 事件监测在 render process 中可以通过 html5 标准文档中的 navigator.onLine 来实现。

文档中提出此类监测方式并不完全有效,需要其他额外方式补充监测。

// main.js
const {app,BrowserWindow} = require('electron');

let onlineStatusWindow;

app.whenReady().then(()=>{
  onlineStatusWindow = new BrowserWindow({width:0,height:0,show:false});
  onlineStatusWindow.loadURL('file://${__dirname}/online-status.html');
})

在 online-status.html 中添加监测的 js , 在 <body> 之前添加:

<script src='renderer.js'></script>
// renderer.js

const alertOnlineStatus = ()=>{window.alert(navigator.onLine?'online':'offline')}

window.addEventListener('online',alertOnlineStatus)
window.addEventListener('offline',alertOnlineStatus)

alertOnlineStatus();

在主线程中若要进行 online/offline 事件监测,需要通过 electron 的 IPC(进程间通信) 工具。

// main.js
const { app, BrowserWindow, ipcMain } = require('electron')
let onlineStatusWindow

app.whenReady().then(() => {
  onlineStatusWindow = new BrowserWindow({ width: 0, height: 0, show: false, webPreferences: { nodeIntegration: true } })
  onlineStatusWindow.loadURL(`file://${__dirname}/online-status.html`)
})

ipcMain.on('online-status-changed', (event, status) => {
  console.log(status)
})
<script src="renderer.js"></script>
// renderer.js
const { ipcRenderer } = require('electron')
const updateOnlineStatus = () => { ipcRenderer.send('online-status-changed', navigator.onLine ? 'online' : 'offline') }

window.addEventListener('online', updateOnlineStatus)
window.addEventListener('offline', updateOnlineStatus)

updateOnlineStatus()

macos appwindow represent doc

不理解用途

本地文件的拖拽

拖动本地文件到web,很多网站均支持;electron 提供了将 web 内容拖动到本地的方法。
实现方式为:在 ondragstart 事件的处理函数中调用 webContents.startDrag(item) 接口

<!--index.html-->
<a href="#" id="drag">Drag me</a>
<script src="renderer.js"></script>
//renderer.js
const { ipcRenderer } = require('electron')

document.getElementById('drag').ondragstart = (event) => {
  event.preventDefault()
  ipcRenderer.send('ondragstart', '/absolute/path/to/the/item')
}
//main.js
const { ipcMain } = require('electron')

ipcMain.on('ondragstart', (event, filePath) => {
  event.sender.startDrag({
    file: filePath,
    icon: '/path/to/icon.png'
  })
})

离屏渲染

离屏渲染将窗体内容渲染到位图,可以在任何地方渲染。elcetron 的离屏渲染与 chromium embeded framework 项目的方法类似。
有两种渲染模式可用,其中仅重绘需要变动部分的模式更为有效。渲染过程可以暂停,继续以及帧率设置。最高帧率为60。
离屏渲染窗口是一个 frameless window。

1.gpu加速

gpu 加速渲染使用 gpu 进行组合。由于帧需要从gpu进行拷贝,因此更需要性能,更耗时。好处在于可以支持 WebGL 和 3D CSS 。

2.软件输出设备

使用cpu进行渲染,通过禁用硬件加速来启用该模式。

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

// 禁用硬件加速,使用 cpu 进行渲染
app.disableHardwareAcceleration()

let win

app.whenReady().then(() => {
  win = new BrowserWindow({
    webPreferences: {
      offscreen: true
    }
  })

  win.loadURL('http://github.com')
  win.webContents.on('paint', (event, dirty, image) => {
    // updateBitmap(dirty, image.getBitmap())
  })
  win.webContents.setFrameRate(30)
})

macos dark mode

macos 自 10.14 mojave 引入了系统范围的 dark mode

const { nativeTheme } = require('electron')

nativeTheme.on('updated', function theThemeHasChanged () {
  updateMyAppTheme(nativeTheme.shouldUseDarkColors)
})

web embeded

若想嵌入第三方内容到 BrowserWindow ,electron 提供了三种方式:

  1. <iframe>
  2. <webview>
  3. BrowserViews

<iframe> 可以显示外部网页,需要 Content Security Policy。为了限制 <iframe> 中 site 的能力,建议使用 sandbox 来控制权限。

<webview> 基于 chromium 的 WebViews,但是 electron 不建议使用这个。

BrowserViews 不是 DOM 的一部分,它们被主进程创建并控制。它们是 window 上层的部分。基本类似于 BrowserWindow 不过得手动控制。

测试框架

electron 提供了 spectron 和 devtron 测试框架便于开发者优化应用。

app.client.auditAccessibility().then(function (audit) {
  if (audit.failed) {
    console.error(audit.message)
  }
})

调试

渲染进程的调试使用 DevTools 即可。
主进程的调试需要使用 Chromium Developer Tools。
v8 崩溃,开发工具会提示崩溃信息。

调试主进程通过 electron --inspect=[port] your/app 的方式,默认的端口号为 5858 。 electron 将会监听该端口。

electron --inspect-brk=[port] your/app 会在第一行执行时暂停。

也可以通过 chrome 进行调试,或者 vscode。

vscode 的配置内容如下:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Main Process",
      "type": "node",
      "request": "launch",
      "cwd": "{workspaceFolder}",
      "runtimeExecutable": "{workspaceFolder}/node_modules/.bin/electron",
      "windows": {
        "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
      },
      "args" : ["."],
      "outputCapture": "std"
    }
  ]
}

使用 selenium 和 webdriver

$ npm install --save-dev spectron
// A simple test to verify a visible window is opened with a title
const Application = require('spectron').Application
const assert = require('assert')

const myApp = new Application({
  path: '/Applications/MyApp.app/Contents/MacOS/MyApp'
})

const verifyWindowIsVisibleWithTitle = async (app) => {
  await app.start()
  try {
    // Check if the window is visible
    const isVisible = await app.browserWindow.isVisible()
    // Verify the window is visible
    assert.strictEqual(isVisible, true)
    // Get the window's title
    const title = await app.client.getTitle()
    // Verify the window's title
    assert.strictEqual(title, 'My App')
  } catch (error) {
    // Log any failures
    console.error('Test failed', error.message)
  }
  // Stop the application
  await app.stop()
}

verifyWindowIsVisibleWithTitle(myApp)
$ npm install electron-chromedriver
$ ./node_modules/.bin/chromedriver
Starting ChromeDriver (v2.10.291558) on port 9515
Only local connections are allowed.

npm install selenium-webdriver
const webdriver = require('selenium-webdriver')

const driver = new webdriver.Builder()
  // The "9515" is the port opened by chrome driver.
  .usingServer('http://localhost:9515')
  .withCapabilities({
    chromeOptions: {
      // Here is the path to your Electron binary.
      binary: '/Path-to-Your-App.app/Contents/MacOS/Electron'
    }
  })
  .forBrowser('electron')
  .build()

driver.get('http://www.google.com')
driver.findElement(webdriver.By.name('q')).sendKeys('webdriver')
driver.findElement(webdriver.By.name('btnG')).click()
driver.wait(() => {
  return driver.getTitle().then((title) => {
    return title === 'webdriver - Google Search'
  })
}, 1000)

driver.quit()
$ npm install electron-chromedriver
$ ./node_modules/.bin/chromedriver --url-base=wd/hub --port=9515
Starting ChromeDriver (v2.10.291558) on port 9515
Only local connections are allowed.

npm install webdriverio
const webdriverio = require('webdriverio')
const options = {
  host: 'localhost', // Use localhost as chrome driver server
  port: 9515, // "9515" is the port opened by chrome driver.
  desiredCapabilities: {
    browserName: 'chrome',
    'goog:chromeOptions': {
      binary: '/Path-to-Your-App/electron', // Path to your Electron binary.
      args: [/* cli arguments */] // Optional, perhaps 'app=' + /path/to/your/app/
    }
  }
}

const client = webdriverio.remote(options)

client
  .init()
  .url('http://google.com')
  .setValue('#q', 'webdriverio')
  .click('#btnG')
  .getTitle().then((title) => {
    console.log('Title was: ' + title)
  })
  .end()

在 CI 构造系统上进行测试,若无显卡驱动,此时无法运行程序以及进行测试。
若要执行测试,此时可以使用 XVfb 之类的虚拟显示。

application 发布打包

  1. electron-forge
  2. electron-builder
  3. electron-packager

升级

应用升级可以使用内置的 Squirrel框架以及 electron 的 autoUpdater 模块。

electron 维护 update.electronjs.org ,可以使用这个服务来维护更新。

npm install update-electron-app
require('update-electron-app')()

也可以部署一个更新服务器用于更新
可以使用如下服务器进行更新:

  1. hazel
  2. nuts
  3. electron-release-server
  4. nucleus
const { app, autoUpdater, dialog } = require('electron')

const server = 'https://your-deployment-url.com'
const url = `{server}/update/{process.platform}/${app.getVersion()}`

autoUpdater.setFeedURL({ url })

setInterval(() => {
  autoUpdater.checkForUpdates()
}, 60000)

autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName) => {
  const dialogOpts = {
    type: 'info',
    buttons: ['Restart', 'Later'],
    title: 'Application Update',
    message: process.platform === 'win32' ? releaseNotes : releaseName,
    detail: 'A new version has been downloaded. Restart the application to apply the updates.'
  }

  dialog.showMessageBox(dialogOpts).then((returnValue) => {
    if (returnValue.response === 0) autoUpdater.quitAndInstall()
  })
})

autoUpdater.on('error', message => {
  console.error('There was a problem updating the application')
  console.error(message)
})

朝闻道,夕死可矣