Electron使用介绍

[转载自http://get.ftqq.com/7870.get]

<html lang="zh-cmn-Hans">

<head>

<meta charset="utf-8">

<meta property="wb:webmaster" content="6fb2b4de5a6b35cc" />

<link rel="icon"

type="image/png"

href="/static/image/get32px.png">

<title>用Electron开发桌面应用 | @Get社区</title>

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<link href="/static/css/bootstrap.min.css" rel="stylesheet">

<link rel="stylesheet" href="/static/css/app.css?v=2014" type="text/css" >

<link rel="stylesheet" href="/static/css/jdc.icon.css?v=2014" type="text/css" >

<link rel="stylesheet" href="/static/css/jquery.jscrollpane.css" type="text/css" >

<link rel="stylesheet" href="/static/css/../components/jquery.atwho/dist/css/jquery.atwho.min.css" type="text/css" >

<link rel="stylesheet" href="http://cdn.staticfile.org/highlight.js/9.0.0/styles/solarized-light.min.css" type="text/css" >

<link rel="stylesheet" type="text/css" href="/static/css/yue.css">

<link rel="stylesheet" type="text/css" href="/static/css/article-btn.css">

<link rel="stylesheet" type="text/css" href="/static/css/imgshare.css">

<script src="http://cdn.staticfile.org/jquery/2.2.1/jquery.min.js"></script>

<script src="/static/script/bootstrap.min.js"></script>

<script src="/static/script/jquery.jscrollpane.min.js"></script>

<script src="/static/script/jquery.mousewheel.js"></script>

<script src="/static/script/../components/notifyjs/dist/notify-combined.min.js"></script>

<script src="/static/script/../components/jquery.onoff/dist/jquery.onoff.min.js"></script>

<script src="/static/script/../components/jquery.atwho/dist/js/jquery.atwho.min.js"></script>

<script src="/static/script/../components/Caret.js/dist/jquery.caret.min.js"></script>

<script src="/static/script/../components/jquery-waypoints/waypoints.min.js"></script>

<script src="http://cdn.staticfile.org/highlight.js/9.0.0/highlight.min.js"></script>

<script src="/static/script/app.js"></script>

<script type="text/javascript" src="/static/script/jquery.imgshare.js" ></script>

<script type="text/javascript" src="/static/script/timeago.js" ></script>

<script type="text/javascript" src="/static/script/locales/timeago.zh-cn.js" ></script>

<script>

window.zhuge = window.zhuge || [];

window.zhuge.methods = "_init debug identify track trackLink trackForm page".split(" ");

window.zhuge.factory = function(b) {

return function() {

var a = Array.prototype.slice.call(arguments);

a.unshift(b);

window.zhuge.push(a);

return window.zhuge

}

};

for (var i = 0; i < window.zhuge.methods.length; i++) {

var key = window.zhuge.methods[i];

window.zhuge[key] = window.zhuge.factory(key)

}

window.zhuge.load = function(b, x) {

if (!document.getElementById("zhuge-js")) {

var a = document.createElement("script");

var verDate = new Date();

var verStr = verDate.getFullYear().toString()

  • verDate.getMonth().toString() + verDate.getDate().toString();

a.type = "text/javascript";

a.id = "zhuge-js";

a.async = !0;

a.src = (location.protocol == 'http:' ? "http://sdk.zhugeio.com/zhuge-lastest.min.js?v=" : 'https://zgsdk.zhugeio.com/zhuge-lastest.min.js?v=') + verStr;

var c = document.getElementsByTagName("script")[0];

c.parentNode.insertBefore(a, c);

window.zhuge._init(b, x)

}

};

window.zhuge.load('e1189bf28dd14a39a2f72b3a47abc3b7');

</script>

</head>

<body id="body">

<div class="navbar navbar-fixed-top navbar-get" role="navigation" id="theheader">

<div class="container clearfix">

<a class="logo" href="/?c=default">

<span class="get">Get</span>社区</a> <div class="new_message_notice"></div>

<div class="navbar-collapse collapse pull-right">

<ul class="get-cate-nav">

<li class="dropdown active ">

<button class="btn btn-getnar dropdown-toggle" type="button" onclick="location='/?c=default'">

新知

</button>

<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu1">

<li class=""><a href="/">全部</a></li>

<li class="" role="presentation"><a href="/?cate=1" role="menuitem" tabindex="-1">技术</a></li>

<li class="" role="presentation"><a href="/?cate=2" role="menuitem" tabindex="-1">设计</a></li>

<li class="" role="presentation"><a href="/?cate=3" role="menuitem" tabindex="-1">产品</a></li>

<li class="" role="presentation"><a href="/?cate=4" role="menuitem" tabindex="-1">创业</a></li>

<li class=""><a href="/?a=kb">我的</a></li>

<li class=""><a href="/?a=feed">我关注的</a></li>

<li class="" ><a href="/?a=submit" target="_blank">+添加新知</a></li>

</ul>

</li>

<li class="dropdown ">

<button class="btn btn-getnar dropdown-toggle" type="button" onclick="location='/?c=card'">

卡片流

</button>

<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu1">

<li class=""><a href="/?c=card">我的首页</a></li>

<li class=""><a href="/?c=card&a=explore">随便看看</a></li>

</ul>

</li>

</ul>

</div>

</div>

</div>

<div class="container">

<div class="row row-offcanvas row-offcanvas-right">

<div class="col-xs-12 col-sm-9">

<div class="get-article-area " id="gacontent">

<h2 class="green">用Electron开发桌面应用</h2>

<p class="exp">用JavaScript,Node.js和Eletron创建发声器应用的详细指南</p>

<div class="yue top20">

<blockquote><p>本文主要介绍了如何用Electron配合JavaScript等web技术创建桌面应用。
</p><p>由 <a href="http://get.jobdeer.com/5571.user" target="_blank">Bazzzinga威</a> 同学翻译自 Medium。<a href="https://medium.com/developers-writing/building-a-desktop-application-with-electron-204203eeb658">https://medium.com/developers-writing/building-a-desktop-application-with-electron-204203eeb658</a>
</p></blockquote><h1>什么是JavaScript桌面应用</h1><p>在我心中,桌面应用一直占据着一个特殊的地位。随着浏览器,移动设备变得越来越强大,被移动和web应用取代的桌面应用呈稳定下滑趋势。但编写桌面应用还是有很多优势的--它们会一直存在于你的开始目录或者Dock栏中,可以被alt(cmd)+tab来回切换,并且大部分比web应用与底层系统联系的更紧密(快捷键,通知推送等)。</p><p>本文中,我会引导你用JavaScript创建一个简单桌面应用,接触相关的概念。</p><p>

</p><p>用JavaScript编写桌面应用的核心思想是编写一套资料库,把它分别打包来兼容各个操作系统。不需要创建原生桌面应用的知识,维护起来更简单。现在,用JavaScript开发桌面应用主要是使用<a href="http://electron.atom.io/" target="_blank">Electron</a>或者<a href="http://nwjs.io/" target="_blank">NW.js</a>。尽管两种工具提供的功能相似,我更喜欢Electron,因为它有一些我认为<a href="http://www.example.com" target="_blank">很重要的优势</a>。到头来,你用哪一个都没有问题。</p><p></p><h1><b>基本假定</b></h1><p>我假定你已经有了文本编辑器(或者IDE),并且安装了<a href="https://nodejs.org/download/" target="_blank">Node.js和npm</a>。同样假定你已经掌握HTML/CSS/JavaScript的知识(如果会Node.js和CommonJS模块更好,不过并不是必需的),这样可以将重点放在学习Electron,而不需要担心创建用户界面(其实就是普通的web页面)。如果你不符合前面几点,你也许会感到有些迷惑,我推荐你看下<a href="https://medium.com/@bojzi/overview-of-the-.-ecosystem-8ec4a0b7a7be" target="_blank">我的前一篇文章</a>来学习基础。</p><p></p><p></p><h1><b>Electron 概述</b></h1><p>简单来说,Electron为用纯JavaScript创建桌面应用提供了运行时。原理是,Electron调用你在package.json中定义的main文件并执行它。main文件(通常被命名为main.js)会创建一个内含渲染完的web页面的应用窗口,并添加与你操作系统的原生GUI(图形界面)交互的功能。</p><p>详细地说,当用Electron启动一个应用,会创建一个主进程。这个主进程负责与你系统原生的GUI进行交互并为你的应用创建GUI(在你的应用窗口)。</p><p></p><p>
</p><p></p><p>仅启动主进程并不能给你的应用用户创建应用窗口。窗口是通过main文件里的主进程调用叫BrowserWindow的模块创建的。每个浏览器窗口会运行自己的渲染进程。渲染进程会在窗口中渲染出web页面(引用了CSS,JavaScript,图片等的HTML文件)。web页面是<a href="https://www.chromium.org/" target="_blank">Chromium</a>渲染的,因为各系统下标准是统一的的,所以兼容性很好。</p><p>举例来说,如果你有一个计算器应用,主进程会初始化一个窗口来呈现实际的web页面(计算器)。</p><p></p><p></p><p>虽说只有主进程才和系统原生GUI交互,还是有技术可以把部分任务转到渲染进程中运行。</p><p>主进程通过一套<a href="https://github.com/atom/electron/tree/master/docs/api" target="_blank">可直接调用的Electron模块</a>与原生GUI交互,桌面应用可以使用所有的Node模块,如用<a href="https://github.com/mikaelbr/node-notifier" target="_blank">node-notifier模块</a>来推送系统通知,<a href="https://www.npmjs.com/package/request" target="_blank">request模块</a>来发起HTTP请求等。</p>
<h1>Hello, world!</h1><h2><b>练习用资料库</b>
</h2><p>现在,让我们做好准备,用传统的「Hello,World」来开始。</p><p>本指南的同步练习资料库是<a href="https://github.com/bojzi/sound-machine-electron-guide" target="_blank">sound-machine-tutorial</a>。首先把资料库clone下来:</p><p></p><pre class="">git clone <a href="https://github.com/bojzi/sound-machine-electron-guide.git" rel="nofollow">https://github.com/bojzi/sound-machine-electron-guide.git</a></pre><p>进入sound-machine-tutorial文件夹,用下面的代码在git的tag之间切换:</p><pre class="">git checkout <tag-name></pre><p>我会提示你该用哪个tag:</p><pre class="">请切换至:
git checkout 00-blank-repository</pre><p>当你clone完代码,切换到你想要的tag,运行:</p><pre class="">npm install</pre><p></p><p>这样你安装好全部Node模块了。</p><p>如果你不能转换到另一个tag,最简单的办法是重置你的资料库状态再切换:</p><p></p><pre class="">git add -A
git reset --hard</pre><p><b>开始</b></p><pre class="">请切换到00-blank-repository这个tag:
git checkout 00-blank-repository</pre><p>在项目文件夹中新建package.json文件,写入下面的内容:</p><p></p><p>这个简单的package.json文件:</p><ul><li>设置应用的名字和版本号,</li><li>设置Electron主进程运行的脚本(main.js),</li><li>设置一个很实用的快捷键--在你的CLI(命令行)中可以用「npm start」方便地启动应用。</li></ul><p>现在该安装Electron了,最简单的方式是用npm为你的操作系统安装预构建的二进制文件。并在package.json文件中将它设置为开发依赖(用--save-dev命令后缀自动设置)。在CLI中运行命令:</p><pre class="">npm install --save-dev electron-prebuilt</pre><p></p><p>预构建的二进制文件是为所在的操作系统量身订造的,可以运行「npm start」。我们将它作为开发依赖安装是因为只在开发过程中用到它。</p><p></p><p>就这样,Electron开发所需的一切都准备好了。</p><h2><b>跟世界打个招呼</b></h2><p>新建app文件夹,在其中新建有下面代码的index.html文件:</p><pre class=""><h1>Hello, world!</h1></pre><p></p><p>在项目的根目录下新建一个main.js文件。Electron的主进程将用它来启动并创建「Hello, world」桌面应用。main.js中的代码:</p><pre class="">'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');

});

