Основное содержание этой главы:
- Используйте JavaScript
Set
Структура данных для отслеживания нескольких окон- Облегчает связь между основным процессом и несколькими процессами рендеринга.
- Используйте Node API, чтобы проверить, на какой платформе работает приложение.
Теперь, когда начинается Fire Sale, создается окно для пользовательского интерфейса. Когда окно закрывается, приложение закрывается. Хотя такое поведение вполне приемлемо, мы часто хотим иметь возможность открывать несколько независимых окон. В этой главе мы преобразуем Fire Sale из приложения с одним окном в приложение, поддерживающее несколько окон. Попутно мы изучим новые API Electron, а также недавно добавленный JavaScript. Мы также рассмотрим решения проблем, возникающих при настройке главного процесса для взаимодействия с одним процессом средства визуализации и его рефакторинге для управления переменным числом процессов средства визуализации. Полный код в конце этой главы можно найти по адресу http://tinyurl.com/y4z9oj69. Однако у нас есть第4章-使用本机文件对话框和帮助进程间通讯
начинается ветка.
Рисунок 5.1. В главе 4 мы установили связь между основным процессом и процессом визуализации.
Рисунок 5.2. В этой главе мы обновим Fire Sale для поддержки нескольких окон и облегчения связи между ними.
Мы начинаем с создания экземпляра структуры данных Set, которая была добавлена в JavaScript в 2015 году и которая отслеживает все окна пользователя. Затем мы создаем функцию для управления жизненным циклом одного окна. После этого мы модифицируем функцию, созданную в главе 4, чтобы предложить пользователю выбрать файл и открыть его, чтобы он указывал на нужное окно. Кроме того, мы будем иметь дело с некоторыми распространенными непредвиденными обстоятельствами и другими проблемами, такими как окна, которые блокируют друг друга.
Создание и управление несколькими окнами
Наборы — это новая структура данных в JavaScript, добавленная в спецификации ES2015. Set — это набор уникальных элементов, в массиве могут быть повторяющиеся значения. Я решил использовать набор вместо массива, потому что проще удалять элементы. В этом листинге показано, как создать его в JavaScript.Set
.
Листинг 5.1. Создание коллекции, которая отслеживает новые окна: ./app/main.js
const windows = new Set();
Для массивов мы либо находим индекс окна и удаляем его, либо создаем массив без этого окна. Ни один из методов не похож на вызов Setdelete
метод и передача ссылки на окно для удаления.
Со структурой данных, которая отслеживает все окна приложения, следующим шагом будет созданиеBrowserWindow
(Листинг 5.2) Переместите из «Готового» слушателя событий из приложения в свою собственную функцию.
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
instance и добавьте его в набор окон, который мы создали в листинге 5.1. Далее мы повторяем шаги из предыдущих глав, чтобы создать новое окно. Закрытие окна удаляет его из коллекции, и, наконец, мы возвращаем ссылку на только что созданное окно, которое нам понадобится в следующей главе.
Когда приложение будет готово, вызовите новыйcreateWindow()
функции, как показано в листинге ниже. Приложения должны запускаться так же, как и до реализации этого изменения, но оно также закладывает основу для создания дополнительных окон в других контекстах.
Листинг 5.3. Создание окна, когда приложение готово: ./app/main.js
app.on('ready', () => {
createWindow();
});
Приложение запускается, как и раньше, но если вы попытаетесь нажать кнопку «Открыть файл», вы заметите, что оно не работает. Это потому, что мы все еще ссылаемся в некоторых местах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);
});
Когда мы реализовали разметку для пользовательского интерфейса в главе 3, мы включили кнопку «Новый файл». Теперь мы реализуем и импортируем в основной процесс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);
});
Улучшите опыт создания новых окон
После реализации прослушивателя событий из предыдущей главы и нажатия новой кнопки «Файл» вы можете не понять, правильно ли он работает. Возможно, вы заметили, что тени вокруг окна потускнели, или, возможно, вы щелкнули и перетащили новое окно, и предыдущее окно ниже открылось.
Небольшая проблема, с которой мы столкнулись сейчас, заключается в том, что каждое новое окно появляется в той же позиции по умолчанию, что и первое окно, и полностью закрывает его. Более очевидно, что новое окно создается, если оно немного смещено от предыдущего, как показано на рис. 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
В macOS многие (но не все) приложения остаются открытыми, даже когда все окна закрыты. Например, если вы закроете все окна в Chrome, приложение по-прежнему будет активно в доке и по-прежнему будет отображаться в переключателе приложений. Fire Sale не может этого сделать.
В первых нескольких главах это может быть приемлемо. У нас есть только одно окно, и мы не можем создавать другие окна. В этом разделе мы позволяем приложениям оставаться открытыми только в macOS. По умолчанию, когда Электрон запускает свой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 — это операционная система UNIX, на которой построена macOS. В листинге 5.11 мы проверяем,process.platform
равныйdarwin
Если да, то приложение работает на MacOS, надеемся вернутьfalse
чтобы предотвратить действие по умолчанию.
Сохранение работоспособности приложения — это полдела. Что произойдет, если пользователь нажмет на приложение в доке, не открывая окно? В этом случае 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
};
Суммировать
- При создании приложения Electron с несколькими окнами мы не можем усилить основной процесс для отправки данных из окна.
- Мы можем использовать электрон
remote
Модуль запрашивает ссылку на себя у окна в процессе визуализации и отправляет эту ссылку при общении с основным процессом. - Приложения в macOS не всегда закрываются, когда все окна закрыты, мы можем использовать Node
process
объект, чтобы определить, на какой платформе работает приложение. - если
process.platform
даdarwin
, приложение работает на macOS. - При прослушивании заявки
windows-all-closed
В функции события верните false, чтобы предотвратить выход из приложения. - В macOS приложение запускается, когда пользователь щелкает значок в доке.
activate
мероприятие. -
activate
Мероприятие содержитhasVisibleWindows
Логическое значение, переданное в качестве второго параметра функции обратного вызова. Если окно в данный момент открыто, тоtrue
; если окна нет, тоfalse
. Мы можем использовать его, чтобы решить, следует ли открывать новое окно.