Практика разработки приложений Electron IM

внешний интерфейс Electron

Команда фронтенда Могуцзе официально обосновалась в Наггетс.Надеюсь все не поскупятся на ваши похвалы(спасибо)!

Ноль, введение

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

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

Эта статья в основном включает:

  1. Шифрование и дешифрование сообщений
  2. сериализация сообщений
  3. сетевой протокол передачи
  4. Собственный протокол передачи данных
  5. Многопроцессная оптимизация
  6. локальное хранилище сообщений
  7. Значок нового сообщения в области уведомлений мигает
  8. Автообновление проекта
  9. межпроцессного взаимодействия
  10. разное

1. Шифрование и дешифрование сообщений

задний план

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

Простой способ реализации

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

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

  1. легко переворачивается. Интерфейсный код относительно легко реконструировать.
  2. Плохая работа. В компании могут быть добавлены группы со многими проектами, и каждая группа будет получать много сообщений, а фронтенд-обработка относительно медленная.
  3. Точно так же, если алгоритмы шифрования и дешифрования реализованы на стороне клиента, разные клиенты, такие как ios и android, должны реализовать один и тот же алгоритм соответственно из-за разных используемых языков разработки, что увеличивает стоимость обслуживания.

Наше решение

Мы используемC++ AddonsПредоставляет возможность реализовать алгоритмы шифрования и дешифрования в C++ SDK, так что js может вызывать модули C++ SDK точно так же, как и модули Node. Это решает все проблемы, упомянутые выше, за один раз.

После разработки аддона используйтеnode-gypдля создания надстроек C++. node-gyp вызовет набор инструментов компиляции на каждой платформе для компиляции в соответствии с файлом конфигурации binding.gyp. Если вы хотите добиться кроссплатформенности, вам необходимо скомпилировать аддон nodejs в соответствии с разными платформами, вbinding.gypНастройте статическую библиотеку ссылок для шифрования и дешифрования по платформе.