</pre><p></p><p>没什么吓人的,不是吗?</p><p>「app」模块会控制应用的生命周期(例如, 对应用的ready状态做出反应)。</p><p>「BrowserWindow」模块为你创建窗口。</p><p>「mainWindow」对象是你应用的主窗口,被声明成null,否则当JavaScript垃圾回收掉这个对象时,窗口会被关闭。</p><p>当「app」捕获ready事件,「BrowserWindow」创建一个800*600大小的窗口。</p><p>浏览器窗口的渲染进程会渲染index.html文件。</p><p>在CLI中键入下面命令启动「Hello, World!」:</p><p></p><pre class="">npm start</pre><p>现在为你的第一个Electron程序欢呼吧。</p><p>

</p><p>
</p>
<p></p><h1>开发真正的应用</h1><h2><b>超棒的发声器</b></h2><p>首先,什么是发声器?</p><p>发声器当你点击不同按钮时会播放不同声音的小设备,大部分是卡通或特效声。是在办公室用来放松心情的,很有趣的工具,随着开发的进行,会碰到的很多新的概念,所以这也是一个很好的开发桌面应用的实例(也是一个非常棒的发声器)。</p><p></p><p>
</p><p>我们将完成的功能和探索的概念包括:</p><ul><li>基础发声器(基本的浏览器窗口初始化),</li><li>关闭发声器(在主进程和渲染进程之间远程通信),</li><li>不需要把焦点切到应用就可以播放声音(全局快捷键),</li><li>创建快捷键的设定界面,来变更键位(Shift,Ctrl和Alt)(保存在用户的个人文件夹设置中),</li><li>加一个托盘图标(远程创建原生GUI元素,了解菜单和托盘图标),</li><li>打包你的应用(把你的应用打包成 Mac,Windows,Linux下可用的版本)。</li></ul>
<h2>实现发声器的基础功能</h2><p><b>应用的结构</b></p><p></p><p>你已经实现了一个运行正常的「Hello World!」应用,现在是时候实现一个发声器应用了。</p><p>典型的发声器功能包括几排按钮,点击播放声音,这些声音大部分是卡通式的或者特效式的(如大笑,鼓掌,玻璃碎裂声等)。</p><p>那就是我们要完成的第一个功能--能对点击能做出响应的发声器。</p><p>
</p><p></p><p>应用结构是比较简单的。</p><p>在应用的根目录,保存着package.json文件,main.js文件和其他文件。</p><p>app文件夹保存HTML文件,其中在css,js,wav和img文件夹中保存相应类型的文件。</p><p>为了简便,web页面所需的全部文件都已经保存在资料库的初始状态中。现在切换到01-start-project这个tag。如果你之前跟着指南操做,创建了「Hello World!」应用,你需要先重置资料库再切换:</p><p></p><pre class="">If you followed along with the "Hello, world!" example:
git add -A
git reset --hard</pre><pre class="">切换到01-start-project这个tag:
git checkout 01-start-project</pre><p></p><p>为了简便,发声器将只有两种声音,但扩展到全部16种声音也非常简单,只需要其他声音和图标文件,修改index.html就可以。</p><p><b>完成主进程</b></p><p>用main.js定义发声器的外观。用下面的代码代替原内容:</p><pre class="">'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');

});

