Разработка кроссплатформенных настольных приложений с помощью JS: от принципа к практике

JavaScript Electron

Управляемое чтение

использоватьElectronКлиентская программа разрабатывалась давно, общее впечатление пока очень хорошее.Есть и подводные камни.Статья от [принцип работы] до [практическое применение].ElectronСделайте систематический вывод. [Несколько картинок, предупреждение о длинном тексте~]

Все коды примеров в этой статье находятся в моемgithub electron-reactВыше статью лучше читать в сочетании с кодом. Кроме тогоelectron-reactтакже может использоваться какElectron + React + Mobx + WebpackРазработка строительных лесов для стека технологий.

1. Настольные приложения

Настольные приложения, также известные как программы с графическим интерфейсом пользователя (Graphical User Interface), но есть некоторые отличия от программ с графическим интерфейсом. Настольное приложение Преобразование программ с графическим интерфейсом пользователя из графического пользовательского интерфейса в «рабочий стол» делает холодную, подобную дереву компьютерную концепцию более гуманной, яркой и динамичной.

Различные клиентские программы, используемые на наших компьютерах, являются настольными приложениями, и в последние годыWEBИ рост мобильных терминалов постепенно затмил настольные приложения, но настольные приложения по-прежнему важны для некоторых повседневных функций или отраслевых приложений.

Традиционными методами разработки настольных приложений обычно являются следующие два:

1.1 Нативная разработка

Скомпилируйте язык прямо в исполняемый файл и вызовите систему напрямуюAPI, полный рисунок пользовательского интерфейса и т. д. Этот тип технологии разработки имеет высокую эффективность работы, но, как правило, скорость разработки ниже, а технические требования выше, например:

  • использоватьC++ / MFCразвиватьWindowsприменение
  • использоватьObjective-CразвиватьMACприменение

1.2 Платформа хостинга

Вначале была локальная разработка и разработка пользовательского интерфейса. После одной компиляции получается промежуточный файл, а операция компиляции или интерпретации вторичной загрузки выполняется через платформу или виртуальную машину. Операционная эффективность ниже, чем у нативной компиляции, но после оптимизации платформы ее эффективность также значительна. С точки зрения скорости разработки, она быстрее собственной технологии компиляции. Например:

  • использоватьC# / .NET Framework(только развиватьWindows应用)
  • Java / Swing

Тем не менее, два вышеупомянутых слишком недружелюбны к фронтенд-разработчикам, и в основном это области, в которые фронтенд-разработчики не вовлекаются.WEBТо, как технология развивалась, клиент появился из ниоткуда.

1.3 Веб-разработка

использоватьWEBтехнологии для разработки, используя движок браузера для завершенияUIрендерить, использоватьNode.jsРеализовать серверную частьJSЗапрограммируйте и вызовите системуAPI, вы можете думать об этом как о клиентской оболочкеWEBприменение.

На интерфейсе,WEBМощная экосистемаUIЭто дает бесконечные возможности, а затраты на разработку и обслуживание относительно низки.WEBФронтенд-разработчики с опытом разработки могут легко начать разработку.

В этой статье речь пойдет об использованииWEBОдна из технологий разработки клиентских программ [electron

2. Электрон

ElectronОтGithubразвиваться сHTML,CSSиJavaScriptБиблиотека с открытым исходным кодом для создания кроссплатформенных настольных приложений.ElectronпоставивChromiumиNode.jsв ту же среду выполнения и упаковать его какMac,WindowsиLinuxПриложение в системе достигает этой цели.

2.1 Причины для разработки с Electron:

  • Используйте сильный экологическийWebРазвитие технологий, низкая стоимость разработки, сильная масштабируемость, более крутойUI
  • Кроссплатформенный, набор кода может быть упакован какWindows、Linux、MacТри комплекта софта, и компиляция быстрая
  • непосредственно на существующемWebРасширьте приложение, чтобы предоставить возможности, которых нет в браузерах.
  • Ты фронтенд 👨💻~

Конечно, надо признать и его недостатки: производительность ниже, чем у нативного десктопного приложения, а конечное упакованное приложение намного больше нативного приложения.

2.2 Опыт разработки

совместимость

Хотя вы все еще используетеWEBтехнология для разработки, но вам больше не нужно учитывать вопросы совместимости, вам нужно только заботиться о своем текущем использованииElectronверсия, соответствующаяChromeверсия, которая, как правило, является достаточно новой, чтобы вы могли использовать последнююAPIи синтаксис, вы также можете вручную обновитьChromeВерсия. Точно так же вам не нужно беспокоиться о проблемах стиля и совместимости кода с различными браузерами.

Среда узла

Это может быть функция, о которой мечтали многие фронтенд-разработчики.WEBиспользуется в интерфейсеNode.jsМощныйAPI, а это значит, что выWEBСтраница может напрямую управлять файлом и вызывать системуAPIи даже манипулировать базой данных. Конечно, помимо полногоNode API, вы также можете использовать дополнительные сотни тысячnpmмодуль.

перекрестный домен

вы можете напрямую использоватьNodeкоторый предоставилrequestМодули выполняют сетевые запросы, а это значит, что вам больше не нужно беспокоиться о междоменном взаимодействии.

мощная расширяемость

с помощьюnode-ffi, обеспечивающий мощную расширяемость приложений (подробнее об этом в последующих главах).

2.3 Кто использует Электрон

Сейчас на рынке много приложенийElectronразвитые, в том числе и наши знакомыеVS Codeклиент,GitHubклиент,Atomклиент и так далее. Впечатленный, в прошлом году Xunlei выпустил Xunlei X10.1Когда текст:

Начиная с версии Xunlei X 10.1, мы полностью переписали основной интерфейс Xunlei с использованием программной среды Electron. Thunder X, использующий новый фреймворк, может отлично поддерживать дисплеи высокой четкости, такие как 2K и 4K, а рендеринг текста в интерфейсе также более четкий и четкий. С технической точки зрения отрисовка интерфейса и обработка событий нового фреймворка более гибкие и эффективные, чем в старом фреймворке, поэтому беглость интерфейса также значительно лучше, чем у Thunderbolt старого фреймворка. Что касается конкретного улучшения? Вы узнаете, когда попробуете.

ты можешь открытьVS Code, нажмите [Справка] [Переключить инструменты разработчика] для отладкиVS Codeклиентский интерфейс.

3. Принцип работы электрона

ElectronКомбинированныйChromium,Node.jsи для вызова собственных функций ОСAPI.

3.1 Chromium

ChromiumдаGoogleдля развитияChromeПроект с открытым исходным кодом, запущенный браузером,ChromiumэквивалентноChromeИнженерная версия или экспериментальная версия, новые функции будут первыми вChromiumОн будет реализован наChromeвверх, такChromeФункция будет относительно отсталой, но более стабильной.

ChromiumзаElectronобеспечить мощныйUIВозможность разработки интерфейсов без оглядки на совместимость.

3.2 Node.js

Node.jsэто пустьJavaScriptПлатформа разработки, работающая на стороне сервера,NodeИспользуйте управляемые событиями, неблокирующиеI/Oмодель должна быть легкой и эффективной.

в одиночествеChromiumне способен к прямому манипулированию роднымGUIспособный,Electronинтегрированы вNodejs, что позволяет ему иметь нижний слой операционной системы при разработке интерфейсаAPIСпособность,Nodejsобычно используется вPath、fs、Cryptoждем модуль вElectronМожно использовать напрямую.

3.3 Системный API

Для того, чтобы обеспечить родную системуGUIслужба поддержки,ElectronВстроенный собственный интерфейс прикладной программы обеспечивает поддержку некоторых системных функций, таких как вызов системных уведомлений и открытие системных папок.

В режиме разработки,Electronвызов системыAPIОн разработан отдельно от интерфейса рисования, давайте посмотрим.ElectronО том, как разделен процесс.

3.4 Основной процесс

ElectronРазличают два вида процессов: основной процесс и процесс рендеринга, каждый из которых отвечает за свои функции.

Electronбегатьpackage.jsonизmainПроцесс скрипта называется основным процессом. ОдинElectronПриложения всегда имеют один и только один главный процесс.

Обязанности:

  • Создать процесс рендеринга (несколько)
  • Управляет жизненным циклом приложения (запуск, выход)APPи правильноAPPпрослушать какое-нибудь событие)
  • Вызывать базовые функции системы и вызывать собственные ресурсы

