2022- 无限咪咪-Electron 小记
Electron 小记
前言
第一次听到
直到最近帮别人开发些小工具,才体会到这门技术的美。对于一些使用人数很少的项目,部署一套

首先要说明一点,这套视频教程的时间比较早,大概
(咪咪看了下官方文档,从版本
Electron 可以理解为就是Google Chrome ,前端代码运行在" 浏览器" 的Tab 内,而Electron 代码控制整个" 浏览器" 。
搭建环境
npm install --save-dev electron@1
不过由于某些原因,安装大概率会出错,可以使用这条命令替代
ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/ npm install electron@1 -D --registry=https://registry.npm.taobao.org
这种包含
项目1 :获取视频时长
第一个项目是一个获取视频时长的程序。选择一个视频,然后在窗口中显示这个视频的时长(秒
最基础的
启动一个最简单的
根目录新建
// 引入依赖
const electron = require("electron");
const { app } = electron;
// app 监听 ready 事件
app.on("ready", () => {
console.log("app ready");
});
{
...
"scripts": {
...
"start": "electron ."
},
...
}
然后在命令输入
npm run start
不出意外的话,控制台会输出
app ready
这时我们的
默认情况下,
electron App process 不会向用户展示任何信息。
现在除了控制台的输出外,屏幕上不会出现窗口,因为窗口是
首先在根目录新增
<!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>首页</title>
</head>
<body>
<h1>首页</h1>
</body>
</html>
在
...
const { app, BrowserWindow } = electron;
// 主窗口作为全局变量
let mainWindow;
app.on("ready", () => {
// 新建 BrowserWindow 实例
mainWindow = new BrowserWindow({});
// 窗口加载 html 文件
// __dirname 代表当前模块的目录名
mainWindow.loadURL(`file://${__dirname}/index.html`);
});
启动命令
npm run start
等待一小会儿后,程序窗口就有了,展示的内容就是我们加载的
至此大概已经对
先来说一下如何选择视频,在
<body>
<h1>首页</h1>
<div>
<form>
<label for="upload-btn">上传视频</label>
<input id="upload-btn" type="file" accept="video/*" />
<button type="submit">获取信息</button>
</form>
</div>
<script>
document.querySelector("form").addEventListener("submit", (e) => {
e.preventDefault();
const file = document.querySelector("#upload-btn").files[0];
debugger;
const { path } = file;
});
</script>
</body>
然后启动程序
npm run start
打开调试器

点击”上传视频“按钮并选择文件,然后点击”获取信息“按钮,程序会停在断点处,鼠标悬浮到

这个上传是通过前端实现的,没有
首先要安装
Windows 用户参考 https://blog.csdn.net/qq_59636442/article/details/124526107
苹果用户使用 Homebrew 安装
安装完成后,在控制台输入
ffmpeg -version
输出相关信息表示安装成功了,然后我们要安装
npm install fluent-ffmpeg
安装完成后,解析视频的准备工作就做好了。
视频教程里将解析视频长度的逻辑放在了
现在我们遇到问题了,刚刚上传的视频文件是在
进程间通信通过
Web 侧发送:ipcRenderer.sendElectron 侧接收:ipcMain.onElectron 侧发送:mainWindow.webContents.sendWeb 侧接收:ipcRenderer.on
通过
...
<script>
// 引入依赖
const electron = require("electron");
const { ipcRenderer } = electron;
document.querySelector("form").addEventListener("submit", (e) => {
e.preventDefault();
const { path } = document.querySelector("#upload-btn").files[0];
// Web 侧发送消息
ipcRenderer.send("video:submit", path);
});
</script>
...
在
const { app, BrowserWindow, ipcMain } = electron;
// 引入 ffmpeg 相关依赖
const ffmpeg = require("fluent-ffmpeg");
let mainWindow;
app.on("ready", () => {
mainWindow = new BrowserWindow({});
mainWindow.loadURL(`file://${__dirname}/index.html`);
});
// Electron 侧接收消息
ipcMain.on("video:submit", (e, path) => {
console.log(path);
// ffmpeg.ffprobe 方法获取视频信息
ffmpeg.ffprobe(path, (err, metadata) => {
console.log(metadata.format.duration);
// 视频时长,单位秒
const d = metadata.format.duration;
// Electron 侧发送消息
mainWindow.webContents.send("video:duration", metadata.format.duration);
});
});
获取视频时长后,再发送给
...
<script>
const electron = require("electron");
const { ipcRenderer } = electron;
// Web 侧接收消息
ipcRenderer.on("video:duration", (e, duration) => {
// 事件处理函数第二个参数就是发送的参数
console.log(duration);
document.querySelector("#video-duration").innerHTML = duration;
});
document.querySelector("form").addEventListener("submit", (e) => {
e.preventDefault();
const { path } = document.querySelector("#upload-btn").files[0];
ipcRenderer.send("video:submit", path);
});
</script>
...
项目小结,通过这个项目,我们学习到
- 如何启动
Electron App - 如何新建窗口
Electron 中如何进程间通信,传递信息- 如何使用
FFmpeg 获取视频信息
项目2 :TODO LIST
第二个项目是一个经典项目——待办列表。通过这个项目我们将学习到如何自定义下拉菜单以及多窗口。
首先新建一个项目,根据前面搭建环境的方法,搭建出一套
index.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>
<h1>TODO LIST</h1>
<ul></ul>
</body>
</html>
我们先增加一个菜单,菜单下拉后有“添加
const menuTemplateItem = {
// 菜单名
label: "",
// 快捷键
accelerator: "",
// 点击事件回调
click() {},
// 下级菜单列表
submenu: [],
};
在
// 菜单模板
const menuTemplate = [
{
// 第一层菜单固定在窗口上方
label: "文件",
// 文件的子菜单
submenu: [
{
label: "添加TODO",
},
{
label: "退出",
accelerator: (() => {
// 通过 process.platform 可以获取用户操作系统信息
// darwin 就是苹果系统
if (process.platform === "darwin") {
return "Command+Q";
} else {
return "Ctrl+Q";
}
})(),
click() {
// 点击退出程序
app.quit();
},
},
],
},
];
定义完菜单模板后,在
const electron = require("electron");
const { app, BrowserWindow, Menu } = electron;
const menuTemplate = [...];
app.on("ready", () => {
mainWindow = new BrowserWindow({});
mainWindow.loadURL(`file://${__dirname}/index.html`);
mainWindow.on("closed", () => {
app.quit();
});
// 通过 Menu.buildFromTemplate 方法将菜单模板转为 electron.Menu 对象
const menu = Menu.buildFromTemplate(menuTemplate);
// 设置应用菜单
Menu.setApplicationMenu(menu);
});
启动程序,可以看到我们自定义菜单了

这时点击“添加
首先新建
<!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>
<form>
<div>
<label>新增TODO</label>
<input type="text" autofocus />
</div>
<button type="submit">提交</button>
</form>
</body>
</html>
新建窗口的逻辑还是通过
// 同 mainWindow 一样,将新增窗口设为全局变量
let addWindow;
function createAddWindow() {
// 和新增主窗口的步骤一样,new BrowserWindow 的实例,然后加载 add.html 作为视图
addWindow = new BrowserWindow({
// 创建实例时,可以传参设定窗口属性
width: 300,
height: 200,
title: "新增TODO",
});
addWindow.loadURL(`file://${__dirname}/add.html`);
// 窗口关闭后,虽然视图消失了,但是 addWindow 指向的对象还在内存中
// 通过手动赋值 null 释放之前的对象
addWindow.on("closed", () => (addWindow = null));
}
菜单模板增加“添加
// ...
const menuTemplate = [
{
label: "文件",
submenu: [
{
label: "添加TODO",
click() {
// 调用创建新增窗口函数
createAddWindow();
},
},
{
label: "退出",
accelerator: (() => {
if (process.platform === "darwin") {
return "Command+Q";
} else {
return "Ctrl+Q";
}
})(),
click() {
app.quit();
},
},
],
},
];
// ...
重新启动程序,然后点击菜单“文件”-“添加

最后就是实现新增
在
<body>
<!-- ... -->
<script>
const electron = require("electron");
const { ipcRenderer } = electron;
document.querySelector("form").addEventListener("submit", (e) => {
e.preventDefault();
// Web 侧发送消息
// 将新增TODO的值发送给 index.js
ipcRenderer.send("todo:add", document.querySelector("input").value);
});
</script>
<!-- ... -->
</body>
const electron = require("electron");
const { app, BrowserWindow, Menu, ipcMain } = electron;
let mainWindow;
let addWindow;
// ...
// Electron 侧接收消息
ipcMain.on("todo:add", (e, newTodo) => {
// Electron 侧发送消息
// 发送给 index.html 那个窗口
mainWindow.webContents.send("todo:add", newTodo);
// 程序一般设计就是新增后,自动关闭新增窗口
addWindow.close();
});
在
<body>
<h1>TODO LIST</h1>
<ul></ul>
<script>
const electron = require("electron");
const { ipcRenderer } = electron;
const ul = document.querySelector("ul");
// Web 侧接收消息
ipcRenderer.on("todo:add", (e, newTodo) => {
console.log(newTodo);
// 将新 TODO 的值显示到页面上
const li = document.createElement("li");
const todo = document.createTextNode(newTodo);
li.appendChild(todo);
ul.appendChild(li);
});
</script>
</body>
重新启动程序,试试看新建

至此我们项目二的主要功能就实现了,了解了自定义菜单与窗口间通信。但是还有一个问题没有解决,细心的童鞋可能发现了,我们自定义菜单后,原来默认的菜单就不见了。

更要命的是,这些菜单不但在窗口中不显示了,它们对应的快捷键也失效了。比如现在我们就无法打开调试页面了,这使得在开发时就很不方便。
如何恢复调试功能呢,其实很简单,在
// process.env.NODE_ENV 来获取 Node.Js 的环境变量,一般开发环境的值设置为 "production"
// 调试功能我们不希望用户在生产环境中使用,所以只有非生产环境,才添加对应的菜单
if (process.env.NODE_ENV !== "production") {
// 菜单模板增加"开发"菜单
// 子菜单为 "打开调试器"
menuTemplate.push({
label: "开发",
submenu: [
{
label: "打开调试器",
click(item, focusedWindow) {
// 点击回调函数的第二个参数是当前 focus 窗口的引用
// 通过 .toggleDevTools 方法打开调试器
focusedWindow.toggleDevTools();
},
},
],
});
}
现在重新运行程序,菜单栏多了“开发”,这样我们就可以开发坏境下打开调试器了。
除了调试器,我们还希望增加刷新页面功能,
if (process.env.NODE_ENV !== "production") {
menuTemplate.push({
label: "开发",
submenu: [
// role 也是菜单模板对象中的属性,通过这个属性可以使用一些预设好的功能
// "reload"就是刷新,还有其他值可以参考官网文档
{ role: "reload" },
{
label: "打开调试器",
click(item, focusedWindow) {
focusedWindow.toggleDevTools();
},
},
],
});
}
再重新启动程序,现在“开发”菜单中不但增加了“Reload”子菜单,而且还增加了快捷键刷新。
项目小结,通过这个项目,我们学习到
- 如何自定义菜单
- 如何进行窗口间通信
项目3 :时钟
到第三个项目了,经过前两个项目的学习,我们现在对
这是个时间管理项目,在状态栏点击程序
这个项目要先去下载作者的模板代码,下载地址
https://github.com/StephenGrider/ElectronCode
下载或克隆下来的项目,进入
ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/ npm install --registry=https://registry.npm.taobao.org
等待依赖安装完毕,在项目根目录新建
const electron = require("electron");
const { app, BrowserWindow } = electron;
let mainWindow;
app.on("ready", () => {
mainWindow = new BrowserWindow({});
mainWindow.loadURL(`file://${__dirname}/src/index.html`);
});
这个项目和我们之前两个项目有一点点区别,这个项目的前端是用
npm run start
等待几秒后,在命令窗口看到下面的输出说明前端项目启动好了
Version: webpack 2.7.0
Time: 2942ms
Asset Size Chunks Chunk Names
bundle.js 231 kB 0 [emitted] main
chunk {0} bundle.js (main) 227 kB [entry] [rendered]
[4] ./src/index.js 721 bytes {0} [built]
[5] (webpack)-dev-server/client?http://localhost:4172 7.93 kB {0} [built]
[6] ./src/components/App.js 9.73 kB {0} [built]
[7] ./src/components/Header.js 1.35 kB {0} [built]
[8] ./src/components/Settings.js 5.62 kB {0} [built]
[9] ./src/components/TasksIndex.js 4.42 kB {0} [built]
[10] ./src/components/TasksShow.js 6.05 kB {0} [built]
[11] ./src/utils/Timer.js 4.48 kB {0} [built]
[12] ./~/sockjs-client/dist/sockjs.js 181 kB {0} [built]
[13] (webpack)-dev-server/client/overlay.js 3.67 kB {0} [built]
[14] (webpack)-dev-server/client/socket.js 1.08 kB {0} [built]
[15] (webpack)/hot nonrecursive ^\.\/log$ 160 bytes {0} [built]
[16] (webpack)/hot/emitter.js 77 bytes {0} [built]
[25] multi (webpack)-dev-server/client?http://localhost:4172 ./src/index.js 40 bytes {0} [built]
+ 12 hidden modules
webpack: Compiled successfully.
此时在新的命令窗口,运行我们
npm run electron
运行成功后,应该会看到以下窗口

走到这里我们项目
// 引入 path 模块
const path = require("path");
const electron = require("electron");
const { app, BrowserWindow, Tray } = electron;
let mainWindow;
// 全局变量保存 Tray 实例
let tray;
app.on("ready", () => {
mainWindow = new BrowserWindow({});
mainWindow.loadURL(`file://${__dirname}/src/index.html`);
// 我这里偷懒了,视频中通过 process.platform 来判断当前操作系统是否是 windows 系统
// windows 系统使用 windows-icon@2x.png, 否则使用 iconTemplate.png
const iconPath = path.join(__dirname, "./src/assets/windows-icon@2x.png");
// 新建 Tray 实例
tray = new Tray(iconPath);
// 增加状态栏 icon 的悬浮提示
tray.setToolTip("提示");
});
重新启动项目(只要将当前

视频中这时没有将
接下来我们添加状态栏
// ...
app.on("ready", () => {
mainWindow = new BrowserWindow({});
mainWindow.loadURL(`file://${__dirname}/src/index.html`);
const iconPath = path.join(__dirname, "./src/assets/windows-icon@2x.png");
tray = new Tray(iconPath);
tray.setToolTip("提示");
// Tray 实例增加 click 事件的监听
tray.on("click", (e) => {
// 通过 window.isVisible() 获取窗口是否显示
if (mainWindow.isVisible()) {
// 如果窗口已经显示就隐藏窗口
mainWindow.hide();
} else {
// 如果窗口隐藏状态就显示
mainWindow.show();
}
});
});
重启项目,此时点击
如果作为普通程序,那做到这一步就差不多了,但是这次我们想实现一个类似小工具的程序,点击状态栏的
要实现这个效果,关键有两件事,第一件需要重设窗口的外观,使其更像“小工具”的弹窗,第二件就是定位窗口的位置,使其出现在状态栏附近,在
// ...
app.on("ready", () => {
// 设置主窗口的初始化参数
mainWindow = new BrowserWindow({
// 窗口宽度
width: 300,
// 窗口高度
height: 500,
// 不展示边框(没有菜单栏)
frame: false,
// 用户不能重新更改窗口大小
resizable: false,
// 默认运行时不展示窗口
show: false,
// 在任务栏不展示程序
skipTaskbar: true,
webPreferences: {
// 窗口不显示时,后台是否继续运行计时器和动画,默认是 true 不运行
backgroundThrottling: false,
},
});
mainWindow.loadURL(`file://${__dirname}/src/index.html`);
// 增加窗口 blur 事件,窗口一旦失去聚焦,自动隐藏窗口
mainWindow.on("blur", () => {
mainWindow.hide();
});
const iconPath = path.join(__dirname, "./src/assets/windows-icon@2x.png");
tray = new Tray(iconPath);
tray.setToolTip("提示");
// icon 的点击回调的第二个参数是 icon 的坐标
tray.on("click", (e, bound) => {
// x - icon 左上角的 x 坐标
// y - icon 左上角的 y 坐标
const { x, y } = bound;
// 通过 window.getBounds 方法动态获取窗口的宽高
const { width, height } = mainWindow.getBounds();
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
// 设置窗口的信息,注意这是 windows 下,任务栏在窗口下方的计算方法
mainWindow.setBounds({
// 窗口左上角的 x 坐标,要比 icon 左上角的 x 坐标,"往左"一半窗口的宽度
x: x - width / 2,
// 窗口左上角 y 坐标,要比 icon 左上角 y 坐标 "往上"窗口的高度
y: y - height,
// 窗口的宽高保持不变
width,
height,
});
mainWindow.show();
}
});
});
(视频中隐藏程序在任务栏的显示是通过
Mac OS X 是面向应用程序的,而Windows 是面向窗口的。
重新运行程序,现在点击状态栏的

屏幕的左上角坐标为
(0, 0) ,越往右边x 越大,越往下边y 越大。屏幕可视范围内,坐标的x 、y 值应该都是正数。窗口、icon 的坐标,都是以自己左上角的那个像素为标准。
(视频中的代码其实有一点点问题,窗口的
最后我们为
// ...
const { app, BrowserWindow, Tray, Menu } = electron;
// ...
// icon 的右键点击菜单模板,模板格式同自定义菜单一样
const menuTemplate = Menu.buildFromTemplate([
{
label: "退出",
click() {
app.quit();
},
},
]);
app.on("ready", () => {
// ...
// tray 增加鼠标右键点击监听
tray.on("right-click", () => {
// tray 通过 .popUpContextMenu 方法弹出菜单
tray.popUpContextMenu(menuTemplate);
});
});
重启程序,现在右键点击
至此项目
项目
- 如何在状态栏实现
icon 相关逻辑 - 窗口的属性设置
项目4 :视频格式转换
最后一个项目,是将输入的视频批量转换格式。这个项目相当于是对前两个
对于比较扎实的掌握了前面三个项目的话,这个项目可以不看。转换视频格式核心是通过