предисловие
Сегодня я представляю вам фреймворк приложений собственной разработки под названием Sugar-Electron, основанный на платформе разработки настольных компьютеров Electron, в надежде улучшить стабильность приложений Electron и помочь командам разработчиков снизить затраты на разработку и обслуживание.
Автор использует Electron как десктопное приложение уже 3 года, и за это время я столкнулся со многими большими и маленькими ямами. Но если подытожить, то самая большая проблема — это стабильность приложения и эффективность разработки. Мы ожидаем, что благодаря этой структуре приложение можно будет оптимизировать в обоих аспектах.
Адрес исходного кода проекта:GitHub.com/сахарные турбины…
Если у вас есть какие-либо вопросы, вы можете отсканировать код, чтобы присоединиться к групповому чату WeChat, чтобы обсудить
О стабильности приложения
Мы знаем, что приложения Electron имеют три основных модуля.
- основной процесс
- процесс рендеринга
- межпроцессного взаимодействия
Так как мы относимся к многооконному (процессу мультирендеринга) приложению, то все общие служебные модули окна будем прописывать в основной модуль процесса, что будет таить в себе скрытую опасность для стабильности всей программы.
В Electron основной процесс контролирует жизненный цикл всей программы, а также отвечает за управление создаваемыми им процессами рендеринга. Как только возникает проблема с кодом основного процесса, возникают следующие условия.
- Основной процесс аномального коллапса не пойман, прямой результат выхода приложения.
- Происходит основной процесс, который напрямую приводит ко всем процессам рендеринга, а UI находится в блокирующем неотвечающем состоянии.
Поэтому в Sugar-Electron мы ввели концепцию сервисного процесса, рассчитывая перенести бизнес-код, изначально написанный в основном процессе, в сервисный процесс (по сути, процесс рендеринга), чтобы сбой, вызванный этими кодами, не сделал вся Программа выходит. Диспетчер основного процесса может перезапустить процесс и восстановить состояние до сбоя при сбое Сервиса, тем самым повысив стабильность и доступность всей программы.
Об эффективности разработки
Electron — это платформа для разработки настольных компьютеров, которая предоставляет основу для разработки настольных приложений. Однако в самом фреймворке отсутствуют условности, поэтому при использовании Electron для разработки приложений системные модули будут разделены на различные типы, а код будет написан по-разному, что значительно увеличит стоимость обучения и снизит эффективность разработки. Разработчики. сахар-электрон разработан в соответствии с соглашением, что снижает затраты на совместную работу команды и повышает эффективность.
характеристика
- Встроенный модуль межпроцессного взаимодействия, поддержка ответа на запрос, методы публикации и подписки
- Встроенный модуль обмена состоянием между процессами, поддержка изменения синхронизации состояния, мониторинг изменения состояния
- Встроенный модуль управления процессами, поддержка централизованного управления модулями процессов
- Встроенный модуль управления конфигурацией, поддержка разработки, тестирования, переключение конфигурации производственной среды
- Встроенный подключаемый модуль для поддержки расширяемого механизма подключаемых модулей.
- Фреймворк менее навязчив, а стоимость доступа к проекту и трансформации низкая.
- постепенное развитие
Принципы дизайна
1. Весь сахарный электрон разработан вокруг процесса рендеринга.Основной процесс действует только как демон для управления процессами (создание, удаление, мониторинг исключений) и планирования (связь процессов, мост функции состояния).
Основной процесс не обрабатывает бизнес-логику, преимущества такой конструкции:
- Может предотвратить сбой основного процесса из-за неперехваченного исключения, что приведет к завершению работы приложения.
- Избегайте блокировки основного процесса, что приведет к блокировке всех процессов рендеринга, что приведет к блокировке пользовательского интерфейса и зависанию.
2. Все бизнес-модули сахара-электрона являются процессами рендеринга. Мы знаем, что к процессам нельзя получить доступ напрямую.Чтобы сделать вызовы между процессами такими же удобными, как и прямые вызовы между модулями с одним и тем же потоком, Sugar-electron предоставляет следующие три модуля:
- Модуль межпроцессного взаимодействия
- Модуль обмена состоянием между процессами
- Модуль управления процессами
3. Для обеспечения того, чтобы ядро фреймворка было достаточно оптимизированным, стабильным и эффективным, очень важна возможность расширения каркаса.По этой причине, сахар-электрон предоставляет настраиваемый механизм подключаемых модулей для расширения возможностей каркаса, что также может способствовать повторному использованию бизнес-логики и даже расширению экосистемы.
Ниже приводится логическое представление фреймворка:
сахара-электрон разработан на основе микроядерной архитектуры, как показано на следующем рисунке:
Ядро его структуры состоит из семи модулей:
- Базовый класс процесса BaseWindow
- Класс сервисного процесса Сервис
- Управление процессами WindowCenter
- Межпроцессное взаимодействие ipc
- Хранилище обмена состоянием между процессами
- конфигурация центра конфигурации
- Плагины управления плагинами
Установить
npm i sugar-electron
строительные леса
npm i sugar-electron-cli -g
sugar-electron-cli init
Основные функции
Базовый класс процесса — BaseWindow
Базовый класс процесса BaseWindow основан на вторичной инкапсуляции BrowserWindow, и сахар-электрон использует BaseWindow в качестве носителя для агрегирования всех основных модулей фреймворка.
Например
Создайте процесс рендеринга с помощью BrowserWindow
// 在主进程中.
const { BrowserWindow } = require('electron')
let win = new BrowserWindow({ width: 800, height: 600, show: false });
win.on('ready-to-show', () => {})
win.loadURL('https://github.com');
Создайте процесс рендеринга с помощью BaseWindow
// 在主进程中.
const { BaseWindow } = require('sugar-electron');
let win = new BaseWindow('winA', {
url: 'https://github.com' // BaseWindow 特有属性,默认打开的页面
width: 800, ght: 600, show: false
});
win.on('ready-to-show', () => {})
const browserWindowInstance = winA.open();
Класс сервисного процесса - Сервис
В реальном развитии бизнеса нам нужен процесс, несущий функции общего модуля бизнес-процесса, для этого и рождается Сервис. Экземпляр процесса службы на самом деле является процессом рендеринга, но разработчику нужно только передать js-файл начальной записи, чтобы создать процесс рендеринга, а BaseWindow — это то же самое, что и совокупность всех основных модулей фреймворка.
Например
// -----------------------主进程-----------------------
const service = new Service('service', path.join(__dirname, 'app.js'), true);
service.on('success', function () {
console.log('service进程启动成功');
});
service.on('fail', function () {
console.log('service进程启动异常');
});
service.on('crashed', function () {
console.log('service进程崩溃'); // 对应webContents.on('crashed')
});
service.on('closed', function () {
console.log('service进程关闭'); // 对应browserWindow.on('closed')
});
Технологическая связь — IPC
Являясь основным модулем межпроцессного взаимодействия, ipc поддерживает три метода взаимодействия:
- запрос-ответ (между рендерером)
- Публикация-подписка (интеррендерер)
- Основной процесс взаимодействует с процессом рендеринга
ответ на запрос
Логический вид:
Например
// 服务进程service
const { ipc } = require('sugar-electron');
// 注册响应服务A1
ipc.response('service-1', (json, cb) => {
console.log(json); // { name: 'winA' }
cb('service-1响应');
});
// 渲染进程winA
const { ipc, windowCenter } = require('sugar-electron');
const r1 = await windowCenter.service.request('service-1', { name: 'winA' });
console.log(r1); // service-1响应
// 等同
const r2 = await ipc.request('service', 'service-1', { name: 'winA' });
console.log(r2); // service-1响应
аномальный
Код состояния 1 | Инструкция 2 |
---|---|
1 | процесс не найден |
2 | Служба регистрации процессов не найдена |
3 | тайм-аут |
опубликовать подписаться
Логический вид:
Например
// 服务进程service
const { ipc } = require('sugar-electron');
setInterval(() => {
ipc.publisher('service-publisher', { name: '发布消息' });
}, 1000);
// winA
const { ipc, windowCenter } = require('sugar-electron');
// 订阅
const unsubscribe = windowCenter.service.subscribe('service-publisher', (json) => {
console.log(json); // { name: '发布消息' }
});
// 等同
const unsubscribe = ipc.subscribe('service', service-publisher', (json) => {
console.log(json); // { name: '发布消息' }
});
// 取消订阅
unsubscribe();
// 等同
windowCenter.service.unsubscribe('service-publisher', cb);
Связь между основным процессом и процессом рендеринга (имя процесса «main», зарезервированное для основного процесса)
Концепция дизайна структуры Sugar-electron Все бизнес-модули завершаются каждым процессом рендеринга, поэтому в основном нет функции связи с основным процессом, но это не исключает сцены, где основной процесс взаимодействует с процессом рендеринга. Таким образом, модуль связи процесса сахара-электрона поддерживает интерфейс связи с основным процессом.
Например
// 主进程
const { ipc } = require('sugar-electron');
ipc.response('test', (data, cb) => {
console.log(data); // 我是渲染进程
cb('我是主进程')
});
// winA
const { windowCenter } = require('sugar-electron');
const res = windowCenter.main.request('test', '我是渲染进程');
console.log(res); // 我是主进程
// 或者
const res = ipc.request('main', 'test', '我是渲染进程');
console.log(res); // 我是主进程
Управление процессами — WindowCenter
Сахар-электрон Все бизнес-модули представляют собой процесс рендеринга. Мы знаем, что между процессами не доступна напрямую, все с модулем управления процессом.
Все процессы рендеринга могут найти соответствующий процесс рендеринга в windowCenter по уникальному ключу, соответствующему имени процесса, так что вызовы между процессами так же удобны, как и прямые вызовы между модулями одного и того же потока.
Например
Требование: открыть winB в winA и установить окно B setSize(400, 400) после завершения инициализации webContents winB
// 主进程
const { BaseWindow, Service, windowCenter } = require('sugar-electron');
// 设置窗口默认设置,详情请参考Electron BrowserWindow文档
BaseWindow.setDefaultOption({
show: false
});
// winA
const winA = new BaseWindow('winA', {
url: `file://${__dirname}/indexA.html`
});
// winB
const winB = new BaseWindow('winB', {
url: `file://${__dirname}/indexB.html`
});
// 创建winA窗口实例
windowCenter.winA.open(); // 等同于winA.open();
// winA
const { windowCenter } = require('sugar-electron');
const winB = windowCenter.winB;
// 创建winB窗口实例
await winB.open();
// 订阅窗口创建完成“ready-to-show”
const unsubscribe = winB.subscribe('ready-to-show', () => {
// 解绑订阅
unsubscribe();
// 设置winB size[400, 400]
const r1 = await winB.setSize(400, 400);
// 获取winB size[400, 400]
const r2 = await winB.getSize();
console.log(r1, r2);
});
==Примечание: дескриптор сервисного процесса также можно получить через windowCenter==
Разделение состояний между процессами — хранилище
Сахар-электрон — это архитектура с несколькими процессами.В бизнес-системе неизбежно совместное состояние нескольких бизнес-процессов. Поскольку память между процессами независима друг от друга и не взаимодействует друг с другом, структура сахара-электрона интегрирует модуль обмена состоянием процесса.
Модуль обмена состоянием процесса разделен на две части:
- Основной процесс объявляет общие данные состояния
- Рендеринг настроек процесса, получение общих данных о состоянии и подписка на изменения состояния
Например
// 主进程——初始化申明state
const { store } = require('sugar-electron');
store.createStore({
state: {
name: '我是store'
},
modules: {
moduleA: {
state: {
name: '我是moduleA'
}
},
moduleB: {
state: {
name: '我是moduleB'
},
modules: {
moduleC: {
state: {
name: '我是moduleC'
}
}
}
}
}
});
// 渲染进程A,订阅state变化
const { store } = require('sugar-electron');
console.log(store.state.name); // 我是store
// 订阅更新消息
const unsubscribe = store.subscribe((data) => {
console.log(store.state.name); // 改变state
unsubscribe(); // 取消订阅
});
// moduleA
const moduleA = store.getModule('moduleA');
console.log(moduleA.state.name); // 我是moduleA
const unsubscribeA = moduleA.subscribe((data) => {
console.log(moduleA.state.name); // 改变moduleA
unsubscribeA(); // 取消订阅
});
// 渲染进程B,设置state
const { store } = require('sugar-electron');
await store.setState({
'name': '改变state'
});
// moduleA
const moduleA = store.getModule('moduleA');
await moduleA.setState({
'name': '改变moduleA'
});
конфигурация - конфигурация
сахарный электрон предоставляет конфигурацию с несколькими средами, которую можно переключать в соответствии с переменными среды, а конфигурация среды генерации загружается по умолчанию.
config
|- config.base.js // 基础配置
|- config.js // 生产配置
|- config.test.js // 测试配置——环境变量env=test
|- config.dev.js // 开发配置——环境变量env=dev
блок-схема:
Например
// 主进程
const { config } = require('sugar-electron');
global.config = config.setOption({ appName: 'sugar-electron', configPath: path.join(__dirname, 'config') });
// 渲染进程
const { config } = require('sugar-electron');
console.log(config);
==Примечания:==
- Файл конфигурации AppData/appName config.json { "env": "переменные среды", "config": "конфигурация" }
- сахар-электрон автоматически инициализируется по умолчанию в соответствии с конфигурацией корневого каталога
Плагины - плагины
Хороший фреймворк неотделим от масштабируемости и повторного использования фреймворка в бизнесе. Разработчики настраивают и настраивают плагины через модуль плагинов для установки плагинов.
==Использование плагина требует трех шагов:==
- индивидуальный пакет
- config проблема с конфигурацией каталога plugins.js установка плагина конфигурации
- Используйте плагины
Пакет плагинов
// 1、自定义封装ajax插件adpter
const axios = require('axios');
const apis = {
FETCH_DATA_1: {
url: '/XXXXXXX1',
method: 'POST'
}
}
module.exports = {
/**
* 安装插件,自定义插件必备
* @ctx [object] 框架上下文对象{ config, ipc, store, windowCenter, plugins }
* @params [object] 配置参数
*/
install(ctx, params = {}) {
// 通过配置文件读取基础服务配置
const baseServer = ctx.config.baseServer;
return {
async callAPI(action, option) {
const { method, url } = apis[action];
try {
// 通过进程状态共享SDK获取用户ID
const token = ctx.store.state.token;
const res = await axios({
method,
url: `${baseServer}${url}`,
data: option,
timeout: params.timeout // 通过插件配置超时时间
});
if (action === 'LOGOUT') {
// 通过进程间通信模块,告知主进程退出登录
ctx.ipc.sendToMain('LOGOUT');
}
return res;
} catch (error) {
throw error;
}
}
}
}
}
Установка плагина
Настройте установку плагина в каталоге центра конфигурации plugins.js
config
|- config.base.js // 基础配置
|- config.js // 生产配置
|- config.test.js // 测试配置——环境变量env=test
|- config.dev.js // 开发配置——环境变量env=dev
|- plugins.js // 插件配置文件
// 2、配置插件安装
const path = require('path');
exports.adpter = {
// 如果根路径plugins目录有对应的插件名,则不需要配置path或package
path: path.join(__dirname, '../plugins/adpter'), // 插件绝对路径
package: 'adpter', // 插件包名,如果package与path同时存在,则package优先级更高
enable: true, // 是否启动插件
include: ['winA'], // 插件使用范围,如果为空,则所有渲染进程安装
params: { timeout: 20000 } // 传入插件参数
};
использование плагина
// 3、使用插件——winA
const { plugins } = require('sugar-electron');
const res = await plugins.adpter.callAPI('FETCH_DATA_1', {});
Автоматически инициализировать основные модули
Разработчики, которые использовали яйцо, должны знать, что базовый функциональный модуль яйца будет автоматически инициализирован в соответствии с соответствующим каталогом. сахар-электрон также предоставляет возможность автоматической инициализации на основе каталога. Нужно только использовать интерфейс запуска фреймворка, чтобы начать передачу параметров конфигурации, чтобы завершить автоматическую инициализацию основного модуля.
Например
const { start } = require('sugar-electron');
start({
appName: '应用名',
basePath: '启动目录',
configPath: '配置中心目录', // 可选,默认basePath + './config'
storePath: '进程状态共享目录', // 可选,默认basePath + './store'
windowCenterPath: '配置中心目录', // 可选,默认basePath + './windowCenter'
pluginsPath: '插件目录', // 可选,默认basePath + './plugins'
})
Меры предосторожности
1. Поскольку модуль ядра сахара-электрона автоматически определяет среду основного процесса или процесса рендеринга и автоматически выбирает модули для загрузки в разных средах, если вы используете упаковку веб-пакета, код обеих сред будет упакован, и могут возникнуть исключения.
Поэтому, если вы используете webpack для упаковки, внесите сахар-электрон следующим образом:
// 主进程
const { ipc, store, ... } = require('sugar-electron/main')
// 渲染进程
const { ipc, store, ... } = require('sugar-electron/render')
API
start
Интерфейс запуска фреймворка, автоматическое монтирование config, store, windowCenter, модулей плагинов
API основного процесса
/**
* 启动sugar
* @param {object} options 启动参数
* @param {string} options.appName 应用名
* @param {string} options.basePath 启动目录
* @param {string} options.configPath 配置目录,默认basePath + './config'
* @param {string} options.storePath 进程状态共享目录,默认basePath + './store'
* @param {string} options.windowCenterPath 窗口中心目录,默认basePath + './windowCenter'
* @param {string} options.pluginsPath 插件目录,默认basePath + './plugins'
*/
start(opions)
Пример использования
// -----------------------主进程-----------------------
start({
appName: string, 应用名,%appData%目录
basePath: string, 启动目录
configPath: string, 配置目录
storePath: string, 进程状态共享目录
windowCenterPath: string, 窗口中心目录
pluginsPath: string 插件目录
});
BaseWindow
/**
* 主进程调用
* @param {string} name
* @param {object} option
*/
new BaseWindow(name, option);
API основного процесса
setDefaultOptions [метод класса] Устанавливает конфигурацию окна по умолчанию
/**
* @param {object} option 参考electron BrowserWindow
*/
setDefaultOptions(option)
open [метод экземпляра] Создает экземпляр BrowserWindow
/**
* @param {object} option 参考electron BrowserWindow
* @return {browserWindow}
*/
open(option)
getInstance [метод экземпляра]
/**
* @return {browserWindow}
*/
getInstance(option)
издатель [метод экземпляра] публикует уведомление в текущем окне, обратитесь к модулю ipc
/**
* @param {string} eventName 通知事件名
* @param {object} param 参数
* @return {browserWindow}
*/
publisher(eventName, param)
Пример использования
// -----------------------主进程-----------------------
const { BaseWindow } = require('sugar-electron');
BaseWindow.setDefaultOptions({
width: 600,
height: 800,
show: false,
...
});
const winA = new BaseWindow('winA', { url: 'https://github.com' });
const instance = winA.open({...}); // 创建窗口
instance === winA.getInstance(); // true
Service
API основного процесса
/*
* 创建服务进程
* @param {string} name 服务进程名
* @param {string} path 启动入口文件路径(绝对路径)
* @param {boolean} devTool 是否打开调试工具,默认false
*/
new Service(name = '', path = '', openDevTool = false);
Пример использования
// -----------------------主进程-----------------------
const service = new Service('service', path.join(__dirname, 'app.js'), true);
service.on('success', function () {
console.log('service进程启动成功');
});
service.on('fail', function () {
console.log('service进程启动异常');
});
service.on('crashed', function () {
console.log('service进程崩溃'); // 对应webContents.on('crashed')
});
service.on('closed', function () {
console.log('service进程关闭'); // 对应browserWindow.on('closed')
});
windowCenter
Основной процесс, API процесса рендеринга
windowCenter: object 进程集合key=进程名 value=进程实例,默认{}
Пример использования
// -----------------------主进程-----------------------
const service = new Service('service', path.join(__dirname, 'app.js'), true);
const winA = new BaseWindow('winA', {});
const winB = new BaseWindow('winB', {});
windowCenter['service'] === service; // true
windowCenter['winA'] === winA; // true
windowCenter['winB'] === winB; // true
windowCenter['winA'].open(); // 创建winA窗口实例,同步调用
windowCenter['winA'].on('ready-to-show', () => {
windowCenter['winA'].setFullscreen(true);
});
// -----------------------渲染进程-----------------------
// 渲染进程接口调用实际上是通过ipc通道通知主进程进程接口调用,所以接口异步而非同步
(async () => {
await windowCenter['winA'].open(); // 创建winA窗口实例,异步Promise调用
windowCenter['winA'].subscribe('ready-to-show', async () => {
await windowCenter['winA'].setFullscreen(true);
});
})()
ipc
API основного процесса ответ ответ
/**
* 注册响应服务
* @param {string} eventName 事件名
* @param {function} callback 回调
* 使用方式,渲染进程ipc.request('main', eventName, param)
*/
response(eventName, callback)
API рендерера
setDefaultRequestTimeout устанавливает время ожидания ответа
setDefaultRequestTimeout(timeout = 0);
запрос запрос
/**
* @param {string} toId 进程ID(注册通信进程模块名)
* @param {string} eventName 事件名
* @param {any} data 请求参数
* @param {number} timeout 超时时间,默认20s *
* @return 返回Promise对象
*/
request(toId, eventName, data, timeout)
ответ ответ
/**
* 注册响应服务
* @param {string} eventName 事件名
* @param {function} callback 回调
*/
response(eventName, callback)
служба ответа на отказ от регистрации
/**
* @param {string} eventName 事件名
* @param {function} callback 回调
*/
unresponse(eventName, callback)
издатель публикует
/**
* @param {string} eventName 事件名
* @param {any} param 参数
*/
publisher(eventName, param)
подписаться подписаться
/**
* @param {string} toId 进程ID(注册通信进程模块名)
* @param {string} eventName 事件名
* @param {function} callback 回调
*/
subscribe(toId, eventName, callback)
отписаться отписаться
/**
* @param {string} toId 进程ID(注册通信进程模块名)
* @param {string} eventName 事件名
* @param {function} callback 回调
*/
unsubscribe(toId, eventName, callback)
Пример использования
// ---------------------winA---------------------
const { ipc } = require('sugar-electron');
// 注册响应服务A1
ipc.response('get-data', (json, cb) => {
console.log(json); // { name: 'winB' }
cb('winA响应');
});
// ---------------------winB---------------------
const { ipc, windowCenter } = require('sugar-electron');
const btn1 = document.querySelector('#btn1');
const { winA } = windowCenter;
btn1.onclick = () => {
const r1 = await winA.request('get-data', { name: 'winB' });
console.log(r1); // winA响应
// 等同
const r2 = await ipc.request('get-data', 'get-data', { name: 'winB' });
console.log(r2); // winA响应
}
store
API основного процесса
Состояние инициализации createStore
/**
* @param {object} store
*/
createStore(store)
API рендерера
setState установить состояние
/**
* 单个值设置
* @param {string} key
* @param {any} value
* @return 返回Promise对象
*/
setState(key, value)
/**
* 批量设置
* @param {object} state
* @return 返回Promise对象
*/
setState(state)
подписаться подписаться на уведомление об изменении значения текущего модуля
/**
* @param {function} cb 订阅回调
* @return {function} 返回注销订阅function
*/
subscribe(cb)
отписаться отписаться
/**
* @param {funtion} cb 订阅回调
*/
unsubscribe(cb)
getModule Получить модуль
/**
* 获取module
* @param {string} moduleName 模块名
* @return {object} module
* 返回:setState: 设置当前模块state;subscribe: 订阅;unsubscribe: 注销订阅;getModule: 获取当前模块的子模块;getModules
*/
getModule(moduleName)
getModules получить все модули
/**
* @return {array} [module, module, module]
*/
getModules()
Пример использования
// 主进程——初始化申明state
const { store } = require('sugar-electron');
store.createStore({
state: {
name: '我是store'
},
modules: {
moduleA: {
state: {
name: '我是moduleA'
}
},
moduleB: {
state: {
name: '我是moduleB'
},
modules: {
moduleC: {
state: {
name: '我是moduleC'
}
}
}
}
}
});
// 渲染进程
const { store } = require('sugar-electron');
store.state.name; // 我是store
// 订阅更新消息
const unsubscribe = store.subscribe((data) => {
console.log('更新:', data); // 更新:{ name: '我是store1' }
});
await store.setState({
'name': '我是store1'
});
unsubscribe(); // 取消订阅
// moduleA
const moduleA = store.getModule('moduleA');
moduleA.state.name; // 我是moduleA
const unsubscribeA = moduleA.subscribe((data) => {
console.log('更新:', data); // 更新:{ name: '我是moduleA1' }
});
await moduleA.setState({
'name': '我是moduleA1'
});
moduleA.unsubscribe(cb); // 取消订阅