Вызываемый API:

  • Node.js API
  • Electronпредусмотрен основной процессAPI(включая некоторые системные функции иElectronДополнительные возможности)

3.5 Процесс рендеринга

так какElectronиспользовалChromiumпоказыватьwebстраница, так чтоChromiumТакже используется многопроцессорная архитектура. каждыйElectronсерединаwebСтраница работает в собственном процессе рендеринга.

Основной процесс использует экземпляр BrowserWindow для создания страницы. Каждый экземпляр BrowserWindow запускает страницу в своем собственном процессе рендеринга. Когда экземпляр BrowserWindow уничтожается, соответствующий процесс рендеринга также прекращается.

Вы можете думать о процессе рендеринга как об окне браузера, он может существовать несколько и независимо друг от друга, но в отличие от браузера, он может вызыватьNode API.

Обязанности:

  • использоватьHTMLиCSSвизуализировать интерфейс
  • использоватьJavaScriptСделайте некоторое взаимодействие с интерфейсом

Вызываемый API:

  • DOM API
  • Node.js API
  • Electronпредоставленный процесс рендерингаAPI

4. Электронные основы

4.1 Electron API

В предыдущем разделе мы упомянули, что рендерер и основной процесс можно вызывать соответственно.Electron API. всеElectronизAPIВсе они назначены типу процесса. многиеAPIможно использовать только в основном процессе, некоторыеAPIЕго можно использовать только в процессе рендеринга, и можно использовать некоторые основные процессы и процессы рендеринга.

Вы можете получить его следующим образомElectron API

const { BrowserWindow, ... } = require('electron')

Ниже приведены некоторые часто используемыеElectron API:

В следующих главах мы выберем часто используемые модули для подробного ознакомления.

4.2 Использование API Node.js

вы можете в то же времяElectronИспользование основного процесса и процесса визуализацииNode.js API,) все вNode.jsгодный к употреблениюAPI,существуетElectronтакже можно использовать в .

import {shell} from 'electron';
import os from 'os';

document.getElementById('btn').addEventListener('click', () => { 
  shell.showItemInFolder(os.homedir());
})

Одно очень важное замечание: нативные модули Node.js (то есть модули, которые необходимо скомпилировать из исходного кода, прежде чем их можно будет использовать) необходимо скомпилировать, прежде чем их можно будет использовать с Electron.

4.3 Технологическая коммуникация

Хотя основной процесс и процесс рендеринга имеют разные обязанности, они также должны взаимодействовать и общаться друг с другом.

Например: вwebнативное управление страницамиGUIРесурсы очень опасны и могут легко привести к утечке ресурсов. так вwebстраница, на которой не разрешены прямые вызовы нативныхGUIСвязанныйAPI. Если процесс рендеринга хочет быть нативнымGUIЧтобы работать, вы должны связаться с основным процессом и запросить основной процесс для завершения этих операций.

4.4 Процесс рендеринга взаимодействует с основным процессом

ipcRendererЯвляетсяEventEmitterпример. Вы можете использовать некоторые из предоставляемых им методов для отправки синхронных или асинхронных сообщений из процесса визуализации в основной процесс. Также возможно получать сообщения, на которые отвечает основной процесс.

Представлен в процессе рендерингаipcRenderer:

import { ipcRenderer } from 'electron';

Отправить асинхронно:

пройти черезchannelОтправьте сообщение синхронизации основному процессу, которое может нести любые параметры.

Внутри параметр сериализуется какJSON, поэтому цепочка функций и прототипов для объекта параметра не отправляется.

ipcRenderer.send('async-render', '我是来自渲染进程的异步消息');

Отправить синхронно:

 const msg = ipcRenderer.sendSync('sync-render', '我是来自渲染进程的同步消息');

Примечание. Отправка синхронного сообщения заблокирует весь процесс рендеринга до тех пор, пока не будет получен ответ от основного процесса.

Основной процесс прослушивает сообщения:

ipcMainмодульEventEmitterЭкземпляр класса. При использовании в основном процессе он обрабатывает асинхронные и синхронные сообщения, отправленные из процесса визуализации (веб-страницы). Сообщения, отправленные из процесса визуализации, будут отправлены в этот модуль.

ipcMain.on:мониторchannel, когда получено новое сообщениеlistenerВоляlistener(event, args...)форма называется.

  ipcMain.on('sync-render', (event, data) => {
    console.log(data);
  });

4.5 Основной процесс взаимодействует с процессом рендеринга

в основном процессе черезBrowserWindowизwebContentsОтправьте сообщение процессу рендеринга, поэтому вы должны сначала найти соответствующий процесс рендеринга, прежде чем отправлять сообщение.BrowserWindowобъект. :

const mainWindow = BrowserWindow.fromId(global.mainId);
 mainWindow.webContents.send('main-msg', `ConardLi]`)

Отправлено согласно источнику:

существуетipcMainВ функции обратного вызова, которая принимает сообщение, передайте первый параметрeventсвойстваsenderВы можете получить исходный код процесса рендерингаwebContentsобъект, мы можем напрямую ответить на сообщение с помощью этого объекта.

  ipcMain.on('sync-render', (event, data) => {
    console.log(data);
    event.sender.send('main-msg', '主进程收到了渲染进程的【异步】消息!')
  });

Монитор процесса рендеринга:

ipcRenderer.on:мониторchannel, когда приходит новое сообщение, оно будет передано черезlistener(event, args...)перечислитьlistener.

ipcRenderer.on('main-msg', (event, msg) => {
    console.log(msg);
})

4.6 Принцип связи

ipcMainиipcRendererобаEventEmitterЭкземпляр класса.EventEmitterклассNodeJSсобытийная основа, состоящая изNodeJSсерединаeventsэкспорт модуля.

EventEmitterВ основе этого лежит инкапсуляция функций запуска событий и прослушивания событий. Он реализует интерфейсы, требуемые моделью событий, в том числеaddListener,removeListener, emitи другие инструменты и методы.JavaScriptСобытия аналогичны, с использованием подхода публикации/подписки (наблюдателя), с использованием внутренних_eventsСписок для записи зарегистрированных обработчиков событий.

мы проходимipcMainиipcRendererизon、sendПрослушивание и отправка сообщенийEventEmitterОпределены связанные интерфейсы.

4.7 remote

remoteМодуль связывается с основным процессом для процесса рендеринга (веб-страница) (IPC) обеспечивает простой способ. использоватьremoteмодуль, вы можете позвонитьmainметоды объекта процесса без необходимости явно отправлять межпроцессные сообщения, аналогичныеJavaизRMI.

import { remote } from 'electron';

remote.dialog.showErrorBox('主进程才有的dialog模块', '我是使用remote调用的')

Но на самом деле, когда мы вызываем метод или функцию удаленного объекта или создаем новый объект через удаленный конструктор, мы фактически отправляем синхронное межпроцессное сообщение.

пройти вышеremoteвызов модуляdialogв примере. мы создаем в процессе рендераdialogОбъект на самом деле не находится в нашем процессе рендеринга, он просто позволяет основному процессу создать объект.dialogобъект и возвращает соответствующий удаленный объект в процесс рендеринга.

4.8 Рендеринг межпроцессного взаимодействия

ElectronПроцессы рендеринга не могут взаимодействовать друг с другом, поэтому мы можем установить станцию ​​ретрансляции сообщений в основном процессе.

Связь между процессами рендеринга сначала отправляет сообщение основному процессу, а ретрансляционная станция основного процесса получает сообщение и распределяет его в соответствии с условиями.

4.9 Обмен данными процесса рендеринга

Самый простой способ обмена данными между двумя процессами рендеринга — использовать то, что уже реализовано в браузере.HTML5 API. Лучшим решением является использованиеStorage API,localStorage,sessionStorageилиIndexedDB。

Как и в браузере, это хранилище эквивалентно постоянному хранению части данных в приложении. Иногда вам не нужно такое хранилище, просто обмен данными на время жизни текущего приложения. В этот момент вы можете использоватьElectronвнутриIPCреализация механизма.

Сохраняйте данные в глобальной переменной в основном процессе и используйте их в нескольких процессах рендеринга.remoteмодуль для доступа к нему.

Инициализируйте глобальные переменные в основном процессе:

global.mainId = ...;
global.device = {...};
global.__dirname = __dirname;
global.myField = { name: 'ConardLi' };

Читать в процессе рендеринга:

import { ipcRenderer, remote } from 'electron';

const { getGlobal } = remote;

const mainId = getGlobal('mainId')
const dirname = getGlobal('__dirname')
const deviecMac = getGlobal('device').mac;

Изменено в процессе рендеринга:

getGlobal('myField').name = 'code秘密花园';

Несколько процессов рендеринга совместно используют глобальные переменные одного и того же основного процесса, так что может быть достигнут эффект совместного использования и передачи данных между процессами рендеринга.

5. Окно

5.1 BrowserWindow

основной технологический модульBrowserWindowИспользуется для создания и управления окнами браузера.

  mainWindow = new BrowserWindow({
    width: 1000,
    height: 800,
    // ...
  });
  mainWindow.loadURL('http://www.conardli.top/');

ты сможешьздесьПосмотреть все параметры его конструкции.

5.2 Безрамные окна

Безрамочное окно — это окно без хрома, а части окна (например, панели инструментов) не являются частью веб-страницы.

существуетBrowserWindowв конструктивных параметрахframeУстановить какfalseВы можете указать, что окно является окном без полей.После того, как панель инструментов будет скрыта, возникнут две проблемы:

  • 1. Кнопки управления окнами (минимизация, полноэкранный режим, кнопка закрытия) будут скрыты
  • 2. Невозможно перетащить мобильное окно

можно указать, указавtitleBarStyleчтобы снова отображать кнопки панели инструментов, установите для него значениеhiddenВозвращает полноразмерное окно содержимого со скрытой строкой заголовка и по-прежнему имеет стандартные элементы управления окном в верхнем левом углу.

new BrowserWindow({
    width: 200,
    height: 200,
    titleBarStyle: 'hidden',
    frame: false
  });

