Создайте безопасное настольное приложение с помощью Electron Forge и React

React Программирование и разработка

В этой статье мы создадим простое настольное приложение с помощью Electron и React. Это будет небольшой текстовый редактор под названием «Блокнот», который автоматически сохраняет изменения по мере ввода, аналогично FromScratch. Мы уделим внимание обеспечению безопасности приложения с помощью Electron Forge, современного инструмента сборки, предоставляемого командой Electron.

Electron Forge — это «полноценный инструмент для создания, публикации и установки современных приложений Electron». Он обеспечивает удобную среду разработки, а также настраивает все необходимое для создания приложения для нескольких платформ (хотя мы не будем касаться этого в этой статье).

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

Вы можете найти код готового приложения на GitHub.

Setup

В этом руководстве предполагается, что на вашем компьютере установлен Node. Если это не так, перейдите на официальную страницу загрузки и скачайте нужные двоичные файлы для своей системы или используйте диспетчер версий, например nvm. Мы также предположим, что Git установлен в рабочем состоянии.

Ниже я буду использовать два важных термина: «основной» и «средство визуализации». Приложения Electron «управляются» файлом JavaScript Node.js. Этот файл называется «основным» процессом, и он отвечает за все, что связано с операционной системой, а также за создание окон браузера. Эти окна браузера запускают Chromium и называются частью «рендерера» Electron, потому что это часть, которая фактически отображает что-то на экране.

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

Во-первых, нам нужно настроить Electron Forge с шаблоном веб-пакета. Вот как мы можем сделать это с помощью одной команды терминала:

$ npx create-electron-app scratchpad --template=webpack

Выполнение этой команды займет некоторое время, поскольку она настраивает и настраивает все, от Git до webpack и package.jsonфайла. Когда это будет сделано и мы войдем cdв этот каталог, мы увидим следующее:

➜  scratchpad git:(master) ls
node_modules
package.json
src
webpack.main.config.js
webpack.renderer.config.js
webpack.rules.js

Мы пропустим node_modulesи package.json, а прежде чем заглянуть в srcпапку, давайте пройдемся по файлам webpack, поскольку их три. Это потому, что Electron на самом деле запускает два файла JavaScript: один для части Node.js, называемой «основным», в которой он создает окна браузера и взаимодействует с остальной частью операционной системы, и часть Chromium, называемая «рендерер», которая является та часть, которая действительно появляется на вашем экране.

Третий файл webpack webpack.rules.js- это место, где устанавливается любая общая конфигурация между Node.js и Chromium, чтобы избежать дублирования.

Ладно, теперь пора заглянуть в srcпапку:

➜  src git:(master) ls
index.css
index.html
main.js
renderer.js

Не слишком много: файл HTML и CSS, а также файл JavaScript как для основного, так и для рендерера. Выглядит хорошо. Мы откроем их позже в статье.

Добавление React

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

Во-первых, это devDependencies:

npm install --save-dev @babel/core @babel/preset-react babel-loader

Далее следуют React и React-dom как обычные зависимости:

npm install --save react react-dom

После установки всех зависимостей нам нужно научить webpack поддерживать JSX. Мы можем сделать это в любом из webpack.renderer.jsили webpack.rules.js, но мы будем следовать руководству и добавим следующий загрузчик в webpack.rules.js:

module.exports = [
  ...
  {
    test: /\.jsx?$/,
    use: {
      loader: 'babel-loader',
      options: {
        exclude: /node_modules/,
        presets: ['@babel/preset-react']
      }
    }
  },
];

Хорошо, это должно сработать. Давайте быстро протестируем его, открыв src/renderer.jsи заменив его содержимое следующим:

import './app.jsx';
import './index.css';

Затем создайте новый файл src/app.jsxи добавьте следующее:

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(<h2>Hello from React in Electron!</h2>, document.body);

Мы можем проверить, работает ли это, запустив npm startв консоли. Если он откроет окно с надписью «Hello from React in Electron!», Все в порядке.

Вы могли заметить, что инструменты разработчика открыты, когда отображается окно. Это из-за этой строки в main.jsфайле:

mainWindow.webContents.openDevTools();

