背景
以前用迅雷下载大片时,只需要复制torrent
种子立即唤醒迅雷应用下载,特别好用。那么web
是如何唤醒桌面应用的呢?
本文将讲述Windows
和Mac
如何唤醒Electron
应用。
Electron 基础架构
Electron
是一个可以用 JavaScript、HTML
和 CSS
构建桌面应用程序的库。这些应用程序能打包到 Mac、Windows
和 Linux
系统上运行,也能上架到 Mac
和 Windows
的 App Store
。
Electron
结合了 Chromium、Node.js
以及 操作系统本地的 API
(如打开文件窗口、通知、图标等)。
Electron
与 Chromium
在架构上很相似。Chromium
运行时有一个 Browser Process
,以及一个或者多个 Renderer Process
。
Renderer Process
顾名思义负责渲染Web页面。Browser Process
则负责管理各个 Renderer Process
以及其他部分(比如菜单栏,收藏夹等等),如下图:
在 Electron
中,结构仍然类似,不过这里是一个 Main Process
管理多个 Renderer Process
。
而且在 Renderer Process
可以使用 Node.js
的 API
,这就赋予来 Electron
极大的能力,以下是主进程以及渲染进程可以访问到的API
:
如何将 Chromium 与 Node 整合
Electron
最让人兴奋的地方在于 Chromium
与 Node
的整合。通俗的讲,我们可以在 Chromium
的控制台上做任何 Node
可以做的事。
能够做这个整合,首先得益于 Chromium
和 Node.js
都是基于 v8
引擎来执行 js
的,所以给了一种可能,他们是可以一起工作的。
但是有一个问题,Chromium
和 Node.js
的事件循环机制不同。我们知道,Node.js
是基于 libuv
的,Chromium
也有一套自己的事件循环方式,要让他们一起工作,就必须整合这两个事件循环机制。
如上图所示,Electron
采用了这样一种方式,它起了一个新的线程轮询 libuv
中的 backend fd
,从而监听 Node.js
中的事件,一旦发现有新的事件发生,就会立即把它 post
到 Chromium
的事件循环中,唤醒主线程处理这个事件。
Electron 与 NW.js 的对比以及区别
和 Electron
同样出名的跨平台桌面应用开源库还有 NW.js
。他们都有非常出名的应用,例如用Electron
开发的有 vscode
,用 NW.js
开发的有钉钉。
Electron
的原名叫 Atom Shell
,NW.js
的原名叫 node-webkit
;他们起初是同一个作者开发,而且这个这个作者是国人,先向大佬致敬,为我们开源这么优秀的开源工具。后来种种原因分为两个产品,一个命名为 NW.js
(英特尔公司提供技术支持)、 另一命名为 Electron
(Github
公司提供技术支持)。
(1)两者在GitHub
上的数据对比
nw.js (36.7k star, 4051 commits, 256 releases, 748 open issues, 5862 closed)
electron (81.7k star, 23364 commits, 849 releases, 1047 open issues, 11612 closed)
可以看出 `Electron` 更加活跃。
(2)两者程序的入口不同
在 `NW.js` 中,应用的主入口是网页或者`JS`脚本。 你需要在 `package.json` 中指定一个`html`或者`js`文件,一旦应用的主窗口(在`html`作为主入口点的情况下)或脚本被执行,应用就会在浏览器窗口打开。
在 `Electron` 中,入口是一个 `JavaScript` 脚本。 不同于直接提供一个`URL`,你需要手动创建一个浏览器窗口,然后通过 `API` 加载 `HTML` 文件。 你还可以监听窗口事件,决定何时让应用退出。
`Electron` 的工作方式更像 `Node.js `运行时 ,`Electron` 的` APIs `更加底层。
(3)Node 集成
在 NW.js
,网页中的 Node
集成需要通过给 Chromium
打补丁来实现。但在 Electron
中,我们选择了另一种方式:通过各个平台的消息循环与 libuv
的循环集成,避免了直接在 Chromium
上做改动。这就意味着 Electron
迭代的成本更低。
搭建一个最简单的 Electron
初始化项目
Electron
应用程序遵循与其他 Node.js
项目相同的结构。 首先创建一个文件夹并初始化 npm
包。
mkdir my-electron-app && cd my-electron-app
npm init
init
初始化命令会提示您在项目初始化配置中设置一些值 为本教程的目的,有几条规则需要遵循:
entry point
应为main.js
(其他命名也可以,对应主文件即可).author
与description
可为任意值,但对于应用打包是必填项。
你的 package.json
文件应该像这样:
{
"name": "my-electron-app",
"version": "1.0.0",
"description": "Hello World!",
"main": "main.js",
"author": "Jane Doe",
"license": "MIT"
}
然后,将 electron
包安装到应用的开发依赖中。
最后,您希望能够执行 Electron
如下所示,在您的 package.json
配置文件中的scripts
字段下增加一条start
命令:
{
"scripts": {
"start": "electron ."
}
}
start
命令能让您在开发模式下打开您的应用
注意:此脚本命令将告诉 Electron
在您项目根目录运行,如果目录不对您的应用将立即抛出一个错误提示您它无法找到要运行的应用
任何 Electron
应用程序的入口都是 main
文件。 这个文件控制了主进程,它运行在一个完整的Node.js
环境中,负责控制您应用的生命周期,显示原生界面,执行特殊操作并管理渲染器进程
执行期间,Electron
将依据应用中 package.json
配置下main
字段中配置的值查找此文件
要初始化这个main
文件,需要在您项目的根目录下创建一个名为main.js
的空文件。
创建页面
在可以为我们的应用创建窗口前,我们需要先创建加载进该窗口的内容。 在 Electron
中,每个窗口中无论是本地的HTML
文件还是远程URL
都可以被加载显示。
此处将采用本地HTML
的方式。 在您的项目根目录下创建一个名为index.html
的文件:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
We are using Node.js <span id="node-version"></span>,
Chromium <span id="chrome-version"></span>,
and Electron <span id="electron-version"></span>.
</body>
</html>
主文件创建界面
现在您有了一个页面,将它加载进应用窗口中。 要做到这一点,你需要两个Electron
模块:
(1)app
模块,它控制应用程序的事件生命周期。
(2)BrowserWindow
模块,它创建和管理应用程序 窗口。
因为主进程运行着Node.js
,您可以在文件头部将他们导入作为公共JS
模块:
const { app, BrowserWindow } = require('electron')
然后,添加一个createWindow()
方法来将index.html
加载进一个新的BrowserWindow
实例。
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600
})
win.loadFile('index.html')
}
接着,调用createWindow()
函数来打开您的窗口。
在 Electron
中,只有在 app
模块的 ready
事件被激发后才能创建浏览器窗口。 您可以通过使用 app.whenReady() API
来监听此事件。 在whenReady()
成功后调用createWindow()
。
app.whenReady().then(() => {
createWindow()
})
此时,您的电子应用程序应当成功 打开显示您页面的窗口!
管理窗口的生命周期
虽然你现在可以打开一个浏览器窗口,但你还需要一些额外的模板代码使其看起来更像是各平台原生的。 应用程序窗口在每个OS
下有不同的行为,Electron
将在app
中实现这些约定的责任交给开发者们。
一般而言,你可以使用 进程 全局的 platform
属性来专门为某些操作系统运行代码。
关闭所有窗口时退出应用 (Windows & Linux)
在Windows
和Linux
上,关闭所有窗口通常会完全退出一个应用程序。
为了实现这一点,监听 app
模块的 'window-all-closed'
事件,并在用户不是在 macOS (darwin)
上运行时调用 app.quit()
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
如果没有窗口打开则打开一个窗口 (macOS)
当 Linux
和 Windows
应用在没有窗口打开时退出了,macOS
应用通常即使在没有打开任何窗口的情况下也继续运行,并且在没有窗口可用的情况下激活应用时会打开新的窗口。
为了实现这一特性,监听 app
模块的 activate
事件,并在没有浏览器窗口打开的情况下调用你仅存的 createWindow()
方法。
因为窗口无法在 ready
事件前创建,你应当在你的应用初始化后仅监听 activate
事件。 通过在您现有的 whenReady()
回调中附上您的事件监听器来完成这个操作。
app.whenReady().then(() => {
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
此时,您的窗口控件应功能齐全!
通过预加载脚本从渲染器访问Node.js
现在,最后要做的是输出Electron
的版本号和它的依赖项到你的web
页面上。
在主进程通过Node
的全局 process
对象访问这个信息是微不足道的。 然而,你不能直接在主进程中编辑DOM
,因为它无法访问渲染器 文档 上下文。 它们存在于完全不同的进程!
这是将 预加载 脚本连接到渲染器时派上用场的地方。 预加载脚本在渲染器进程加载之前加载,并有权访问两个 渲染器全局 (例如 window
和 document
) 和 Node.js
环境。
创建一个名为 preload.js 的新脚本如下:
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const dependency of ['chrome', 'node', 'electron']) {
replaceText(`${dependency}-version`, process.versions[dependency])
}
})
上面的代码访问 Node.js process.versions
对象,并运行一个基本的 replaceText
辅助函数将版本号插入到 HTML
文档中。
要将此脚本附加到渲染器流程,请在你现有的 BrowserWindow
构造器中将路径中的预加载脚本传入 webPreferences.preload
选项。
// 在文件头部引入 Node.js 中的 path 模块
const path = require('path')
// 修改现有的 createWindow() 函数
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
}
// ...
额外:将功能添加到您的网页内容
此刻,您可能想知道如何为您的应用程序添加更多功能。
对于与您的网页内容的任何交互,您想要将脚本添加到您的渲染器进程中。 由于渲染器运行在正常的 Web
环境中,因此您可以在 index.html
文件关闭 </body>
标签之前添加一个 <script>
标签,来包括您想要的任意脚本:
<script src="./renderer.js"></script>
renderer.js
中包含的代码接下来可以使用与前端开发相同的 JavaScript API
和工具,例如使用 webpack
打包并最小化您的代码或 React
来管理您的用户界面。
打包
1、添加Electron Forge
作为应用程序的开发依赖项,并使用其import
命令设置Forge
的脚手架:
npm install --save-dev @electron-forge/cli
npx electron-forge import
✔ Checking your system
✔ Initializing Git Repository
✔ Writing modified package.json file
✔ Installing dependencies
✔ Writing modified package.json file
✔ Fixing .gitignore
We have ATTEMPTED to convert your app to be in a format that electron-forge understands.
Thanks for using "electron-forge"!!!
2、使用Forge
的package
命令打包
npm run package
$ electron-forge package
✔ Checking your system
✔ Preparing to Package Application for arch: x64
✔ Preparing native dependencies
✔ Packaging Application
Done in 37.24s.
Electron Forge
创建包所在的out
文件夹:
// Example for Windows
out/
├── ...
└── out/my-electron-app-win32-x64/my-electron-app.exe
完成上述步骤后,您应该有一个功能齐全的应用程序,如下所示:
注册伪协议
在浏览器调起 electron
应用是基于自定义的伪协议(URL Scheme
协议),URL Scheme
协议可以理解为用于实现在浏览器跳转应用的协议。Windows
系统与Mac
系统注册伪协议方式不尽相同
Windows
在 windows
下当注册协议的时候, 会将应用程序的协议写入到 windows
中的注册表中,往注册表的 HKCR (HKEY_CALSSES_ROOT)
目录下添加一条记录来完成该协议的注册。
示例:在注册表的HKEY_CALSSES_ROOT
目录下,添加一个electron
字段
通过这种方法写入的协议, 需要在打包才能正常唤醒, 在开发环境下不能调起本地应用。因为开发环境下调用 app.setAsDefaultProtocolClient
注册协议时应用路径会指向当前项目的node_modules\electron\dist\xxx.exe
Mac
在 Mac
上面,我们是要将协议写入到mac
应用中的 info.plist
文件中,每个打包好的app
文件中都会存在一个info.plist
文件,Info.plist
文件的位置:
在 finder
中右键图标, 点击 显示包内容 在出现的 Contents
文件夹中:
electron-builder配置注册协议
方式一
如果你是用electron-builder
打包,可在package.json
配置中照如下写入:
通过 extendInfo
中添加数组, 数组中的 值将会被写入 Info.plist
文件中:
方式二
"build": {
"win": {
"target": [
"nsis" // 打包成一个独立的 exe 安装程序
],
"signingHashAlgorithms": [
"sha1",
"sha256"
],
},
"nsis": {
"oneClick": false, // 是否一键安装
"perMachine": true, // 安装的时候是否为所有用户安装
"allowToChangeInstallationDirectory": true, // 是否允许用户改变安装目录, 默认为false
"deleteAppDataOnUninstall": true, // 是否在卸载时删除应用程序数据
// 指定要包含 nsis 的脚本,基于内置的nsis脚本进一步扩展
// for win - 将协议写入主机的脚本
"include": "script/urlProtoco.nsh"
},
"protocols": [ // for macOS - 用于在主机注册指定协议
{
"name": "your-protocol-name",
"schemes": [
"your-protocol-name"
]
}
]
},
for win
- 将协议写入主机的脚本
// urlProtoco.nsh
!macro customInstall
DetailPrint "Register your-protocol-name URI Handler"
DeleteRegKey HKCR "your-protocol-name"
WriteRegStr HKCR "your-protocol-name" "" "URL:your-protocol-name"
WriteRegStr HKCR "your-protocol-name" "URL Protocol" ""
WriteRegStr HKCR "your-protocol-name\shell" "" ""
WriteRegStr HKCR "your-protocol-name\shell\Open" "" ""
WriteRegStr HKCR "your-protocol-name\shell\Open\command" "" "$INSTDIR\${APP_EXECUTABLE_FILENAME} %1"
!macroend
setAsDefaultProtocolClient
app.setAsDefaultProtocolClient(protocol[, path, args])
protocol String
- 协议的名称, 不包含 ://。 例如,如果你希望应用处理electron://
链接,请以electron
为参数调用此方法。path String
(可选) (Windows
) -Electron
可执行文件路径。 默认为process.execPath
args String
(Windows
) - 传递给可执行文件的参数。 默认为空数组。
返回Boolean
- 是否成功调用。
将当前可执行文件的设置为协议(也就是 URI scheme
) 的默认处理程序。 该方法允许你将应用更深入地集成到操作系统中。 一旦注册了,所有 your-protocol://
开头的链接将使用当前可执行文件打开。 整个链接,包括协议部分,将作为参数传递给你的应用程序。
注意: 在 macOS
上,您只能注册已添加到应用程序的 info.plist
中的协议,这个列表在运行时不能修改。 然而,你可以在构建时通过 Electron Forge, Electron Packager
, 或通过文本编辑器编辑info.plist
文件的方式修改。 有关详细信息,请参阅 Apple's documentation
注意: 在 Windows Store
环境下(当打包为 appx
),此 API
对所有调用都返回 true
,但它设置的注册表项将无法通过其他应用程序访问。 为了注册你的 Windows Store
应用程序作为默认的协议处理程序,你必须 在你的清单中声明协议。
我们在main.js
中加入如下代码:
const PROTOCOL = 'electron';
app.setAsDefaultProtocolClient(PROTOCOL) // 注册协议
网页唤醒electron应用
web
端来唤醒本地Electron
应用思路:
(1)安装electron
桌面应用,会触发app.setAsDefaultProtocolClient
注册协议,例如 protocol
(2)macOS
系统通过监听 open-url
事件获取协议链接;windows
通过监听 second-instance
事件获取协议链接
(3)web
端来唤醒本地Electron
应用,主线程获取到唤醒参数后传递给渲染线程
单实例运行
首先,每次打开一个协议 URL
,系统都会启动一个新的应用。这就需要应用自己去判断,把 URL
当做参数传给已有的应用,还是自己直接处理。
Electron
提供了一个简单的方法,来获取一个锁,只有第一个调用的实例才能获取成功,后面的其他实例则把参数传过去,然后退出就可以了。
const { app } = require('electron');
// 获取单实例锁
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
// 如果获取失败,说明已经有实例在运行了,直接退出
app.quit();
}
Mac系统获取唤醒参数
在 macOS
系统上面, 通过监听 open-url
方法可以获取到我们打开应用程序上面的链接
app.on('open-url', (event, url) => {
// ...
});
windows系统获取唤醒参数
window
下通过URL Schemes
协议调起应用时分两种情况
- 应用处于打开状态,会触发
second-instance
事件并接收这个URL
。 - 应用处于未打开状态,在网页端通过浏览器调起应用之后不会触发
second-instance
事件;这个时候需要主动判断应用是否是从网页端调起,并主动触发second-instance
事件;
在window
里面判断是否是从网页端的标准:如果是通过url schema
启动,其启动参数会超过1个
以下为在应用 ready
之后,判断是否是在 window
下主动调起客户端,并主动触发 second-instance
事件的实现逻辑;
// 当应用启动完成后,主动判断应用是否是从网页中调起
const _handleAfterReady = () => {
// windows如果是通过url schema启动则发出时间处理
// 启动参数超过1个才可能是通过url schema启动
if (process.argv.length > 1) {
if (!app.isReady()) {
app.once("browser-window-created", () => {
// app 未打开时,通过 open-url打开 app,此时可能还没 ready,需要延迟发送事件
// 此段ready延迟无法触发 service/app/ open-url 处理,因为saga初始化需要时间
app.emit("second-instance", null, process.argv);
});
} else {
app.emit("second-instance", null, process.argv);
}
}
};
触发 second-instance
事件之后的具体逻辑与 mac
类似,不过针对获取到的参数的处理方式还是存在一些差异
app.on('second-instance', async (event, argv) => {
// Windows 下通过协议URL启动时,URL会作为参数,所以需要在这个事件里处理
if (process.platform === 'win32') {
// ...
}
});
web唤醒应用
const src = `ksxm://conversation/route/${storeId || ''}?paramsId=${inputParamsId || paramsId}&code=${code}`;
const iframe = document.createElement('iframe');
iframe.src = src;
iframe.style.display = 'none';
document.body.appendChild(iframe);
// 或者
window.location.href = `ksxm://conversation/route/${storeId || ''}?paramsId=${inputParamsId || paramsId}&code=${code}`;
渲染进程获取唤醒参数
唤醒electron
应用时可能会携带一些参数,如何在渲染进程中获取这些参数呢?这里利用主线程与渲染线程的通讯方式解决
通过BrowserWindow
实例中的webContents
向渲染进程发送消息,渲染进程监听获取消息
发送消息
// windows
app.on('second-instance', async (event, argv) => {
// Windows 下通过协议URL启动时,URL会作为参数,所以需要在这个事件里处理
if (process.platform === 'win32') {
/** 向渲染进程传递唤醒参数 */
win.webContents.send('awaken', { awakenArgs: decodeURI(argv) })
}
});
// Mac
app.on('open-url', (event, url) => {
/** 向渲染进程传递唤醒参数 */
win.webContents.send('awaken', { awakenArgs: decodeURI(url) })
});
收到消息
// 如果是原生js写法,在预加载脚本获取参数,不写在html是因为浏览器require,当然你写构建脚本处理
// preload.js
const { ipcRenderer } = require('electron')
ipcRenderer.on('awaken', (event, params) => {
console.log('params', params)
})
// 如果是集成React
const { ipcRenderer } = require('electron')
const Demo = () => {
useEffect(() => {
ipcRenderer.on('awaken', (event, params) => {
console.log('params', params)
})
})
return <></>
}
附上项目地址:https://github.com/Revelation...
唤醒electron应用免登录流
假设有这么一种场景,用户A
在电商平台上看准了商家B
挂的货物,但希望在价格方面跟商家B沟通一下,现不考虑手机、微信以及其他的一些通讯方式,用户A
只能通过电商平台提供的通讯桌面应用跟商家B
沟通,现在的问题是用户A
触发唤醒通讯桌面应用跟商家B
沟通时需要登录,这个登录环节特别的多余,使人体验不好。
其实可以这样,用户A在触发唤醒时向网关服务授权层请求授权,拿到临时凭证code
,将code
作为参数加入到唤醒协议链接中,桌面应用客户端收到code
便向服务端请求token
,拿到登录状态token
后就可以免登录,请求受保护资源
大致流程如下:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。