</pre><p></p><p></p><p>我们用传给「app」模块的尺寸参数,自定义了新建窗口的大小,设定它是固定尺寸并且无边栏。它会浮在你的桌面上,就像真的发声机一样。</p><p>现在的问题是 -- 如何移动一个没有边栏的窗口(没有标题栏),如何关闭它?</p><p>我很快就会讲解自定义窗口(应用)关闭(并介绍一种主进程和渲染进程通信的方法),但拖动部分很简单,在index.css(app/css文件夹下)文件中:</p><p></p><pre class="">html,

body {

...

-webkit-app-region: drag;

...

}

</pre><p>-webkit-app-region:drag;使整个html变成一个可拖动的对象。现在有一个问题,你不能点击可拖动对象里的按钮。答案就是-webkit-app-region: no-drag;能定义不可拖动(但是可以点击)的对象,参考index.css的中的代码:</p><pre class="">.button-sound {

...

-webkit-app-region: no-drag;

}

</pre><p><b>在窗口中显示发声器</b></p><p></p><p>main.js文件现在可以新建一个窗口来显示发声器。如果用npm start启动应用,你可以看到发声器非常逼真。现在点击没有反应,这并不奇怪,我们只有一个静态的web页面。</p><p>添加下面的代码到index.js(app/js文件夹)文件中会添加交互效果:</p><p></p><pre class="">'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();

});

}

