P.S. Каждая часть — это часть, сама по себе смысла не имеет, чтобы обзавестись необходимым контекстом и не испытывать когнитивный диссонанс от отсутствия так необходимых блоков текста начните читать с 1 части
После того, как все части приложения были разработаны, их можно объединить в одно большое приложение.
Еще раз сделаю обзор на API всех компонентов.
OpenVPN— основной компонент, отвечающий за установку и использование VPN соединения.
Configs — Компонент, отвечающий за хранение и загрузку OpenVPN конфигов.
Electron компоненты.
Vpn — Основной элемент управления приложением.
Notify — Элемент представления уведомлений.
Context — элемент навигации по приложению.
Setting — Элемент настройки приложения.
Callback — Элемент обратной связи.
Самое время убедиться, каким действительно простым получается мой подход к разработке многооконных приложений на Electron.
Основной код, небольшой код с которого начинается разработка приложения.
Базовый код, явно демонстрирующий способ взаимодействия компонентов между собой.
Полный код основного файла index.js.
Остается только запустить.
11 часть — Сборка приложения под Windows
После того, как все части приложения были разработаны, их можно объединить в одно большое приложение.
Еще раз сделаю обзор на API всех компонентов.
OpenVPN— основной компонент, отвечающий за установку и использование VPN соединения.
API OpenVPN
const OpenVPN = require('./../../app/components/OpenVPN')
const ovpn = new OpenVPN()
// Обработка статусов ответов
ovpn.on(({ id, message }, other) => {
if (id == 10 || id == 11) {
// Установка
ovpn.installer()
}
console.log(`id: ${id} | message: ${message}`)
})
// Подключение
ovpn.connect(`${__dirname}/config.ovpn`, { reconnect: true })
setTimeout(() => {
// Отключение
ovpn.disconnect()
}, 30000)
Configs — Компонент, отвечающий за хранение и загрузку OpenVPN конфигов.
API Configs
const Configs = require('./../../app/components/configs')
Configs.load().then(length => {
console.log(`Загружено конфигов: ${length}`)
const search = Configs.get({
CountryLong: 'Japan'
, Ping: 22 // 1-22
, NumVpnSessions: '-' // empty
, Score: '-' // empty
, Speed: 13311 // bit
, Uptime: 3600000 // ms
, TotalUsers: '-' // empty
, TotalTraffic: '-' // empty
})
console.log(`Найдено конфигов: ${search.length}`)
}, () => {
console.log(`Ошибка загрузки конфигов`)
})
Electron компоненты.
Vpn — Основной элемент управления приложением.
API Vpn
const { app } = require('electron')
, VPN = require('./../../app/components/vpn')
app.on('ready', async() => {
const Vpn = new VPN()
// Только после того как окно инициализируется программа продолжит исполнятся
await Vpn.ready()
// Показываем окно
Vpn.show()
// Показываем Tray
Vpn.showTray()
// Обработчик подключения
Vpn.onConnect(() => {
console.log('Connect')
// Передаем в окно статус
Vpn.setStatus('waiting', 'yellow')
setTimeout(() => {
console.log('Connected!')
// Передаем в окно статус
Vpn.setStatus('resolve', 'blue')
}, 4000)
})
// Обработчик отключения
Vpn.onDisconnect(() => {
console.log('Disconnect')
// Передаем в окно статус
Vpn.setStatus('reject', 'red')
})
Vpn.onContext(() => {
console.log('Context menu')
})
// Переподключатель если в течении 5 сек после объявления
// не будет вызвана функция Vpn.stopReconnect()
// произойдет переподключение
Vpn.reconnect(next => {
console.log('Reconnect')
next()
}, 5000)
// Предотвращает или останавливает переподключение
//stopReconnect()
// Обработчик клика на Tray
Vpn.onTrayClick(() => {
// Имитация подключения и отключения
if (!Vpn.status) {
Vpn.connect()
} else {
Vpn.disconnect()
}
})
// Обработчик клика правой кнопкой мыши на Tray
Vpn.onTrayRightClick(() => {
if (Vpn.isVisible()) {
Vpn.hide()
} else {
Vpn.show()
}
})
})
Notify — Элемент представления уведомлений.
API Notify
const { app } = require('electron')
, VPN = require('./../../app/components/vpn')
, NOTIFY = require('./../../app/components/notify')
app.on('ready', async() => {
const Vpn = new VPN()
, Notify = new NOTIFY(Vpn.root)
// Только после того как окна инициализируются программа продолжит исполнятся
await Promise.all([
Notify.ready(),
Vpn.ready()
])
Vpn.show()
Vpn.showTray()
const SASI_FSB = [{
title: 'Да',
return: 'Да, подключайте меня!'
}, {
title: 'Нет',
return: false
}]
setTimeout(() => {
// Уведомление типа "alert"
Notify.alert('Прогресс не остановить!', 3000, () => {
// Уведомление типа "confirm"
Notify.confirm('Вы хотите подключиться к VPN', CONFIRN_FSB, data => {
console.log(data)
if (data == false) {
// Устанавливает тип уведомления
Notify.setType('static')
// Уведомление типа "confirm"
Notify.confirm('Вы хотите выйти ?', SASI_FSB, data => {
if (data != false) {
app.quit()
}
})
} else {
Vpn.setStatus('resolve', 'blue')
}
})
})
}, 2000)
// Устанавливает тип уведомления
// Notify.setType('static')
// Уведомление типа "alert"
// Notify.alert('Прогресс не остановить!', 3000, () => {})
// Уведомление типа "confirm"
//Notify.confirm('Вы хотите подключиться к VPN', SASI_FSB, console.log)
Context — элемент навигации по приложению.
API Context
const { app } = require('electron')
, VPN = require('./../../app/components/vpn')
, CONTEXT = require('./../../app/components/context')
app.on('ready', async() => {
const Vpn = new VPN(),
Context = new CONTEXT(Vpn.root)
// Только после того как окна инициализируются программа продолжит исполнятся
await Promise.all([
Context.ready(),
Vpn.ready()
])
Vpn.show()
Vpn.showTray()
Vpn.onContext(() => Context.show())
// обработчики
Context.onCheckIP(() => {
console.log('Определить IP')
})
Context.onUpdate(() => {
console.log('Обновить сервера')
})
Context.onSetting(() => {
console.log('Настройки')
})
Context.onCallback(() => {
console.log('Обратная связь')
})
Context.onHidden(() => {
console.log('Свернуть')
})
Context.onExit(() => {
console.log('Выход')
})
})
Setting — Элемент настройки приложения.
API Setting
const { app } = require('electron')
, SETTING = require('./../../app/components/setting')
app.on('ready', async() => {
const Setting = new SETTING()
// Только после того как окно инициализируются программа продолжит исполнятся
await Setting.ready()
// Показывает окно
Setting.show()
// Обработчик сохранения
Setting.onSave(async () => {
// Запрашиваем настройки
const vpn_setting = await Setting.get()
console.log(vpn_setting)
})
})
Callback — Элемент обратной связи.
API Callback
const { app } = require('electron')
, CALLBACK = require('./../../app/components/callback')
app.on('ready', async() => {
const Callback = new CALLBACK()
await Callback.ready()
// Показываем окно
Callback.show()
})
Самое время убедиться, каким действительно простым получается мой подход к разработке многооконных приложений на Electron.
Основной код, небольшой код с которого начинается разработка приложения.
Код
const { app, Menu } = require('electron')
, ipify = require('ipify')
, OPENVPN = require('./components/OpenVPN')
, Configs = require('./components/configs')
, NOTIFY = require('./components/notify')
, CONTEXT = require('./components/context')
, CALLBACK = require('./components/callback')
, SETTING = require('./components/setting')
, VPN = require('./components/vpn')
app.on('ready', async() => {
const Vpn = new VPN()
, Notify = new NOTIFY(Vpn.root)
, Context = new CONTEXT(Vpn.root)
, Callback = new CALLBACK(Context.root)
, Setting = new SETTING(Context.root)
await Promise.all([
Notify.ready(),
Context.ready(),
Callback.ready(),
Setting.ready(),
Vpn.ready()
])
// все окна инициализированы и готовы к взаимодействию
// многие ошибки возникают на почве не инициализированных окон
})
Базовый код, явно демонстрирующий способ взаимодействия компонентов между собой.
Код
const { app, Menu } = require('electron')
, ipify = require('ipify')
, OPENVPN = require('./components/OpenVPN')
, Configs = require('./components/configs')
, NOTIFY = require('./components/notify')
, CONTEXT = require('./components/context')
, CALLBACK = require('./components/callback')
, SETTING = require('./components/setting')
, VPN = require('./components/vpn')
// кнопки
const CONFIRM_BUTTON_CATEGORY = [{
title: 'Да',
return: true
}, {
title: 'Нет',
return: false
}],
CONFIRM_BUTTON_CONNECT = [{
title: 'Настр.',
return: true
}, {
title: 'Обнов. серв.',
return: false
}]
app.on('ready', async() => {
const Vpn = new VPN()
, Notify = new NOTIFY(Vpn.root)
, Context = new CONTEXT(Vpn.root)
, Callback = new CALLBACK(Context.root)
, Setting = new SETTING(Context.root)
await Promise.all([
Notify.ready(),
Context.ready(),
Callback.ready(),
Setting.ready(),
Vpn.ready()
])
//o// Завершение инициализации окон //o//
Context.onUpdate(() => (
Notify.alert(`Обновление серверов...`, 2000, () =>
Configs.load().then(
length => Notify.alert(`Доступно: ${length} серверов`),
error => Notify.confirm(`VPN сервера не обновлены. Попробовать еще раз ?`, CONFIRM_BUTTON_CATEGORY, reload => {
reload && Context.update()
})
)
)
))
Context.onCheckIP(() =>
Notify.alert(`Определение IP ...`, 2000, () =>
ipify().then(ip => Notify.alert(`IP: ${ip}`))
)
)
Context.onSetting(() => Setting.show())
Context.onCallback(() => Callback.show())
Context.onHidden(() => {
Vpn.hide()
Context.hide()
Callback.hide()
Setting.hide()
Notify.setType('static')
})
Context.onExit(() => app.quit())
Vpn.onContext(() => Context.show())
Vpn.showTray()
Vpn.show()
})
Полный код основного файла index.js.
Код
const { app, Menu } = require('electron')
, ipify = require('ipify')
, OPENVPN = require('./components/OpenVPN')
, Configs = require('./components/configs')
, NOTIFY = require('./components/notify')
, CONTEXT = require('./components/context')
, CALLBACK = require('./components/callback')
, SETTING = require('./components/setting')
, VPN = require('./components/vpn')
const CONFIRM_BUTTON_CATEGORY = [{
title: 'Да',
return: true
}, {
title: 'Нет',
return: false
}],
CONFIRM_BUTTON_CONNECT = [{
title: 'Настр.',
return: true
}, {
title: 'Обнов. серв.',
return: false
}]
app.on('ready', async() => {
const Vpn = new VPN()
, Notify = new NOTIFY(Vpn.root)
, Context = new CONTEXT(Vpn.root)
, Callback = new CALLBACK(Context.root)
, Setting = new SETTING(Context.root)
await Promise.all([
Notify.ready(),
Context.ready(),
Callback.ready(),
Setting.ready(),
Vpn.ready()
])
//o// Завершение инициализации окон //о//
Context.onUpdate(() => (
Notify.alert(`Обновление серверов...`, 2000, () =>
Configs.load().then(
length => Notify.alert(`Доступно: ${length} серверов`),
error => Notify.confirm(`VPN сервера не обновлены. Попробовать еще раз ?`, CONFIRM_BUTTON_CATEGORY, reload => {
reload && Context.update()
})
)
)
))
Context.onCheckIP(() =>
Notify.alert(`Определение IP ...`, 2000, () =>
ipify().then(ip => Notify.alert(`IP: ${ip}`))
)
)
Context.onSetting(() => Setting.show())
Context.onCallback(() => Callback.show())
Context.onHidden(() => {
Vpn.hide()
Context.hide()
Callback.hide()
Setting.hide()
Notify.setType('static')
})
Context.onExit(() => app.quit())
Vpn.onContext(() => Context.show())
Vpn.showTray()
Vpn.onTrayClick(() => {
if (Vpn.status) {
Vpn.disconnect()
} else {
Vpn.disconnect()
Vpn.connect()
}
})
Vpn.onTrayRightClick(() => {
if (!Vpn.isVisible()) {
Vpn.show()
Notify.setType('fly')
return
}
Vpn.hide()
Context.hide()
Callback.hide()
Setting.hide()
Notify.setType('static')
})
Setting.onSave(async() => {
const vpn_setting = await Setting.get()
const configs = Configs.get(vpn_setting)
if (configs.length != 0) {
Notify.alert(`Доступно: ${configs.length} серверов`, 6000)
} else {
Notify.confirm('Нет доступных для подключения серверов. Изменить настройки или обновить сервера ?', CONFIRM_BUTTON_CONNECT, config => {
if (config) {
Setting.show()
} else {
Context.update()
}
})
}
})
const {
AutoUpdate, Permutation, StartHidden
} = await Setting.get()
if (Permutation) {
Vpn.center()
}
if (StartHidden) {
Vpn.hide()
Notify.setType('static')
} else {
Vpn.show()
}
if (AutoUpdate) {
Context.update()
}
Configs.get({}).length == 0 && Context.update()
//o// С в этой части кода происходит взаимодействие с основным модулем //о//
const OpenVPN = new OPENVPN()
OpenVPN.on(({
id, message
}, {
config_information, reconnect
}) => {
if (id == 1) {
Vpn.setStatus('waiting', 'yellow')
}
if (id == 2) {
Vpn.stopReconnect()
Vpn.setStatus('resolve', 'blue')
Notify.alert('VPN подключен', 2000, () =>
Notify.alert(config_information, 30000)
)
}
if (id == 3) {
Vpn.setStatus('waiting', 'yellow')
Notify.alert('Переподключение к VPN', 4000)
if (reconnect) {
Vpn.reconnect(next => {
Notify.alert('Переподключение продолжается слишком долго, автоматическая смена VPN сервера', 4000, () => {
next()
})
})
}
}
if (id == 4) {
Vpn.setStatus('reject', 'red')
reconnect && Vpn.reconnect(next => next())
}
if (id == 5) {
if (!Vpn.isReconnect) {
Vpn.stopReconnect()
Vpn.setStatus('reject', 'red')
}
}
if (id == 6) {
reconnect && Vpn.reconnect(next => {
Notify.alert('Переподключение продолжается слишком долго, автоматическая смена VPN сервера', 4000, () => {
next()
})
})
}
if (id == 7) {
Notify.alert('TCP-соединение с VPN не удалось', 4000, () => {
Vpn.setStatus('reject', 'red')
Vpn.disconnect()
})
reconnect && Vpn.reconnect(next => next())
}
if (id == 8) {
reconnect && Vpn.reconnect(next => {
Notify.alert('Подключение продолжается слишком долго, автоматическая смена VPN сервера', 4000, () => {
next()
})
})
}
if (id == 9) {
Vpn.stopReconnect()
Vpn.setStatus('waiting', 'orange')
Notify.alert('Установка OpenVPN')
}
if (id == 10 || id == 11) {
Notify.confirm('Не удается подключиться к OpenVPN, установить необходимые компоненты ?', CONFIRM_BUTTON_CATEGORY, install => {
if (install) {
OpenVPN.installer()
} else {
Notify.confirm('Выйти из JS.VPN-Client ?', CONFIRM_BUTTON_CATEGORY,
exit => exit && app.quit()
)
}
})
}
if (id == 12) {
Vpn.setStatus('reject', 'red')
Notify.confirm('Не удалось установить TAP адаптер, попробовать еще раз ?', CONFIRM_BUTTON_CATEGORY, install => {
if (install) {
OpenVPN.installer()
} else {
Notify.confirm('Выйти из JS.VPN-Client ?', CONFIRM_BUTTON_CATEGORY,
exit => exit && app.quit()
)
}
})
}
if (id == 13) {
Vpn.setStatus('reject', 'red')
Notify.alert('Необходимые компоненты установлены!')
}
if (id == 14) {
Vpn.setStatus('reject', 'red')
Notify.alert('Неизвестная ошибка')
reconnect && Vpn.reconnect(next => next())
}
console.log(`${id} | ${message}`)
})
Vpn.onConnect(async() => {
const vpn_setting = await Setting.get()
const configs = Configs.get(vpn_setting),
random_config = parseInt(Math.random() * (configs.length - 1)),
conf = configs[random_config]
if (!conf) {
return Notify.confirm('Нет доступных для подключения серверов. Изменить настройки или обновить сервера ?', CONFIRM_BUTTON_CONNECT, config => {
if (config) {
Setting.show()
} else {
Context.update()
}
})
}
const config_information =
`IP: ${conf.IP}<br>` +
`Страна: ${conf.CountryLong.length > 9 ? `${conf.CountryLong.slice(0, 9)}..` : conf.CountryLong}<br>` +
`${conf.Ping != '-' && `Ping: ${conf.Ping}ms <br> `}` +
`${(conf.Speed || conf.Speed != 0) ? `Скорость: ${(conf.Speed / 1024 / 1024).toFixed(2)}Mb <br> ` : ''}` +
`${(conf.Uptime || conf.Speed != 0) ? `Uptime: ${parseUptime(conf.Uptime)} <br> ` : ''}` +
`Подключено: ${conf.NumVpnSessions}`
OpenVPN.connect(conf.path, {
reconnect: vpn_setting.AutoReconnect,
config_information
})
})
Vpn.onDisconnect(vpn_setting => {
OpenVPN.disconnect()
})
})
//о// Прочие функции для оформления //о//
const declOfNum = (number, titles) => {
cases = [2, 0, 1, 1, 1, 2];
return titles[(number % 100 > 4 && number % 100 < 20) ? 2 : cases[(number % 10 < 5) ? number % 10 : 5]];
}
const parseUptime = (l) => {
const day = parseInt(l / 60000 / 60 / 24)
const hours = parseInt(l / 60000 / 60)
const min = parseInt(l / 60000)
if (day) {
return `${day} ${declOfNum(day, ['день', 'дня', 'дней'])}`
}
if (hours) {
return `${hours} ${declOfNum(hours, ['час', 'часа', 'часов'])}`
}
if (min) {
return `${min} ${declOfNum(min, ['минута', 'минуты', 'минут'])}`
}
return 'нет данных'
}
Остается только запустить.
electron .
11 часть — Сборка приложения под Windows
Навигация
1 часть — Вводная
2 часть — Разработка
3 часть — OpenVPN компонент
4 часть — Configs компонент
5 часть — Vpn компонент
6 часть — Notify компонент
7 часть — Context компонент
8 часть — Setting компонент
9 часть — Callback компонент
10 часть — Объединение всех компонентов
11 часть — Сборка приложения под Windows
2 часть — Разработка
3 часть — OpenVPN компонент
4 часть — Configs компонент
5 часть — Vpn компонент
6 часть — Notify компонент
7 часть — Context компонент
8 часть — Setting компонент
9 часть — Callback компонент
10 часть — Объединение всех компонентов
11 часть — Сборка приложения под Windows
limassolsk
Вы написали уже десяток статей на эту тему, но они все выглядят не как статьи, а как куски кода.
Вы не рассматривали вариант опубликовать код на гитхабе, а здесь написать одну статью о вашем проекте?
Хабр всё таки больше для статей, а не репозиторий исходного кода.
Если бы разработчики openvpn стали выкладывать сюда свой код, то обычные статьи найти на хабре было бы просто нереально.
JsusDev Автор
Все что я хотел сказать, я сказал в 1 части. Сейчас я Демонстрирую ход разработки, не беспокойтесь осталась последняя часть.