5.3 Перетаскивание окна

По умолчанию окна без полей нельзя перетаскивать. Мы можем использовать интерфейсCSSАтрибуты-webkit-app-region: dragВручную укажите область перетаскивания.

В безрамочном окне поведение перетаскивания может конфликтовать с выделением текста, что можно изменить, установив-webkit-user-select: none;Отключить выделение текста:

.header {
  -webkit-user-select: none;
  -webkit-app-region: drag;
}

Вместо этого установите внутри перетаскиваемой области-webkit-app-region: no-dragЗатем вы можете указать конкретную область без перетаскивания.

5.4 Прозрачное окно

поставивtransparentпараметры установлены наtrue, вы также можете сделать бескаркасное окно прозрачным:

new BrowserWindow({
    transparent: true,
    frame: false
  });

5.5 Webview

использоватьwebviewметка наElectronВстраивайте «чужой» контент в приложение. Иностранный контент включен вwebviewв контейнере. Встраивание страниц в ваше приложение может контролировать макет и перерисовку входящего контента.

иiframeразные,webviewЗапуск в другом процессе, отличном от приложения. Он не имеет таких же разрешений, как ваша веб-страница, и все взаимодействия между приложением и встроенным содержимым будут асинхронными.

6. Диалог

dialogмодуль обеспечиваетapiдля отображения собственных системных диалогов, таких как окно открытия файла,alertкоробка, такwebПриложения могут предоставлять пользователям те же возможности, что и системные приложения.

Примечание: диалоговое окно является основным модулем процесса, вы можете использовать удаленный доступ, если хотите вызвать его в процессе рендеринга.

6.1 Сообщение об ошибке

dialog.showErrorBoxИспользуется для отображения модального диалогового окна с сообщением об ошибке.

 remote.dialog.showErrorBox('错误', '这是一个错误弹框!')

6.2 Диалоги

dialog.showErrorBoxИспользуется для вызова системных диалогов, для которых можно указать несколько различных типов: "none", "info", "error", "question" или "warning".

В Windows «вопрос» отображает тот же значок, что и «информация», если вы не используете параметр «значок» для установки значка. В macOS «предупреждение» и «ошибка» отображают один и тот же значок предупреждения.

remote.dialog.showMessageBox({
  type: 'info',
  title: '提示信息',
  message: '这是一个对话弹框!',
  buttons: ['确定', '取消']
}, (index) => {
  this.setState({ dialogMessage: `【你点击了${index ? '取消' : '确定'}!!】` })
})

6.3 Файловый ящик

dialog.showOpenDialogИспользуется для открытия или выбора системного каталога.

remote.dialog.showOpenDialog({
  properties: ['openDirectory', 'openFile']
}, (data) => {
  this.setState({ filePath: `【选择路径:${data[0]}】 ` })
})

6.4 Информационное окно

Рекомендуется использовать непосредственноHTML5 API, который можно использовать только в процессе визуализации.

let options = {
  title: '信息框标题',
  body: '我是一条信息~~~',
}
let myNotification = new window.Notification(options.title, options)
myNotification.onclick = () => {
  this.setState({ message: '【你点击了信息框!!】' })
}

7. Система

7.1 Получить системную информацию

пройти черезremoteПерейти к основному процессуprocessобъект, вы можете получить информацию о каждой версии текущего приложения:

  • process.versions.electron:electronИнформация о версии
  • process.versions.chrome:chromeИнформация о версии
  • process.versions.node:nodeИнформация о версии
  • process.versions.v8:v8Информация о версии

Получить текущий корневой каталог приложения:

remote.app.getAppPath()

использоватьnodeизosМодуль получает текущую корневую директорию системы:

os.homedir();

7.2 Скопируйте и вставьте

Electronкоторый предоставилclipboardДоступно как в процессе визуализации, так и в основном процессе для выполнения операций копирования и вставки в системный буфер обмена.

Запишите в буфер обмена как обычный текст:

clipboard.writeText(text[, type])

Получить содержимое буфера обмена в виде обычного текста:

clipboard.readText([type])

7.3 Скриншоты

desktopCapturerИнформация об источниках мультимедиа, используемых для захвата аудио и видео с рабочего стола. Его можно вызвать только во время процесса рендеринга.

Ниже приведен пример кода, который делает снимок экрана и сохраняет его:

  getImg = () => {
    this.setState({ imgMsg: '正在截取屏幕...' })
    const thumbSize = this.determineScreenShotSize()
    let options = { types: ['screen'], thumbnailSize: thumbSize }
    desktopCapturer.getSources(options, (error, sources) => {
      if (error) return console.log(error)
      sources.forEach((source) => {
        if (source.name === 'Entire screen' || source.name === 'Screen 1') {
          const screenshotPath = path.join(os.tmpdir(), 'screenshot.png')
          fs.writeFile(screenshotPath, source.thumbnail.toPNG(), (error) => {
            if (error) return console.log(error)
            shell.openExternal(`file://${screenshotPath}`)
            this.setState({ imgMsg: `截图保存到: ${screenshotPath}` })
          })
        }
      })
    })
  }

  determineScreenShotSize = () => {
    const screenSize = screen.getPrimaryDisplay().workAreaSize
    const maxDimension = Math.max(screenSize.width, screenSize.height)
    return {
      width: maxDimension * window.devicePixelRatio,
      height: maxDimension * window.devicePixelRatio
    }
  }

8. Меню

Меню приложения может помочь нам быстро добраться до той или иной функции, не прибегая к ресурсам интерфейса клиента.В целом меню делится на два типа:

  • Меню приложения: вверху приложения, доступно глобально
  • Контекстное меню: Вы можете настроить отображение любой страницы, настроить вызов, например контекстное меню

Electronпредоставляет намMenuМодуль используется для создания собственных меню приложений и контекстных меню, это основной модуль процесса.

ты можешь пройтиMenuстатический методbuildFromTemplate(template), используя пользовательский шаблон меню для создания объекта меню.

templateЯвляетсяMenuItemмассив, давайте посмотримMenuItemНесколько важных параметров:

  • label: текст, отображаемый в меню
  • click: обработчик события после нажатия на меню
  • role: Системные предопределенные меню, например.copy(копия),paste(вставить),minimize(минимизировать)...
  • enabled: указывает, включен ли элемент, это свойство можно изменить динамически.
  • submenu: подменю, а такжеMenuItemмассив

Рекомендация: Лучше указать любой пункт меню, роль которого соответствует стандартной роли, чем пытаться реализовать поведение в функции щелчка вручную. Встроенное ролевое поведение обеспечит наилучшее локальное взаимодействие.

Следующий пример представляет собой простое менюtemplate.

const template = [
  {
    label: '文件',
    submenu: [
      {
        label: '新建文件',
        click: function () {
          dialog.showMessageBox({
            type: 'info',
            message: '嘿!',
            detail: '你点击了新建文件!',
          })
        }
      }
    ]
  },
  {
    label: '编辑',
    submenu: [{
      label: '剪切',
      role: 'cut'
    }, {
      label: '复制',
      role: 'copy'
    }, {
      label: '粘贴',
      role: 'paste'
    }]
  },
  {
    label: '最小化',
    role: 'minimize'
  }
]

8.1 Меню приложения

использоватьMenuстатический методsetApplicationMenu, чтобы создать меню приложения вWindowsиLinuxначальство,menuБудет установлено как меню верхнего уровня каждого окна.

Примечание. Это приложение API должно вызываться после события готовности модуля.

Мы можем обрабатывать меню по-разному в зависимости от разных жизненных циклов приложений и разных систем.

app.on('ready', function () {
  const menu = Menu.buildFromTemplate(template)
  Menu.setApplicationMenu(menu)
})

app.on('browser-window-created', function () {
  let reopenMenuItem = findReopenMenuItem()
  if (reopenMenuItem) reopenMenuItem.enabled = false
})

app.on('window-all-closed', function () {
  let reopenMenuItem = findReopenMenuItem()
  if (reopenMenuItem) reopenMenuItem.enabled = true
})

if (process.platform === 'win32') {
  const helpMenu = template[template.length - 1].submenu
  addUpdateMenuItems(helpMenu, 0)
}

8.2 Контекстное меню

использоватьMenuметод экземпляраmenu.popupНастраиваемое всплывающее контекстное меню.

    let m = Menu.buildFromTemplate(template)
    document.getElementById('menuDemoContainer').addEventListener('contextmenu', (e) => {
      e.preventDefault()
      m.popup({ window: remote.getCurrentWindow() })
    })

8.3 Ярлыки

В опциях меню мы можем указатьacceleratorсвойства, чтобы указать сочетания клавиш для операций:

  {
    label: '最小化',
    accelerator: 'CmdOrCtrl+M',
    role: 'minimize'
  }

Кроме того, мы также можем использоватьglobalShortcutзарегистрировать глобальные сочетания клавиш.

    globalShortcut.register('CommandOrControl+N', () => {
      dialog.showMessageBox({
        type: 'info',
        message: '嘿!',
        detail: '你触发了手动注册的快捷键.',
      })
    })

CommandOrControl означает клавишу Command в macOS и клавишу Control в Linux и Windows.

9. Печать

Во многих случаях печать, используемая в программе, невидима для пользователя. И если вы хотите гибко управлять содержимым печати, вам часто нужно использовать принтер, чтобы предоставить намapiЧтобы разработать снова, этот метод разработки очень громоздкий и сложный в разработке. Первое использование в бизнесеElectronНа самом деле, это использование его функции печати, поэтому я представлю здесь еще кое-что.

ElectronПредоставленный API-интерфейс печати может очень гибко управлять отображением параметров печати и может записывать содержимое печати через html.ElectronСуществует два способа печати: один — напрямую вызвать принтер для печати, другой — распечатать наpdf.

И есть два типа объектов, которые могут вызывать print:

  • пройти черезwindowизwebcontentОбъекту, использующему этот метод, требуется отдельное окно печати, которое можно скрыть, но коммуникационный вызов относительно сложен.
  • используя страницуwebviewЭлемент вызывает печать, вы можете поставитьwebviewОн скрыт на вызываемой странице, а способ связи относительно прост.

Вышеуказанные два метода имеют обаprintиprintToPdfметод.

9.1 Вызов системной печати

contents.print([options], [callback]);

В конфигурации печати (варианты) всего три простых настройки:

  • silent: Не отображать ли конфигурацию печати при печати (будет ли печатать без вывода сообщений)
  • printBackground: печатать ли фон
  • deviceName: имя устройства принтера

Во-первых, нам нужно настроить имя принтера, который мы используем, и мы должны сначала определить, доступен ли принтер, прежде чем вызывать print.

использоватьwebContentsизgetPrintersМетод может получить список принтеров, которые были настроены на текущем устройстве.Обратите внимание, что конфигурация недоступна, но драйвер был установлен на этом устройстве.

пройти черезgetPrintersПолученный объект принтера:электрон JS.org/docs/API/body…

Нас здесь волнуют только двое,nameиstatus,statusза0означает, что принтер доступен.

printвторой параметрcallbackЭто обратный вызов, используемый для определения того, запущена ли задача печати, а не обратный вызов после завершения задачи печати. Следовательно, будет выдано общее задание на печать, и будет вызвана функция обратного вызова, и будут возвращены параметры.true. Этот обратный вызов не определяет, действительно ли печать была успешной.

    if (this.state.curretnPrinter) {
      mainWindow.webContents.print({
        silent: silent, printBackground: true, deviceName: this.state.curretnPrinter
      }, () => { })
    } else {
      remote.dialog.showErrorBox('错误', '请先选择一个打印机!')
    }

9.2 Печать в PDF

printToPdfИспользование основных иprintто же самое, ноprintЭлементов конфигурации очень мало, иprintToPdfРасширены многие свойства. Просматривая здесь исходный код, я обнаружил, что еще много не вставленных в документ, около 30, в том числе возможность настроить поля печати, печать верхних и нижних колонтитулов и т.д.

contents.printToPDF(options, callback)

