作者:kmokidd
链接:https://zhuanlan.zhihu.com/p/20225295
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
使用 JavaScript、Node.js 和 Electron 创造专属于你的声效器
JavaScript 桌面应用是什么
即使在移动端和云端大行其道而,桌面端日渐落末的现在,我的心中仍然为桌面应用留有一个特殊的位置。和 Web 应用比起来桌面应用的优点还是很多的:只要把它们放在开始菜单栏或者 dock 上,你就能随时打开它们;还可以通过 alt-tab 或者 cmd-tab 切换应用;和操作系统之间的交互更良好(快捷键,通知栏等)。
我将会在这篇文章中指导构建一个简单的桌面应用。当然,你也将了解到在使用 JavaScript 构建桌面应用的时候要哪些重要的概念。
使用 JavaScript 开发桌面应用意味着在打包(package application)的时候你会需要根据操作系统的不同发出不同的命令。这一行为是将原生桌面应用兼容不同平台的概念抽象出来,方便维护应用。现在,我们可以借助 Electron 或者 NW.js 开发一个桌面应用。其实这两者提供的或多或少差不多的特性,但对于我来说,还是更偏向于 Electron。在做出选择之前,先详细了解它们并考虑各种情况,就不会选错的。
基本假设
开始教程之前,请允许我假设你已经有了一个常用的的编辑器(或者 IDE),系统中也安装了 Node.js 和 npm,并有基础的 HTML/CSS/JavaScript (对 Node.js 的 CommonJS 模块概念有所了解是最好,但不强求) 知识。如果以上知识你并不了解,为了防止这篇文章看到你头昏脑胀,推荐你先看看之前我写过的博文,补充一下基础知识。
万事俱备,现在就把精力集中在学习 Electron 上,不要再担心界面的事情(将会构建的界面本质上就是普通的 Web 页面而已)。
Electron 概览
简而言之,Electron 提供了一个实时构建桌面应用的纯 JavaScript 环境。Electron 可以获取到你定义在 package.json 中 main 文件内容,然后执行它。通过这个文件(通常我们称之为 main.js),可以创建一个应用窗口,这个应用窗口包含一个渲染好的 web 界面,还可以和系统原生的 GUI 交互。
具体来说,就是当你启动了一个 Electron 应用,就有一个主进程(main process )被创建了。这条进程将负责创建出应用的 GUI(也就是应用的窗口),并处理用户与这个 GUI 之间的交互。
但直接启动 main.js 是无法显示应用窗口的,在 main.js 中通过调用BrowserWindow模块才能将使用应用窗口。然后每个浏览器窗口将执行它们各自的渲染器进程( renderer process )。渲染器进程将会处理一个真正的 web 页面(HTML + CSS + JavaScript),将页面渲染到窗口中。鉴于 Electron 使用的是基于 Chrominum 的浏览器内核,你就不太需要考虑兼容的问题。
举个例子,如果你只想做一个计算器,那你的 main process 只会做一件事情:实例化一个窗口,并内置了一个计算器的界面(这个界面是你用 HTML、CSS 和 JavaScript 写的)。
虽然理论上只有 main process 才能和原生 GUI 产生交互,但其实我们可以通过一些手段让 renderer process 与原生 GUI 交互(在后文中你将学习到如何实现)。
main process 可以通过 Electron 中的一些模块直接和原生 GUI 交互。你的桌面应用可以使用任意的 Node 模块,比如用 node-notifier 显示系统通知,用 request 发出 HTTP 请求……
Hello, world!
做好前期准备,现在让我们从 Hello World 开始吧!
使用的 repo
这篇教程是基于一个声效器教程的 github 仓库,请使用下面的命令将它克隆到本地:
git clone https://github.com/bojzi/sound-machine-electron-guide.git
然后查看一下,你可以看看这个仓库中有哪些 tag:
git checkout
我们将跟随这些 tag 将声效器一步步构建出来:
git checkout 00-blank-repository
拉取(checkout)目标 tag 之后,执行:
npm install
这么做能保证项目所依赖的 Node 模块都会被拉取。
如果你无法切换到某一个 tag,最简单的解决方式就是重置仓库,然后再 checkout:
git add -A
git reset --hard
开工
先把 tag 为 ‘00-blank-repository’ 拉取下拉:
git checkout 00-blank-repository
在项目文件夹中创建一个 package.json 文件,并在文件中加入以下内容:
{
“name”: “sound_machine”,
“version”: “0.1.0”,
“main”: “./main.js”,
“scripts”: {
“start”: “electron .”
}
}
这个 package.json 的作用是:
确定应用的名字和版本号,
告诉 Electron main.js 是 main process 的入口,
定义启动口令 - 在 CLI (终端或者命令行)中执行 npm start 即可完成依赖安装。
现在快把 Electron 安装上吧。最简单的安装方式应该是通过 npm 安装预构建好的二进制文件,然后把它作为开发依赖(development dependency)写入 package.json 中(安装时带上 --save-dev 参数即可自动写入依赖)。在 CLI 中进入项目目录,执行下面的命令:
npm install --save-dev electron-prebuilt
预构建的二进制文件会根据操作系统不同而不同的,通过执行 npm start 安装。我们以开发依赖的方式使用它,是因为在项目构建中只有在开发阶段才会使用到 Electron。
以上,就是本次 Electron 教程所需要的全部东西了。
对世界说 Hi
创建一个 app 文件夹,在文件夹中新建 index.html 文件,并写入以下内容:
Hello, world!
在项目的根目录创建 main.js 文件。Electron 主线程的入口是这个 JS 文件,然后 “Hello world!” 页面也通过它显示出来:
‘use strict’;
var app = require(‘app’);
var BrowserWindow = require(‘browser-window’);
var mainWindow = null;
app.on(‘ready’, function() {
mainWindow = new BrowserWindow({
height: 600,
width: 800
});
mainWindow.loadUrl('file://' + __dirname + '/app/index.html');
});
看起来并不难吧?
app 模块控制着应用的生命周期(比如,当应用进入准备状态(ready status)的时候要采取什么行动)。
BrowserWindow 模块控制窗口的创建。
mainWindow 对象就是你的应用窗口的主界面,当 JavaScript 垃圾回收机制被触发时窗口就会被关闭,此时该对象的值是null。
当 app 获取到 ready 事件后,我们通过 BrowserWindow 创建一个 800x600 窗口。
这个 window 的渲染器线程将会渲染 index.html 文件。
执行下面这行代码,看看我们的应用是什么样的:
npm start
现在沐浴在这个 app 的圣光中吧。
开发一个真正的应用
华丽丽的声效器
开始之前,我要问个问题:什么是声效器?
声效器是一个小设备,当你按下不同按键的时候,它会发出不同声音,比如卡通音或者效果音。在办公室里听到这样有趣的声音,好像整个人都明亮起来了呢。用这个例子作为探索如何使用 Electron 是个很棒的主意。
具体来说,我们将会实现以下功能,并涉及到以下知识:
声效器的基础(实例化浏览器窗口),
关闭声效器(主进程和渲染器进程之间的通信),
随时播放声音(全局快捷键),
创建一个快捷修饰键(修饰键,modifier keys, 指的是 Shift、Ctrl 和 Alt 键)设置页面(并将用户设置保存在主目录下),
添加一个托盘图标(创建原生 GUI 元素、了解菜单和托盘图标的使用),
将应用打包到 Mac、Windows 和 Linux 平台。
实现声效器的基本功能
开始构建以及应用的结构
在开发过 “Hello World” 应用之后,现在可以着手制做我们的声效器了。
一个典型的声效器会有很多的按钮,你需要按下那些按钮才能让机器发声,通常会是拟声词(比如笑声、掌声、打碎玻璃的声音等等)。
响应点击 – 这是我们要做的第一件事情。
我们的应用结构非常简单直白。
在应用的根目录中,要有一个 package.json、main.js 和其他全局所需的应用文件。
app/ 目录中要包含 HTML 文件、CSS 目录、JS 目录、wav 目录还有图片目录。
出于简化这个教程的目的,所有和网页设计相关的文件都已经在一开始就放在仓库中了。请在命令行中输入git checkout 01-start-project 获取。现在,请你可以输入以下命令,重置你的仓库并拉取新的 tag:
If you followed along with the “Hello, world!” example:
git add -A
git reset --hard
Follow along with the tag 01-start-project:
git checkout 01-start-project
在本教程中,我们只使用两种声效,后面再找一些别的音效和图标,修改* index.js *就将它们扩展成有16种音效的声效器。
main process 的其他内容
首先找到 main.js 中定义声效器外形的部分,用下面这段替换掉:
‘use strict’;
var app = require(‘app’);
var BrowserWindow = require(‘browser-window’);
var mainWindow = null;
app.on(‘ready’, function() {
mainWindow = new BrowserWindow({
frame: false,
height: 700,
resizable: false,
width: 368
});
mainWindow.loadUrl('file://' + __dirname + '/app/index.html');
});
当窗口被定义了大小,我们也就是在自定义这个窗口,使得它不可拉伸没有框架,让它看起来就像一个真正的声效器浮在桌面上。
现在问题来了 – 要如何移动或者关闭一个没有标题栏的窗口。
很快我就会说到自定义窗口(和应用)的关闭动作,还会谈到如何在主进程和渲染器进程中通信。不过现在让我们先把目光聚焦到“拖拽效果”上。你可以在 app/css 目录下找到 index.css 文件:
html,
body {
…
-webkit-app-region: drag;
…
}
-webkit-app-region: drag;把整个 html 都变成了一个可拖拽的对象。现在问题来了,在可拖拽的对象上你怎么点击啊?!好的,可能你会想到把 html 中某个部分的这个属性值设置为no-drag;,那就允许该元素不可拖拽(但可以点击了)。让我们想想下面这段 index.css 片段:
.button-sound {
…
-webkit-app-region: no-drag;
}
展示声效器
现在通过 main.js 文件可以创建一个新窗口,并在窗口中显示出声效器的界面。如果通过npm start启动应用,你将会看到一个有动态效果的声效器。因为我们就是从一个静态页面开始,所以现在你看到的也是不会动的页面:
将下面这段代码保存到 index.js 文件中(位置在 app/js 目录下),运行后应用后,你会发现可以与声效器交互了:
‘use strict’;
var soundButtons = document.querySelectorAll(‘.button-sound’);
for (var i = 0; i < soundButtons.length; i++) {
var soundButton = soundButtons[i];
var soundName = soundButton.attributes[‘data-sound’].value;
prepareButton(soundButton, soundName);
}
function prepareButton(buttonEl, soundName) {
buttonEl.querySelector(‘span’).style.backgroundImage = ‘url("img/icons/’ + soundName + ‘.png")’;
var audio = new Audio(__dirname + '/wav/' + soundName + '.wav');
buttonEl.addEventListener('click', function () {
audio.currentTime = 0;
audio.play();
});
}
通过上面这段代码,我们:
获取声音按钮,
迭代访问按钮的data-sound属性值,
给每个按钮加上背景图,
通过 HTMLAudioElement 接口给每个按钮都添加一个点击事件,使之可以播放音频,
通过下面这行命令运行你的应用吧:
npm start
通过远程事件从浏览窗口中关闭应用
接着拉取02-basic-sound-machine的内容:
git checkout 02-basic-sound-machine
简单来说 - 应用窗口(渲染器进程)不应该和 GUI 发生交互(也就是不应该和“关闭窗口”有关联),Electron 的官方教程上说了:
考虑到在网页中直接调用原生的 GUI 容易造成资源溢出,这很危险,开发者不能这么使用。如果开发者想要在网页上执行 GUI 操作,必须要通过渲染器进程和主进程的通信实现。
Electron 为主进程和渲染器进程提供了 ipc (跨进程通信)模块,ipc 模块允许接收和发送通信频道的信息。频道由字符串表示(比如“channel-1”,“channel-2”这样),可以用于区分不同的信息接收者。传递的信息中也可以包含数据。根据接收到的信息,订阅者可以做出响应。信息传递的最大好处就是做到分离任务 – 主进程不需要知道是哪些渲染器进程发送了信息。
这正是我们想要做的 – 将主进程(main.js)订阅到“关闭主窗口”频道中,当用户点击关闭按钮时,从渲染器进程(index.js)向该频道发送信息。
Add the following to main.js to subscribe to a channel:
将下面的代码实现了频道订阅,将它添加到 main.js 中:
var ipc = require(‘ipc’);
ipc.on(‘close-main-window’, function () {
app.quit();
});
把 ipc 模块包含进来之后,从频道中订阅信息就非常简单了:过 on() 方法和频道名称,再加上一个回调函数就行了。
要向该频道发送信息,就要把下面的代码加入 index.js 中:
var ipc = require(‘ipc’);
var closeEl = document.querySelector(‘.close’);
closeEl.addEventListener(‘click’, function () {
ipc.send(‘close-main-window’);
});
我们依然需要把 ipc 模块引入到文件中,给关闭按钮绑定点击事件。当点击了关闭按钮时,通过 send() 方法发送一条信息到“关闭主窗口”频道。
不要忘记在在 index.css 中将关闭按钮设置为不可拖拽:
.settings {
…
-webkit-app-region: no-drag;
}
就这样,我们的应用现在可以通过按钮关掉了。ipc 的通信可以通过事件和参数的传递变得很复杂,在后文中会有传递参数的例子。
通过全局快捷键播放声音
拉取03-closable-sound-machine:
git checkout 03-closable-sound-machine
声效器的地基已经打的不错。但是我们还面临着使用性的问题 – 这个应用要始终保持在桌面最前方,且可以被重复点击。
这就是全局快捷键要介入的地方。Electron 提供了全局快捷模块(global shortcut module)允许开发者捕获组合键并做出相应的反应。在 Eelctron 中组合键被称为加速器,它以字符串的形式被记录下(比如 “Ctrl+Shift+1”)。
因为我们想要捕获到原生的 GUI 事件(全局快捷键),并执行应用窗口事件(播放声音),我们将使用 ipc 模块从主进程发送信息到渲染器进程。
在看代码之前,还有两件事情要我们考虑:
全局快捷键会在 app 的 ready 事件被触发后注册(相关代码片段要被包含在 ‘ready’ 中)
通过 ipc 模块从主进程向渲染器进程发送信息,你必须使用窗口对象的引用(类似于createdWindow.webContents.send(‘channel’))。
记住上面的两点了吗?现在让我们来改写* main.js *吧:
var globalShortcut = require(‘global-shortcut’);
app.on(‘ready’, function() {
… // 之前写过的代码
globalShortcut.register('ctrl+shift+1', function () {
mainWindow.webContents.send('global-shortcut', 0);
});
globalShortcut.register('ctrl+shift+2', function () {
mainWindow.webContents.send('global-shortcut', 1);
});
});
首先,要先引入 global-shortcut 模块,当应用进入ready状态之时,我们将会注册两个快捷键 – ‘Ctrl+Shift+1’ 和 ‘Ctrl+Shift+2’。这两个快捷键可以通过不同的参数向“全局快捷键”频道( “global-shortcut”channel)发送信息。通过参数匹配到到底要播放哪种声音,将下面的代码加入 index.js 中:
ipc.on(‘global-shortcut’, function (arg) {
var event = new MouseEvent(‘click’);
soundButtons[arg].dispatchEvent(event);
});
为了保证整个架构足够简单,我们将会用 soundButtons 选择器模拟按钮的点击播放声音。当发送的信息是“1”时,我们将会获取 soundButtons[1] 元素,触发鼠标点击事件(注意:在生产环境中的应用,你需要封装好播放声音的代码,然后执行它)。
在新窗口中通过用户设置配置 modifier keys
下面请拉取04-global-shortcuts-bound:
git checkout 04-global-shortcuts-bound
通常我们会同时运行好多个应用,声效器中设置的快捷键很可能已经被占用了。所以现在要引入一个设置界面,允许用户更改修饰键(modifier keys)的原因(Ctrl、Alt 和 Shift)。
要完成这一个功能,我们需要做下面这些事情:
在主界面上添加设置按钮,
实现一个设置窗口(设置页面上有对应的HTML、CSS 和 JS),
开启和关闭设置窗口,以及更新全局快捷键的 ipc 信息,
从用户的系统中读写存储设置信息的 JSON 文件。
piu~ 以上就是我们要做的。
设置按钮和设置窗口
和关闭主窗口类似,我们将会把事件绑定到设置按钮上,(settings button),在* index.js *中加入发送给频道的信息:
var settingsEl = document.querySelector(‘.settings’);
settingsEl.addEventListener(‘click’, function () {
ipc.send(‘open-settings-window’);
});
当点击了设置按钮,将会有一条信息向“打开设置窗口”这个频道发送。* main.js 可以响应这个事件,并打开一个新窗口,将以下代码加入 main.js *中:
var settingsWindow = null;
ipc.on(‘open-settings-window’, function () {
if (settingsWindow) {
return;
}
settingsWindow = new BrowserWindow({
frame: false,
height: 200,
resizable: false,
width: 200
});
settingsWindow.loadUrl('file://' + __dirname + '/app/settings.html');
settingsWindow.on('closed', function () {
settingsWindow = null;
});
});
这一步和之前的类似,我们将会打开一个新的窗口。唯一的不同点就是,为了防止实例化两个一样的对象,我们将会检查设置窗口是否已经被打开了。
当上述代码成功执行之后,我们需要再添加一个关闭设置窗口的动作。类似的,我们需要向频道中发送一条信息,但这次是从* settings.js 中发送(关闭按钮的事件是在 settings.js 中)。新建 settings.js *文件,并添加以下代码(如果已经有该文件,就直接在原文件中添加):
‘use strict’;
var ipc = require(‘ipc’);
var closeEl = document.querySelector(‘.close’);
closeEl.addEventListener(‘click’, function (e) {
ipc.send(‘close-settings-window’);
});
在 main.js 中监听该频道:
ipc.on(‘close-settings-window’, function () {
if (settingsWindow) {
settingsWindow.close();
}
});
现在,设置窗口已经可以实现我们的逻辑了。
用户设置的读写
执行05-settings-window-working:
git checkout 05-settings-window-working
设置窗口的交互过程是,存储设置信息以及刷新应用:
创建一个 JSON 文件用于读写用户设置,
用这个设置初始化设置窗口,
通过用户的操作更新这个设置文档,
通知主进程要更新设置页面。
我们可以把实现读写设置的部分直接写进 main.js 中,但是如果把这部分独立成模块,可以随处引用这样不是更好吗?
使用 JSON 做配置文件
现在我们要创建一个 configuration.js 文件,再将这个文件引入到项目中。Node.js 使用了 CommonJS 作为编写模块的规范,也就是说你需要将你的 API 和这个 API 中可用的函数都要暴露出来。
为了更简单地读写文件,我们将会使用 nconf 模块,这个模块封装了 JSON 文件的读写。但首先,我们需要将这个模块包含到项目中来:
npm install --save nconf
这行命令意味着 nconf 模块将会作为应用依赖被安装到项目中,当我们要发布应用的时候,这个模块会被一起打包给用户(save-dev 参数会使安装的模块只出现在开发阶段,发布应用的时候不会被包含进去)。
在根目录下创建 configuration.js 文件,它的内容非常简单:
‘use strict’;
var nconf = require(‘nconf’).file({file: getUserHome() + ‘/sound-machine-config.json’});
function saveSettings(settingKey, settingValue) {
nconf.set(settingKey, settingValue);
nconf.save();
}
function readSettings(settingKey) {
nconf.load();
return nconf.get(settingKey);
}
function getUserHome() {
return process.env[(process.platform == ‘win32’) ? ‘USERPROFILE’ : ‘HOME’];
}
module.exports = {
saveSettings: saveSettings,
readSettings: readSettings
};
我们要把文件位置和文件名传 nconf 模块(用 Node.js 的 process.env 获取到文件的位置),具体路径会根据平台而异。
通过 nconf 模块的 set() 和 get() 方法结合文件操作的 save() 和 load(),我们可以实现设置文件的读写操作,然后通过 module.exports 将接口暴露到外部。
初始化默认的快捷键设置
在讲设置交互之前,为了避免用户是第一次打开这个应用,要先初始化一个设置文件。我们将会以数组的形式储存热键,对应的键是 “shortcutKeys”,储存在 main.js 中,我们需要把 configuration 模块包含到项目中:
‘use strict’;
var configuration = require(‘./configuration’);
app.on(‘ready’, function () {
if (!configuration.readSettings(‘shortcutKeys’)) {
configuration.saveSettings(‘shortcutKeys’, [‘ctrl’, ‘shift’]);
}
…
}
我们需要先检测键 ‘shortcutKeys’ 是否已经有对应的值了,如果没有我们需要初始化一个值。
在 main.js 中,我们将重写全局快捷键的注册方法,在之后我们更新设置的时候,会直接调用这个方法。将原来的注册代码改成以下内容:
app.on(‘ready’, function () {
…
setGlobalShortcuts();
}
function setGlobalShortcuts() {
globalShortcut.unregisterAll();
var shortcutKeysSetting = configuration.readSettings('shortcutKeys');
var shortcutPrefix = shortcutKeysSetting.length === 0 ? '' : shortcutKeysSetting.join('+') + '+';
globalShortcut.register(shortcutPrefix + '1', function () {
mainWindow.webContents.send('global-shortcut', 0);
});
globalShortcut.register(shortcutPrefix + '2', function () {
mainWindow.webContents.send('global-shortcut', 1);
});
}
上述方法重置了全局快捷键的值,从设置中读取热键的数组,将它传入加速器兼容字符串(Accelerator-compatible)并注册新键。
设置窗口的交互
回到 settings.js 文件,我们需要绑定点击事件来改变我们的全局快捷键。首先,我们将会遍历复选框,记录下被勾选的选项(从 configuration 模块中读值):
var configuration = require(‘…/configuration.js’);
var modifierCheckboxes = document.querySelectorAll(‘.global-shortcut’);
for (var i = 0; i < modifierCheckboxes.length; i++) {
var shortcutKeys = configuration.readSettings(‘shortcutKeys’);
var modifierKey = modifierCheckboxes[i].attributes[‘data-modifier-key’].value;
modifierCheckboxes[i].checked = shortcutKeys.indexOf(modifierKey) !== -1;
... // Binding of clicks comes here
}
现在我们需要绑定复选框的行为。考虑到设置窗口(和它的渲染器进程)是不允许改变 GUI 绑定的。这说明我们需要从 setting.js 中发送信息(之后会处理这个信息的):
for (var i = 0; i < modifierCheckboxes.length; i++) {
…
modifierCheckboxes[i].addEventListener('click', function (e) {
bindModifierCheckboxes(e);
});
}
function bindModifierCheckboxes(e) {
var shortcutKeys = configuration.readSettings(‘shortcutKeys’);
var modifierKey = e.target.attributes[‘data-modifier-key’].value;
if (shortcutKeys.indexOf(modifierKey) !== -1) {
var shortcutKeyIndex = shortcutKeys.indexOf(modifierKey);
shortcutKeys.splice(shortcutKeyIndex, 1);
}
else {
shortcutKeys.push(modifierKey);
}
configuration.saveSettings('shortcutKeys', shortcutKeys);
ipc.send('set-global-shortcuts');
}
这段代码看起来比较长,但事实上它很简单。我们将会遍历所有的复选框,并绑定点击事件,在每次点击的时候检查设置数组中是否包含有热键。根据检查结果,更改数组,将结果保存到设置中,并向主进程发送信息,更新我们的全局快捷键。
现在的工作就是在 main.js 中将 ipc 信息订阅到“设置全局快捷键”频道,并更新我们的全局快捷键:
ipc.on(‘set-global-shortcuts’, function () {
setGlobalShortcuts();
});
就这么简单,我们的全局快捷键已经可配置了!
菜单中要放什么?
接下来拉取 06-shortcuts-configurable:
git checkout 06-shortcuts-configurable
另一个在桌面应用中的重要概念就是“菜单”,比如右键菜单(点击右键出现的菜单)、托盘菜单(通常会有一个托盘 icon)和应用菜单(在 OS X 中)等等。
在这一节中,我们将会添加一个托盘菜单。我们也将会借此机会尝试在 remote 模块中使用别的进程间的通信方式。
remote 模块从渲染器进程到主进程完成 RPC 类型的调用。将模块引入的时候,这个模块是在主进程中被实例化的,所以它们的方法也会在主进程中被执行。实际开发中,这个行为是在远程地请求 index.js 中的原生 GUI 模块,然后又在 main.js 中调用 GUI 的方法。这么做的话,你需要在 index.js 中将 BrowserWindow 模块引入,然后实例化一个新的浏览器窗口。其实在主进程中有一个同步的调用,实际上是这个调用创建了新的浏览器窗口。
现在让我们来看看要怎么样创建一个菜单,并在渲染器进程中将它绑定到一个托盘图标上。将下面这段代码加入 index.js 中:
var remote = require(‘remote’);
var Tray = remote.require(‘tray’);
var Menu = remote.require(‘menu’);
var path = require(‘path’);
var trayIcon = null;
if (process.platform === ‘darwin’) {
trayIcon = new Tray(path.join(__dirname, ‘img/tray-iconTemplate.png’));
}
else {
trayIcon = new Tray(path.join(__dirname, ‘img/tray-icon-alt.png’));
}
var trayMenuTemplate = [
{
label: ‘Sound machine’,
enabled: false
},
{
label: ‘Settings’,
click: function () {
ipc.send(‘open-settings-window’);
}
},
{
label: ‘Quit’,
click: function () {
ipc.send(‘close-main-window’);
}
}
];
var trayMenu = Menu.buildFromTemplate(trayMenuTemplate);
trayIcon.setContextMenu(trayMenu);
原生的 GUI 模块(菜单和托盘)通过remote模块包含进来比较安全。
OS X 支持图片模板(将图片文件名以 ‘Template’ 结尾,就会被定义成为图片模板),托盘图标可以通过模板来定义,这样我们的图标就会有“暗黑”和“光明”两个主题了。其他的操作系统用正常的图标就行。
在 Electron 中有很多绑定菜单的方法。这里介绍的方法只是创建了一个菜单模板(将菜单项用数组的方式存储),然后通过这个模板创建菜单,托盘 icon 再绑定上这个菜单,就实现了我们的菜单功能。
应用打包
接下来拉取 07-ready-for-packaging:
git checkout 07-ready-for-packaging
如果你做了一个应用结果人们连下载都下载不了,怎么会有人用呢?
通过 electron-packager 你可以将应用打包到全平台。这一步骤在 shell 中就可以完成,将应用打包好以后就能发布了。
它可以作为一个命令行应用或者作为开发应用过程中的一步,构建一个更复杂的开发场景不是这篇文章要谈的内容,不过我们将通过 npm 脚本让应用打包更简单一点。用 electron-packager 打包的命令是这样的:
electron-packager
以上命令:
将目录切换到项目所在路径,
参数 ‘name of project’ 是你的项目名,参数 ‘plateform’ 确定了你要构建哪个平台的应用(Windows、Mac 还是 Linux),
参数 ‘architecture’ 决定了使用 x86 还是 x64 还是两个架构都用,
决定了使用的 Electron 版本。
第一次打包应用需要比较久的时间,因为所有平台的二进制文件都需要下载,之后打包应用会比较快了。
在 Mac 上我是这么做的:
electron-packager ~/Projects/sound-machine SoundMachine --all --version=0.30.2 --out=~/Desktop --overwrite --icon=~/Projects/sound-machine/app/img/app-icon.icns
首先你要将图标的格式转换成 .icns(在 Mac 上)或者 .ico(在 Windows 上),网络上有工具可以把 PNG 做这样的转换(确保下载的图片的扩展名是 .icns 而不是 .hqx)。如果从非 Windows 的系统上打包了 Windows 的应用,你应该需要处理一下路径(Mac 用户可以用 brew,Linux 用户可以用 apt-get)。
每次都要执行这么长的一句命令一点都不合理。所以你可以在 package.json 中添加另一个脚本。首先,将electron-packager 作为 development dependency 安装:
npm install --save-dev electron-packager
然后在 package.json 中添加以下内容:
“scripts”: {
“start”: “electron .”,
“package”: “electron-packager ./ SoundMachine --all --out ~/Desktop/SoundMachine --version 0.30.2 --overwrite --icon=./app/img/app-icon.icns”
}
接着执行:
npm run-script package
打包命令启动了 electron-packager,在当前目录中查看项目,在 Desktop 目录中构建。如果你使用的是 Windows,脚本内容需要一些细微的更新。
声效器目前是 100MB 大小,不要担心,当你压缩它之后,所占空间会减半。
如果你对此还有更大的计划,可以看看 electron-builder,它是根据 electron-packager 构建出的应用打包再做自动安装的处理。
添加其他的特性
现在你可以尝试开发别的功能了。
这里有一些方案,可以启发你的灵感:
应用的使用手册,说明了有那些快捷键和应用作者,
在应用中给使用手册添加一个图标和菜单入口,
构建一个打包脚本,用于快速构建和分发,
使用* node-notifier 添加一个提示系统,告诉用户正在播放哪一个声音,
使用 lodash 让你的代码更加干净、具有更好的扩展性,
在打包之前不要忘了压缩你的 CSS 和 JavaScript,
结合上文提到的 node-notifier *和一个服务器端的调用,通知用户是否需要更新版本……
还有一个值得一试的东西 – 将代码中关于浏览器窗口的逻辑抽离出来,通过类似 browserify 的工具创建一个和声效器一样的网页。一份代码,两个产品(桌面端和 Web 引用)。酷毙了!
更深入研究 Electron
我们只是尝试了 Electron 的冰山一角,想要知道监控主机的电源情况、获取当前窗口的信息(比如光标的位置)等,Eletron 都能帮你做到。
对于所有的内置工具(通常在开发 Electron 应用时使用),查看 Electron API 文档。
这些文档在 Electron 的 github 仓库中都能找到。
Sindre Sorhus 正在维护一份 Electron 资源清单,在那个上面你可以看到很多非常酷的项目,还能了解到一些系统架构做的很好的 Electron 应用,这些都能给你的开发带来灵感。
Electron 是基于 io.js 的,大部分 Node.js 模块都可以兼容,可以使用它们扩展你的应用。去 npmjs.com 上看看有没有合适的。
这样就够了吗?
当然不。
现在,可以来构建一个更大型的应用了。在这篇文章中,我几乎没有说到如何使用外部的库或者构建工具来构建一个应用,不过用 ES6 和 Typescript 的语法结合 Angular 和 React 来构建 Electron 应用也很简单,还可以用 gulp 或 grunt 构建流程。
干嘛不用你最喜欢的语言,框架和工具,来试试构建一个 Filckr 同步工具(借助 Filckr API 和 node-filckrapi)或者一个 Gmail 客户端(使用 Google 的官方 Node.JS 客户端库?)
选一个自己感兴趣的项目,开工吧!
原文链接:Building a desktop application with Electron