Пока можно оставить это, так как он нам пригодится во время работы. Мы main.jsвернемся к этому позже в статье, когда настроим его безопасность и другие параметры.

Что касается ошибки и предупреждений в консоли, их можно спокойно игнорировать. Установка компонента React document.bodyдействительно может быть проблематичной из-за того, что сторонний код вмешивается в него, но мы не веб-сайт и не запускаем какой-либо код, который не принадлежит нам. Электрон тоже дает нам предупреждение, но мы разберемся с этим позже.

Создание нашей функциональности

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

Для начала мы добавим CodeMirror и react -codemirror, чтобы получить простой в использовании редактор:

npm install --save react-codemirror codemirror

Настроим CodeMirror. Во-первых, нам нужно открыть src/renderer.jsи импортировать и потребовать немного CSS. CodeMirror поставляется с несколькими разными темами, поэтому выберите ту, которая вам нравится, но в этой статье мы будем использовать тему «Материал». Теперь ваш renderer.js должен выглядеть так:

import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/material.css';
import './app.jsx';
import './index.css';

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

Затем в нашем app.jsxфайле мы собираемся импортировать наш CodeMirrorкомпонент следующим образом:

import CodeMirror from 'react-codemirror';

Создайте новый компонент React, app.jsxкоторый добавляет CodeMirror:

const ScratchPad = () => {
  const options = {
    theme: "material"
  };

  const updateScratchpad = newValue => {
    console.log(newValue)
  }

  return <CodeMirror
    value="Hello from CodeMirror in React in Electron"
    onChange={updateScratchpad}
    options={options} />;
}

Также замените функцию рендеринга, чтобы загрузить наш компонент ScratchPad:

ReactDOM.render(<ScratchPad />, document.body);

Когда мы запустим приложение сейчас, мы должны увидеть текстовый редактор с текстом «Hello from CodeMirror in React in Electron». Когда мы введем его, обновления будут отображаться в нашей консоли.

Мы также видим белую рамку и то, что наш редактор на самом деле не заполняет все окно, так что давайте что-нибудь с этим сделаем. Несмотря на то, что мы делаем, что мы будем делать некоторые уборку в наших index.htmlи index.cssфайлах.

Во-первых, index.htmlдавайте удалим все внутри элемента body, так как оно нам все равно не нужно. Затем мы изменим заголовок на «Блокнот», чтобы в строке заголовка не было надписи «Привет, мир!» по мере загрузки приложения.

Мы также добавим Content-Security-Policy. То, что это означает, слишком много для рассмотрения в этой статье ( MDN имеет хорошее введение ), но, по сути, это способ предотвратить выполнение сторонним кодом того, чего мы не хотим делать. Здесь мы говорим разрешить скрипты только из нашего источника (файла) и ничего больше.

В общем, наш index.html будет очень пустым и будет выглядеть так:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Scratchpad</title>
    <meta http-equiv="Content-Security-Policy" content="script-src 'self';">
  </head>
  <body></body>
</html>

Теперь перейдем к index.css. Мы можем удалить все, что там сейчас, и заменить на это:

html, body {
  position: relative;
  width:100vw;
  height:100vh;
  margin:0;
  background: #263238;
}

.ReactCodeMirror,
.CodeMirror {
  position: absolute;
  height: 100vh;
  inset: 0;
}

Это делает пару вещей:

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

Обратите внимание, как мы используем inset, сокращенное свойство CSS для значений top, right, bottom и left. Поскольку мы знаем, что наше приложение всегда будет работать в Chromium версии 89, мы можем использовать современный CSS, не беспокоясь о поддержке!

Так что это довольно хорошо: у нас есть приложение, которое мы можем запустить и которое позволяет нам вводить текст. Сладкий!

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

Теперь, возможно, вы также заметили, что даже если мы добавили цвет фона для htmlи bodyэлементов, окно остается белым цветом, а мы загружаем приложение. Это потому, что загрузка нашего index.cssфайла занимает несколько миллисекунд. Чтобы улучшить то, как это выглядит, мы можем настроить окно браузера так, чтобы он имел определенный цвет фона при его создании. Итак, перейдем к нашему main.jsфайлу и добавим цвет фона. Измените свой mainWindowтак, чтобы он выглядел так:

const mainWindow = new BrowserWindow({
  width: 800,
  height: 600,
  backgroundColor: "#263238",
});

И теперь, когда вы начинаете, белая вспышка должна исчезнуть!

Сохраняем наш блокнот на диск

Когда я объяснял Electron ранее в этой статье, я сделал его немного проще, чем он есть на самом деле. Хотя у Electron есть основной процесс и процесс рендеринга, в последние годы фактически появился третий контекст — сценарий предварительной загрузки.

Идея сценария предварительной загрузки заключается в том, что он действует как мост между основным (который может получить доступ ко всем API-интерфейсам Node.js) и средством визуализации (чего определенно не должно быть!). В нашем сценарии предварительной загрузки мы можем добавить функции, которые могут взаимодействовать с основным процессом, а затем предоставлять их процессу визуализации таким образом, чтобы это не влияло на безопасность нашего приложения.

Итак, давайте сделаем обзор того, что мы хотим сделать:

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

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

const fs = require('fs');

Затем нам нужно выбрать место для нашего сохраненного текстового файла. Здесь мы собираемся использовать appDataпапку, которая является автоматически созданным местом для вашего приложения для хранения информации. Вы можете получить это с помощью app.getPathфункции, поэтому давайте добавим filenameпеременную в main.jsфайл прямо перед createWindowфункцией:

const filename = `${app.getPath('userData')}/content.txt`;

После этого нам понадобятся две функции: одна для чтения файла и одна для сохранения файла. Мы назовем их loadContentи saveContent, и вот как они выглядят:

const loadContent = async () => {
  return fs.existsSync(filename) ? fs.readFileSync(filename, 'utf8') : '';
}

const saveContent = async (content) => {
  fs.writeFileSync(filename, content, 'utf8');
}

Они оба однострочные с использованием встроенных fsметодов. Для loadContentначала нам нужно проверить, существует ли уже файл (поскольку его не будет там при первом запуске!), А если нет, мы можем вернуть пустую строку.

saveContentеще проще: когда он вызывается, мы вызываем его writeFileс именем файла, содержимым и убеждаемся, что он сохранен как UTF8.

Теперь, когда у нас есть эти функции, нам нужно их подключить. И способ сообщить об этом — через IPC, межпроцессное взаимодействие. Давайте настроим это дальше.

Настройка IPC

Во-первых, нам нужно импортировать ipcMainиз Electron, поэтому убедитесь, что ваша require(’Electron’)строка main.jsвыглядит так:

const { app, BrowserWindow, ipcMain } = require('electron');

IPC позволяет отправлять сообщения от модуля рендеринга к основному (и наоборот). Сразу под saveContentфункцией добавьте следующее:

ipcMain.on("saveContent", (e, content) =>{
  saveContent(content);
});

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

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

Давайте создадим этот preload.jsфайл в srcкаталоге и свяжем его mainWindowтак:

const mainWindow = new BrowserWindow({
  width: 800,
  height: 600,
  backgroundColor: "#263238",
  webPreferences: {
    preload: path.join(__dirname, 'preload.js'),
  }
});

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

const { ipcRenderer, contextBridge } = require("electron");

contextBridge.exposeInMainWorld(
  'scratchpad',
  {
    saveContent: (content) => ipcRenderer.send('saveContent', content)
  }
)

contextBridge.exposeInMainWorldпозволяет нам добавить функцию saveContentв наш renderer.jsфайл, не делая доступными все Electron и Node. Таким образом, средство визуализации знает только об этом, saveContentне зная, как и где сохраняется контент. Первый аргумент, «блокнот», — это глобальная переменная, которая saveContentбудет доступна в. Чтобы вызвать ее в нашем приложении React, мы это делаем window.scratchpad.saveContent(content);.

Давай сделаем это сейчас. Открываем наш app.jsxфайл и обновляем updateScratchpadфункцию вот так:

const updateScratchpad = newValue => {
  window.scratchpad.saveContent(newValue);
};

Вот и все. Теперь каждое внесенное нами изменение записывается на диск. Но когда мы закрываем и снова открываем приложение, оно снова пустое. Нам также необходимо загрузить контент при первом запуске.