</pre><p></p><p>代码很简单,我们:</p><ul><li>查询所有声音按钮,</li><li>遍历所有的按钮读取data-sound属性,</li><li>给每个按钮加背景图,</li><li>给每个按钮加一个点击事件来播放音频(调用<a href="https://developer.mozilla.org/en/docs/Web/API/HTMLAudioElement" target="_blank">HTML AudioElement接口</a>)</li></ul><p>CLI中输入下面命令来测试应用:</p><p></p><pre class="">npm start</pre><p>

</p><p>
</p>
<h2>用远程事件从浏览器窗口关闭应用</h2><pre class="">请切换到02-basic-sound-machine这个tag:
git checkout 02-basic-sound-machine</pre><p>简要重述--应用窗口(更准确的说是渲染进程)应该不能与GUI(用来关闭窗口)通信的,官方的<a href="https://github.com/atom/electron/blob/master/docs/tutorial/quick-start.md" target="_blank">Electron快速入门指南</a>写到:</p><blockquote>在web页面,不允许调用原生GUI相关的API,因为在web页面管理原生GUI资源是很危险的,会很容易泄露资源。如果你想在web页面施行GUI操作,web页面的渲染进程必须要与主进程通信,请求主进程来完成这些操作。</blockquote><p>Electron提供<a href="https://github.com/atom/electron/blob/master/docs/api/ipc-renderer.md" target="_blank">ipc(进程间通信)模块</a>来实现这类通信。ipc模块可实现从通道订阅消息,发送消息给通道的订阅者,通道区分消息的接收者,用字符来标识(例如,通道1,通道2)。消息也可以包含数据。当接收到消息,订阅者可以做出反应,甚至回复消息。消息最大的好处就是隔离 -- 主进程不必知道哪个渲染进程发出消息。</p><p>
</p><p></p><p>这正是我们在做的 -- 主进程(main.js)订阅「close-main-window」通道的消息,关闭按钮被点击时,渲染进程(index.js)通过通道发出消息。</p><p>在main.js里添加下面的代码,从通道订阅消息:</p><p></p><pre class="">var ipc = require('ipc');

ipc.on('close-main-window', function () {

app.quit();

});

</pre><p></p><p>引入ipc模块后,通过通道订阅消息就变得很简单,on()方法设置订阅的通道名,定义回调函数。</p><p>渲染进程要通过通道发送消息,将下面代码加入index.js:</p><pre class="">var ipc = require('ipc');

var closeEl = document.querySelector('.close');

closeEl.addEventListener('click', function () {

ipc.send('close-main-window');

});

</pre><p></p><p></p><p>同样,我们引入ipc模块,给关闭按钮的元素绑定一个click事件。当点击关闭按钮时,通过「close-main-window」通道的send()方法发送消息。</p><p>这里还有个小问题,如果不注意会卡住你,我们已经讨论过--可拖动区域的可点击性。index.css需要把关闭按钮定义成不可拖动:</p><pre class="">.settings {

...

-webkit-app-region: no-drag;

}

</pre><p></p><p>就这样,现在可以点击关闭按钮关闭我们的应用了。因为要监听事件或传递参数,通过ipc模块通信比较复杂。我们后面会看到一个传递参数的例子。</p>


