Electron相关笔记

Electron相关笔记

首先丢一个官方文档地址:https://electronjs.org/docs

开发环境

https://electronjs.org/docs/tutorial/development-environment

简单的应用示例

https://electronjs.org/docs/tutorial/first-app

electron-vue相关

https://github.com/SimulatedGREG/electron-vue

安装

在已经全局安装好vue-cli的前提下

vue init simulatedgreg/electron-vue my-project

安装依赖项并运行应用程序

cd my-project
npm install 
npm run dev

好了,这个熟悉的结构。什么都有了。基本上能跑起来了,然后就是一些细节的问题了。

前置知识点

在使用Electron的API之前明确一下,Electron中有的两种进程类型,主进程和渲染器进程。下面摘抄官方文档对这个两个进程的说明。

Electron 运行 package.jsonmain 脚本的进程被称为主进程。 在主进程中运行的脚本通过创建web页面来展示用户界面。 一个 Electron 应用总是有且只有一个主进程。

由于 Electron 使用了 Chromium 来展示 web 页面,所以 Chromium 的多进程架构也被使用到。 每个 Electron 中的 web 页面运行在它自己的渲染进程中。

在普通的浏览器中,web页面通常在沙盒环境中运行,并且无法访问操作系统的原生资源。 然而 Electron 的用户在 Node.js 的 API 支持下可以在页面中和操作系统进行一些底层交互。

EN

主进程和渲染进程之间的区别

主进程使用 BrowserWindow 实例创建页面。 每个 BrowserWindow 实例都在自己的渲染进程里运行页面。 当一个 BrowserWindow 实例被销毁后,相应的渲染进程也会被终止。

主进程管理所有的web页面和它们对应的渲染进程。 每个渲染进程都是独立的,它只关心它所运行的 web 页面。

在页面中调用与 GUI 相关的原生 API 是不被允许的,因为在 web 页面里操作原生的 GUI 资源是非常危险的,而且容易造成资源泄露。 如果你想在 web 页面里使用 GUI 操作,其对应的渲染进程必须与主进程进行通讯,请求主进程进行相关的 GUI 操作。

题外话:进程间通讯

Electron为主进程( main process)和渲染器进程(renderer processes)通信提供了多种实现方式,如可以使用ipcRendereripcMain模块发送消息,使用 remote模块进行RPC方式通信。 这里也有一个常见问题解答:web页面间如何共享数据

然后就可以耍了。

需求和实现

1、主进程和渲染进程间的通讯

主进程:

// 隐藏主进程
ipcMain.on('hide-main-win', async () => {
    mainWindow.hide();
    console.log(`隐藏主程序窗口`)
});
// 触发器调用
ipcMain.on('dispatch-trigger', (event, arg) => {
    triggerDispatch(arg)
    console.log(`调用触发器参数: ${arg}`)
});

渲染进程:

// 不带参数
this.$electron.ipcRenderer.send('hide-main-win');
// 带参数
this.$electron.ipcRenderer.send('dispatch-trigger', data.data.nodeValue);

2、托盘

tray.js

let appIcon = null;