callbackФункция вызывается после сбоя печати или после успешного завершения печати для получения информации об ошибке печати или включенияPDFбуфер данных.

    const pdfPath = path.join(os.tmpdir(), 'webviewPrint.pdf');
    const webview = document.getElementById('printWebview');
    const renderHtml = '我是被临时插入webview的内容...';
    webview.executeJavaScript('document.documentElement.innerHTML =`' + renderHtml + '`;');
    webview.printToPDF({}, (err, data) => {
      console.log(err, data);
      fs.writeFile(pdfPath, data, (error) => {
        if (error) throw error
        shell.openExternal(`file://${pdfPath}`)
        this.setState({ webviewPdfPath: pdfPath })
      });
    });

Печать в этом примере выполняется с помощьюwebviewсделано, позвонивexecuteJavaScriptметод может быть динамически направленwebviewВставьте печать.

9.3 Выбор двух схем печати

упоминалось выше, используяwebviewиwebcontentВы можете вызвать функцию печати, используяwebcontentДля печати, во-первых, должно быть окно печати, это окно нельзя создать в любой момент для печати, которая более требовательна к производительности. Вы можете запускать его во время работы программы и делать мониторинг событий.

Этот процесс должен связаться с вызывающей стороной для печати.Общий процесс выглядит следующим образом:

Видно, что общение очень громоздкое, используяwebviewПечать может дать тот же эффект, но метод связи проще, потому что процесс рендеринга иwebviewОбщение не обязательно должно проходить через основной процесс, это можно сделать следующими способами:

  const webview = document.querySelector('webview')
  webview.addEventListener('ipc-message', (event) => {
    console.log(event.channel)
  })
  webview.send('ping');

  const {ipcRenderer} = require('electron')
  ipcRenderer.on('ping', () => {
    ipcRenderer.sendToHost('pong')
  })

ранее предназначенный дляELectronраспечатать написанноеDEMO:electron-print-demoинтересно можетcloneПроверьте это.

9.4 Пакет функций печати

Ниже приведены несколько инкапсуляций функций инструментов для общих функций печати.

/**
 * 获取系统打印机列表
 */
export function getPrinters() {
  let printers = [];
  try {
    const contents = remote.getCurrentWindow().webContents;
    printers = contents.getPrinters();
  } catch (e) {
    console.error('getPrintersError', e);
  }
  return printers;
}
/**
 * 获取系统默认打印机
 */
export function getDefaultPrinter() {
  return getPrinters().find(element => element.isDefault);
}
/**
 * 检测是否安装了某个打印驱动
 */
export function checkDriver(driverMame) {
  return getPrinters().find(element => (element.options["printer-make-and-model"] || '').includes(driverMame));
}
/**
 * 根据打印机名称获取打印机对象
 */
export function getPrinterByName(name) {
  return getPrinters().find(element => element.name === name);
}

10. Защита программы

10.1 Сбои

Мониторинг сбоев — важная функция защиты для каждой клиентской программы.

  • 1. Своевременно загружайте журнал сбоев и будильник
  • 2. Программа мониторинга аварийно завершает работу, предлагая пользователю перезапустить программу.

electronпредоставил намcrashReporterЧтобы помочь нам зарегистрировать сбой, мы можем передатьcrashReporter.startЧтобы создать отчет о сбоях:

const { crashReporter } = require('electron')
crashReporter.start({
  productName: 'YourName',
  companyName: 'YourCompany',
  submitURL: 'https://your-domain.com/url-to-submit',
  uploadToServer: true
})

При сбое программы журнал сбоев будет храниться во временной папке с именемYourName Crashesв папке с файлами.submitURLИспользуется для указания сервера загрузки журнала сбоев. Прежде чем запускать отчет о сбоях, вы можете сделать это, вызвавapp.setPath('temp', 'my/custom/temp')API для настройки пути сохранения этих временных файлов. Вы также можете пройтиcrashReporter.getLastCrashReport()чтобы получить дату последнего отчета о сбое иID.

мы можем пройтиwebContentsизcrashedДля отслеживания сбоя процесса рендеринга, а также сбоя некоторых основных процессов было протестировано срабатывание этого события. Так что мы можем согласно ГосподуwindowУничтожен он или нет, зависит от разной логики рестарта, вот логика всего краш-мониторинга:

import { BrowserWindow, crashReporter, dialog } from 'electron';
// 开启进程崩溃记录
crashReporter.start({
  productName: 'electron-react',
  companyName: 'ConardLi',
  submitURL: 'http://xxx.com',  // 上传崩溃日志的接口
  uploadToServer: false
});
function reloadWindow(mainWin) {
  if (mainWin.isDestroyed()) {
    app.relaunch();
    app.exit(0);
  } else {
    // 销毁其他窗口
    BrowserWindow.getAllWindows().forEach((w) => {
      if (w.id !== mainWin.id) w.destroy();
    });
    const options = {
      type: 'info',
      title: '渲染器进程崩溃',
      message: '这个进程已经崩溃.',
      buttons: ['重载', '关闭']
    }
    dialog.showMessageBox(options, (index) => {
      if (index === 0) mainWin.reload();
      else mainWin.close();
    })
  }
}
export default function () {
  const mainWindow = BrowserWindow.fromId(global.mainId);
  mainWindow.webContents.on('crashed', () => {
    const errorMessage = crashReporter.getLastCrashReport();
    console.error('程序崩溃了!', errorMessage); // 可单独上传日志
    reloadWindow(mainWindow);
  });
}

10.2 Свернуть в трей

Иногда мы хотим, чтобы пользователь не закрывал программу нажатием кнопки закрытия, а сворачивал программу в трей и выполнял настоящую операцию выхода в трее.

Во-первых, прослушайте событие закрытия окна, предотвратите поведение по умолчанию операции закрытия пользователя и скройте окно.

function checkQuit(mainWindow, event) {
  const options = {
    type: 'info',
    title: '关闭确认',
    message: '确认要最小化程序到托盘吗?',
    buttons: ['确认', '关闭程序']
  };
  dialog.showMessageBox(options, index => {
    if (index === 0) {
      event.preventDefault();
      mainWindow.hide();
    } else {
      mainWindow = null;
      app.exit(0);
    }
  });
}
function handleQuit() {
  const mainWindow = BrowserWindow.fromId(global.mainId);
  mainWindow.on('close', event => {
    event.preventDefault();
    checkQuit(mainWindow, event);
  });
}