<h2>用全局快捷键播放声音</h2><pre class="">请切换到名为03-closable-sound-machine的tag:
git checkout 03-closable-sound-machine</pre><p></p><p>基础的发声器工作顺利,但是我们有一个易用性问题--如果发声器一定需要切到应用窗口,再点击才能播放,这个发声器有什么用?</p><p>这时我们需要的就是全局快捷键。Electron提供一个<a href="https://github.com/atom/electron/blob/master/docs/api/global-shortcut.md" target="_blank">全局快捷键模块</a>,允许你监听自定义的键盘组合并做出反应。键盘组合也被叫做<a href="https://github.com/atom/electron/blob/master/docs/api/accelerator.md" target="_blank">加速器</a>,是一系列键盘点击组成的字符串(例如 “Ctrl+Shift+1”)。</p><p></p><p>
</p><p></p><p>既然我们想要捕捉一个原生GUI事件(全局快捷键),然后在应用窗口做出反应(播放声音),我们仍用ipc模块在主进程和渲染进程之间通信。</p><p>在深入到代码层面前,有两件事要考虑:</p><ol><li>全局快捷键应在app的「ready」事件触发后被注册(在ready代码块中),</li><li>当通过ipc从主进程发送消息到渲染进程的时候,你要引用到那个窗口(就像「createWindow.webContent.send('channel')」)</li></ol><p>记住这些,现在用下面的代码来修改我们的main.js文件:</p><pre class="">var globalShortcut = require('global-shortcut');

app.on('ready', function() {

... // existing code from earlier

globalShortcut.register('ctrl+shift+1', function () {

mainWindow.webContents.send('global-shortcut', 0);

});

globalShortcut.register('ctrl+shift+2', function () {

mainWindow.webContents.send('global-shortcut', 1);

});

});

</pre><p></p><p>首先,我们需要引入global-shortcut模块。然后当我们的程序加载完成,我们注册两个快捷键--一个响应Ctrl,Shift,1组合键,另一个响应Ctrl,Shift,2组合键。两者都会通过「global-shortcut」通道发送一条带一个参数的消息。我们用这些参数来播放相应的声音。在index.js中加入以下代码:</p><pre class="">ipc.on('global-shortcut', function (arg) {

var event = new MouseEvent('click');

soundButtons[arg].dispatchEvent(event);

});

</pre><p>为了方便,我们会模拟一次按钮点击,用我们创建的soundButton选择器给按钮绑定一个播放声音。当收到带有参数1的消息,我们在soundButton[1]元素上模拟一次鼠标点击(在正式环境的应用,你应该封装播放声音的代码,并执行它)。</p>


<h2>在新的窗口修改键位配置</h2><pre class="">切换到名为04-global-shortcuts-bound的tag:
git checkout 04-global-shortcuts-bound</pre><p></p><p>系统同时运行很多应用程序,我们预想的快捷键可能已经被占用了。这正是我们将要新建一个设置窗口,保存我们想要的键位修改的原因。</p><p>要实现这个目标,我们需要:</p><ul><li>主窗口要有一个设置按钮,</li><li>一个设置窗口(需要相应的HTML,CSS和JavaScript文件),</li><li>ipc消息用来打开,关闭设置窗口及更新全局快捷键,</li><li>保存或读取用户系统里JSON格式的设置文件。</li></ul><p><b>设置按钮和设置窗口</b></p><p></p><p>类似关闭主窗口,当点击设置按钮时我们通过通道从index.js发送消息。将下面代码加入index.js:</p><p></p><pre class="">var settingsEl = document.querySelector('.settings');

settingsEl.addEventListener('click', function () {

ipc.send('open-settings-window');

});

</pre><p>点击设置按钮后,通道「open-settings-window」会发送一条消息到主进程。main.js现在需要做出响应,新建一个窗口,将下面代码插入main.js:</p><pre class="">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;

});

});

</pre><p></p><p>没有什么新概念,我们会像打开主窗口一样打开新的设置窗口。不同之处是要先检查设置窗口是不是已经被打开,以防重复打开。</p><p>打开后,需要一种方法关闭设置窗口。同样的,我们会通过通道发送一条消息,但这次消息是从settings.js发出,将下面代码写入setting.js:</p><pre class="">'use strict';

var ipc = require('ipc');

var closeEl = document.querySelector('.close');

closeEl.addEventListener('click', function (e) {

ipc.send('close-settings-window');

});

</pre><p></p><p>在main.js里面监听那个通道,代码如下:</p><pre class="">ipc.on('close-settings-window', function () {

if (settingsWindow) {

settingsWindow.close();

}

});

</pre><p></p><p>我们的设置窗口就完成了。</p><p><b>保存和读取用户的设置</b></p><p></p><pre class="">切换到名为05-settings-window-working的tag:
git checkout 05-settings-window-working</pre><p></p><p>与设置窗口交互,保存设置,再读取到我们的应用的过程大致是这样的:</p><ul><li>编写一个可以保存和读取我们在JSON文件中保存设置信息的办法,</li><li>初始化设置窗口时,显示这些设置,</li><li>通过客户的交互更新设置,</li><li>通知主程序新的设置。</li></ul><p>我们可以简单的保存和读取main.js中的设置,但模块把逻辑抽象出来,以便我们可以在不同的地方引用,这看看起来更好。</p><p></p><p><b>Working with a JSON configuration</b></p><p>那就是我们新建configuration.js的原因。Node.js用<a href="https://nodejs.org/docs/latest/api/modules.html" target="_blank">CommonJS模块规范</a>,这意味着你只可以暴露你的API,而其他文件或方法会引用API提供的方法。</p><p>

