Vk-io: Architektur: Was ist der „offizielle“ beste Weg, einen Bot auf VK-IO zu organisieren

Erstellt am 14. Dez. 2020  ·  2Kommentare  ·  Quelle: negezor/vk-io

Hallo, ich habe mir Ihre Analyse von Bots auf YouTube angesehen, und alle hatten ein großes Problem: Der gesamte Code ist ein Fußtuch für 5.000 Zeilen. Daher habe ich eine Frage: Wie sehen Sie, dass ein Projekt mit VK-IO gestaltet werden sollte. Denn in seiner jetzigen Form ist es entweder ein Fußtuch oder ein Index + Config + ein Bündel von Dateien, die ungefähr so ​​​​aussehen:

Frame 4

/** <strong i="8">@filename</strong>: config.ts */
export const vk = new VK({ /* ... */ })
/** <strong i="11">@filename</strong>: feat1.ts */
import { vk } from "./config"

vk.updates.on('message', async(ctx, next) => {
  /* code */
})
/** <strong i="14">@filename</strong>: index.ts */
import { vk } from "./config"
import "./feat1"

vk.updates.startPolling().then(() => console.log("Bot works!"))

Und es hat auch Probleme:

  1. Die Reihenfolge der Aufnahme von Features in den Index wirkt sich auf das Verhalten des Bots aus
  2. Sie können vergessen, eine Funktion zu verbinden
  3. Um bequem arbeiten zu können, muss die initialisierte Instanz des VK in der config
  4. Es ist schwierig, die Beziehungen zwischen Merkmalen nachzuverfolgen

Würde gerne Ihre Antwort darauf hören und es sehr explizit in der Dokumentation duplizieren, und möglicherweise README.md

vk-io

Hilfreichster Kommentar

Ich werde nicht sagen, dass dies der „offizielle“ Ansatz ist, ich werde nur meine persönlichen Vorlieben und ein bisschen den praktischen Teil teilen.

Allgemeines Design

Ich ziehe es vor, den Monorepo- Ansatz zum Organisieren verteilter Module zu verwenden (die Bibliothek verwendet ihn tatsächlich). Sie können eine fertige Vorlage für Dienste nehmen und die Bot-Implementierung mithilfe von Abstraktionen von der Bibliothek trennen (da jede bahnbrechende Änderung viel Aufmerksamkeit erfordert, um sie anzupassen). Auch der Einsatz virtueller Maschinen für identische Bedingungen in Entwicklung und Produktion bietet sich an, hier hilft beispielsweise Docker .

Die Architektur

Bot-Abhängigkeiten müssen explizit sein, d. h. kein Hinzufügen von "Features" mit einem einzigen Import, sonst beginnt hier ein echter Debugging-Albtraum. Abstrakter Code:

// commands/random.ts
import { Command } from '@my-project/core';
import { getRandomIntegerInRange } from '@my-project/utils';

export const randomCommand = new Command({
    slug: 'random',

    aliases: [
        'рандом',
        'random'
    ],

    description = 'рандмоное число в промежутке';

    arguments: [
        {
            type: 'integer',
            key: 'min',
            label: 'минк/макс',
            default: null
        },
        {
            type: 'integer',
            key: 'max',
            label: 'минк/макс',
            default: null
        }
    ],

    handler(context) {
        // Работаем с аргументами, а не текстом
        let { min = null, max = null } = context.commander.params;

        if (min === null && max === null) {
            min = 0;
            max = 100;
        } else if (max === null) {
            max = min;
            min = 0;
        }

        const result = getRandomIntegerInRange(min, max);

        return context.answer({
            text: `число в промежутке ${min} - ${max}: ${result}`
        });
    }
});

// commands/index.ts
export * from './random';

// bot.ts
import {
    Bot,

    SessionManager,
    RedisSessionStorage,

    RateLimitManager,

    CommanderManager
} from '@my-project/core';

import * as commands from './commands';

const sessionManager = new SessionManager({
    storage: new RedisSessionStorage({})
});

const rateLimitManager = new RateLimitManager({
    maxPerSecond: 1
});

const commanderManager = new CommanderManager();

for (const command of Object.values(commands)) {
    commanderManager.add(command);
}

const bot = new Bot({
    // ...options
});

// Это может быть кастомная цепочка middleware в боте
bot.incoming.on('message', sessionManager.middleware);
bot.incoming.on('message', rateLimitManager.middleware);
bot.incoming.on('message', commanderManager.middleware);

bot.start()
    .then(() => {
        console.log('Bot started', error);
    })
    .catch((error: Error) => {
        console.error('Error starting bot', error);

        process.exit(1);
    });

Wichtige Dinge aus dem obigen Code:

  1. Das Basispaket @my-project/core implementiert, das die für den Bot notwendigen Dinge enthält.
  2. Befehle verwenden Parameter und realisieren so Dispatcher . Warum ist das notwendig? Alles ist sehr einfach - Sie können den Befehl von überall mit den angegebenen Parametern aufrufen. Aus dem Text analysieren wir die im Befehl beschriebenen Argumente auf beliebige Weise, und die Schaltflächen auf der Tastatur sind einfach bereits damit belegt und an den von uns benötigten Befehl adressiert. So haben wir eine Duplizierung der Logik vermieden und die Validierung von Argumenten organisiert. Zum Beispiel einen Befehl von einem anderen aufrufen:
export const dndCommand = new Command({
    // ...
    handler(context) {
        return context.commander.enter('random', {
            params: {
                min: 1,
                max: 20
            }
        });
    }
});
  1. Jedes verwendete Manager-Modul hat seinen eigenen Verantwortungsbereich und gibt ein klares Verständnis dafür, was verwendet und verfügbar ist.

Fazit