На данный момент программу уже не найти, а в панели задач программы нет, поэтому нам нужно создать панель задач и следить за событиями.

Использование платформы Windowsicoфайл для лучших результатов

export default function createTray() {
  const mainWindow = BrowserWindow.fromId(global.mainId);
  const iconName = process.platform === 'win32' ? 'icon.ico' : 'icon.png'
  tray = new Tray(path.join(global.__dirname, iconName));
  const contextMenu = Menu.buildFromTemplate([
    {
      label: '显示主界面', click: () => {
        mainWindow.show();
        mainWindow.setSkipTaskbar(false);
      }
    },
    {
      label: '退出', click: () => {
        mainWindow.destroy();
        app.quit();
      }
    },
  ])
  tray.setToolTip('electron-react');
  tray.setContextMenu(contextMenu);
}

11. Возможность расширения

Во многих случаях ваше приложение должно взаимодействовать с внешними устройствами.Как правило, производители предоставляют вам комплекты разработки для аппаратных устройств.Эти комплекты разработки в основномC++пиши, пользуйсяelectronВ случае разработки у нас нет возможности напрямую вызыватьC++Возможности кода, которыми мы можем воспользоватьсяnode-ffiдля достижения этой функции.

node-ffiпредоставляет мощный набор инструментов дляNode.jsсреда с использованием чистогоJavaScriptВызовите интерфейс библиотеки динамической компоновки. Его можно использовать для создания привязок интерфейса для библиотек без использования каких-либоC++код.

Уведомлениеnode-ffiнельзя вызывать напрямуюC++код, вам нужноC++Код скомпилирован в виде динамической библиотеки: вWindowsследующийDll,существуетMac OSследующийdylib ,Linuxдаso.

node-ffiнагрузкаLibraryограничено и может обрабатывать толькоCСтильLibrary.

Вот простой пример:

const ffi = require('ffi');
const ref = require('ref');
const SHORT_CODE = ref.refType('short');


const DLL = new ffi.Library('test.dll', {
    Test_CPP_Method: ['int', ['string',SHORT_CODE]], 
  })

testCppMethod(str: String, num: number): void {
  try {
    const result: any = DLL.Test_CPP_Method(str, num);
    return result;
  } catch (error) {
    console.log('调用失败~',error);
  }
}

this.testCppMethod('ConardLi',123);

В приведенном выше коде мы используемffiУпаковкаC++Созданная интерфейсом библиотека динамических ссылокtest.dllи использоватьrefСделайте некоторое сопоставление типов.

использоватьJavaScriptПри вызове этих методов сопоставления рекомендуется использоватьTypeScriptсогласовать типы параметров, т.к. слабо типизированыJavaScriptПри вызове интерфейсов на строго типизированных языках могут возникнуть непредвиденные риски.

С помощью этой возможности фронтенд-разработчики также могутIOTПоле красуется 😎~

12. Экологический выбор

Как правило, наши приложения могут работать в нескольких средах (production,beta,uat,moke,development...), разные среды разработки могут соответствовать разным внутренним интерфейсам или другим конфигурациям, мы можем создать простую функцию выбора среды в клиентской программе, чтобы помочь нам развиваться более эффективно.

Конкретные стратегии заключаются в следующем:

  • В среде разработки мы напрямую заходим на страницу выбора окружения, читаем выбранное окружение и в ответ выполняем операцию перенаправления
  • Сохранить запись выбора среды в меню для переключения во время разработки
const envList = ["moke", "beta", "development", "production"];
exports.envList = envList;
const urlBeta = 'https://wwww.xxx-beta.com';
const urlDev = 'https://wwww.xxx-dev.com';
const urlProp = 'https://wwww.xxx-prop.com';
const urlMoke = 'https://wwww.xxx-moke.com';
const path = require('path');
const pkg = require(path.resolve(global.__dirname, 'package.json'));
const build = pkg['build-config'];
exports.handleEnv = {
  build,
  currentEnv: 'moke',
  setEnv: function (env) {
    this.currentEnv = env
  },
  getUrl: function () {
    console.log('env:', build.env);
    if (build.env === 'production' || this.currentEnv === 'production') {
      return urlProp;
    } else if (this.currentEnv === 'moke') {
      return urlMoke;
    } else if (this.currentEnv === 'development') {
      return urlDev;
    } else if (this.currentEnv === "beta") {
      return urlBeta;
    }
  },
  isDebugger: function () {
    return build.env === 'development'
  }
}

Тринадцать, упаковка

Последний и самый важный шаг — упаковать написанный код в исполняемый файл..appили.exeзапускаемый файл.

Здесь я делаю две части атмосферы упаковки, упаковку процесса рендеринга и упаковку основного процесса.

13.1 Пакетирование и обновление процесса рендеринга

В общем, большая часть нашего кода бизнес-логики завершается в процессе рендеринга. В большинстве случаев нам нужно только обновить и обновить процесс рендеринга без изменения основного кода процесса. Упаковка нашего процесса рендеринга фактически такая же, как и общая процесс.webНет большой разницы в упаковке проекта, используйтеwebpackПросто упакуйте это.

Здесь я рассказываю о преимуществах упаковки процесса рендеринга отдельно:

в упаковкеhtmlиjsфайл, мы обычно загружаем его на наш внешний сервер статических ресурсов, а затем сообщаем серверу, что наш процесс рендеринга имеет обновление кода, которое можно назвать отдельным обновлением процесса рендеринга.

Обратите внимание, что, в отличие от обновлений оболочки, обновления средства визуализации доступны только на сервере статических ресурсов.htmlиjsОбновление файла не требует повторной загрузки клиента обновления, поэтому каждый раз, когда мы запускаем программу и обнаруживаем, что в автономном пакете есть обновление, мы можем напрямую обновлять и читать последнюю версию файла статического ресурса, даже если программа запущена, нашей программе нужно только принудительно обновить страницу, чтобы прочитать последние статические ресурсы, такое обновление очень удобно для пользователя.