</p><p>为了让保存和读取更简便,使用nconf模块,它已经为我们抽象出读取和写入JSON文件的方法,非常符合我们的需求。但首先,我们要在CLI中执行下面的命令将它引入项目中:
</p><pre class="">npm install --save nconf</pre><p></p><p>npm将nconf模块作为应用的依赖安装。在我们打包应用给终端用户时(相对用save-dev参数会只在开发环境中引入模块)将被引入和使用。</p><p>configuration.js文件非常的简单,在项目根目录下新建configuration.js文件,写入代码:</p><pre class="">'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

};

</pre><p></p><p></p><p>nconf只需要知道你的设置要保存到哪里,这里我们设置为客户的主文件夹和一个文件名。获取用户的主文件夹非常简单,只需要区别不同系统调用Node.js(process.env)(如用getUserHome()方法)。</p><p>通过nconf的内建方法来保存或读取设置(set()方法保存,get()方法读取,用save()和load()方法进行文件操作),用符合CommonJS规范的module.exports语法来导出API。</p><p><b>初始化修改的快捷键</b></p><p></p><p>在我们进行设置的交互之前,应初始化设置,以防我们先启动应用丢失设置信息。我们把变更键保存在一个数组中,数组以「shortcutKeys」为键,在main.js里初始化,我们首先要引用configuration模块:</p><pre class="">'use strict';

var configuration = require('./configuration');

app.on('ready', function () {

if (!configuration.readSettings('shortcutKeys')) {

configuration.saveSettings('shortcutKeys', ['ctrl', 'shift']);

}

...

}

</pre><p></p><p>尝试读取「shortcutKeys」键对应的值,如果读取不到,就设置一个初始值。</p><p>现在要重写main.js中的全局快捷键,这个方法可以在后面更新设置的时候直接调用。 去掉原来在main.js中注册快捷键的方法,改成:</p><pre class="">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);

});

}

</pre><p></p><p>方法会重置全局快捷键,现在我们可以设置新的快捷键,从设置文件读取变更键数组,转换<a href="https://github.com/atom/electron/blob/master/docs/api/accelerator.md" target="_blank">类加速器规则字符串</a>,再注册全局快捷键。</p><p><b>与设置窗口交互</b></p><p>回到settings.js,我们要绑定click事件来修改我们的全局快捷键。首先,我们遍历所有勾选的复选框(从configuration模块中读取):</p><pre class="">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

}

</pre><p></p><p>现在我们要给复选框绑定行为。记得设置窗口(渲染进程)不能改动GUI绑定。这意味着我们需要从setting.js通过ipc发送消息(后面会处理消息):</p><pre class="">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');

}

</pre><p></p><p>我们遍历了所有的复选框,绑定click事件,在每次点击时判断是否含有变更键。然后根据结果,修改数组,保存结果到设置,再给主进程发送消息,它会更新我们的全局快捷键。</p><p>下面要在main.js里的设置「set-global-shortcuts」这个ipc通道来更新我们的全局快捷键:</p><pre class="">ipc.on('set-global-shortcuts', function () {

setGlobalShortcuts();

});

</pre><p></p><p></p><p>很简单,像这样,我们的全局快捷键就配置好了!</p>


<h2>菜单上有什么?</h2><pre class="">切换到名为06-shortcuts-configurable的tag:
git checkout 06-shortcuts-configurable</pre><p>对桌面应用来说,另一个重要的概念就是菜单栏。分为上下文菜单(右击菜单),托盘菜单(绑定到托盘图标),应用菜单(在OS X上)等多种。</p><p>
</p><p>在本指南中,我们将添加一个绑定菜单的托盘图标。我们也会利用这次机会探索另一种进程间通信--<a href="https://github.com/atom/electron/blob/master/docs/api/remote.md" target="_blank">remote模块</a>。
</p><p>remote模块实现从渲染进程向主进程发送RPC式调用。你引入模块,在渲染进程操作,方法在主进程被初始化,你调用的方法都在主进程被执行。实际中,这意味着你在index.js远程请求原生的GUI模块,调用它们的方法,都会在main.js中执行。你可以在index.js里引入BrowserWindow模块,初始化一个浏览器窗口。背后的原理是,异步调用新的浏览器窗口的主进程。</p><p>
</p><p>
</p><p>现在我们创建一个菜单,并把它绑定到托盘图标,在index.js中加入下面代码:</p><pre class="">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);

</pre><p></p><p>原生的GUI模块(菜单和托盘)的方法会被远程调用,是很安全的。</p><p>把图标定义成托盘图标。OS X支持图像模板(依照惯例,图像的文件名以「Template」结尾,可以被当做一个模板图像),这让使用深浅色主题变得很容易。其他系统用常规的图标。</p><p>在Electron中有很多种方法创建菜单。我们的方法是创建一个菜单模板(一个包含菜单项的简单数组),用那个模板创建菜单。最后,绑定新的菜单到托盘图标。</p><p></p>


<h2>打包你的应用</h2><pre class="">切换到名为07-ready-for-packaging的tag:
git checkout 07-ready-for-packaging</pre><p></p><p>如果不能让人们下载使用,这样的应用有什么意义?</p><p>
</p><p>
</p><p></p><p>用「<a href="https://github.com/maxogden/electron-packager" target="_blank">electron-packager</a>」为所有系统打包你的应用很简单。简单来说,「electron-packager」帮你完成所有用Electron打包你应用的工作,最终生成你要发布的平台的安装包。</p><p>它可以作为CLI应用或构建过程的一部分,更复杂的构建情况不在本文所涉及范围内,但我们如果能用打包脚本,会使打包更简单。用「electron-packager」比较麻烦,打包应用的基本命令是:</p><p></p><pre class="">electron-packager <location of project> <name of project> <platform> <architecture> <electron version> <optional options></pre><p></p><p>其中,</p><ul><li>location of project是你项目文件夹的位置,</li><li>name of project定义你的项目名,</li><li>platform决定要构建的平台(all 包括Windows,Mac和Linux ),</li><li>architecture决定构建哪个构架下(x86或x64,all表示两者),</li><li>electron version让你选择要用的Electron版本</li></ul><p></p><p>第一次打包用时比较久,因为要下载平台的二进制文件,随后的打包将会快的多。</p><p>我(在Mac系统)打包发声器应用的命令是:</p><p></p><pre class="">electron-packager ~/Projects/sound-machine SoundMachine --all --version=0.30.2 --out=~/Desktop --overwrite --icon=~/Projects/sound-machine/app/img/app-icon.icns</pre><p></p><p>命令的选项理解起来都比较简单。为了获得精美的图标,你首先要找一款类似<a href="http://www.example.com" target="_blank">这个软件</a>可以把PNG文件转换到这些格式的工具,把它转换成.icns格式(Mac用)或者.ico格式(Window用)。如果在非Windows系统给Windows平台的应用打包,你需要安装wine(Mac用户用brew,Linux用户用apt-get)。</p><p>每次都打这么长的命令很不方便,可以在package.json中加另一个脚本。首先,把electron-packager作为开发依赖安装:</p><p></p><pre class="">npm install --save-dev electron-packager</pre><p>现在我们可以在package.json中添加脚本:</p><pre class="">"scripts": {

"start": "electron .",

"package": "electron-packager ./ SoundMachine --all --out ~/Desktop/SoundMachine --version 0.30.2 --overwrite --icon=./app/img/app-icon.icns"

}</pre><p>在命令行里执行下面的命令:</p><pre class="">npm run-script package</pre><p></p><p>这个打包命令会启动electron-packager,在当前目录下找到目标应用文件,打包,保存到桌面。如果你用的是Windows系统,需要修改脚本,不过改动很小。</p><p>当前状态的发声器,最后打包后大小高达100MB。别担心,可以把它压缩到不到一半的容量。</p><p>如果你想要更进一步,可以尝试<a href="https://github.com/loopline-systems/electron-builder" target="_blank">electron-builder</a>,它用electron-packager生成的打包好的文件,可以生成自动安装包。</p><p></p>


<h2>可以添加的其他功能</h2><p></p><p>应用已经打包好,准备就绪。你也可以添加自己想要的功能。</p><p>这是一些想法:</p><ul><li>可以显示应用的快捷键,作者等信息的帮助界面,</li><li>加一个绑定菜单的图标入口可以打开信息界面,</li><li>为了更快的编译和分发,编写打包的脚本,</li><li>用<a href="https://github.com/mikaelbr/node-notifier" target="_blank">node-notifier</a>加入通知功能,推送用户正播放的是什么声音,</li><li>用lodash得到更整洁的代码,</li><li>打包应用前,将你所有的CSS和JavaScript文件用构建工具压缩,</li><li>检查应用是否有新版本,用服务器调用之前介绍的node-notifier并通知客户</li></ul><p>挑战来了--尝试抽取出发声器浏览器窗口的逻辑,用这些逻辑在浏览器中创建web页面,实现相同的发声器。一个代码库--两个产品(桌面应用和web应用),超棒!</p><p></p>
<p></p><h1>深入Electron</h1><p>我们只接触到了Electron比较浅显知识。实际上,实现如查看主机电源选项或在界面上显示多种信息都很简单。这些功能已经内建好,请<a href="https://github.com/atom/electron/tree/master/docs/api" target="_blank">查阅Electronde API文档</a>。</p><p>Electron的API文档只是Electron在Github上资料库的一小部分,其他文件夹也值得一看。</p><p>Sindre Sorhus正在维护<a href="https://github.com/sindresorhus/awesome-electron" target="_blank">超酷的Electron资源列表</a>,你可以在列表中找到很多很酷的项目,也有Electron应用构架方面很好的总结,可以学习之后重构我们的代码。</p><p>最后,Electron是基于io.js(将合并回Node.js)的,兼容绝大部分的Node.js模块,可以用来扩展你的应用,查看<a href="https://www.npmjs.com/" target="_blank">npmjs.com</a>来获取你需要的信息。</p>
<p></p><h1>这就完了?</h1><p>当然不是。</p><p>现在是时候来创建更复杂的应用了。在本指南中,我没有选择用更多函数库和构建工具,只强调了重要的概念。你也可以用ES6或Typescript来写你的应用,使用Angular或React框架,用gulp或Grunt来简化你的构建过程。</p><p>为什么不用你最喜欢的语言,框架和构建工具,配合Flickr API,node-flickrapi创建一个Flickr桌面同步应用呢?或者用Google官方的Node.js函数库创建一个Gmail客户端?</p><p>选一个吸引你的想法,创建一个资料库,开始做吧。</p></div>

<div class="bbox">

本文来自<a href="http://get.ftqq.com/36.version" target="_blank">GET社区翻译计划</a>,每翻译一篇高质量文章,可获100元奖金。翻译计划赞助商:

<ul class="binfo">

<li><a href="https://www.sdk.cn/datas/trending?fr=GET" target="_blank">

</a></li>

</ul>

转载必须保留来源和以上赞助商信息。

</div>

<div class="sinfo exp">

本文由 <a href="/1.user" target="_blank">Easy<span aria-hidden="true" class="jdc-jobdeer jdcicon"></span></a> 第一时间收藏到

GET,原文来自 → <a href="https://medium.com/@bojzi/building-a-desktop-application-with-electron-204203eeb658" target="_blank"> medium.com </a>

</div>

<ul class="get-article-actions">

<li><a href="http://snap.ftqq.com/imget.php?title=%E3%80%8C+%E7%94%A8Electron%E5%BC%80%E5%8F%91%E6%A1%8C%E9%9D%A2%E5%BA%94%E7%94%A8+%E3%80%8D%E7%94%A8JavaScript%EF%BC%8CNode.js%E5%92%8CEletron%E5%88%9B%E5%BB%BA%E5%8F%91%E5%A3%B0%E5%99%A8%E5%BA%94%E7%94%A8%E7%9A%84%E8%AF%A6%E7%BB%86%E6%8C%87%E5%8D%97%E3%80%82+via+%40GET%E7%A4%BE%E5%8C%BA+-+%E9%9D%A2%E6%9C%9D%E7%9F%A5%E8%AF%86%EF%BC%8C%E7%AE%80%E6%B4%81%E9%9B%85%E8%87%B4+%E2%86%92+&url=http%3A%2F%2Fget.ftqq.com%2F7870.gets&source=4289046952&pic=" target="_blank">分享到微博</a></li>

<li class="kblink-7870"><a href="javascript:add2kb('7870','top');" >收藏本文</a></li>

<li class="followlink-1"><a href="javascript:follow('1');void(0);">关注@Easy</a></li>

</ul>

<div class="visible-xs-block wxlast">

<center>

<p>「GetParty」</p>

<p>关注微信号,推送好文章</p>

<p>微信中长按图片即可关注</p>

<p><a href="/?fr=qrbt" class="btn btn-success">更多精选文章</a></p>

</center>

</div>

<div id="article-7870">

<a href="javascript:toggle_inline_comment('7870','article');void(0);" class="get" name="comment"><span class="jdc-coffee"></span>评论(4)</a>

<div class="top20"></div>

</div>

<script>toggle_inline_comment('7870','article');

$(".timeago").timeago();</script>

<div class="hbox"></div>

</div>

<script>

$( document ).ready(function() {

highlight();

// 支持图片分享到微博

setTimeout( function()

{

$('.get-article-area').imgShare();

} , 2000 );

});

</script>

</div>

<div class="hidden-xs col-sm-3 sidebar-offcanvas" id="sidebar" role="navigation">

<input class="nh" type="checkbox" onchange="change2('https://medium.com/@bojzi/building-a-desktop-application-with-electron-204203eeb658');$(this).prop('checked', false);" />

<script type="text/javascript">

$('input.nh').onoff();

</script>

<script>

$('.sinfo.exp').waypoint(function(direction) {

if( direction == 'down' )

{

$("#wechat_code").show();

}

if( direction == 'up' )

{

$("#wechat_code").hide();

}

},{ offset:'bottom-in-view'});

</script>

<div id="wechat_code">

<p>关注微信号,推送好文章</p>

</div> </div>

</div>

<div class="get-last"></div>

<div class="get-user-menu">

<a href="/?c=weibo&a=login" class="link">微博一键登入</a><span class="top" ><a href="#"><span class="jdc-arrow-up-thick"></span></a></span>

</div>

<div class="modal fade" id="get_float" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" >

<div class="modal-dialog">

</div>

</div>

<div id="get_aside" class="clearfix">

<div class="pull-right"><a href="javascript:hide_aside();void(0);" class="get btn">×</a></div>

<div class="content"><center class="top50">

</center></div>

</div>

<div id="shadow_dom"></div>

<script type="text/javascript">

//emojify.run();

//$('#get_float').modal();

$(window).scroll( function()

{

var value = $(document).scrollTop();

//console.log('in value='+value);

if ( value > 0 )

$("#theheader").addClass('smaller');

else

$("#theheader").removeClass('smaller');

});

if( $(".snapbox").length > 0 )

$(".snapbox").append($("<div class='floatingfooter'></div>"));

$( document ).ready(function() {

$( '.yue a[href^="http://"]' ).attr( 'target','_blank' );

$( '.yue a[href^="https://"]' ).attr( 'target','_blank' );

});

</script>

</div>

</body>

</html>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容