Mogujie front end team officially settled in nuggets, hope you don’t be stingy with your hands of Praise (bixin)!

Zero, introduce

In the last section, Electron introduced the basic use of Electron from zero to one. The introduction is relatively simple. According to the article, a simple prototype project can be basically made step by step.

This article introduces some of the issues to consider when developing the Electron IM application.

This paper mainly includes:

  1. Message encryption and decryption
  2. Message serialization
  3. Network Transport protocol
  4. Private data communication protocol
  5. Multiprocess optimization
  6. Message local store
  7. The tray icon of a new message blinks
  8. Automatic project update
  9. Interprocess communication
  10. other

I. Message encryption and decryption

background

For chat software, the confidentiality of the message is more important, who also do not want their chat content leaked or even exposed in front of everyone. Therefore, when sending and receiving information, we need to do some encryption and decryption operations to ensure that the information is encrypted when it is transmitted on the network.

Simple implementation method

You may immediately think that this is not easy, project to write a method of encryption and decryption. Decrypt the received message and encrypt the sent message. After receiving the encrypted message, the server directly stores it.

This is fine in theory, but there are some disadvantages to writing encryption and decryption methods directly on the client side.

  1. It’s easy to reverse. Front-end code is easy to reverse.
  2. Poor performance. There may be groups with many projects in the company, each group will receive a lot of messages, and the front-end processing is slow.
  3. Similarly, if encryption and decryption algorithms are implemented on both clients, ios, Android and other clients need to implement the same algorithm respectively because they use different development languages, which increases maintenance costs.

Our plan

We use the ability provided by C++ Addons to realize encryption and decryption algorithm in C++ SDK, so that js can call C++ SDK module just like calling Node module. This solves all the problems mentioned above at once.

After developing addon, use node-gyp to build C++ Addons. Node-gyp calls the compilation toolset on each platform according to the binding.gyp configuration file. If you want to implement cross-platform, you need to compile Nodejs addon for each platform and configure the static link library for encryption and decryption in binding.gyp for each platform.

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

Of course, you can also add support for more platforms as needed, such as Linux and Unix.

Node-addon-api can be used to encapsulate adons for c++ code processes. The Node-Addon-API package encapsulates the N-API and smooths out compatibility issues between NodeJS versions. Encapsulation greatly reduces the cost of writing node addon for non-professional c++ developers. From violence to NAN to NAPI — The evolution of node.js native module development

After the. Node file is packed, process.platform can be called when the electron application is running to determine the running platform and load the addon of the corresponding platform.

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

Message serialization and deserialization

background

It is inefficient to decode and transmit chat messages directly through JSON.

Our plan

Here we introduce Google’s Protocol Buffer to improve efficiency. For more information about Protocol buffers, see the reference article at the bottom.

Protocol Buffers can be used in node environments using protobufjs packages.

npm i protobuff -S
Copy the code

The proto file is then converted to pbjson.js using the PBJS command

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

To support back-end INT64 data in JS, use the protobuf under the Long package configuration.

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();
};
Copy the code

Next comes the message compression conversion, converting the JS string to pb format.

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

// Encapsulate data
let encodedMsg = PbJson.lookupType('pb-api').ctor.encode(data).finish();

// Unpack data
let decodedMsg = PbJson.lookupType('pb-api').ctor.decode(buff);
Copy the code

Network transmission protocol

Transport layer protocols include UDP and TCP. UDP has good real-time performance but poor reliability. TCP is used here. The application layer uses THE WS protocol to maintain long connections to ensure real-time transmission of messages, and the HTTPS protocol to transmit status data other than messages. Here is an example of implementing a simple WS management class.

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); }... }Copy the code

HTTPS protocol is not going to be introduced, you use it every day.

Private data communication protocol

The previous steps have realized serialization and deserialization of chat messages, as well as sending and receiving messages through Websocket, but not directly sending chat messages. We also need a data communication protocol. Add some attributes to the message, such as ID to associate the sent and received message, type to mark the message type, version to mark the version of the calling interface, API to mark the calling interface, and so on. Then define an encoding format, wrap the message in ArrayBuffer, and send it in WS as a binary stream.

