原文首发在我的博客,欢迎关注!
前言
前段时间,我用electron-vue开发了一款跨平台(目前支持主流三大桌面操作系统)的免费开源的图床上传应用——PicGo,在开发过程中踩了不少的坑,不仅来自应用的业务逻辑本身,也来自electron本身。在开发这个应用过程中,我学了不少的东西。因为我也是从0开始学习electron,所以很多经历应该也能给初学、想学electron开发的同学们一些启发和指示。故而写一份Electron的开发实战经历,用最贴近实际工程项目开发的角度来阐述。希望能帮助到大家。
预计将会从几篇系列文章或方面来展开:
electron-vue入门Main进程和Renderer进程的简单开发引入基于Lodash的JSON database——lowdb跨平台的一些兼容措施通过CI发布以及更新的方式开发插件系统——CLI部分开发插件系统——GUI部分命令行调用与系统级别右键菜单的实现想到再写...
说明
PicGo
是采用electron-vue
开发的,所以如果你会vue
,那么跟着一起来学习将会比较快。如果你的技术栈是其他的诸如react
、angular
,那么纯按照本教程虽然在render端(可以理解为页面)的构建可能学习到的东西不多,不过在main端(Electron
的主进程)应该还是能学习到相应的知识的。
如果之前的文章没阅读的朋友可以先从之前的文章跟着看。本文主要是基于PicGo
v2.1.0版本更新的重要内容做的讲述。
命令行调用
我们在使用一些Electron
开发的应用程序的时候,可以发现有些程序是可以通过命令行唤起的。比如VSCode
,在macOS的.bash_profile
里可以设置alias code='/Applications/Visual\ Studio\ Code.app/Contents/Resources/app/bin/code'
,这样就可以在命令行里通过code xxx.js
来调用VSCode打开文件了。如果想打开当前目录,可以通过code .
,如果想打开某个目录code xxx
等等。
命令行调用里其实还涉及到一个问题,有的时候我们的应用是个「单例应用」,也就是不能「多开」。如何在只能单开的应用里,也实现命令行调用呢?比如PicGo
,在软件打开的时候,命令行调用它也能上传图片,而不是打开一个新的PicGo
窗口。没事,下面会详细说明。
实现命令行调用
首先我们要来实现命令行调用。其实Electron
的命令行调用没有什么特殊的地方,与在Node.js
端很类似。我以PicGo
举例:
当我们在Windows下安装好了PicGo
之后,可以在安装目录里找到PicGo.exe
。你有没有想过在命令行里运行这个exe
会怎么样呢?在安装目录里打开powershell
,输入.\PicGo.exe
,你会发现PicGo
已经被打开了。如果我是加了一些参数打开会怎么样呢.\PicGo.exe upload
我们可以在main
进程里的ready
事件里把命令行参数打印出来:
app.on('ready', () => {console.log(process.argv) // ['D:\\PicGo.exe', 'upload']})复制代码
关键出现了,我们可以通过process.argv
这个在Node.js
端获取命令行参数的关键变量同样获得Electron
被命令行打开后的命令行参数。那么我们就可以在main
进程的ready
阶段通过获取的process.argv
参数来实现我们对应的功能。
对于PicGo而言,如果通过命令行打开它,并且传递了upload xxx.jpg
的话,我们就可以认为用户需要调用PicGo来实现上传一张图片。那么我们可以这么做(以下是实例代码):
import path from 'path'import fs from 'fs-extra'const getUploadFiles = (argv = process.argv, cwd = process.cwd()) => {files = argv.slice(2) // 过滤['D:\\PicGo.exe', 'upload']这两个参数,直接获取需要上传的图片路径let result = []if (files.length > 0) { // 如果图片列表不为空result = files.map(item => {if (path.isAbsolute(item)) { // 如果是绝对路径return {path: item}} else {let tempPath = path.join(cwd, item) // 如果是相对路径,就拼接if (fs.existsSync(tempPath)) { // 判断文件是否存在return {path: tempPath}} else {return null}}}).filter(item => item !== null) // 排除为null的路径}return result // 返回结果}复制代码
拿到图片列表后就执行自带的上传逻辑即可。下面说说单开应用的命令行调用注意事项。
实现单例应用的命令行调用
Electron
的发展很快,本文讲述的Electron
版本为当前最新的v4.1.4
,所以关于实现单例应用的api
也是跟随官方文档走的,如果你的Electron版本不是v4.x
,那么需要找对应版本的Electron
文档。
当前版本下实现单例应用的官方例子是:
const { app } = require('electron')let myWindow = nullconst gotTheLock = app.requestSingleInstanceLock() // 拿到单例锁if (!gotTheLock) { // 如果一个应用二次打开,那么getTheLock为falseapp.quit() // 立即退出二次打开的应用} else {app.on('second-instance', (event, commandLine, workingDirectory) => { // 一个应用尝试打开第二个实例时触发// Someone tried to run a second instance, we should focus our window.if (myWindow) {if (myWindow.isMinimized()) myWindow.restore()myWindow.focus()}})// Create myWindow, load the rest of the app, etc...app.on('ready', () => {})}复制代码
注意有个second-instance
事件。当我们试图在打开一个单例应用之后再打开这个应用的时候,就会触发这个事件。并且这个事件的回调函数里,有commandLine
和workingDeirectory
,实际上它们就是process.argv
和对应的cwd
(执行路径)。因此我们可以在这个事件里书写当应用试图被二次打开的时候应该做的事的逻辑。以下依然以PicGo举例:
app.on('second-instance', (event, commandLine, workingDirectory) => {let files = getUploadFiles(commandLine, workingDirectory)if (files === null || files.length > 0) { // 如果有文件列表作为参数,说明是命令行启动if (files === null) { // 如果为null说明是让PicGo上传剪贴板的图片uploadClipboardFiles()} else { // 否则说明是让PicGo上传具体的图片文件// ...uploadChoosedFiles(win.webContents, files)}} else { // 如果files === [] 说明并不是命令行启动或者并没有带额外参数if (settingWindow) { // 说明用户是点击了PicGo图标启动,那么这个时候把原有的窗口调出来并focus即可if (settingWindow.isMinimized()) {settingWindow.restore()}settingWindow.focus()}}})复制代码
这里我们通过读取commandLine
参数,来判断用户是用命令行来调用PicGo
上传图片的,还是仅仅是通过PicGo
的图标再次打开PicGo
的。关键的逻辑就是判断commandLine
里有没有关键的参数,从而得出是否是从命令行调用我们的应用的。如果用户仅仅是通过PicGo
图标再次打开PicGo
,那么我们应该把之前打开过的窗口复原并激活,告诉用户你之前已经打开过这个应用了。当然具体的业务逻辑不能一概而论,这里只是我对PicGo
的一点理解,只需知道核心是监听second-instance
事件即可。
以下是上述实现的截图,注意命令行输出都只在第一个终端进程里,说明我们实现了单例应用的命令行调用:
macOS的命令行调用
其实这个章节到上面基本结束。不过我想起我演示的是在Windows下做的,相对简单。而macOS下的命令行调用Electron
应用会有个坑,所以还是要说一下为好。(由于我没有Linux机器,所以Linux部分就不说明了,有兴趣的朋友可以测试一下跟我反馈!)
大家都知道macOS
的应用基本是放在Application
下的,所以我们会很自然想到直接命令行调用它们:
open /Applications/PicGo.app复制代码
但是这样做并不能传递参数进去,因为执行命令的是open
。
所以我们需要到更深层次的路径启动PicGo
并传递参数进去:
/Applications/PicGo.app/Contents/MacOS/PicGo upload xxx.jpg复制代码
只有这样才能像Windows那样类似PicGo.exe
来实现调用。
值得注意的是,Electron
的macOS应用想要在生产阶段打开debug
模式查看console
的输出也是到上述应用的对应目录下:
/Applications/PicGo.app/Contents/MacOS/PicGo --debug复制代码
而Widnows
相对简单,只需要:
.\PicGo.exe --debug复制代码
(Linux请自测)
系统级别右键菜单
在实现了命令行调用的功能之后,我就在考虑给PicGo加上原生的系统右键菜单。这样做的好处是用户可以直接在一张图片上右键->通过PicGo上传。例如:
Windows下:
macOS下:
接下来说说二者在实现上不同的地方。(Linux没有测试,欢迎有兴趣的小伙伴测试一下跟我说说~)
Windows
Windows的右键菜单的原理其实很简单,在注册表里写入值就行。篇幅原因不会对Windows注册表的知识做过多的展开。我们只关注往哪里写值,写哪些值才能实现我们要的效果。
首先我们可以看看VScode是如何实现右键菜单「Open with Code」的。
在系统里按快捷键WIN+R
然后输入regedit
打开注册表编辑器,我们来找到VSCode
的右键菜单所在地:
HKEY_CLASSES_ROOT
→*
→shell
→VSCode
:
可以看到一个「默认」的属性下的数据为「Open w&ith Code」,这个就是我们看到的菜单名。而一个叫「Icon」的属性下的数据为VSCode
的exe
安装路径。所以可以认为这个Icon
可以获取exe
的Icon
并显示到菜单上。
不过这里还没有看到如何将文件路径作为参数传入VScode
的。继续看:
HKEY_CLASSES_ROOT
→*
→shell
→VSCode
→command
:
在command
目录下我们看到了如下数据:
"C:\Users\PiEgg\AppData\Local\Programs\Microsoft VS Code\Code.exe" "%1"
可以看出这个%1
就是作为参数传给Code.exe
的。有了VSCode
作为参考,给自己的Electron
应用实现一个系统级别的右键菜单也不难了。有人可能会说我可以在应用启动阶段通过某些npm
包(比如windows-registry)来实现对注册表的写入。
不过实际上,在Windows
平台,如果你是用electron-builder
打包的话有一个更简洁的解决方案,那就是编写NSIS
脚本来实现,对此electron-builder
官方给出的文档可以一看。
本文不对NSIS
脚本做过多的描述,你只需要知道它是用来生成Windows
安装界面的一门脚本语言,你可以通过它来控制安装(卸载)界面都有哪些元素。并且它可以接入安装的生命周期,做一些操作,比如写入注册表。我们利用这个特性,来给PicGo做一个安装阶段写入注册表的操作,实现系统级别的右键菜单。
electron-builder
给NSIS
暴露的钩子主要有customHeader
,preInit
,customInit
,customInstall
,customUnInstall
,等等。
我们可以在customInstall
阶段通过获取用户安装PicGo的路径$INSTDIR
来实现对注册表关键值的写入。自己书写的installer.nsh
默认放在项目的build
目录下,那么electron-builder
在构建Windows
应用的时候将会自动读取这个文件以及package.json
里的配置来生成安装界面。
写入注册表的格式大概是这样:
WriteRegStr <reg-path> <your-reg-path> <attr-name> <value>复制代码
以下是PicGo的installer.nsh
,仅供参考:
!macro customInstallWriteRegStr HKCR "*\shell\PicGo" "" "Upload pictures w&ith PicGo"WriteRegStr HKCR "*\shell\PicGo" "Icon" "$INSTDIR\PicGo.exe"WriteRegStr HKCR "*\shell\PicGo\command" "" '"$INSTDIR\PicGo.exe" "upload" "%1"'!macroend!macro customUninstallDeleteRegKey HKCR "*\shell\PicGo"!macroend复制代码
注意HKCR
即是注册表目录HKEY_CLASSES_ROOT
的缩写。在写value
的时候如果要写多个参数,可以用单引号包起来。attr-name
不写即为默认。相信有了VSCode
的右键菜单注册表说明,你也能看得懂上面的PicGo的脚本了。同时注意我们应该在卸载阶段将之前写的注册表删除,以免用户卸载了应用之后菜单还在,上述脚本的后面部分是是在做这个事情。
因为上一章实现了命令行调用,所以我们的菜单就可以通过'"$INSTDIR\PicGo.exe" "upload" "%1"'
来实现菜单调用命令了。
macOS
macOS的话可以通过实现自动化脚本来生成右键菜单。打开automator
:
然后新建一个快速操作
:
将快速操作的工作流程限制到图像文件
,并且只作用于访达.app
里,同时在左侧菜单里找到shell
组件,将其拖拽到右侧编辑区:
将shell
选择成/bin/bash
,传递输入选成作为自变量
。
然后将默认的内容改成如下(实际上就差不多是之前说的macOS
下如何命令行调用Electron
应用的写法):
/Applications/PicGo.app/Contents/MacOS/PicGo upload "$@" > /dev/null 2>&1 &复制代码
其中macOS的快捷操作里,是通过"$@"
来作为参数传递的。
如何作为右键菜单?只要把你生成的这个workflow文件(夹),放到~/Library/Services
这个目录下就行了。
这样你就在你右键菜单里看到它:
如果你的服务项过多的话,会在服务的二级菜单里看到它:
其中,菜单名就是你生成的这个workflow的文件(夹)名。
那么生成了这个workflow之后,我们如何实现不让用户手动创建,而是自动帮他们放到~/Library/Services
目录下呢?macOS没有Windows那么方便的安装工具脚本语言,那么我们可以在main
进程里手动来实现这个功能。下面是PicGo的beforeOpen.js,其中我们将我们生成的workflow
文件(夹)放到项目的static
目录下。
import fs from 'fs-extra'import path from 'path'import os from 'os'if (process.env.NODE_ENV !== 'development') {global.__static = path.join(__dirname, '/static').replace(/\\/g, '\\\\')}if (process.env.DEBUG_ENV === 'debug') {global.__static = path.join(__dirname, '../../../static').replace(/\\/g, '\\\\')}function beforeOpen () {const dest = `${os.homedir}/Library/Services/Upload pictures with PicGo.workflow`if (fs.existsSync(dest)) { // 判断是否存在return true} else { // 如果不存在就复制过去try {fs.copySync(path.join(__static, 'Upload pictures with PicGo.workflow'), dest)} catch (e) {console.log(e)}}}export default beforeOpen复制代码
然后在主进程里加入这个方法,并判断是否在macOS下运行:
// main/index.jsimport beforeOpen from './utils/beforeOpen'// ...if (process.platform === 'darwin') {beforeOpen()}// ...复制代码
这样用户在安装PicGo之后,打开软件之后,他的右键菜单就多了一个「Upload pictures with PicGo」项了。
小结
至此,一个Electron
应用的命令行调用以及系统级别右键菜单的实现就讲述完了。当然可能还有其他实现的方式,以及更细致的实现(比如还能支持文件夹右键等等)。我在这里也只是一个抛砖引玉,其他的实现或者更好的实现方式需要自己摸索啦。当然本文没有Linux的相关内容,主要是我时间有限并且没有Linux机器,所以也希望有兴趣的朋友自己在Linux下实现了本文的功能后也能跟我说说~
本文很多都是我在开发PicGo
的时候碰到的问题、踩的坑。也许文中简单的几句话背后就是我无数次的查阅和调试。希望这篇文章能够给你的electron-vue
开发带来一些启发。文中相关的代码,你都可以在PicGo和PicGo-Core的项目仓库里找到,欢迎star~如果本文能够给你带来帮助,那么将是我最开心的地方。如果喜欢,欢迎关注我的博客以及本系列文章的后续进展。(PS: 下一篇文章应该会讲述一下如何构建一个Electron应用可扩展的快捷键系统。)
注:文中的图片除未特地说明之外均属于我个人作品,需要转载请私信
参考文献
感谢这些高质量的文章、问题等:
一个还不错的图床工具-PicUploaderPassing command line arguments to electron executable (after installing an already packaged app)Command Line Arguments in Dev ModeElectron app Docs以及没来得及记录的那些好文章,感谢你们!