本章主要內容:javascript
- 使用JavaScript
Set
數據結構跟蹤多個窗口- 促進主進程和多個渲染器進程之間的通訊
- 使用Node APIs檢查應用程序運行在那個平臺上
如今,當Fire Sale啓動時,它爲UI建立一個窗口。當該窗口關閉時,應用程序退出。雖然這種行爲徹底能夠接受,但咱們一般但願可以打開多個獨立的窗口。在本章中,咱們將Fire Sale從一個單窗口應用程序轉換爲一個支持多個窗口的應用程序。在此過程當中,咱們將探索新的Electron APIs以及一些最近添加的JavaScript。咱們還將探討在將一個主進程配置爲與一個渲染器進程通訊,並對其進行重構以管理可變數量的渲染器進程時出現的問題的解決方案。本章末尾的完整代碼能夠在http://tinyurl.com/y4z9oj69。 然而咱們從第4章-使用本機文件對話框和幫助進程間通信
的分支開始。html
圖5.1 在第四章中,咱們創建了主進程和一個渲染進程之間的通訊。java
圖5.2 在本章中,咱們將更新Fire Sale以支持多個窗口並促進他們之間的溝通。node
咱們首先實例化一個Set數據結構,該結構於2015年添加到JavaScript中,跟蹤用戶的全部窗口。接下來,咱們建立一個函數來管理單個窗口的生命週期。在這以後,咱們修改在第4章中建立的函數,以提示用戶選擇一個文件並打開它以指向正確的窗口。此外,咱們還將處理一些常見的突發狀況和沿途出現的其餘問題,好比互相遮擋的窗口。linux
Sets 是JavaScript的一個新的數據結構,是在ES2015規範中添加的。Set是惟一元素的集合;數組中能夠有重複的值。我選擇使用set而不是數組,由於這樣更容易刪除元素。這個清單顯示瞭如何用JavaScript建立一個Set
。web
列表5.1 建立一個跟蹤新窗口的集合: ./app/main.jswindows
const windows = new Set();
複製代碼
對於數組,咱們要麼找到窗口的索引並刪除它,要麼建立一個沒有該窗口的數組。這兩種方法都不像調用Set上的delete
方法並將引用傳遞給要刪除的窗口那樣簡單。數組
有了跟蹤應用程序全部窗口的數據結構,下一步是將建立BrowserWindow
(列表5.2)從應用程序的"ready"事件監聽器移到它本身的函數中。瀏覽器
const createWindow = exports.createWindow = () => {
let newWindow = new BrowserWindow({
show: false,
webPreferences: {
// WebPreferences中的nodeIntegrationInWorker選項設置爲true
nodeIntegration: true
}
});
newWindow.loadFile('app/index.html');
newWindow.once('ready-to-show', () => {
newWindow.show();
});
newWindow.on('closed', () => {
windows.delete(newWindow); //從已關閉的窗口Set中移除引用
newWindow = null;
});
windows.add(newWindow); //將窗口添加到已打開時設置的窗口
return newWindow;
};
複製代碼
這個createWindow()
函數建立一個BrowserWindow
實例並將其添加到咱們在清單5.1中建立的一組窗口中。接下來,咱們重複前面幾章中建立新窗口的步驟。關閉窗口將其從集合中移除,最後,咱們返回對剛剛建立的窗口的引用,咱們下一章須要這個參考資料。markdown
當應用程序準備好,調用新的createWindow()
函數,以下面的清單所示。應用程序應該以與實現此更改以前相同的方式啓動,但它也爲在其餘上下文中建立額外的窗口奠基了基礎。
列表5.3 在應用程序就緒時建立窗口: ./app/main.js
app.on('ready', () => {
createWindow();
});
複製代碼
應用程序像之前同樣啓動,可是若是您嘗試單擊Open File按鈕,您會注意到它已經壞了。這是由於咱們仍然在一些地方引用mainWindow
。它在dialog.showOpenDialog()
中引用,以在macOS中將對話框顯示爲工做表。最重要的是,在從文件系統讀取文件內容並將其發送到窗口以後,openFile()
中引用了它。
擁有多個窗口會引起一個問題:咱們將文件路徑和內容發送到那個窗口?爲了支持多個窗口,這兩個函數必須引用應該顯示對話框的窗口和發送內容,如圖5.3所示。
圖5.3 要肯定要將文件的內容發送到那個窗口,渲染器進程在與調用
getFileFromUser()
的主進程通訊時必須發送對自身的引用。
在清單5.4中,讓咱們重構getFileFromUser()
函數,以接受一個給定的窗口做爲一個參數,而不是老是假設範圍中有一個mainWindow實例。
列表5.4 重構
getFileFromUser()
以處理特定的窗口: ./app/main.js
const getFileFromUser = exports.getFileFromUser = (targetWindow) => { //獲取對瀏覽器窗口的引用,以肯定應該顯示文件對話框的窗口,而後加載用戶選擇的文件。
const files = dialog.showOpenDialog(targetWindow, { //showopendialog()獲取對瀏覽器窗口對象的引用。
properties: ['openFile'],
filters: [
{ name: 'Text Files', extensions: ['txt'] },
{ name: 'Markdown Files', extensions: ['md', 'markdown'] }
]
});
if (files) { openFile(targetWindow, files[0]); } // openFile()函數做用是:獲取對瀏覽器窗口對象的引用,以肯定那個窗口應該接受用戶打開的文件的內容。
};
複製代碼
在代碼清單中,咱們修改了getFileFromUser()
,將對窗口的引用做爲參數。我避免命名參數窗口,由於它可能與瀏覽器中的全局對象混淆。在用戶選擇了一個文件以後,除了文件路徑以外,咱們還將targetWindow
傳遞給openFile()
,以下所示。
列表5.5 重構openFile()以處理特定的窗口: ./app/main.js
const openFile = exports.openFile = (targetWindow, file) => { // 接受對瀏覽器窗口對象的引用
const content = fs.readFileSync(file).toString();
targetWindow.webContents.send('file-opened', file, content); // 將文件的內容發送到提供的瀏覽器窗口
};
複製代碼
從文件系統讀取文件內容以後,咱們將文件的路徑和內容做爲第一個參數傳入併發送到窗口。這就提出了一個問題:咱們如何得到對窗口的引用。
使用remote
模塊從渲染器進程調用getFileFromUser()
,以便與主進程通訊。正如咱們在前一章中看到的,remote
模塊包含對全部模塊的引用,不然這些模塊只對主進程可用。原來remote
還有一些其餘方法,尤爲是remote
還有一些其餘方法,尤爲是remote.getCurrentWindow()
,它返回對調用它的BrowserWindow
實例,以下所示。
列表5.6 在渲染器進程中獲取對當前窗口的引用: ./app/renderer.js
const currnetWindow = remote.getCurrentWindow();
複製代碼
如今咱們有了對窗口的引用,完成該特性的最後一步是將它傳遞給getFileFromUser()
。這讓主進程中的函數知道它們正在使用的是什麼瀏覽器窗口。
openFileButton.addEventListener('click', () => {
mainProcess.getFileFromUser(currnetWindow);
});
複製代碼
當咱們在第三章中爲UI實現Markup時,咱們包括了一個New File按鈕。咱們如今在主進程中實現並導入createWindow()
函數,咱們也能夠很快地把那個按鈕鏈接起來。
列表5.8 向newFileButton添加監聽器: ./app/renderer.js
newFileButton.addEventListener('click', ()=> {
mainProcess.createWindow();
})
複製代碼
咱們能夠在主進程中對多個窗口的實現作一些加強,可是咱們已經完成了本章的渲染器進程。下面是app/renderer.js中文件的全部代碼。
列表5.9 newFileButton在渲染器進程中的實現: ./app/renderer.js
const { remote, ipcRenderer } = require('electron');
const mainProcess = remote.require('./main.js')
const currnetWindow = remote.getCurrentWindow();
const marked = require('marked');
const markdownView = document.querySelector('#markdown');
const htmlView = document.querySelector('#html');
const newFileButton = document.querySelector('#new-file');
const openFileButton = document.querySelector('#open-file');
const saveMarkdownButton = document.querySelector('#save-markdown');
const revertButton = document.querySelector('#revert');
const saveHtmlButton = document.querySelector('#save-html');
const showFileButton = document.querySelector('#show-file');
const openInDefaultButton = document.querySelector('#open-in-default');
const renderMarkdownToHtml = (markdown) => {
htmlView.innerHTML = marked(markdown, { sanitize: true });
};
markdownView.addEventListener('keyup', (event) => {
const currentContent = event.target.value;
renderMarkdownToHtml(currentContent);
});
newFileButton.addEventListener('click', () => {
mainProcess.createWindow();
});
openFileButton.addEventListener('click', () => {
mainProcess.getFileFromUser(currentWindow);
});
ipcRenderer.on('file-opened', (event, file, content) => {
markdownView.value = content;
renderMarkdownToHtml(content);
});
複製代碼
在實現上一章中的事件監聽器以後單擊new File按鈕,您可能會對它是否正常工做感到困惑。您可能已經注意到窗口周圍的陰影變暗了,或者您可能單擊並拖動了新窗口,並顯示了下面的前一個窗口。
咱們如今遇到的一個小問題是,每一個新窗口都出如今與第一個窗口相同的默認位置,而且徹底遮住了它。更明顯的是,若是新窗口與前一個窗口稍微偏移,就會建立新窗口,如圖5.4所示。這個清單顯示瞭如何偏移窗口。
清單5.10 基於當前焦點窗口偏移新窗口: ./app/main.js
const createWindow = exports.createWindow = () => {
let x,y;
const currentWindow = BrowserWindow.getFocusedWindow(); //獲取當前活動的瀏覽器窗口。
if(currentWindow) { //若是上一步中有活動窗口,則根據當前活動窗口的右下方設置下一個窗口的座標
const [ currentWindowX, currentWindowY ] = currentWindow.getPosition();
x = currentWindowX + 10;
y = currentWindowY +10;
}
let newWindow = new BrowserWindow({
x,
y,
show: false,
webPreferences: {
// WebPreferences中的nodeIntegrationInWorker選項設置爲true
nodeIntegration: true
}
}); //建立新窗口,首先使用x和y座標隱藏它。若是上一步中代碼運行了,則設置這些值;若是沒有運行,則未定義這些值,在這種狀況下,將在默認位置建立窗口。
newWindow.loadFile('app/index.html');
newWindow.once('ready-to-show', () => {
newWindow.show();
});
newWindow.on('closed', () => {
windows.delete(newWindow);
newWindow = null;
});
windows.add(newWindow);
return newWindow;
};
複製代碼
除了使用new
關鍵字實例化實例外,BrowserWindow
模塊還有本身的方法。咱們可使用BrowserWindow.getFocusedWindow()得到對用戶當前正在使用的窗口的引用。當應用程序第一次準備好並調用createWindow()
時,沒有一個焦點窗口,`BrowserWindow.getFocusedWindow()
返回undefined
。若是有一個窗口,咱們調用它的getWindow()
方法,該方法返回一個此窗口的x和y座標的數組。咱們將把這些值存儲在條件塊以外的兩個變量中,並將它們傳遞給BrowserWindow構造函數。若是它們仍然是未定義的(例如,沒有焦點窗口),那麼Electron將使用缺省值,就像咱們實現此功能以前所作的那樣。圖5.4顯示了與第一個窗口相比的第二個窗口偏移量。
圖5.4 新窗口偏移當前窗口
這不是實現此功能的惟一方法。或者,您能夠跟蹤初始的x和y位置,並在每一個新窗口上增長這些值。或者,您能夠爲默認的x和y值添加一點隨機性,這樣每一個窗口都是稍微偏移量。我把這些方法留給讀者做爲練習。
在macOS中,即便全部的窗口都關閉了,許多(但不是全部)應用程序仍然保持打開狀態。例如,若是您關閉了Chrome中的全部窗口,應用程序在dock中仍然出於活動狀態,而且仍然出如今應用程序切換器中。Fire Sale不能作到這點。
在前幾張章中,這多是能夠接受的。咱們只有一個窗口,沒法建立其餘窗口。在本節中,咱們只容許應用程序在macOS中保持打開狀態。默認狀況下,當Electron觸發它的window-all-closed
事件時,它將退出應用程序。若是咱們想要阻止這種行爲,咱們必須監聽這個事件,而且在macOS上運行時有條件地阻止它關閉。
列表5.11 在關閉全部窗口時保持應用程序的活動狀態: ./app/main.js
app.on('window-all-closed', () => {
if(process.platform === 'darwin') { //檢查應用程序是否在macOS上運行
return false; //若是是,則返回false以防止默認操做
}
app.quit(); //若是不是,則退出應用程序
});
複製代碼
process
對象由Node提供,不須要配置全局可用。process.platform
返回當前執行應用程序的平臺名稱。在截至寫做時間點,process.platform
返回七個字符串之一: aix
,darwin
,freebsd
,linux
,openbsd
,sunos
或win32
。Darwin是構建macOS的UNIX操做系統。在清單5.11中,咱們檢查了是否process.platform
等於darwin
,若是是,則應用程序正在macOS上運行,咱們但願返回false
以阻止默認操做的發生。
保持應用程序的活動是成功的一半,若是用戶單擊dock中的應用程序而沒有打開窗口,會發生什麼?在這種狀況下,Fire Sale應該打開一個新窗口並顯示給用戶,以下所示。
圖5.12 在應用程序打開時建立一個窗口,但沒有窗口: ./app/main.js
app.on('activate', (event, hasVisibleWindows) => { //Electron提供了hasVisibleWindows參數,它將是一個布爾值。
if(!hasVisibleWindows) { createWindow(); } //若是用戶激活應用程序時沒有可見窗口,則建立一個。
});
複製代碼
activate
事件將兩個參數傳遞給提供的回調函數。第一個是event
對象,第二個是布爾值,若是任何窗口均可見,則返回true
;若是全部窗口都關閉,則返回false
.對於後者,咱們調用本章前面編寫的createWindow()
函數。
activate
事件只在macOS上觸發,可是有不少緣由能夠解釋爲何您可能選擇讓您的應用程序在Windows或Linux上保持打開狀態,特別是若是應用程序正在運行後臺進程,而您但願繼續運行這些進程,即便該窗口被關閉。另外一種可能性是,您的應用程序能夠隱藏,或者使用全局快捷方式顯示,或者從托盤或菜單欄中顯示。咱們將在後面的章節中實現這些。
經過這兩個額外的事件,咱們將Fire Sale從單窗口應用程序轉換爲支持多窗口的應用。這個清單顯示了主進程當前狀態的代碼。
列表5.13 在主進程中實現多個窗口: ./app/main.js
const{ app, BrowserWindow,dialog } = require('electron');
const fs = require('fs');
const windows = new Set();
app.on('ready', () => {
createWindow();
});
app.on('window-all-closed', () => {
if(process.platform === 'darwin') {
return false;
}
});
app.on('activate', (event, hasVisibleWindows) => {
if(!hasVisibleWindows) { createWindow(); }
});
const createWindow = exports.createWindow = () => {
let x,y;
const currentWindow = BrowserWindow.getFocusedWindow();
if(currentWindow) {
const [ currentWindowX, currentWindowY ] = currentWindow.getPosition();
x = currentWindowX + 10;
y = currentWindowY +10;
}
let newWindow = new BrowserWindow({
x,
y,
show: false,
webPreferences: {
// WebPreferences中的nodeIntegrationInWorker選項設置爲true
nodeIntegration: true
}
});
newWindow.loadFile('app/index.html');
newWindow.once('ready-to-show', () => {
newWindow.show();
});
newWindow.on('closed', () => {
windows.delete(newWindow);
newWindow = null;
});
windows.add(newWindow);
return newWindow;
};
const getFileFromUser = exports.getFileFromUser = (targetWindow) => {
const files = dialog.showOpenDialog(targetWindow, {
properties: ['openFile'],
filters: [
{ name: 'Text Files', extensions: ['txt'] },
{ name: 'Markdown Files', extensions: ['md', 'markdown'] }
]
});
if (files) { openFile(targetWindow, files[0]); } // A
};
const openFile = (targetWindow, file) => {
const content = fs.readFileSync(file).toString();
targetWindow.webContents.send('file-opened', file, content); // B
};
複製代碼
remote
模塊向渲染器進程中的窗口請求對自身的引用,並在與主進程通訊時發送該引用。process
對象來肯定應用程序在那個平臺上運行。process.platform
是darwin
,則應用程序在macOS上運行。windows-all-closed
事件的函數中,返回false從而防止應用程序退出。activate
事件。activate
事件包含一個名爲hasVisibleWindows
的布爾值,做爲傳遞給回調函數的第二個參數。 若是當前有窗口打開,則爲true
;若是沒有窗口,則爲false
。咱們能夠用它來決定是否應該打開一個新窗口。