this.$electron.ipcMain.on('put-in-tray', (event) => {
  const iconName = process.platform === 'win32' ? 'assets/img/windows-icon.png' : 'assets/img/iconTemplate.png';
  const iconPath = path.join(__dirname, iconName);
  appIcon = new this.$electron.Tray(iconPath);

  // 托盘菜单
  let template = [{
    label: '编辑',
    submenu: [{
      label: '撤销',
      accelerator: 'CmdOrCtrl+Z',
      role: 'undo'
    }, {
      label: '重做',
      accelerator: 'Shift+CmdOrCtrl+Z',
      role: 'redo'
    }, {
      type: 'separator'
    }, {
      label: '剪切',
      accelerator: 'CmdOrCtrl+X',
      role: 'cut'
    }, {
      label: '复制',
      accelerator: 'CmdOrCtrl+C',
      role: 'copy'
    }, {
      label: '粘贴',
      accelerator: 'CmdOrCtrl+V',
      role: 'paste'
    }, {
      label: '全选',
      accelerator: 'CmdOrCtrl+A',
      role: 'selectall'
    }]
  }, {
    label: '查看',
    submenu: [{
      label: '重载',
      accelerator: 'CmdOrCtrl+R',
      click: (item, focusedWindow) => {
        if (focusedWindow) {
          // 重载之后, 刷新并关闭所有之前打开的次要窗体
          if (focusedWindow.id === 1) {
            this.$electron.BrowserWindow.getAllWindows().forEach(win => {
              if (win.id > 1) win.close()
            })
          }
          focusedWindow.reload()
        }
      }
    }, {
      label: '切换全屏',
      accelerator: (() => {
        if (process.platform === 'darwin') {
          return 'Ctrl+Command+F'
        } else {
          return 'F11'
        }
      })(),
      click: (item, focusedWindow) => {
        if (focusedWindow) {
          focusedWindow.setFullScreen(!focusedWindow.isFullScreen())
        }
      }
    }, {
      label: '切换开发者工具',
      accelerator: (() => {
        if (process.platform === 'darwin') {
          return 'Alt+Command+I'
        } else {
          return 'Ctrl+Shift+I'
        }
      })(),
      click: (item, focusedWindow) => {
        if (focusedWindow) {
          focusedWindow.toggleDevTools()
        }
      }
    }, {
      type: 'separator'
    }, {
      label: '应用程序菜单演示',
      click: function (item, focusedWindow) {
        if (focusedWindow) {
          const options = {
            type: 'info',
            title: '应用程序菜单演示',
            buttons: ['好的'],
            message: '此演示用于 "菜单" 部分, 展示如何在应用程序菜单中创建可点击的菜单项.'
          };
          this.$electron.dialog.showMessageBox(focusedWindow, options, function () {
          })
        }
      }
    }]
  }, {
    label: '窗口',
    role: 'window',
    submenu: [{
      label: '最小化',
      accelerator: 'CmdOrCtrl+M',
      role: 'minimize'
    }, {
      label: '关闭',
      accelerator: 'CmdOrCtrl+W',
      role: 'close'
    }, {
      type: 'separator'
    }, {
      label: '重新打开窗口',
      accelerator: 'CmdOrCtrl+Shift+T',
      enabled: false,
      key: 'reopenMenuItem',
      click: () => {
        this.$electron.app.emit('activate')
      }
    }]
  }, {
    label: '帮助',
    role: 'help',
    submenu: [{
      label: '学习更多',
      click: () => {
        this.$electron.shell.openExternal('http://electron.atom.io')
      }
    }]
  }];

  /*  const contextMenu = Menu.buildFromTemplate([{
      label: '移除',
      click: () => {
        event.sender.send('tray-removed')
      }
    }]); */

  const contextMenu = this.$electron.Menu.buildFromTemplate(template);

  appIcon.setToolTip('在托盘中的 Electron 示例.');
  appIcon.setContextMenu(contextMenu)
});

this.$electron.ipcMain.on('remove-tray', () => {
  appIcon.destroy()
});

index.js

import {createTray} from './tray/index.js'
// 创建新的托盘
function createNewTray() {
    const iconName = process.platform === 'win32' ? '/img/windows-icon.png' : '/img/iconTemplate.png';
    const iconPath = path.join(__static, iconName);
    appIcon = new Tray(iconPath);
    createTray(mainWindow, winURL, appIcon);
}
app.on('ready', (data) => {
    createNewTray();
});

3、日志记录

安装插件 electron-log 地址: https://github.com/megahertz/electron-log

安装:

npm i electron-log

使用:

const log = require('electron-log');
console.log = log.log;
console.error = log.error;
// log.transports.file.levels = ['error', 'warn', 'info', 'verbose', 'debug', 'silly'];
log.transports.console.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}';
// 1GB = 1073741824  1MB = 1048576 1kB = 1024
log.transports.file.maxSize = 1 * 1048576;
// 设置日志名称
let date = moment(new Date()).format('YYYY-MM-DD');
log.transports.file.fileName = `${date}-MAIN.log`;
// 日志输出路径
log.transports.file.getFile()

