Электронные практические заметки

Electron

Автор: Чжоу Цюань

Платформа социального куба — это платформа JD.com для создания активности в социальных сетях. Она имеет множество встроенных шаблонов. Каждый шаблон имеет шаблон JSON для создания формы. После того, как учащиеся и продавцы настроят эту форму, они могут создать страницу активности. Шаблон JSON — это стандартные структурированные данные, содержащие такие поля, как имя, тип, тип элемента управления, валидатор, значение по умолчанию и многое другое. Раньше использовался рукописный JSON, который был очень неэффективен и подвержен ошибкам. В соответствии с характеристиками его структурированных данных его можно редактировать с помощью графического интерфейса.Мы основаны наElectronСсылаться наНастольный клиент GithubАрхитектура написаларедактор, который генерирует JSON при заполнении формы. Итак, здесь я записываю моменты, которые можно записать во время разработки этого редактора Electron, и моменты, которые стоит изучить из кода клиента Github Desktop.

APP

1. Об Электроне

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

Это официальное введение от Electron. Основываясь на платформе Electron, мы можем использовать знакомый стек интерфейсных технологий для разработки настольных приложений. Процесс, в котором Electron запускает основной скрипт package.json, называется основным процессом (далее — основной). Сценарий, который запускается в основном процессе, отображает пользовательский интерфейс, создавая веб-страницу (далее именуемую средством визуализации). Приложение Electron всегда имеет один и только один главный процесс. main используется для создания приложений и окон браузера, это полноценный процесс Node, который не может получить интерфейсы DOM и BOM. В окне браузера, созданном main, запускается процесс рендеринга, который может получать такие интерфейсы, как DOM и BOM, а также может использовать Node API. Два типа процессов могут взаимодействовать через интерфейс IPC, предоставляемый Electron.

2. Создание среды разработки

Мы узнали, что Electron делится на два типа процессов: main и renderer. Поэтому при создании среды разработки нельзя настроить один веб-пакет, как обычное внешнее приложение. и мы хотим добиться

  1. Один щелчок, чтобы запустить среду разработки
  2. Упаковка в один клик
  3. Публикуйте в один клик

Затем вам понадобятся два файла конфигурации webpack.

один для среды разработки --webpack.dev.ts.

// webpack.dev.ts
const mainConfig = merge({}, base.mainConfig, config, {
  watch: true
})

const rendererConfig = merge({}, base.rendererConfig, config, {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.styl$/,
        use: ['style-loader', 'css-loader', 'stylus-loader'],
      }
    ]
  },
  devServer: {
    contentBase: path.join(__dirname, base.outputDir),
    port: 8000,
    hot: true,
    inline: true,
    historyApiFallback: true,
    writeToDisk: true
  },
})

module.exports = [rendererConfig, mainConfig]

Другой для производственной среды --webpack.prod.ts.

const config: webpack.Configuration = {
  mode: 'production',
  devtool: 'source-map',
}

const mainConfig = merge({}, base.mainConfig, config)

const rendererConfig = merge({}, base.rendererConfig, config, {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
      {
        test: /\.styl$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'stylus-loader'],
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({ filename: 'renderer.css' }),
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false,
      reportFilename: 'renderer.report.html',
    }),
  ],
})

module.exports = [mainConfig, rendererConfig]

Вот ссылка на рабочий стол, использующий Typescript для написания файлов конфигурации веб-пакета. С интерфейсом редактор файла конфигурации веб-пакета может быть автоматически завершен. Для конкретного использования, пожалуйста, обратитесь к документации по веб-пакету.Веб-пакет Просто .org/config как у ATI...

Каждый файл конфигурации экспортирует массив, который является объектами конфигурации main и renderer соответственно.

Использование webpack-dev-server для запуска может реализовать горячее обновление рендерера, а основной использует режим просмотра webpack.

{
  "compile:dev": "webpack-dev-server --config scripts/webpack.dev.ts"
}

использоватьnodemonПосле прослушивания компиляции основного продукта, nodemon для отслеживания изменений в повторном запускеelectron .Перезапустите приложение, которое косвенно реализует перезагрузку main.

