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.json的main脚本的进程被称为主进程。 在主进程中运行的脚本通过创建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)通信提供了多种实现方式,如可以使用
ipcRenderer和ipcMain模块发送消息,使用 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);
}
——待补充——