4、electron使用flash

首先要下载flash插件,放到项目的lib文件夹下面

// 使用flash插件
const {resolve} = require('path');
let pluginName;
switch (process.platform) {
    case 'win32':
        pluginName = 'pepflashplayer.dll';
        break;
    case 'darwin':
        pluginName = 'PepperFlashPlayer.plugin';
        break;
    case 'linux':
        pluginName = 'libpepflashplayer.so';
        break;
}

let plugins = resolve(__dirname, '../../lib/' + pluginName);

if (__dirname.includes(".asar")) {
    plugins = require('path').join(process.resourcesPath + '/lib/' + pluginName)
}

// flash插件问题
app.commandLine.appendSwitch('ppapi-flash-path', plugins);
// 可选:指定 flash 的版本
// app.commandLine.appendSwitch('ppapi-flash-version', '17.0.0.169')

5、程序开机自启动

// 设置开机自启动
app.setLoginItemSettings({openAtLogin: true});

6、Electron打开其他应用程序

trigger.js

// 命令子进程名称
let workerProcess;
const exec = require('child_process').exec;

// 执行调用程序
export async function runExec(cmdStr, cmdPath) {
 // 执行命令行,如果命令不需要路径,或就是项目根目录,则不需要cwd参数:
 console.log('cmdStr:', cmdStr, 'cmdPath:', cmdPath);
 // workerProcess = exec(cmdPath, {});
 workerProcess = exec(cmdStr, {cwd: cmdPath});
 // 不受child_process默认的缓冲区大小的使用方法,没参数也要写上{}:workerProcess = exec(cmdStr, {})
 
 // 打印正常的后台可执行程序输出
 workerProcess.stdout.on('data', function (data) {
  console.log('打印正常的后台可执行程序输出: ' + data);
 });
 
 // 打印错误的后台可执行程序输出
 workerProcess.stderr.on('data', function (data) {
  console.log('打印错误的后台可执行程序输出: ' + data);
 });
 
 // 退出之后的输出
 workerProcess.on('close', function (code) {
  console.log('程序退出:' + code);
 });
 
 // 储存应用PID
 _PIDs.forEach(item => {
  if (`start ${item.name}` == cmdStr) {
   item.pid = workerProcess.pid
  }
 });
}

export function triggerDispatch(arg) {
 let cmd = '';
 let path = '';
 
 // 替换路径反斜杠
 let testPath = arg.dialog.path.replace(/\\/g, "-");
 
 // 截取程序名
 let name = testPath.split('-').pop();
 
 // 处理路径格式
 testPath = testPath.substr(0, testPath.lastIndexOf('-'));
 
 console.log('应用名称', name);
 console.log('应用命令', cmd);
 
 if (arg.active) {
  switch (process.platform) {
   case 'win32':
    cmd = 'start ';
    // 应用执行命令
    cmd = cmd + name;
    // 路径处理斜杆
    path = testPath.replace(/\-/g, "\\");
    console.log('应用路径', path);
    // 收集已打开应用
    _PIDs.push({name: name});
    break;
   
   case 'linux':
    cmd = 'xdg-open';
    break;
   
   case 'darwin':
    cmd = 'open -a ';
    cmd = cmd + '/Applications/' + arg.dialog.name + '.app';
    console.log('应用路径', cmd);
    break;
  }
 } else {
  switch (process.platform) {
   case 'win32':
    cmd = 'taskkill /IM ' + name + ' /T ' + ' /F';
    _PIDs.forEach((item, index) => {
     if (item.name == name) {
      _PIDs.splice(index, 1);
      console.log('删除已关闭的应用后', _PIDs)
     }
    });
    break;
   
   case 'linux':
    cmd = 'xdg-open';
    break;
   
   case 'darwin':
    cmd = 'pkill ' + arg.dialog.name;
    break;
  }
 }
 
 runExec(cmd, path);
 console.log('_PIDs', _PIDs)
 // event.sender.send('asynchronous-message123', arg);
}