Nodemon is a utility that will monitor for any changes in your source and automatically restart your server.

{
  "app": "electron .",
  "app:watch": "nodemon --watch 'dest/main.js' --exec npm run app",
}

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

Советы: сообщество открытого исходного кода лучшеelectron-webpack, HMR for both renderer and main processes

Рабочая среда использует webpack для последовательной компиляции main и renderer. Использовать после компиляцииelectron-builderПакет. Это обеспечивает упаковку в один клик.

Из-за отсутствия набора инструментов выпуск в один клик невозможен, поэтому его можно упаковать и выпустить только вручную (подробно описано ниже).

Вот полные скрипты.

{
  "scripts": {
    "start": "run-p -c compile:dev typecheck:watch app:watch",
    "dist": "npm run compile:prod && electron-builder build --win --mac",
    "compile:dev": "webpack-dev-server --config scripts/webpack.dev.ts",
    "compile:prod": "npm run clean && webpack --config scripts/webpack.prod.ts",
    "app": "electron .",
    "app:watch": "nodemon --watch 'dest/main.js' --exec npm run app",
    "clean": "rimraf dest dist",
    "typecheck": "tsc --noEmit",
    "typecheck:watch": "tsc --noEmit --watch",
    "lint": "eslint src --ext .ts,.js --fix",
    "release:patch": "standard-version --release-as patch && git push --follow-tags origin master && npm run dist",
    "release:minor": "standard-version --release-as minor && git push --follow-tags origin master && npm run dist",
    "release:major": "standard-version --release-as major && git push --follow-tags origin master && npm run dist",
    "repush": "git push --follow-tags origin master && npm run dist"
  },
}

3. Структура каталогов

1. Структура каталогов проекта

src
├── lib
│   ├── cube
│   ├── databases
│   ├── enviroment
│   ├── files
│   ├── local-storage
│   ├── log
│   ├── shell
│   ├── stores
│   ├── update
│   ├── validator
│   └── watcher
├── main
│   ├── app-window.ts
│   ├── event-bus.ts
│   ├── index.ts
│   ├── keyboard
│   └── menu
├── models
│   ├── popup.ts
│   └── project.ts
└── renderer
    ├── App.tsx
    ├── assets
    ├── components
    ├── index.html
    ├── index.tsx
    ├── pages
    └── types

Имитирует рабочий стол в своей структуре каталогов. В основном каталоге хранится код, относящийся к основному процессу, включая вход в приложение, создание окон, меню, сочетания клавиш и т. д., а каталог рендерера — это код для всего слоя рендеринга пользовательского интерфейса. Каталог lib — это какой-то код бизнес-логики, который не имеет ничего общего с пользовательским интерфейсом и не имеет ничего общего с main. models хранят некоторые модели предметной области.

2. Спецификации CSS

В этом React-проекте нет такой схемы, как css-модули. Вместо этого используйте спецификацию, такую ​​как БЭМ, которая может формировать пространство имен для достижения модульности.Преимущество этого заключается в том, что стиль может быть лучше охвачен.

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

stylesheets
  ├── common.styl
  ├── components
  │   ├── editor.styl
  │   ├── empty-guide.styl
  │   ├── find-in-page.styl
  │   ├── reindex.styl
  │   ├── sidebar.styl
  │   ├── source-viewer.styl
  │   └── upload.styl
  ├── index.styl
  └── reset.styl

Три, связь IPC

Межпроцессное взаимодействие (IPC, InterProcess Communication) относится к распространению или обмену информацией между различными процессами.

Связь между основным процессом Electron и процессом рендеринга обеспечивается Electron.ipcMainа такжеipcRendererбыть реализованным.

1. основная сторона

Отправка сообщения рендереру окна в main может быть выполнена с помощьюwindow.webContents.send. Прослушивание сообщения рендерера в главной сторонеipcMain.on.

// 在主进程中.
const { ipcMain } = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
  console.log(arg) // prints "ping"
  event.reply('asynchronous-reply', 'pong')
})

ipcMain.on('synchronous-message', (event, arg) => {
  console.log(arg) // prints "ping"
  event.returnValue = 'pong'
})