Ensure sufficient scalability in protocol design; otherwise, you need to modify the front and back ends at the same time, which is troublesome.

Here’s a simplified example:

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 () {}
}
Copy the code

5. Multi-process optimization

IM interface has many modules, chat module, group management module, history message module and so on. In addition, the message communication logic should not be placed in the same process as the interface logic, so as to avoid the impact of interface delay on message sending and receiving. A simple way to do this is to put different modules into different electorn Windows, since different Windows are managed by different processes, we don’t need to manage the processes ourselves. Let’s implement a window management class.

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

BrowserConfig can be set in a subclass, and different Windows can inherit this base class to set their own window properties. The communication module is used as the background to send and receive data, and does not need to display the window. You can set the window width = 0, height = 0.

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

Message storage

background

IM software may have thousands of contacts and countless chats. If you request access through the network every time, bandwidth is wasted and performance is affected.

discuss

You can use localstorage in electorn, but the size of localstorage is limited. In most cases, only 5M information can be stored.

Some of you might still think of WebSQL, but that technical standard has been deprecated.

The browser’s built-in indexedDB is also an option. There are limitations, though, and there aren’t as many ecosystem tools available as SQLite.

plan

Here we choose SQLite. To use SQLite in Node, you can use the SQlite3 package directly.

You can start by writing a DAO class

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 }); }}); }); }... }Copy the code

Let me write a base Model

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

Other models such as messages, contacts, and so on can inherit this class directly, using common methods such as DELETE /getById/getAll. If you prefer not to write SQLite statements manually, you can introduce a KNEx syntax wrapper. Of course, you can also use ORM directly fashionable, such as TypeOrM and so on.

Use as follows:

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

The tray icon of a new message flashes

Electron does not provide a dedicated tray flash port, we can simply switch the Tray icon to do this.

import { Tray, nativeImage } from 'electron';

class TrayManager {... setState() {// Set the default state
    }
	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((a)= >{ visible = ! visible;if(visible){
				this.tray.setImage(noticeImg);
			}else{
				this.tray.setImage(emptyImg); }},500);
	}

	// Stop flashing
	stopBlink(){
		clearInterval(this.trayTimer);
		this.setState(); }}Copy the code

Viii. Automatic project update

There are generally several different update strategies, one or a combination of which can improve the experience:

The first is a full software update. This method is violent, and the experience is not good. Open the application to check the version change, and directly download the entire application to replace the old version. Change a line of code, let the user rushed down hundreds of megabytes of files;

The second is to detect file changes, download and replace the old file to upgrade;

The third way is to directly place the View layer file on the line, and the electron shell loads the online page access. There are changes to publish online pages can be.

Interprocess communication

In the last article, a student asked how to handle interprocess communication. The electron interprocess communication mainly uses ipcMain and ipcRenderer.

You can write a way to send a message first.

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;
Copy the code

This way, whether in the main process or the renderer process, you can call this method directly to send messages. For some messages with specific functions, some encapsulation can be made. For example, all push messages can encapsulate a method, and the type of the specific push message can be determined by the parameters in the method. The main process processes logic or forwards messages, depending on the message type.

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); }}Copy the code

10 and other

Another student mentioned the log processing function. This has little to do with electron and is a common feature of the Node project. Use third-party packages such as Winston. For local logs, pay attention to the storage path, regular cleaning and other functions. Remote logs can be submitted to the interface. Obtain path can write some general methods, such as:

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;
Copy the code

PS

If you have any questions, please contact me on wechat:

You can also follow my blog front impressions https://wuwb.me/ to track the latest sharing.

Refer to the article

  1. node-cpp-addon
  2. serialization-vs-deserialization
  3. Protobuf performs better than JSON
  4. Type conversion between node.js and C++
  5. npmtrends