Загружаем контент, когда открываем приложение

Мы уже написали эту loadContentфункцию main.js, так что давайте подключим ее к нашему пользовательскому интерфейсу. Мы использовали IPC sendи onдля сохранения контента, поскольку нам не нужно было получать ответ, но теперь нам нужно получить файл с диска и отправить его рендереру. Для этого мы будем использовать IPC invokeи handlefunctions. invokeвозвращает обещание, которое разрешается тем, что handleвозвращает функция.

Начнем с написания обработчика в нашем main.jsфайле, прямо под saveContentобработчиком:

ipcMain.handle("loadContent", (e) => {
  return loadContent();
});

В нашем preload.jsфайле мы вызовем эту функцию и представим ее нашему коду React. К нашему exporeInMainWorldсписку свойств мы добавляем вторую под названием content:

contextBridge.exposeInMainWorld(
  'scratchpad',
  {
    saveContent: (content) => ipcRenderer.send('saveContent', content),
    content: ipcRenderer.invoke("loadContent"),
  }
);

В нашем app.jsxмы можем получить это с помощью window.scratchpad.content, но это обещание, поэтому нам нужно awaitэто сделать перед загрузкой. Для этого мы оборачиваем рендерер ReactDOM в асинхронный IFFE следующим образом:

(async () => {
  const content = await window.scratchpad.content;
  ReactDOM.render(<ScratchPad text={content} />, document.body);
})();

Мы также обновляем наш ScratchPadкомпонент, чтобы использовать свойство text в качестве начального значения:

const ScratchPad = ({text}) => {
  const options = {
    theme: "material"
  };

  const updateScratchpad = newValue => {
    window.scratchpad.saveContent(newValue);
  };

  return (
    <CodeMirror
      value={text}
      onChange={updateScratchpad}
      options={options}
    />
  );
};

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

Мы закончили, правда? Что ж, есть несколько вещей, которые мы можем сделать, чтобы он выглядел немного более «прикладным».

«Быстрая» загрузка

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

Сначала мы добавляем show: falseк нашему new BrowserWindowвызову и добавляем слушателя к ready-to-showсобытию. Здесь мы показываем и фокусируем наше созданное окно:

const mainWindow = new BrowserWindow({
  width: 800,
  height: 600,
  backgroundColor: "#263238",
  show: false,
  webPreferences: {
    preload: path.join(__dirname, 'preload.js'),
  }
});

mainWindow.once('ready-to-show', () => {
  mainWindow.show();
  mainWindow.focus();
});

Пока мы находимся в main.jsфайле, мы также удалим openDevToolsвызов, так как мы не хотим показывать это пользователям:

mainWindow.webContents.openDevTools();

Теперь, когда мы запускаем приложение, отображается окно приложения с уже имеющимся содержимым. Намного лучше!

Сборка и установка приложения

Теперь, когда приложение готово, мы можем его построить. В Electron Forge уже создана команда для этого. Run npm run makeи Forge будет строить приложение и инсталлятор для вашей операционной системы и поместить его в «из папки», все готово для вас, чтобы установить ли сво.exe,.dmgили.deb.

Если вы работаете в Linux и получаете сообщение об ошибке rpmbuild, установите пакет «rpm», например, с помощью sudo apt install rpmв Ubuntu. Если вы не хотите создавать установщик rpm, вы также можете удалить блок «@ electronics-forge / maker-rpm» из файлов makers в вашем package.json.

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

Это действительно минимальный пример интеграции Electron и React. С самим приложением мы можем сделать гораздо больше. Вот несколько идей для вас:

  • Добавьте классную иконку на рабочий стол.
  • Создайте поддержку темного и светлого режимов на основе настроек операционной системы, либо с помощью медиа-запросов, либо с помощью API nativeTheme, предоставленного Electron.
  • Добавьте ярлыки с чем-то вроде mousetrap.js или с помощью ускорителей меню Electron и globalShortcuts.
  • Сохраните и восстановите размер и положение окна.
  • Синхронизация с сервером вместо файла на диске.
Читайте также:  Алгоритмы обхода дерева в Python, которые должен знать каждый разработчик
Оцените статью
bestprogrammer.ru
Добавить комментарий