2. сторона рендерера

Ответ на сообщение синхронизации может использоватьevent.returnValue. Возвращаемое значение сообщения синхронизации может быть прочитано напрямую. Ответ на асинхронные сообщения можно выполнить с помощьюevent.reply. Затем в рендерере вам нужно прослушать ответный канал, чтобы получить возвращаемое значение.

//在渲染器进程 (网页) 中。
const { ipcRenderer } = require('electron')
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong"

ipcRenderer.on('asynchronous-reply', (event, arg) => {
  console.log(arg) // prints "pong"
})
ipcRenderer.send('asynchronous-message', 'ping')

Вы можете видеть, что рендерер можно использоватьipcRenderer.sendОтправьте асинхронное сообщение основному процессу. использоватьipcRenderer.sendSyncОтправить сообщение синхронизации.

В-четвертых, сохранение данных и управление состоянием

1. Комплексное сохранение данных

Существует множество вариантов сохранения данных, напримерelectron-storeи другие решения для хранения на основе файлов JSON. Для более сложных сценариев приложений вы также можете использоватьlowdb,nedb,sqliteЖдать.

Изначально я использовалelectron-store, и всегда была навязчивая идея, что чтение и запись на диск может производиться только в основном процессе, а процесс рендерера отвечает только за отрисовку интерфейса. Таким образом, первоначальный замысел заключается в том, что когда процесс рендеринга визуализирует данные или обновляет данные, ему необходимо передать IPC основному процессу для завершения окончательного чтения и записи диска. В дополнение к нормальной ситуации чтения и записи также необходимо учитывать исключение чтения и записи дисков, что приводит к аномальному потоку данных. И вам также нужно поддерживать генерацию идентификатора самостоятельно. После заимствования из кода Desktop часть сохраняемости данных была переработана и также принята.Dexie, который представляет собой оболочку стандартной базы данных indexedDB браузера. Из его Readme мы видим, что он в основном решает три проблемы indexedDB:

  1. Неоднозначная обработка исключений
  2. запрос отстой
  3. сложный код
import Dexie from 'dexie';

export interface IDatabaseProject {
  id?: number;
  name: string;
  filePath: string;
}

export class ProjectsDatabase extends Dexie {
  public projects: Dexie.Table<IDatabaseProject, number>;
  constructor() {
    super('ProjectsDatabase');

    this.version(1).stores({
      projects: '++id,&name,&filePath',
    });

    this.projects = this.table('projects');
  }
}

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

2. Простое сохранение данных

Некоторое хранилище флагов состояния пользовательского интерфейса (например, отображалось ли всплывающее окно), мы обычно храним этот флаг вlocalStorageсередина. В процессе просмотра исходного кода Desktop было обнаружено, что ониnumber, booleanДанные типа get и set просто инкапсулированы. Очень удобно пользоваться, вот пост дляbooleanобработка типовых данных.

export function getBoolean(key: string): boolean | undefined
export function getBoolean(key: string, defaultValue: boolean): boolean
export function getBoolean(
  key: string,
  defaultValue?: boolean
): boolean | undefined {
  const value = localStorage.getItem(key)
  if (value === null) {
    return defaultValue
  }

  if (value === '1' || value === 'true') {
    return true
  }

  if (value === '0' || value === 'false') {
    return false
  }

  return defaultValue
}

export function setBoolean(key: string, value: boolean) {
  localStorage.setItem(key, value ? '1' : '0')
}

Подробности смотрите в исходном коде

5. Реализация функции

1. Синхронизация версии диска/редактора в режиме реального времени

В общем, то, что мы редактируем в редакторе, на самом деле является копией редактора, который считывает файл с диска в память. Поэтому, если файлы на диске изменены, например, Git переключает ветки, вызывая изменения файлов, или удаляя файлы на диске, переименовывая и т. д., это вызовет несоответствие между версией памяти и версией диска, то есть диск версия опережает версию памяти. Может возникнуть конфликт. Решение этой проблемы очень простое.Вы можете использовать fs.watch/watchFile для мониторинга текущего редактируемого файла.Как только он изменится,прочитайте версию на диске еще раз и обновите версию в памяти для достижения синхронизации. Но fs.watch API не работает из коробки в инженерии, много проблем с совместимостью и некоторые баги. Например

