之前用 Node.js 开发了一款在线版 Mp4 转化器,有同学反映需求本地要安装 ffmpeg,运用起来比较麻烦。其实,咱们能够将该运用转化为 Elctron 桌面版,并将 ffmpeg 打包进去做成便携版,这样不必安装,还不必连网。
最终效果如下:

electron-vite-vue 模板
本项目有两个版别:electron-vite-vue 脚手架版和 JS 原生版。原生版在烘托进程模块运用了很多的模板字符串,没有脚手架版那么便利。究竟 Vue 模板能够绑定数据。依据数据驱动是现代 JS 结构的精华。
模板是依据 Vite 官方的 template-vue-ts 脚手架建立的,所以绝大部分都是 Vite + Vue3 + ts 工程的文件(能够看作Vue 前端,其间 App.vue
是根组件,烘托进程模块的大部分逻辑都写在这儿),除了:
- electron 文件夹:能够看作 Node 后端,其间 main.ts 是主进口,主进程模块的大部分逻辑都写在这儿。
- electron-builder.json5:Electron 专属打包装备文件,JS 原生版的导报装备式放在
package.json
中。
有了 Vite 的加持,能够运用 Node.js ESM 包。
在生成的模板中集成
element-plus
有点问题:tsconfig.json
装备项moduleResolution
设置成Bundler
呼不出代码提示,我改成Node
后就能够了。
如你所见,本质上,Electron 开发就是 JS 全栈开发。
桌面版的特性
桌面版和在线版的 Mp4 转化器比较,咱们共用了视频读取、视频转化代码逻辑。其他部分或多或少有些不同,究竟桌面版有不少原生操作,用户体会体会更好:
- 菜单操作:桌面独有的功用,添加了
视频文件
、协助
两个操作进口 - 多窗口操作:为便利显现,预览视频独自运用了一个窗口
- 挑选视频文件运用 Electron 原生
dialog
:不必上传视频后才能读取视频信息 - 新增本地保存视频文件功用:相同运用 Elctron 原生
dialog
- 运用 Element-plus UI 库:是不是比在线版美丽一些?
- 运用进程间通讯(IPC):不必调用 Web API,离线也能转化
转化成功后会直接翻开预览视频,由于一旦将视频读入内存,再次转化很快就能完结,所以没有做预览视频的其他进口。
自界说菜单
本项目中,咱们运用 Menu.buildFromTemplate(menuTemplate)
来自界说原生运用菜单,template
是一个选项类型的数组,用于构建 MenuItem
。经过模板创立的原生菜单是依据数据驱动的:
// electron/config.ts
import { MenuItem, MenuItemConstructorOptions, shell } from 'electron'
const isMac = process.platform === 'darwin'
const menuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [
{
label: '视频文件',
submenu: [
// ...
{
id: 'saveFile',
label: '保存视频',
accelerator: 'CmdOrCtrl+S',
enabled: false
},
{ type: 'separator' },
isMac
? {
role: 'close',
label: '退出',
accelerator: 'Cmd+Q'
}
: {
role: 'quit',
label: '退出',
accelerator: 'Ctrl+Q'
}
]
},
{
label: '协助',
submenu: [
// ...
{
id: 'support',
label: '技术支持',
click() {
shell.openExternal('mailto:riafan@hotmail.com')
}
}
]
}
]
export { menuTemplate }
在 macOS 大将 menu 设置成运用内菜单,在 Windows 和 Linux 上,menu 将会被设置成窗口顶部菜单。
关于 MenuItem 来说,除了设置 id
和 label
特点外,在本项目中,咱们还设置了:
- accelerator 快捷键:设置为
CmdOrCtrl+S
,表明 macOS 上按 Cmd+S,Windows 上按 Ctrl+S 会保存视频 - enabled 是否激活:本项目中,
保存视频
菜单项默许是禁用的,只要翻开过视频才是激活的 - click 点击菜单项的回调函数:本项目中,点击
技术支持
菜单项会翻开体系发送电子邮件的默许程序 - role 界说菜单项的操作: 本项目中,设置
role: 'close'
会调用体系默许的退出运用操作,省去了对各体系分别设置 click 特点 - type 界说菜单项类型:默许为
normal
。设置为separator
会在菜单项之间显现一条分割线
指定
click
特点,role
特点将被忽略。
翻开视频
进程间通讯 (IPC) 是在 Electron 中构建功用丰富的桌面运用程序的关键部分之一。 由于主进程和烘托器进程在 Electron 的进程模型具有不同的职责,因而 IPC 是履行许多常见使命的唯一办法。
下面是翻开视频的时序图:

关于翻开视频来说,可能是经过挑选菜单项、点击挑选按钮或是拖放到按钮区触发的。点击挑选按钮或是拖放到按钮区是从烘托线程建议的,挑选视频后会回来视频的文件途径,因而应该运用烘托器进程到主进程双向通讯形式。而挑选菜单项是从主线程建议的,能够运用主进程到烘托器进程单向通讯形式。烘托器接收音讯后能够复用双向通讯形式,一样能够回来视频的文件途径。下面运用 contextBridge
API 将这段代码露出给烘托器进程。
// preload.ts
selectFile: () => ipcRenderer.invoke('dialog:selectFile'),
onSelectFile: (callback: () => void) =>
ipcRenderer.on('menu:selectFile', callback),
Electron 主进程侧(main.ts
)需求运用 showOpenDialog
办法翻开对话框挑选一个视频文件:
// electron/main.ts
async function handleSelectFile() {
const { canceled, filePaths } = await dialog.showOpenDialog({
filters: [{ name: fileType, extensions: allowFormats }]
})
if (!canceled) {
return filePaths[0]
}
}
filters
用于规定用户可见或可选的特定类型范围,设置filters
的 extensions
特点会按文件后缀名过滤。假如 extensions
不设置成 ['*']
,则对话框没有 所有文件
选项(Web 运用的文件挑选框总是有的),因而 Electron 中不必写文件类型查验的逻辑,指定文件类型即可。
showOpenDialog
办法会回来用户挑选的文件途径,假如对话框被取消了 ,则回来 undefined
。
读取、转化视频
这一块代码实现和 Web 版差不多,只是数据通讯运用的是 IPC 而不是 Web API。读取、转化视频的预加载脚本如下:
// preload.ts
readFile: (path: string) => ipcRenderer.invoke('video:readFile', path),
convertFile: (params: Params) =>
ipcRenderer.invoke('video:convertFile', params)
如你所见,readFile
和 convertFile
都是运用烘托器进程到主进程双向通讯形式。
// electron/main.ts
.on('end', () => {
console.log('file has been converted succesfully')
createPreview({
width,
height
})
resolve(output)
})
视频转化成功后会另外创立一个窗口来预览视频。
预览视频
当然,创立预览窗口是在 Electron 主进程侧(main.ts
)完结的。
// electron/main.ts
const win = new BrowserWindow({
width: width + 16,
height: height + 88,
webPreferences: {
preload: path.join(__dirname, 'preview.js')
}
})
if (isDev) {
win.loadURL(path.posix.join(VITE_DEV_SERVER_URL, 'preview.html'))
} else {
win.loadFile(path.join(process.env.DIST, 'preview.html'))
}
// ...
win.removeMenu()
每个 Electron 运用都会为每个翻开的运用程序窗口 ( 与每个网页嵌入 ) 生成一个独自的烘托器进程,多窗口意味着多烘托器进程。通常新建运用程序窗口需求初始化窗口的宽高、预加载脚本和加载页面。
此处的预览窗口宽高是依据视频宽高计算出来的。为了安全,咱们还新建了预加载脚本 preview.js
,在运用程序窗口结构办法中的 webPreferences 选项里将其附加到主进程。留意,咱们还需求为其额定装备一个进口,能够在 vite.config.ts
装备:
electron([
// ...
{
entry: 'electron/preview.ts',
}
])
预加载脚本 preview.ts
的代码如下:
// electron/preview.ts
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('electronAPI', {
previewFile: (callback: () => void) =>
ipcRenderer.on('video:preview', callback),
saveFile: (callback: () => void) =>
ipcRenderer.invoke('dialog:saveFile', callback)
})
留意:此处的
saveFile
回调函数与 ‘electron/preload.ts’ 中的saveFile
回调函数是一样的。
由于加载了新页面 preview.html
,但 Vite 默许是单页面的,只要 index.html
这个单一进口,所以咱们还需求为其额定装备一个进口,能够在 vite.config.ts
装备:
build: {
rollupOptions: {
input: {
index: path.join(__dirname, 'index.html'),
preview: path.join(__dirname, 'preview.html'),
}
}
}
留意:开发环境下运行本脚手架,加载页面一定要运用服务器地址。
预览窗口不需求菜单,咱们运用 win.removeMenu()
将其移除。
保存视频
下面是保存视频的时序图:

和翻开视频相似,咱们需求界说其预加载脚本:
// preload.ts
saveFile: () => ipcRenderer.invoke('dialog:saveFile'),
onSaveFile: (callback: () => void) =>
ipcRenderer.on('menu:saveFile', callback)
Electron 主进程侧(main.ts
)需求运用 showSaveDialog
办法翻开对话框来保存视频文件:
async function handleSaveFile() {
const { canceled, filePath } = await dialog.showSaveDialog({
filters: [{ name: fileType, extensions: ['mp4'] }],
defaultPath: path.join(app.getPath('videos'), `${Date.now()}.mp4`)
})
if (!canceled) {
return new Promise((resolve, reject) => {
const rs = fs.createReadStream(output)
const ws = fs.createWriteStream(filePath!)
rs.pipe(ws)
rs.on('end', () => {
resolve('视频保存成功')
}).on('error', (error: any) => {
reject(error)
})
})
}
}
保存视频实际上是个拷贝文件的进程,这儿运用了
fs
模块和pipe
操作。
打包运用程序
之前提过,打包是在 electron-builder
中装备的:
{
directories: {
output: 'release'
},
files: [
'dist-electron',
'dist',
"!**node_modules/ff*-static/bin/!(win32)",
"!**node_modules/ff*-static/bin/win32/ia32"
],
win: {
icon: "res/icon.png",
target: [
{
target: 'portable',
arch: ['x64']
}
],
"artifactName": "${productName}_${version}.${ext}"
}
}
咱们的方针是打包 win32 渠道 x64 架构下的 portable
版别,装备很简单,重点说说 files
。
dist-electron
包含 Node.js 后端打包文件,dist
包含 Vue 前端打包文件。那两个文件正则表达式呢?
运用 Electron 打包的时分设置 asar
为 true
,electron-builder 会智能的把一些 native
的程序(包含 exe履行文件)打包到 app.asar.unpacked
中。也就是说,假如不运用文件正则表达式筛选特定渠道,会仿制 ffmpeg-static
和 ffprobe-static
整个依赖包。那样打包文件就太大了。
留意:咱们需求经过替换来获取ffmpeg 二进制文件的途径,代码如下:
// Get the paths to the packaged versions of the binaries we want to use
const ffmpegPath = require('ffmpeg-static').replace(
'app.asar',
'app.asar.unpacked'
)
const ffprobePath = require('ffprobe-static').path.replace(
'app.asar',
'app.asar.unpacked'
)
// tell the ffmpeg package where it can find the needed binaries.
ffmpeg.setFfmpegPath(ffmpegPath)
ffmpeg.setFfprobePath(ffprobePath)
关于如安在 Electron 运用中包含 ffmpeg 二进制文件,可参考这篇文章。
打包文件是个漫长的进程,如何快速验证打包是否正确呢?
electron-builder --dir
运行这个 electron-builder 命令能够快速生成未紧缩的打包文件,能够协助排查问题。
本项目假如运用 JS 原生版打包,最终这个便携式 Mp4 转化器只要 70 M 左右。要知道,ffmpeg 那两个二进制文件都有60 M 左右。真是爽歪歪!
链接地址
- electron-vite-vue 脚手架版 项目地址
- Elctron + JS 原生版 项目地址
- 原文:Electron Mp4 转化器