// 关掉所有应用
export async function closeAllTrigger() {
 
 _PIDs.length !== 0 ? await _PIDs.forEach(item => {
  // let cmd = 'taskkill /pid ' + item.pid + ' -t' + ' -f';
  let cmd = 'taskkill /IM ' + item.name + ' /T ' + ' /F';
  console.log('quanbu', cmd);
  runExec(cmd);
 }) : null
 
 _PIDs = [];
}

index.js

// 全局挂载 应用子进程PID数组
global._PIDs = [];

7、Electron隐藏主窗体

// 创建主窗体
function createWindow() {
    mainWindow = new BrowserWindow();

    mainWindow.webContents.closeDevTools();
    mainWindow.hide();
}

8、Electron意外退出错误处理

import {app} from 'electron'

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

app.on('window-all-closed', (data) => {
    if (process.platform !== 'darwin') {
        app.quit()
    }
    console.log(`window-all-closed: ${JSON.stringify(data, null, 2)}`);
});

app.on('activate', (data) => {
    if (mainWindow === null) {
        createWindow()
    }
    console.log(`activate: ${JSON.stringify(data, null, 2)}`);
});

function createWindow() {
    mainWindow = new BrowserWindow();
  
    mainWindow.webContents.on('crashed', () => {
        mainWindow.reload()
        console.log(`主进程窗口crashed`)
    });

    mainWindow.on('unresponsive', () => {
        mainWindow.reload()
        console.log(`主进程窗口没响应`)
    });

    mainWindow.on('closed', () => {
        console.log(`主进程窗口关闭`)
    })
}

9、解决Electron页面缓存问题

// 缓存问题
app.commandLine.appendSwitch("--disable-http-cache");

10、Electron一些特殊窗体的设置

import {app, BrowserWindow, ipcMain, Tray, screen} from 'electron'
// 创建主窗体
function createWindow() {
    // 获取全屏幕宽高
    const {width, height} = screen.getPrimaryDisplay().workAreaSize;

    mainWindow = new BrowserWindow({
        //  设置宽高
        width: width,
        height: height,
        // 将设置为 web 页面的尺寸(译注: 不包含边框), 这意味着窗口的实际尺寸将包括窗口边框的大小,稍微会大一点。 默认值为 false.
        useContentSize: true,
        // 窗口透明
        transparent: true,
        // 无边框窗口
        frame: false,
        // 默认窗口标题 默认为"Electron"。 如果由loadURL()加载的HTML文件中含有标签<title>,此属性将被忽略。
        title: 'city-viewer-client',
        // 窗口是否可以改变尺寸. 默认值为true.
        resizable: false,
        // 窗口是否可以移动. 在 Linux 中无效. 默认值为 true.
        movable: false,
        // 窗口是否可以进入全屏状态. 在 macOS上, 最大化/缩放按钮是否可用 默认值为 true。
        fullscreenable: true,
        // 是否在任务栏中显示窗口. 默认值为false.
        skipTaskbar: false,
        webPreferences: {
            // 是否支持插件. 默认值为 false.
            plugins: true,
            // 当设置为 false, 它将禁用同源策略 (通常用来测试网站), 如果此选项不是由开发者设置的,还会把 allowRunningInsecureContent设置为 true. 默认值为 true。
            webSecurity: false
        },
        // 窗口的图标. 在 Windows 上推荐使用 ICO 图标来获得最佳的视觉效果, 默认使用可执行文件的图标.
        icon: resolve(__dirname, '../../build/icons/256x256.png')
    });

    // 关闭开发者工具。
    mainWindow.webContents.closeDevTools();
    // 主窗口隐藏
    mainWindow.hide();
    // 使窗口不显示在任务栏中。
    mainWindow.setSkipTaskbar(true);
    // 启动或停止闪烁窗口, 以吸引用户的注意。
    mainWindow.flashFrame(false);
}

——待补充——