Node.js fs.watch:

  • Doesn't report filenames on MacOS.
  • Doesn't report events at all when using editors like Sublime on MacOS.
  • Often reports events twice.
  • Emits most changes as rename.
  • Does not provide an easy way to recursively watch file trees.

Node.js fs.watchFile:

  • Almost as bad at event handling.
  • Also does not provide any recursive watching.
  • Results in high CPU utilization.

Перечисленные выше моменты исходят изchokidar, который представляет собой модуль Node, предоставляющий готовую возможность прослушивания изменений файлов. просто следитьadd, unlink, changeТакие события, как чтение последней версии текста в редакторе, могут быть достигнуты диском / версией редактора синхронизации.

2. Context-Menu

Рабочий столcontextmenuРеализация (меню правой кнопки мыши) основана на родном IPC, который является относительно круглым.

Первое, что нам нужно знать, это то, чтоMenuклассmain process onlyиз.

в нуждеcontextmenuизJSX.ElementпривязатьonContextMenuмероприятие. Построить массив объектовArray<MenuItem>, и привязать событие триггера для каждого объекта MenuItem, а затем передать объект основному процессу через IPC.Стоит отметить, что в это время массив MenuItem присваивается глобальному объекту и временно сохраняется. Создайте реальный экземпляр MenuItem в основном процессе, привяжите событие click для MenuItem, запишите индекс серийного номера MenuItem при запуске события click MenuItem, а затем передайте индекс процессу визуализации через event.sender.send. После того, как процесс визуализации получает индекс, он извлекает один элемент MenuItem в соответствии с ранее сохраненным глобальным объектом и выполняет связанное событие.

onContextMenu => showContextualMenu (暂存MenuItems,ipcRenderer.send) => icpMain => menu.popup() => MenuItem.onClick(index) => event.sernder.send(index) => MenuItem.action()

Поэтому я использую удаленный объект для защиты описанной выше сложной связи IPC в своем приложении. В процессе рендеринга запускается построение и отображение Меню и привязка событий.

import { remote } from 'electron';
const { MenuItem, dialog, getCurrentWindow, Menu } = remote;

const onContextMenu = (project: Project) => {
  const menu = new Menu();

  const menus = [
    new MenuItem({
      label: '在终端中打开',
      visible: __DARWIN__,
      click() {
        const accessor = new FileAccessor(project.filePath);
        accessor.openInTerminal();
      },
    }),
    new MenuItem({
      label: '在 vscode 中打开',
      click() {
        const accessor = new FileAccessor(project.filePath);
        accessor.openInVscode();
      },
    }),
  ];

  menus.forEach(menu.append);
  menu.popup({ window: getCurrentWindow() });
};

6. Журнал

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

Что касается рабочего стола, их журналы основаны на библиотеке журналов:winston.

Глобальный объект журнала предоставляется как в основном процессе, так и в процессе визуализации, а интерфейсы одинаковы. соответственноdebug, info, warn, error. В процессе рендеринга просто инкапсулируйтеwindow.consoleна объектеdebug, info, warn, errorметод, когда журнал выводится на консоль браузера, он также передается основному процессу через IPC, которым управляет основной процесс.

Основной процесс получает информацию журнала от процесса визуализации и информацию журнала от самого основного процесса. установить дваtransports.winston.transports.Consoleа такжеwinston.transports.DailyRotateFileИспользуется для печати информации журнала на консоли терминала и сохранения ее в файле на диске соответственно. DailyRotateFile указывается в днях, максимальный срок хранения — 14 дней.

Модули установки журнала вводятся соответственно при запуске основного процесса и процесса рендерера. Поскольку метод журнала доступен глобально, его нужно ввести только один раз при запуске процесса. В то же время в среде TS необходимо добавить объявление типа метода журнала.

7. Упаковка, публикация и обновление

В мире с открытым исходным кодом уже есть очень хорошие инструменты для упаковки и распространения.electron-builder. Он объединяет мультиплатформенную упаковку, подписывание, автоматические обновления, публикацию на таких платформах, как Github, и многое другое.