Dies ist der Ansatz, den ich in meinen Bots verwendet habe, und es stellte sich als sehr bequem heraus, ihn von einfachen zu komplexen Bots zu implementieren. Das Paket @my-project/core sollte bestenfalls nur ein Alias ​​einer Bibliothek sein, die schon alles implementiert und getestet hat, und die Datei sieht so aus:

export { Bot, Command } from 'super-bot-library';

export { ViewerManager } from './middlewares';

Bei Änderungen in der Bibliothek kann dann eine der Schnittstellen ausgetauscht werden. Aber niemand verbietet es, die ganze Logik nur für das Projekt zu behalten.

Es gibt eine weitere interessante Implementierung der Bot-Logik auf Hooks, die beispielsweise in React oder Vue verwendet wird. Sie können sie mit diesem Code live anstupsen.

Alle 2 Kommentare

Ich werde nicht sagen, dass dies der „offizielle“ Ansatz ist, ich werde nur meine persönlichen Vorlieben und ein bisschen den praktischen Teil teilen.

Allgemeines Design

Ich ziehe es vor, den Monorepo- Ansatz zum Organisieren verteilter Module zu verwenden (die Bibliothek verwendet ihn tatsächlich). Sie können eine fertige Vorlage für Dienste nehmen und die Bot-Implementierung mithilfe von Abstraktionen von der Bibliothek trennen (da jede bahnbrechende Änderung viel Aufmerksamkeit erfordert, um sie anzupassen). Auch der Einsatz virtueller Maschinen für identische Bedingungen in Entwicklung und Produktion bietet sich an, hier hilft beispielsweise Docker .

Die Architektur

Bot-Abhängigkeiten müssen explizit sein, d. h. kein Hinzufügen von "Features" mit einem einzigen Import, sonst beginnt hier ein echter Debugging-Albtraum. Abstrakter Code:

// commands/random.ts
import { Command } from '@my-project/core';
import { getRandomIntegerInRange } from '@my-project/utils';

export const randomCommand = new Command({
    slug: 'random',

    aliases: [
        'рандом',
        'random'
    ],

    description = 'рандмоное число в промежутке';

    arguments: [
        {
            type: 'integer',
            key: 'min',
            label: 'минк/макс',
            default: null
        },
        {
            type: 'integer',
            key: 'max',
            label: 'минк/макс',
            default: null
        }
    ],

    handler(context) {
        // Работаем с аргументами, а не текстом
        let { min = null, max = null } = context.commander.params;

        if (min === null && max === null) {
            min = 0;
            max = 100;
        } else if (max === null) {
            max = min;
            min = 0;
        }

        const result = getRandomIntegerInRange(min, max);

        return context.answer({
            text: `число в промежутке ${min} - ${max}: ${result}`
        });
    }
});

// commands/index.ts
export * from './random';

// bot.ts
import {
    Bot,

    SessionManager,
    RedisSessionStorage,

    RateLimitManager,

    CommanderManager
} from '@my-project/core';

import * as commands from './commands';

const sessionManager = new SessionManager({
    storage: new RedisSessionStorage({})
});

const rateLimitManager = new RateLimitManager({
    maxPerSecond: 1
});

const commanderManager = new CommanderManager();

for (const command of Object.values(commands)) {
    commanderManager.add(command);
}

const bot = new Bot({
    // ...options
});

// Это может быть кастомная цепочка middleware в боте
bot.incoming.on('message', sessionManager.middleware);
bot.incoming.on('message', rateLimitManager.middleware);
bot.incoming.on('message', commanderManager.middleware);

bot.start()
    .then(() => {
        console.log('Bot started', error);
    })
    .catch((error: Error) => {
        console.error('Error starting bot', error);

        process.exit(1);
    });

Wichtige Dinge aus dem obigen Code:

  1. Das Basispaket @my-project/core implementiert, das die für den Bot notwendigen Dinge enthält.
  2. Befehle verwenden Parameter und realisieren so Dispatcher . Warum ist das notwendig? Alles ist sehr einfach - Sie können den Befehl von überall mit den angegebenen Parametern aufrufen. Aus dem Text analysieren wir die im Befehl beschriebenen Argumente auf beliebige Weise, und die Schaltflächen auf der Tastatur sind einfach bereits damit belegt und an den von uns benötigten Befehl adressiert. So haben wir eine Duplizierung der Logik vermieden und die Validierung von Argumenten organisiert. Zum Beispiel einen Befehl von einem anderen aufrufen:
export const dndCommand = new Command({
    // ...
    handler(context) {
        return context.commander.enter('random', {
            params: {
                min: 1,
                max: 20
            }
        });
    }
});
  1. Jedes verwendete Manager-Modul hat seinen eigenen Verantwortungsbereich und gibt ein klares Verständnis dafür, was verwendet und verfügbar ist.

Fazit

Dies ist der Ansatz, den ich in meinen Bots verwendet habe, und es stellte sich als sehr bequem heraus, ihn von einfachen zu komplexen Bots zu implementieren. Das Paket @my-project/core sollte bestenfalls nur ein Alias ​​einer Bibliothek sein, die schon alles implementiert und getestet hat, und die Datei sieht so aus:

export { Bot, Command } from 'super-bot-library';

export { ViewerManager } from './middlewares';

Bei Änderungen in der Bibliothek kann dann eine der Schnittstellen ausgetauscht werden. Aber niemand verbietet es, die ganze Logik nur für das Projekt zu behalten.

Es gibt eine weitere interessante Implementierung der Bot-Logik auf Hooks, die beispielsweise in React oder Vue verwendet wird. Sie können sie mit diesem Code live anstupsen.

Danke für die sehr gute und ausführliche Antwort. Und ich möchte Sie bitten, das Thema offen zu lassen, damit andere es auch lesen können

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen