小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

使用 Electron 構(gòu)建桌面應(yīng)用

 昵稱597197 2016-01-06
kmokidd · 4 個(gè)月前

譯者注:前一段時(shí)間有過(guò)關(guān)于NW.js 的翻譯,有興趣的朋友可以一起讀來(lái)看看。另,譯者尚未實(shí)踐本教程,預(yù)計(jì)將在下周使用新Mac實(shí)踐。若在此之前有朋友踩坑,請(qǐng)通知譯者,譯者將在實(shí)踐后更改之。

使用 JavaScript、Node.js 和 Electron 創(chuàng)造專屬于你的聲效器

JavaScript 桌面應(yīng)用是什么

即使在移動(dòng)端和云端大行其道而,桌面端日漸落末的現(xiàn)在,我的心中仍然為桌面應(yīng)用留有一個(gè)特殊的位置。和 Web 應(yīng)用比起來(lái)桌面應(yīng)用的優(yōu)點(diǎn)還是很多的:只要把它們放在開(kāi)始菜單欄或者 dock 上,你就能隨時(shí)打開(kāi)它們;還可以通過(guò) alt-tab 或者 cmd-tab 切換應(yīng)用;和操作系統(tǒng)之間的交互更良好(快捷鍵,通知欄等)。

我將會(huì)在這篇文章中指導(dǎo)構(gòu)建一個(gè)簡(jiǎn)單的桌面應(yīng)用。當(dāng)然,你也將了解到在使用 JavaScript 構(gòu)建桌面應(yīng)用的時(shí)候要哪些重要的概念。

使用 JavaScript 開(kāi)發(fā)桌面應(yīng)用意味著在打包(package application)的時(shí)候你會(huì)需要根據(jù)操作系統(tǒng)的不同發(fā)出不同的命令。這一行為是將原生桌面應(yīng)用兼容不同平臺(tái)的概念抽象出來(lái),方便維護(hù)應(yīng)用?,F(xiàn)在,我們可以借助 Electron 或者 NW.js 開(kāi)發(fā)一個(gè)桌面應(yīng)用。其實(shí)這兩者提供的或多或少差不多的特性,但對(duì)于我來(lái)說(shuō),還是更偏向于 Electron。在做出選擇之前,先詳細(xì)了解它們并考慮各種情況,就不會(huì)選錯(cuò)的。


基本假設(shè)

開(kāi)始教程之前,請(qǐng)?jiān)试S我假設(shè)你已經(jīng)有了一個(gè)常用的的編輯器(或者 IDE),系統(tǒng)中也安裝了 Node.js 和 npm,并有基礎(chǔ)的 HTML/CSS/JavaScript (對(duì) Node.js 的 CommonJS 模塊概念有所了解是最好,但不強(qiáng)求) 知識(shí)。如果以上知識(shí)你并不了解,為了防止這篇文章看到你頭昏腦脹,推薦你先看看之前我寫(xiě)過(guò)的博文,補(bǔ)充一下基礎(chǔ)知識(shí)。

萬(wàn)事俱備,現(xiàn)在就把精力集中在學(xué)習(xí) Electron 上,不要再擔(dān)心界面的事情(將會(huì)構(gòu)建的界面本質(zhì)上就是普通的 Web 頁(yè)面而已)。

Electron 概覽

簡(jiǎn)而言之,Electron 提供了一個(gè)實(shí)時(shí)構(gòu)建桌面應(yīng)用的純 JavaScript 環(huán)境。Electron 可以獲取到你定義在 package.json 中 main 文件內(nèi)容,然后執(zhí)行它。通過(guò)這個(gè)文件(通常我們稱之為 main.js),可以創(chuàng)建一個(gè)應(yīng)用窗口,這個(gè)應(yīng)用窗口包含一個(gè)渲染好的 web 界面,還可以和系統(tǒng)原生的 GUI 交互。

具體來(lái)說(shuō),就是當(dāng)你啟動(dòng)了一個(gè) Electron 應(yīng)用,就有一個(gè)主進(jìn)程(main process )被創(chuàng)建了。這條進(jìn)程將負(fù)責(zé)創(chuàng)建出應(yīng)用的 GUI(也就是應(yīng)用的窗口),并處理用戶與這個(gè) GUI 之間的交互。

但直接啟動(dòng) main.js 是無(wú)法顯示應(yīng)用窗口的,在 main.js 中通過(guò)調(diào)用BrowserWindow模塊才能將使用應(yīng)用窗口。然后每個(gè)瀏覽器窗口將執(zhí)行它們各自的渲染器進(jìn)程( renderer process )。渲染器進(jìn)程將會(huì)處理一個(gè)真正的 web 頁(yè)面(HTML + CSS + JavaScript),將頁(yè)面渲染到窗口中。鑒于 Electron 使用的是基于 Chrominum 的瀏覽器內(nèi)核,你就不太需要考慮兼容的問(wèn)題。

舉個(gè)例子,如果你只想做一個(gè)計(jì)算器,那你的 main process 只會(huì)做一件事情:實(shí)例化一個(gè)窗口,并內(nèi)置了一個(gè)計(jì)算器的界面(這個(gè)界面是你用 HTML、CSS 和 JavaScript 寫(xiě)的)。

雖然理論上只有 main process 才能和原生 GUI 產(chǎn)生交互,但其實(shí)我們可以通過(guò)一些手段讓 renderer process 與原生 GUI 交互(在后文中你將學(xué)習(xí)到如何實(shí)現(xiàn))。

main process 可以通過(guò) Electron 中的一些模塊直接和原生 GUI 交互。你的桌面應(yīng)用可以使用任意的 Node 模塊,比如用 node-notifier 顯示系統(tǒng)通知,用 request發(fā)出 HTTP 請(qǐng)求……


Hello, world!

做好前期準(zhǔn)備,現(xiàn)在讓我們從 Hello World 開(kāi)始吧!


使用的 repo

這篇教程是基于一個(gè)聲效器教程的 github 倉(cāng)庫(kù),請(qǐng)使用下面的命令將它克隆到本地:

git clone https://github.com/bojzi/sound-machine-electron-guide.git

然后查看一下,你可以看看這個(gè)倉(cāng)庫(kù)中有哪些 tag:

git checkout <tag-name>

我們將跟隨這些 tag 將聲效器一步步構(gòu)建出來(lái):

git checkout 00-blank-repository

拉?。╟heckout)目標(biāo) tag 之后,執(zhí)行:

npm install

這么做能保證項(xiàng)目所依賴的 Node 模塊都會(huì)被拉取。

如果你無(wú)法切換到某一個(gè) tag,最簡(jiǎn)單的解決方式就是重置倉(cāng)庫(kù),然后再 checkout:

git add -A
git reset --hard

開(kāi)工

先把 tag 為 ‘00-blank-repository’ 拉取下拉:

git checkout 00-blank-repository

在項(xiàng)目文件夾中創(chuàng)建一個(gè) package.json 文件,并在文件中加入以下內(nèi)容:

{
    "name": "sound_machine",
    "version": "0.1.0",
    "main": "./main.js",
    "scripts": {
        "start": "electron ."
    }
}

這個(gè) package.json 的作用是:

  • 確定應(yīng)用的名字和版本號(hào),

  • 告訴 Electron main.js 是 main process 的入口,

  • 定義啟動(dòng)口令 - 在 CLI (終端或者命令行)中執(zhí)行 npm start 即可完成依賴安裝。

現(xiàn)在快把 Electron 安裝上吧。最簡(jiǎn)單的安裝方式應(yīng)該是通過(guò) npm 安裝預(yù)構(gòu)建好的二進(jìn)制文件,然后把它作為開(kāi)發(fā)依賴(development dependency)寫(xiě)入 package.json 中(安裝時(shí)帶上 --save-dev 參數(shù)即可自動(dòng)寫(xiě)入依賴)。在 CLI 中進(jìn)入項(xiàng)目目錄,執(zhí)行下面的命令:

npm install --save-dev electron-prebuilt

預(yù)構(gòu)建的二進(jìn)制文件會(huì)根據(jù)操作系統(tǒng)不同而不同的,通過(guò)執(zhí)行 npm start 安裝。我們以開(kāi)發(fā)依賴的方式使用它,是因?yàn)樵陧?xiàng)目構(gòu)建中只有在開(kāi)發(fā)階段才會(huì)使用到 Electron。

以上,就是本次 Electron 教程所需要的全部東西了。


對(duì)世界說(shuō) Hi

創(chuàng)建一個(gè) app 文件夾,在文件夾中新建 index.html 文件,并寫(xiě)入以下內(nèi)容:

<h1>Hello, world!</h1>

在項(xiàng)目的根目錄創(chuàng)建 main.js 文件。Electron 主線程的入口是這個(gè) JS 文件,然后 “Hello world!” 頁(yè)面也通過(guò)它顯示出來(lái):

'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');});

看起來(lái)并不難吧?

app 模塊控制著應(yīng)用的生命周期(比如,當(dāng)應(yīng)用進(jìn)入準(zhǔn)備狀態(tài)(ready status)的時(shí)候要采取什么行動(dòng))。

BrowserWindow 模塊控制窗口的創(chuàng)建。

mainWindow 對(duì)象就是你的應(yīng)用窗口的主界面,當(dāng) JavaScript 垃圾回收機(jī)制被觸發(fā)時(shí)窗口就會(huì)被關(guān)閉,此時(shí)該對(duì)象的值是null。

當(dāng) app 獲取到 ready 事件后,我們通過(guò) BrowserWindow 創(chuàng)建一個(gè) 800x600 窗口。

這個(gè) window 的渲染器線程將會(huì)渲染 index.html 文件。

執(zhí)行下面這行代碼,看看我們的應(yīng)用是什么樣的:

npm start

現(xiàn)在沐浴在這個(gè) app 的圣光中吧。



開(kāi)發(fā)一個(gè)真正的應(yīng)用


華麗麗的聲效器

開(kāi)始之前,我要問(wèn)個(gè)問(wèn)題:什么是聲效器?

聲效器是一個(gè)小設(shè)備,當(dāng)你按下不同按鍵的時(shí)候,它會(huì)發(fā)出不同聲音,比如卡通音或者效果音。在辦公室里聽(tīng)到這樣有趣的聲音,好像整個(gè)人都明亮起來(lái)了呢。用這個(gè)例子作為探索如何使用 Electron 是個(gè)很棒的主意。


具體來(lái)說(shuō),我們將會(huì)實(shí)現(xiàn)以下功能,并涉及到以下知識(shí):

  • 聲效器的基礎(chǔ)(實(shí)例化瀏覽器窗口),

  • 關(guān)閉聲效器(主進(jìn)程和渲染器進(jìn)程之間的通信),

  • 隨時(shí)播放聲音(全局快捷鍵),

  • 創(chuàng)建一個(gè)快捷修飾鍵(修飾鍵,modifier keys, 指的是 Shift、Ctrl 和 Alt 鍵)設(shè)置頁(yè)面(并將用戶設(shè)置保存在主目錄下),

  • 添加一個(gè)托盤圖標(biāo)(創(chuàng)建原生 GUI 元素、了解菜單和托盤圖標(biāo)的使用),

  • 將應(yīng)用打包到 Mac、Windows 和 Linux 平臺(tái)。

實(shí)現(xiàn)聲效器的基本功能


開(kāi)始構(gòu)建以及應(yīng)用的結(jié)構(gòu)

在開(kāi)發(fā)過(guò) “Hello World” 應(yīng)用之后,現(xiàn)在可以著手制做我們的聲效器了。

一個(gè)典型的聲效器會(huì)有很多的按鈕,你需要按下那些按鈕才能讓機(jī)器發(fā)聲,通常會(huì)是擬聲詞(比如笑聲、掌聲、打碎玻璃的聲音等等)。

響應(yīng)點(diǎn)擊 – 這是我們要做的第一件事情。

我們的應(yīng)用結(jié)構(gòu)非常簡(jiǎn)單直白。


在應(yīng)用的根目錄中,要有一個(gè) package.json、main.js 和其他全局所需的應(yīng)用文件。

app/ 目錄中要包含 HTML 文件、CSS 目錄、JS 目錄、wav 目錄還有圖片目錄。

出于簡(jiǎn)化這個(gè)教程的目的,所有和網(wǎng)頁(yè)設(shè)計(jì)相關(guān)的文件都已經(jīng)在一開(kāi)始就放在倉(cāng)庫(kù)中了。請(qǐng)?jiān)诿钚兄休斎雊it checkout 01-start-project 獲取?,F(xiàn)在,請(qǐng)你可以輸入以下命令,重置你的倉(cāng)庫(kù)并拉取新的 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

在本教程中,我們只使用兩種聲效,后面再找一些別的音效和圖標(biāo),修改* index.js *就將它們擴(kuò)展成有16種音效的聲效器。


main process 的其他內(nèi)容

首先找到 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');});

當(dāng)窗口被定義了大小,我們也就是在自定義這個(gè)窗口,使得它不可拉伸沒(méi)有框架,讓它看起來(lái)就像一個(gè)真正的聲效器浮在桌面上。

現(xiàn)在問(wèn)題來(lái)了 – 要如何移動(dòng)或者關(guān)閉一個(gè)沒(méi)有標(biāo)題欄的窗口。

很快我就會(huì)說(shuō)到自定義窗口(和應(yīng)用)的關(guān)閉動(dòng)作,還會(huì)談到如何在主進(jìn)程和渲染器進(jìn)程中通信。不過(guò)現(xiàn)在讓我們先把目光聚焦到“拖拽效果”上。你可以在 app/css 目錄下找到 index.css 文件:

html,body {
    ...
    -webkit-app-region: drag;
    ...}

-webkit-app-region: drag;把整個(gè) html 都變成了一個(gè)可拖拽的對(duì)象。現(xiàn)在問(wèn)題來(lái)了,在可拖拽的對(duì)象上你怎么點(diǎn)擊啊?!好的,可能你會(huì)想到把 html 中某個(gè)部分的這個(gè)屬性值設(shè)置為no-drag;,那就允許該元素不可拖拽(但可以點(diǎn)擊了)。讓我們想想下面這段 index.css 片段:

.button-sound {
    ...
    -webkit-app-region: no-drag;
}

展示聲效器

現(xiàn)在通過(guò) main.js 文件可以創(chuàng)建一個(gè)新窗口,并在窗口中顯示出聲效器的界面。如果通過(guò)npm start啟動(dòng)應(yīng)用,你將會(huì)看到一個(gè)有動(dòng)態(tài)效果的聲效器。因?yàn)槲覀兙褪菑囊粋€(gè)靜態(tài)頁(yè)面開(kāi)始,所以現(xiàn)在你看到的也是不會(huì)動(dòng)的頁(yè)面:

將下面這段代碼保存到 index.js 文件中(位置在 app/js 目錄下),運(yùn)行后應(yīng)用后,你會(huì)發(fā)現(xiàn)可以與聲效器交互了:

'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();
    });}

通過(guò)上面這段代碼,我們:

  • 獲取聲音按鈕,

  • 迭代訪問(wèn)按鈕的data-sound屬性值,

  • 給每個(gè)按鈕加上背景圖,

  • 通過(guò) HTMLAudioElement 接口給每個(gè)按鈕都添加一個(gè)點(diǎn)擊事件,使之可以播放音頻,

通過(guò)下面這行命令運(yùn)行你的應(yīng)用吧:

npm start


通過(guò)遠(yuǎn)程事件從瀏覽窗口中關(guān)閉應(yīng)用

接著拉取02-basic-sound-machine的內(nèi)容:

git checkout 02-basic-sound-machine

簡(jiǎn)單來(lái)說(shuō) - 應(yīng)用窗口(渲染器進(jìn)程)不應(yīng)該和 GUI 發(fā)生交互(也就是不應(yīng)該和“關(guān)閉窗口”有關(guān)聯(lián)),Electron 的官方教程上說(shuō)了:

考慮到在網(wǎng)頁(yè)中直接調(diào)用原生的 GUI 容易造成資源溢出,這很危險(xiǎn),開(kāi)發(fā)者不能這么使用。如果開(kāi)發(fā)者想要在網(wǎng)頁(yè)上執(zhí)行 GUI 操作,必須要通過(guò)渲染器進(jìn)程和主進(jìn)程的通信實(shí)現(xiàn)。

Electron 為主進(jìn)程和渲染器進(jìn)程提供了 ipc (跨進(jìn)程通信)模塊,ipc 模塊允許接收和發(fā)送通信頻道的信息。頻道由字符串表示(比如“channel-1”,“channel-2”這樣),可以用于區(qū)分不同的信息接收者。傳遞的信息中也可以包含數(shù)據(jù)。根據(jù)接收到的信息,訂閱者可以做出響應(yīng)。信息傳遞的最大好處就是做到分離任務(wù) – 主進(jìn)程不需要知道是哪些渲染器進(jìn)程發(fā)送了信息。


這正是我們想要做的 – 將主進(jìn)程(main.js)訂閱到“關(guān)閉主窗口”頻道中,當(dāng)用戶點(diǎn)擊關(guān)閉按鈕時(shí),從渲染器進(jìn)程(index.js)向該頻道發(fā)送信息。

Add the following to main.js to subscribe to a channel: 
將下面的代碼實(shí)現(xiàn)了頻道訂閱,將它添加到 main.js 中:

var ipc = require('ipc');ipc.on('close-main-window', function () {
    app.quit();});

把 ipc 模塊包含進(jìn)來(lái)之后,從頻道中訂閱信息就非常簡(jiǎn)單了:過(guò) on() 方法和頻道名稱,再加上一個(gè)回調(diào)函數(shù)就行了。

要向該頻道發(fā)送信息,就要把下面的代碼加入 index.js 中:

var ipc = require('ipc');var closeEl = document.querySelector('.close');closeEl.addEventListener('click', function () {
    ipc.send('close-main-window');});

我們依然需要把 ipc 模塊引入到文件中,給關(guān)閉按鈕綁定點(diǎn)擊事件。當(dāng)點(diǎn)擊了關(guān)閉按鈕時(shí),通過(guò) send() 方法發(fā)送一條信息到“關(guān)閉主窗口”頻道。

不要忘記在在 index.css 中將關(guān)閉按鈕設(shè)置為不可拖拽:

.settings {
    ...
    -webkit-app-region: no-drag;}

就這樣,我們的應(yīng)用現(xiàn)在可以通過(guò)按鈕關(guān)掉了。ipc 的通信可以通過(guò)事件和參數(shù)的傳遞變得很復(fù)雜,在后文中會(huì)有傳遞參數(shù)的例子。


通過(guò)全局快捷鍵播放聲音

拉取03-closable-sound-machine:

git checkout 03-closable-sound-machine

聲效器的地基已經(jīng)打的不錯(cuò)。但是我們還面臨著使用性的問(wèn)題 – 這個(gè)應(yīng)用要始終保持在桌面最前方,且可以被重復(fù)點(diǎn)擊。

這就是全局快捷鍵要介入的地方。Electron 提供了全局快捷模塊(global shortcut module)允許開(kāi)發(fā)者捕獲組合鍵并做出相應(yīng)的反應(yīng)。在 Eelctron 中組合鍵被稱為加速器,它以字符串的形式被記錄下(比如 “Ctrl+Shift+1”)。

因?yàn)槲覀兿胍东@到原生的 GUI 事件(全局快捷鍵),并執(zhí)行應(yīng)用窗口事件(播放聲音),我們將使用 ipc 模塊從主進(jìn)程發(fā)送信息到渲染器進(jìn)程。


在看代碼之前,還有兩件事情要我們考慮:

  1. 全局快捷鍵會(huì)在 app 的 ready 事件被觸發(fā)后注冊(cè)(相關(guān)代碼片段要被包含在 ‘ready’ 中)

  2. 通過(guò) ipc 模塊從主進(jìn)程向渲染器進(jìn)程發(fā)送信息,你必須使用窗口對(duì)象的引用(類似于createdWindow.webContents.send(‘channel’))。

記住上面的兩點(diǎn)了嗎?現(xiàn)在讓我們來(lái)改寫(xiě)* main.js *吧:

var globalShortcut = require('global-shortcut');

app.on('ready', function() {
    ... // 之前寫(xiě)過(guò)的代碼

    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 模塊,當(dāng)應(yīng)用進(jìn)入ready狀態(tài)之時(shí),我們將會(huì)注冊(cè)兩個(gè)快捷鍵 – ‘Ctrl+Shift+1’ 和 ‘Ctrl+Shift+2’。這兩個(gè)快捷鍵可以通過(guò)不同的參數(shù)向“全局快捷鍵”頻道( “global-shortcut”channel)發(fā)送信息。通過(guò)參數(shù)匹配到到底要播放哪種聲音,將下面的代碼加入 index.js 中:

ipc.on('global-shortcut', function (arg) {
    var event = new MouseEvent('click');
    soundButtons[arg].dispatchEvent(event);
});

為了保證整個(gè)架構(gòu)足夠簡(jiǎn)單,我們將會(huì)用 soundButtons 選擇器模擬按鈕的點(diǎn)擊播放聲音。當(dāng)發(fā)送的信息是“1”時(shí),我們將會(huì)獲取 soundButtons[1] 元素,觸發(fā)鼠標(biāo)點(diǎn)擊事件(注意:在生產(chǎn)環(huán)境中的應(yīng)用,你需要封裝好播放聲音的代碼,然后執(zhí)行它)。


在新窗口中通過(guò)用戶設(shè)置配置 modifier keys

下面請(qǐng)拉取04-global-shortcuts-bound:

git checkout 04-global-shortcuts-bound

通常我們會(huì)同時(shí)運(yùn)行好多個(gè)應(yīng)用,聲效器中設(shè)置的快捷鍵很可能已經(jīng)被占用了。所以現(xiàn)在要引入一個(gè)設(shè)置界面,允許用戶更改修飾鍵(modifier keys)的原因(Ctrl、Alt 和 Shift)。

要完成這一個(gè)功能,我們需要做下面這些事情:

  • 在主界面上添加設(shè)置按鈕,

  • 實(shí)現(xiàn)一個(gè)設(shè)置窗口(設(shè)置頁(yè)面上有對(duì)應(yīng)的HTML、CSS 和 JS),

  • 開(kāi)啟和關(guān)閉設(shè)置窗口,以及更新全局快捷鍵的 ipc 信息,

  • 從用戶的系統(tǒng)中讀寫(xiě)存儲(chǔ)設(shè)置信息的 JSON 文件。

piu~ 以上就是我們要做的。


設(shè)置按鈕和設(shè)置窗口

和關(guān)閉主窗口類似,我們將會(huì)把事件綁定到設(shè)置按鈕上,(settings button),在* index.js *中加入發(fā)送給頻道的信息:

var settingsEl = document.querySelector('.settings');
settingsEl.addEventListener('click', function () {
    ipc.send('open-settings-window');
});

當(dāng)點(diǎn)擊了設(shè)置按鈕,將會(huì)有一條信息向“打開(kāi)設(shè)置窗口”這個(gè)頻道發(fā)送。* main.js 可以響應(yīng)這個(gè)事件,并打開(kāi)一個(gè)新窗口,將以下代碼加入 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;
    });
});

這一步和之前的類似,我們將會(huì)打開(kāi)一個(gè)新的窗口。唯一的不同點(diǎn)就是,為了防止實(shí)例化兩個(gè)一樣的對(duì)象,我們將會(huì)檢查設(shè)置窗口是否已經(jīng)被打開(kāi)了。

當(dāng)上述代碼成功執(zhí)行之后,我們需要再添加一個(gè)關(guān)閉設(shè)置窗口的動(dòng)作。類似的,我們需要向頻道中發(fā)送一條信息,但這次是從* settings.js 中發(fā)送(關(guān)閉按鈕的事件是在 settings.js 中)。新建 settings.js *文件,并添加以下代碼(如果已經(jīng)有該文件,就直接在原文件中添加):

'use strict';

var ipc = require('ipc');

var closeEl = document.querySelector('.close');
closeEl.addEventListener('click', function (e) {
    ipc.send('close-settings-window');
});

在 main.js 中監(jiān)聽(tīng)該頻道:

ipc.on('close-settings-window', function () {
    if (settingsWindow) {
        settingsWindow.close();
    }
});

現(xiàn)在,設(shè)置窗口已經(jīng)可以實(shí)現(xiàn)我們的邏輯了。


用戶設(shè)置的讀寫(xiě)

執(zhí)行05-settings-window-working:

git checkout 05-settings-window-working

設(shè)置窗口的交互過(guò)程是,存儲(chǔ)設(shè)置信息以及刷新應(yīng)用:

  • 創(chuàng)建一個(gè) JSON 文件用于讀寫(xiě)用戶設(shè)置,

  • 用這個(gè)設(shè)置初始化設(shè)置窗口,

  • 通過(guò)用戶的操作更新這個(gè)設(shè)置文檔,

  • 通知主進(jìn)程要更新設(shè)置頁(yè)面。

我們可以把實(shí)現(xiàn)讀寫(xiě)設(shè)置的部分直接寫(xiě)進(jìn) main.js 中,但是如果把這部分獨(dú)立成模塊,可以隨處引用這樣不是更好嗎?

使用 JSON 做配置文件

現(xiàn)在我們要?jiǎng)?chuàng)建一個(gè) configuration.js 文件,再將這個(gè)文件引入到項(xiàng)目中。Node.js 使用了 CommonJS 作為編寫(xiě)模塊的規(guī)范,也就是說(shuō)你需要將你的 API 和這個(gè) API 中可用的函數(shù)都要暴露出來(lái)。



為了更簡(jiǎn)單地讀寫(xiě)文件,我們將會(huì)使用 nconf 模塊,這個(gè)模塊封裝了 JSON 文件的讀寫(xiě)。但首先,我們需要將這個(gè)模塊包含到項(xiàng)目中來(lái):

npm install --save nconf

這行命令意味著 nconf 模塊將會(huì)作為應(yīng)用依賴被安裝到項(xiàng)目中,當(dāng)我們要發(fā)布應(yīng)用的時(shí)候,這個(gè)模塊會(huì)被一起打包給用戶(save-dev 參數(shù)會(huì)使安裝的模塊只出現(xiàn)在開(kāi)發(fā)階段,發(fā)布應(yīng)用的時(shí)候不會(huì)被包含進(jìn)去)。

在根目錄下創(chuàng)建 configuration.js 文件,它的內(nèi)容非常簡(jiǎn)單:

'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 獲取到文件的位置),具體路徑會(huì)根據(jù)平臺(tái)而異。

通過(guò) nconf 模塊的 set() 和 get() 方法結(jié)合文件操作的 save() 和 load(),我們可以實(shí)現(xiàn)設(shè)置文件的讀寫(xiě)操作,然后通過(guò) module.exports 將接口暴露到外部。

初始化默認(rèn)的快捷鍵設(shè)置

在講設(shè)置交互之前,為了避免用戶是第一次打開(kāi)這個(gè)應(yīng)用,要先初始化一個(gè)設(shè)置文件。我們將會(huì)以數(shù)組的形式儲(chǔ)存熱鍵,對(duì)應(yīng)的鍵是 “shortcutKeys”,儲(chǔ)存在 main.js 中,我們需要把 configuration 模塊包含到項(xiàng)目中:

'use strict';var configuration = require('./configuration');app.on('ready', function () {
    if (!configuration.readSettings('shortcutKeys')) {
        configuration.saveSettings('shortcutKeys', ['ctrl', 'shift']);
    }
    ...}

我們需要先檢測(cè)鍵 ‘shortcutKeys’ 是否已經(jīng)有對(duì)應(yīng)的值了,如果沒(méi)有我們需要初始化一個(gè)值。

在 main.js 中,我們將重寫(xiě)全局快捷鍵的注冊(cè)方法,在之后我們更新設(shè)置的時(shí)候,會(huì)直接調(diào)用這個(gè)方法。將原來(lái)的注冊(cè)代碼改成以下內(nèi)容:

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);
    });
}

上述方法重置了全局快捷鍵的值,從設(shè)置中讀取熱鍵的數(shù)組,將它傳入加速器兼容字符串(Accelerator-compatible)并注冊(cè)新鍵。

設(shè)置窗口的交互

回到 settings.js 文件,我們需要綁定點(diǎn)擊事件來(lái)改變我們的全局快捷鍵。首先,我們將會(huì)遍歷復(fù)選框,記錄下被勾選的選項(xiàng)(從 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}

現(xiàn)在我們需要綁定復(fù)選框的行為??紤]到設(shè)置窗口(和它的渲染器進(jìn)程)是不允許改變 GUI 綁定的。這說(shuō)明我們需要從 setting.js 中發(fā)送信息(之后會(huì)處理這個(gè)信息的):

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');
}

這段代碼看起來(lái)比較長(zhǎng),但事實(shí)上它很簡(jiǎn)單。我們將會(huì)遍歷所有的復(fù)選框,并綁定點(diǎn)擊事件,在每次點(diǎn)擊的時(shí)候檢查設(shè)置數(shù)組中是否包含有熱鍵。根據(jù)檢查結(jié)果,更改數(shù)組,將結(jié)果保存到設(shè)置中,并向主進(jìn)程發(fā)送信息,更新我們的全局快捷鍵。

現(xiàn)在的工作就是在 main.js 中將 ipc 信息訂閱到“設(shè)置全局快捷鍵”頻道,并更新我們的全局快捷鍵:

ipc.on('set-global-shortcuts', function () {
    setGlobalShortcuts();});

就這么簡(jiǎn)單,我們的全局快捷鍵已經(jīng)可配置了!


菜單中要放什么?

接下來(lái)拉取 06-shortcuts-configurable:

git checkout 06-shortcuts-configurable

另一個(gè)在桌面應(yīng)用中的重要概念就是“菜單”,比如右鍵菜單(點(diǎn)擊右鍵出現(xiàn)的菜單)、托盤菜單(通常會(huì)有一個(gè)托盤 icon)和應(yīng)用菜單(在 OS X 中)等等。

在這一節(jié)中,我們將會(huì)添加一個(gè)托盤菜單。我們也將會(huì)借此機(jī)會(huì)嘗試在 remote 模塊中使用別的進(jìn)程間的通信方式。


remote 模塊從渲染器進(jìn)程到主進(jìn)程完成 RPC 類型的調(diào)用。將模塊引入的時(shí)候,這個(gè)模塊是在主進(jìn)程中被實(shí)例化的,所以它們的方法也會(huì)在主進(jìn)程中被執(zhí)行。實(shí)際開(kāi)發(fā)中,這個(gè)行為是在遠(yuǎn)程地請(qǐng)求 index.js 中的原生 GUI 模塊,然后又在 main.js 中調(diào)用 GUI 的方法。這么做的話,你需要在 index.js 中將 BrowserWindow 模塊引入,然后實(shí)例化一個(gè)新的瀏覽器窗口。其實(shí)在主進(jìn)程中有一個(gè)同步的調(diào)用,實(shí)際上是這個(gè)調(diào)用創(chuàng)建了新的瀏覽器窗口。

現(xiàn)在讓我們來(lái)看看要怎么樣創(chuàng)建一個(gè)菜單,并在渲染器進(jìn)程中將它綁定到一個(gè)托盤圖標(biāo)上。將下面這段代碼加入 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 模塊(菜單和托盤)通過(guò)remote模塊包含進(jìn)來(lái)比較安全。

OS X 支持圖片模板(將圖片文件名以 ‘Template’ 結(jié)尾,就會(huì)被定義成為圖片模板),托盤圖標(biāo)可以通過(guò)模板來(lái)定義,這樣我們的圖標(biāo)就會(huì)有“暗黑”和“光明”兩個(gè)主題了。其他的操作系統(tǒng)用正常的圖標(biāo)就行。

在 Electron 中有很多綁定菜單的方法。這里介紹的方法只是創(chuàng)建了一個(gè)菜單模板(將菜單項(xiàng)用數(shù)組的方式存儲(chǔ)),然后通過(guò)這個(gè)模板創(chuàng)建菜單,托盤 icon 再綁定上這個(gè)菜單,就實(shí)現(xiàn)了我們的菜單功能。


應(yīng)用打包

接下來(lái)拉取 07-ready-for-packaging:

git checkout 07-ready-for-packaging

如果你做了一個(gè)應(yīng)用結(jié)果人們連下載都下載不了,怎么會(huì)有人用呢?

通過(guò) electron-packager 你可以將應(yīng)用打包到全平臺(tái)。這一步驟在 shell 中就可以完成,將應(yīng)用打包好以后就能發(fā)布了。


它可以作為一個(gè)命令行應(yīng)用或者作為開(kāi)發(fā)應(yīng)用過(guò)程中的一步,構(gòu)建一個(gè)更復(fù)雜的開(kāi)發(fā)場(chǎng)景不是這篇文章要談的內(nèi)容,不過(guò)我們將通過(guò) npm 腳本讓?xiě)?yīng)用打包更簡(jiǎn)單一點(diǎn)。用 electron-packager 打包的命令是這樣的:

electron-packager <location of project> <name of project> <platform> <architecture> <electron version> <optional options>

以上命令:

  • 將目錄切換到項(xiàng)目所在路徑,

  • 參數(shù) ‘name of project’ 是你的項(xiàng)目名,參數(shù) ‘plateform’ 確定了你要構(gòu)建哪個(gè)平臺(tái)的應(yīng)用(Windows、Mac 還是 Linux),

  • 參數(shù) ‘a(chǎn)rchitecture’ 決定了使用 x86 還是 x64 還是兩個(gè)架構(gòu)都用,

  • 決定了使用的 Electron 版本。

  • 第一次打包應(yīng)用需要比較久的時(shí)間,因?yàn)樗衅脚_(tái)的二進(jìn)制文件都需要下載,之后打包應(yīng)用會(huì)比較快了。

在 Mac 上我是這么做的:

electron-packager ~/Projects/sound-machine SoundMachine --all --version=0.30.2 --out=~/Desktop --overwrite --icon=~/Projects/sound-machine/app/img/app-icon.icns

首先你要將圖標(biāo)的格式轉(zhuǎn)換成 .icns(在 Mac 上)或者 .ico(在 Windows 上),網(wǎng)絡(luò)上有工具可以把 PNG 做這樣的轉(zhuǎn)換(確保下載的圖片的擴(kuò)展名是 .icns 而不是 .hqx)。如果從非 Windows 的系統(tǒng)上打包了 Windows 的應(yīng)用,你應(yīng)該需要處理一下路徑(Mac 用戶可以用 brew,Linux 用戶可以用 apt-get)。

每次都要執(zhí)行這么長(zhǎng)的一句命令一點(diǎn)都不合理。所以你可以在 package.json 中添加另一個(gè)腳本。首先,將electron-packager 作為 development dependency 安裝:

npm install --save-dev electron-packager

然后在 package.json 中添加以下內(nèi)容:

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

接著執(zhí)行:

npm run-script package

打包命令啟動(dòng)了 electron-packager,在當(dāng)前目錄中查看項(xiàng)目,在 Desktop 目錄中構(gòu)建。如果你使用的是 Windows,腳本內(nèi)容需要一些細(xì)微的更新。

聲效器目前是 100MB 大小,不要擔(dān)心,當(dāng)你壓縮它之后,所占空間會(huì)減半。

如果你對(duì)此還有更大的計(jì)劃,可以看看 electron-builder,它是根據(jù) electron-packager 構(gòu)建出的應(yīng)用打包再做自動(dòng)安裝的處理。


添加其他的特性

現(xiàn)在你可以嘗試開(kāi)發(fā)別的功能了。

這里有一些方案,可以啟發(fā)你的靈感:

  • 應(yīng)用的使用手冊(cè),說(shuō)明了有那些快捷鍵和應(yīng)用作者,

  • 在應(yīng)用中給使用手冊(cè)添加一個(gè)圖標(biāo)和菜單入口,

  • 構(gòu)建一個(gè)打包腳本,用于快速構(gòu)建和分發(fā),

  • 使用* node-notifier *添加一個(gè)提示系統(tǒng),告訴用戶正在播放哪一個(gè)聲音,

  • 使用* lodash *讓你的代碼更加干凈、具有更好的擴(kuò)展性,

  • 在打包之前不要忘了壓縮你的 CSS 和 JavaScript,

  • 結(jié)合上文提到的* node-notifier *和一個(gè)服務(wù)器端的調(diào)用,通知用戶是否需要更新版本……

還有一個(gè)值得一試的東西 – 將代碼中關(guān)于瀏覽器窗口的邏輯抽離出來(lái),通過(guò)類似 browserify 的工具創(chuàng)建一個(gè)和聲效器一樣的網(wǎng)頁(yè)。一份代碼,兩個(gè)產(chǎn)品(桌面端和 Web 引用)??釘懒耍?/p>

更深入研究 Electron

我們只是嘗試了 Electron 的冰山一角,想要知道監(jiān)控主機(jī)的電源情況、獲取當(dāng)前窗口的信息(比如光標(biāo)的位置)等,Eletron 都能幫你做到。

對(duì)于所有的內(nèi)置工具(通常在開(kāi)發(fā) Electron 應(yīng)用時(shí)使用),查看 Electron API 文檔

這些文檔在 Electron 的 github 倉(cāng)庫(kù)中都能找到。

Sindre Sorhus 正在維護(hù)一份 Electron 資源清單,在那個(gè)上面你可以看到很多非??岬捻?xiàng)目,還能了解到一些系統(tǒng)架構(gòu)做的很好的 Electron 應(yīng)用,這些都能給你的開(kāi)發(fā)帶來(lái)靈感。

Electron 是基于 io.js 的,大部分 Node.js 模塊都可以兼容,可以使用它們擴(kuò)展你的應(yīng)用。去  上看看有沒(méi)有合適的。

這樣就夠了嗎?

當(dāng)然不。

現(xiàn)在,可以來(lái)構(gòu)建一個(gè)更大型的應(yīng)用了。在這篇文章中,我?guī)缀鯖](méi)有說(shuō)到如何使用外部的庫(kù)或者構(gòu)建工具來(lái)構(gòu)建一個(gè)應(yīng)用,不過(guò)用 ES6 和 Typescript 的語(yǔ)法結(jié)合 Angular 和 React 來(lái)構(gòu)建 Electron 應(yīng)用也很簡(jiǎn)單,還可以用 gulp 或 grunt 構(gòu)建流程。

干嘛不用你最喜歡的語(yǔ)言,框架和工具,來(lái)試試構(gòu)建一個(gè) Filckr 同步工具(借助 Filckr API 和 node-filckrapi)或者一個(gè) Gmail 客戶端(使用 Google 的官方 Node.JS 客戶端庫(kù)?)

選一個(gè)自己感興趣的項(xiàng)目,開(kāi)工吧!

原文鏈接:Building a desktop application with Electron

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多