Обратите внимание, что раз мы настроили таким образом, это означает, что процесс рендеринга и основной процесс полностью отделены от упаковки и обновления.Файлы, которые мы читаем при запуске главного окна, больше не должны быть локальными файлами, а будут размещены на статике. сервер ресурсов после упаковки документ.

Для облегчения разработки здесь мы можем различать локальную и онлайн-загрузку разных файлов:

function getVersion (mac,current){
  // 根据设备mac和当前版本获取最新版本
}
export default function () {
  if (build.env === 'production') {
    const version = getVersion (mac,current);
    return 'https://www.xxxserver.html/electron-react/index_'+version+'.html';
  }
  return url.format({
    protocol: 'file:',
    pathname: path.join(__dirname, 'env/environment.html'),
    slashes: true,
    query: { debugger: build.env === "development" }
  });
}

конкретныйwebpackКонфигурация больше не будет здесь выкладываться, вы можете зайти в мойgithub electron-reactиз/scriptsПосмотреть в каталоге.

Здесь следует отметить, что в среде разработки мы можем комбинироватьwebpackизdevServerиelectronкоманда для запускаapp:

  devServer: {
    contentBase: './assets/',
    historyApiFallback: true,
    hot: true,
    port: PORT,
    noInfo: false,
    stats: {
      colors: true,
    },
    setup() {
      spawn(
        'electron',
        ['.'],
        {
          shell: true,
          stdio: 'inherit',
        }
      )
        .on('close', () => process.exit(0))
        .on('error', e => console.error(e));
    },
  },//...

13.2 Основная технологическая упаковка

Основной процесс состоит в том, чтобы упаковать всю программу в работающую клиентскую программу.Обычно используются две схемы упаковки.electron-packagerиelectron-builder.

electron-packagerМне кажется немного громоздким упаковывать конфигурацию, и он может упаковать приложение только непосредственно как исполняемую программу.

Здесь я рекомендую использоватьelectron-builder, он не только имеет удобную конфигурациюprotocolфункция, встроенныйAuto Update, простая конфигурацияpackage.jsonВся работа по упаковке может быть завершена, и пользовательский опыт очень хороший. иelectron-builderПриложение может быть не только упаковано непосредственно вexe appи другие исполняемые программы, также могут быть упакованы вmsi dmgи другие форматы установочных пакетов.

ты сможешьpackage.jsonУдобно для различных конфигураций:

 "build": {
   "productName": "electron-react", // app中文名称
   "appId": "electron-react",// app标识
   "directories": { // 打包后输出的文件夹
     "buildResources": "resources",
     "output": "dist/"
   }
   "files": [ // 打包后依然保留的源文件
     "main_process/",
     "render_process/",
   ],
   "mac": { // mac打包配置
     "target": "dmg",
     "icon": "icon.ico"
   },
   "win": { // windows打包配置
     "target": "nsis",
     "icon": "icon.ico"
   },
   "dmg": { // dmg文件打包配置
     "artifactName": "electron_react.dmg",
     "contents": [
       {
         "type": "link",
         "path": "/Applications",
         "x": 410,
         "y": 150
       },
       {
         "type": "file",
         "x": 130,
         "y": 150
       }
     ]
   },
   "nsis": { // nsis文件打包配置
     "oneClick": false,
     "allowToChangeInstallationDirectory": true,
     "shortcutName": "electron-react"
   },
 }

воплощать в жизньelectron-builderПри упаковке команды вы можете указать параметры для упаковки.

 --mac, -m, -o, --macos   macOS打包
 --linux, -l              Linux打包
 --win, -w, --windows     Windows打包
 --mwl                    同时为macOS,Windows和Linux打包
 --x64                    x64 (64位安装包)
 --ia32                   ia32(32位安装包) 

Для обновлений основного процесса вы можете использоватьelectron-builderавтономныйAuto Updateмодуль, вelectron-reactТак же реализован модуль ручного обновления.Ввиду нехватки места я не буду их здесь повторять.Если интересно,можете зайти на мойgithubПроверятьmainвнизupdateмодуль.

13.3 Оптимизация упаковки

electron-builderв упаковкеAppОно намного больше, чем нативное клиентское приложение с той же функцией.Даже если это пустое приложение, объем должен быть в100mbвыше. есть много причин:

Первый момент: для достижения кроссплатформенного эффекта каждыйElectronПриложение содержит всюV8двигатель иChromiumядро.

Второй момент: при упаковке всеnode_modulesУпаковано, все знают приложениеnode_moduleОбъем очень большой, что также делаетElectronПричина в большом размере запакованного приложения.

Первый пункт мы не можем изменить, мы можем оптимизировать объем приложения из второго пункта:ElectronТолько при упаковкеdenpendenciesзависимости упакованы безdevDependenciesЗависимости в пакете упакованы. Поэтому мы должны максимально уменьшитьdenpendenciesзависимости в. В описанном выше процессе мы используемwebpackУпакуйте процесс рендеринга, чтобы все зависимости процесса рендеринга можно было переместить вdevDependencies.

В качестве альтернативы мы также можем использовать doublepackajson.jsonСпособ оптимизации, поместить зависимости, используемые только в среде разработки, в корневую директорию всего проекта.package.jsonЗатем установите зависимости, связанные с платформой или требуемые во время выполнения, вappПод содержанием. Подробнее см.two-package-structure.

Ссылаться на

Адрес исходного кода этого проекта:GitHub.com/con AR DL i/Harsh…

резюме

Я надеюсь, что вы сможете достичь следующих результатов после прочтения этой статьи:

  • учитьсяElectronОсновной принцип работы
  • владелецElectronОсновные принципы развития
  • учитьсяElectronОб основном использовании всплывающих окон, печати, защиты, упаковки и других функций

Если в статье есть ошибки, исправьте их в комментариях, если статья вам поможет, ставьте лайк и подписывайтесь.

Если вы хотите читать больше качественных статей, вы можете подписаться на меняgithubблог, твоя звезда✨, лайки и внимание - движущая сила моего постоянного творчества!

Рекомендую обратить внимание на мой паблик WeChat [code secret garden], каждый день выкладывать качественные статьи, будем общаться и расти вместе.

Подписавшись на официальную учетную запись, ответьте на [Добавить группу], чтобы включить вас в высококачественную группу внешнего интерфейса.