{
    "targets": [{
        "conditions": [
            ["OS=='mac'", {
                "libraries": [
                    "<(module_root_dir)/lib/mac/security.a"
                ]
            }],
            ["OS=='win'", {
                "libraries": [
                    "<(module_root_dir)/lib/win/security.lib"
                ]
            }],
            ...
        ]
        ...
    }]

Конечно, при необходимости вы также можете добавить поддержку других платформ, таких как Linux и Unix.

При инкапсуляции аддона для процесса кода С++ вы можете использоватьnode-addon-api. пара пакетов node-addon-apiN-APIУпаковано и сгладить проблемы совместимости между версиями Nodejs. Инкапсуляция значительно снижает стоимость надписи узлов для несовместителей C ++ разработчиков. Для таких понятий, как узел-Addon-API, N-API и NAN, пожалуйста, обратитесь кмертвая лунастатья одноклассникаОт грубой силы к NAN и NAPI — эволюция разработки нативных модулей Node.js

После того, как файл .node упакован, его можно вызывать во время работы электронного приложения.process.platformОпределите работающую платформу и соответственно загрузите аддон соответствующей платформы.

if (process.platform === 'win32') {
	addon = require('../lib/security_win.node');
} else {
	addon = require('../lib/security_mac.node');
}

Во-вторых, сериализация и десериализация сообщений.

задний план

Относительно неэффективно декодировать и передавать сообщения чата напрямую через JSON.

Наше решение

Здесь мы представляемProtocol BufferПовысить эффективность. Для получения дополнительной информации о протокольном буфере вы можете проверить справочную статью внизу.

Использование буфера протокола в среде узла может быть использованоprotobufjsМешок.

npm i protobuff -S

затем пройтиpbjsКоманда преобразует прото-файл в Pbjson.js.

pbjs -t json-module --sparse --force-long -w commonjs -o src/im/data/pbJson.js proto/*.proto

Для поддержки внутренних данных формата int64 в js вам необходимо использоватьlongprotobuf в конфигурации пакета.

var Long = require("long");
$protobuf.util.Long = Long;
$protobuf.configure();
$protobuf.util.LongBits.prototype.toLong = function toLong (unsigned) {
    return new $protobuf.util.Long(this.lo | 0, this.hi | 0, Boolean(unsigned)).toString();
};

Последнее представляет собой сжатие и преобразование сообщения, а строка js преобразуется в формат pb.

import PbJson from './path/to/src/im/data/pbJson.js';

// 封装数据
let encodedMsg = PbJson.lookupType('pb-api').ctor.encode(data).finish();

// 解封数据
let decodedMsg = PbJson.lookupType('pb-api').ctor.decode(buff);

3. Протокол сетевой передачи

протокол транспортного уровняЕсть UDP, TCP и т.д. UDP имеет хорошую производительность в реальном времени, но низкую надежность. Здесь используется протокол TCP. Прикладной уровень использует протокол WS для поддержания длительного соединения для обеспечения передачи сообщений в реальном времени и протокол HTTPS для передачи других данных о состоянии, отличных от сообщений. Вот пример реализации простого класса управления WS.

import { EventEmitter } from 'events';
const webSocketConfig = 'wss://xxxx';
class SocketServer extends EventEmitter {
    connect () {
        if(this.socket){
			this.removeEvent(this.socket);
			this.socket.close();
		}
		this.socket = new WebSocket(webSocketConfig);
		this.bindEvents(this.socket);
        return this;
    }
    close () {}
    async getSocket () {
    }
    bindEvents() {}
    removeEvent() {}
    onMessage (e) {
        // 消息解包
        let decodedMSg = 'xxx;
        this.emit(decodedMSg);
    }
    async send(sendData) {
        const socket = await this.getSocket()
        socket.send(sendData);
    }
    ...
}

Протокол HTTPS не будет введен, все используют его каждый день.

4. Частный протокол передачи данных

На предыдущих шагах реализована сериализация и десериализация сообщений чата, а также реализована отправка и получение сообщений через веб-сокет, но отправлять сообщения чата напрямую таким образом пока невозможно. нам нужен еще одинпротокол передачи данных. Добавьте некоторые атрибуты к сообщению, такие как id, чтобы связать отправленные и полученные сообщения, type, чтобы отметить тип сообщения, version, чтобы отметить версию вызывающего интерфейса, api, чтобы отметить вызываемый интерфейс и т. д. Затем определите формат кодирования, оберните сообщение ArrayBuffer, отправьте его в ws и передайте как двоичный поток.

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

Вот упрощенный пример:

class PocketManager extends EventEmitter {
    encode (id, type, version, api, payload) {
		let headerBuffer = Buffer.alloc(8);
        let payloadBuffer = Buffer.alloc(0);
        let offset = 0;
        let keyLength = Buffer.from(id).length;
        headerBuffer.writeUInt16BE(keyLength, offset);
        offset += 2;
        headerBuffer.write(id, offset, offset + keyLength, 'utf8');
        ...
        payloadBuffer = Buffer.from(payload);
		return Buffer.concat([headerBuffer, payloadBuffer], 8 + payloadBuffer.length);
    }
    decode () {}
}

Пять, многопроцессная оптимизация

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

import { EventEmitter } from 'events';
class BaseWindow extends EventEmitter {
    open () {}
    close () {}
    isExist () {}
    destroy() {}
    createWindow() {
        this.win = new BrowserWindow({
			...this.browserConfig,
		});
    }
    ...
}

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

class ImWindow extends BaseWindow {
    browserConfig = {
		width: 0,
		height: 0,
		show: false,
    }
    ...
}

6. Хранение сообщений

задний план

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

Обсуждать

Localstorage можно использовать в электронном, но localstorage имеет ограничение по размеру.На самом деле, большинство из них могут хранить только 5M информации.Если размер хранилища будет превышен, будет сообщено об ошибке.

Некоторые учащиеся также могут подуматьwebsql, но от этого технического стандарта отказались.

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

строить планы

Здесь мы выбираем sqlite. Использование sqlite в узле можно использовать напрямуюsqlite3Мешок.

Вы можете сначала написать класс DAO

import sqlite3 from 'sqlite3';
class DAO {
    constructor(dbFilePath) {
        this.db = new sqlite3.Database(dbFilePath, (err) => {
            //
        });
    }
    run(sql, params = []) {
        return new Promise((resolve, reject) => {
            this.db.run(sql, params, function (err) {
                if (err) {
                    reject(err);
                } else {
                    resolve({ id: this.lastID });
                }
            });
        });
    }
    ...
}

Напишите еще одну базовую модель

class BaseModel {
    constructor(dao, tableName) {
        this.dao = dao;
        this.tableName = tableName;
    }
    delete(id) {
        return this.dao.run(`DELETE FROM ${this.tableName} WHERE id = ?`, [id]);
    }
    ...
}

Другие модели, такие как сообщения и контакты, могут напрямую наследовать этот класс и повторно использовать общие методы, такие как delete/getById/getAll. Если вам не нравится писать операторы SQLite вручную, вы можете ввестиknexСинтаксическая оболочка. Конечно, вы также можете использовать его напрямуюorm,НапримерtypeormКакой.

Используйте следующим образом:

const dao = new AppDAO('path/to/database-file.sqlite3');
const messageModel = new MessageModel(dao);

7. Мигает значок нового сообщения в области уведомлений.

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

import { Tray, nativeImage } from 'electron';

class TrayManager {
    ...
    setState() {
        // 设置默认状态
    }
	startBlink(){
		if(!this.tray){
			return;
		}
		let emptyImg = nativeImage.createFromPath(path.join(__dirname, './empty.ico'));
		let noticeImg = nativeImage.createFromPath(path.join(__dirname, './newMsg.png'));
		let visible;
		clearInterval(this.trayTimer);
		this.trayTimer = setInterval(()=>{
			visible = !visible;
			if(visible){
				this.tray.setImage(noticeImg);
			}else{
				this.tray.setImage(emptyImg);
			}
		},500);
	}

	//停止闪烁
	stopBlink(){
		clearInterval(this.trayTimer);
		this.setState();
	}
}

8. Автоматическое обновление проекта

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

Во-первых, это полное обновление программного обеспечения. Этот метод является более жестоким, и опыт не очень хорош.Откройте приложение, чтобы проверить изменение версии, и напрямую повторно загрузите все приложение, чтобы заменить старую версию. Измените строку кода, чтобы пользователи могли быстро просматривать файлы объемом в сотни мегабайт;

Во-вторых, обнаруживать изменения файлов, загружать и заменять старые файлы для обновления;

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

9. Межпроцессное взаимодействие

В предыдущей статье некоторые студенты спрашивали, как организовать взаимодействие между процессами. Электронная межпроцессная связь в основном используетсяipcMainа такжеipcRenderer.

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

import { remote, ipcRenderer, ipcMain } from 'electron';

function sendIPCEvent(event, ...data) {
    if (require('./is-electron-renderer')) {
        const currentWindow = remote.getCurrentWindow();
        if (currentWindow) {
            currentWindow.webContents.send(event, ...data);
        }
        ipcRenderer.send(event, ...data);
        return;
    }
    ipcMain.emit(event, null, ...data);
}
export default sendIPCEvent;

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

class ipcMainManager extends EventEmitter {
    constructor() {
        ipcMain.on('imPush', (name, data) => {
            this.emit(name, data);
        })
        this.listern();
    }
    listern() {
        this.on('imPush', (name, data) => {
            //
        });
    }
}
class ipcRendererManager extends EventEmitter {
    push (name, data) {
        ipcRenderer.send('imPush', name, data);
    }
}

10. Другие

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

import electron from 'electron';
function getUserDataPath() {
    if (require('./is-electron-renderer')) {
        return electron.remote.app.getPath('userData');
    }
    return electron.app.getPath('userData');
}
export default getUserDataPath;

PS

Если у вас есть вопросы, добавьте меня в WeChat:

Также следите за моим блогомВнешний вид https://wuwb.me/, чтобы отслеживать последние акции.

Справочная статья

  1. node-cpp-addon
  2. serialization-vs-deserialization
  3. Protobuf работает лучше, чем JSON
  4. Преобразование типов между Node.js и C++
  5. npmtrends