Учитывая, что этот инструмент можно использовать только в интрасети, его нельзя опубликовать на Github, а подписать без Apple Developer Tools невозможно.electron-builderОн упакован на локальном компьютере. Если он выпущен, его можно упаковать и загрузить только вручную. Пользователи могут только вручную загрузить установочный пакет для покрытия установки. Он не может быть автоматически обновлен, как VSCODE.

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

Как реализовать эту функцию в ограниченных условиях?

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

Сервер идентифицирует последнюю версию читаемого файла: она обновляется каждый раз при упаковке.package.json, поэтому прямо ставимpackage.jsonПросто загрузите его на неавторизованный CDN и запросите этот файл при обновлении.

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

Обновить логику в коде приложения:

import got from 'got';
import semver from 'semver';
import { app, remote, BrowserWindow } from 'electron';

const realApp = app || remote.app;
const currentVersion = realApp.getVersion();

export async function checkForUpdates(window: BrowserWindow, silent: boolean = false) {
  const url = `http://yourcdn/package.json?t=${Date.now()}`;
  try {
    const response = await got(url);
    const pkg = JSON.parse(response.body);
    log.debug('检查更新,云端版本:', pkg.version);
    log.debug('当前版本', currentVersion);
    if (semver.lt(currentVersion, pkg.version)) {
      window.webContents.send('update-available', pkg.version);
    } else {
      window.webContents.send('update-not-available', silent);
    }
  } catch (error) {
    window.webContents.send('update-error', silent);
  }
}

Когда основной процесс приложения запущен, пользователь щелкает меню приложения.检查更新Этот метод вызывается, когда процесс пользовательского интерфейса уведомляется об отправке уведомления. Мы ожидаем обновления, когда основной процесс приложения начинает молчать в случае сбоя или отсутствия обновления, не беспокоя пользователя, поэтому конвейер IPC может обеспечитьsilentпараметр. После обнаружения обновления пользователь может быть уведомлен, и пользователь может перейти к последней версии тегов Gitlab, щелкнув обновление, и помочь пользователю загрузить последнюю версию для ручной установки.

8. Другие

1. devtools

Сторона рендеринга разработки приложений Electron также отлаживается с помощью инструментов разработчика Chrome. Для React расширения devtools для таких ящиков, как Mobx, также доступны черезelectron-devtools-installerустановить. Вызывается после создания окна приложенияelectron-devtools-installerпровестиmobx,reactи т.д. Установка расширения.

const { default: installExtension, MOBX_DEVTOOLS, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer');
const extensions = [REACT_DEVELOPER_TOOLS, MOBX_DEVTOOLS];
for (const extension of extensions) {
  try {
    installExtension(extension);
  } catch (e) {
    // log.error(e);
  }
}

2. Сохраняйте размер окна

Для настольных приложений общим требованием является повторное открытие после закрытия, а также восстановление размера и положения окна при последнем открытии. Это относительно просто реализовать, прослушать событие изменения размера окна и записать информацию об окне в папку данных приложения текущего пользователя, то естьapp.getPath(appData). В следующий раз, когда вы запустите окно создания приложения, вы можете прочитать этот файл, чтобы установить информацию окна. В сообществе с открытым исходным кодом уже есть библиотека, которая инкапсулирует эту функцию:electron-window-state

const windowStateKeeper = require('electron-window-state');
let win;

app.on('ready', function () {
  let mainWindowState = windowStateKeeper({
    defaultWidth: 1000,
    defaultHeight: 800
  });

  win = new BrowserWindow({
    'x': mainWindowState.x,
    'y': mainWindowState.y,
    'width': mainWindowState.width,
    'height': mainWindowState.height
  });

  mainWindowState.manage(win);
});

Просто укажите размер окна по умолчанию и все остальноеelectron-window-stateЭто все сделано для нас.


Если вы считаете, что этот контент ценен для вас, пожалуйста, поставьте лайк и подпишитесь на нашуОфициальный сайтА на нашем официальном аккаунте WeChat (WecTeam) каждую неделю публикуются качественные статьи:

WecTeam