Startseite / Artikel / Erstellung eines modularen JavaScript-Automatisierungstools von Grund auf

Erstellung eines modularen JavaScript-Automatisierungstools von Grund auf

Erfahren Sie, wie Sie Playwright, Cheerio, SQLite und Commander zu einem wiederverwendbaren Node.js-Workflow-Engine kombinieren können, der sich von einem Skript zu einem verkaufbaren Automatisierungsprodukt entwickelt.

2080 Wörter

1. Ein wiederkehrendes Problem wurde zum Ausgangspunkt

Irgendwann stellt man fest, dass man immer wieder dieselbe Art von Aufgabe ausführt.

Eine Seite laden.

Auf dieser Seite nach nützlichen Informationen suchen.

Die relevanten Teile herausfiltern.

Sie an einem Ort für später speichern.

Zur nächsten Seite wechseln.

Den Kreislauf erneut beginnen.

Niemand dieser Schritte ist für sich genommen schwierig, doch die ständige Wiederholung ist absurd.

Anstatt also einfach ein weiteres JavaScript-Framework zu nutzen, um nur wieder einen Dashboard zu erstellen, ist ein sinnvolleres Vorgehen die Entwicklung von etwas, das tatsächlich genutzt werden kann: einem JavaScript-Tool für Automatisierung und Datenerfassung.

Die Idee dahinter ist einfach:

Eine Aufgabe dem Programm übergeben → JavaScript kümmert sich um den repetitiven Teil → strukturierte Ergebnisse erhalten.

Eine frühe Version eines solchen Tools benötigt nur wenige Technologien:

  • Node.js
  • Playwright
  • Cheerio
  • SQLite
  • Commander

Diese Kombination reicht aus, um ein kleines Skript in etwas zu verwandeln, das bereits an ein echtes Produkt erinnert.

2. Playwright als erstes Baustein

Ziel war es, JavaScript die Steuerung eines echten Browsers zu ermöglichen.

Playwright macht das viel einfacher, als erwartet.

const { chromium } = require("playwright");

async function visitWebsite(url) {
    const browser = await chromium.launch({
        headless: false
    });

    const page = await browser.newPage();

    await page.goto(url, {
        waitUntil: "domcontentloaded"
    });

    console.log(
        "Page title:",
        await page.title()
    );

    console.log(
        "Current URL:",
        page.url()
    );

    await browser.close();
}

visitWebsite(
    "https://example.com"
);

Zum ersten Mal, wenn man so etwas ausführt, wirkt es fast zu einfach.

JavaScript öffnet einen Browser.

JavaScript navigiert zu einer Seite.

JavaScript liest den Inhalt der Seite.

JavaScript schließt den Browser wieder.

Allein das bietet bereits eine Grundlage, auf der man Tests im Browser, Überwachungsfunktionen, wiederkehrende Arbeitsabläufe sowie allgemeine Automatisierungen entwickeln kann.

3. Vom Koordinatensystem zu Elementen

Eines, was man von Anfang an vermeiden sollte, ist anfällige Automatisierung.

Wenn man dem Programm etwas wie Folgendes mitteilt:

Klicken Sie an dieser genauen Stelle.

bedeutet das, dass die Automatisierung bereits bei geringsten Veränderungen im Layout ausfallen kann.

Ein besseres Vorgehen ist es, Code zu schreiben, der beschreibt, womit der Benutzer tatsächlich interagiert, anstatt nur den Platz auf dem Bildschirm anzugeben.

async function searchPage(page, query) {
    await page
        .getByRole("textbox")
        .fill(query);

    await page
        .getByRole("button", {
            name: "Search"
        })
        .click();

    await page.waitForLoadState(
        "domcontentloaded"
    );
}

So ist die Wartung im Laufe der Zeit weitaus einfacher.

Der Code besagt nicht:

Klicken Sie auf den Punkt mit den Koordinaten 742, 381.

Sondern:

Finden Sie das Eingabefeld und die Suchschaltfläche.

Es handelt sich dabei um eine kleine Gestaltungsempfehlung, doch sie macht die Browserautomatisierung später erheblich unkomplizierter.

4. Daten aus der Seite extrahieren

Sobald der Browser in der Lage ist, sich selbstständig auf einer Seite zu bewegen, ist der nächste Schritt, dass er Informationen sammelt.

Stellen Sie sich beispielsweise eine Seite vor, die mit Produktkarten gefüllt ist.

async function extractProducts(page) {
    return page
        .locator(".product-card")
        .evaluateAll(cards => {
            return cards.map(card => {

                const name =
                    card
                        .querySelector(".product-name")
                        ?.textContent
                        ?.trim();

                const price =
                    card
                        .querySelector(".price")
                        ?.textContent
                        ?.trim();

                return {
                    name,
                    price
                };
            });
        });
}

Zu diesem Zeitpunkt besucht der Browser nicht mehr nur Seiten.

Er wandelt den Inhalt der Seite in JavaScript-Objekte um.

Dadurch werden die Daten für weitere Verarbeitungen nutzbar.

const products =
    await extractProducts(page);

console.log(
    JSON.stringify(
        products,
        null,
        2
    )
);

Sobald die Informationen eine echte Struktur haben, können Sie sie speichern, vergleichen, analysieren oder an einen anderen Teil der Anwendung weitergeben.

5. Einsatz von Cheerio für das HTML-Parsing

Playwright ist dann besonders nützlich, wenn Sie tatsächlich einen laufenden Browser benötigen.

Oft haben Sie jedoch bereits das HTML zur Hand und müssen Chromium überhaupt nicht starten.

Dann kommt Cheerio ins Spiel.

const cheerio = require("cheerio");

function parseProducts(html) {
    const $ = cheerio.load(html);

    const products = [];

    $(".product-card").each(
        (_, element) => {

            const name = $(element)
                .find(".product-name")
                .text()
                .trim();

            const price = $(element)
                .find(".price")
                .text()
                .trim();

            products.push({
                name,
                price
            });
        }
    );

    return products;
}

Die Verfügbarkeit beider Tools ist ein echter Vorteil.

Playwright kümmert sich um die Interaktion mit dem Browser.

Cheerio übernimmt die leichte HTML-Parsing-Funktion.

Auf diese Weise wird eine vollständige Browserinstanz nur dann verwendet, wenn sie tatsächlich notwendig ist, während ein einfacher Parser für den Rest sorgt.

6. Bereitstellung eines Speichers für die Automatisierung mit SQLite

Der Speicher war die nächste Herausforderung, die gelöst werden musste.

Falls ein Skript heute Daten sammelt, wo befinden sich diese Daten dann morgen?

Die Lösung bestand darin, SQLite einzusetzen.

const sqlite3 = require("sqlite3").verbose();

const db = new sqlite3.Database(
    "automation.db"
);

db.run(`
    CREATE TABLE IF NOT EXISTS products (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        price TEXT,
        source TEXT,
        created_at DATETIME
            DEFAULT CURRENT_TIMESTAMP
    )
`);

Ab dort kümmerte sich eine speziell dafür vorgesehene Funktion um das Speichern jedes Ergebnisses.

function saveProduct(product, source) {
    return new Promise(
        (resolve, reject) => {

            db.run(
                `
                INSERT INTO products
                (name, price, source)
                VALUES (?, ?, ?)
                `,
                [
                    product.name,
                    product.price,
                    source
                ],
                error => {

                    if (error) {
                        reject(error);
                        return;
                    }

                    resolve();
                }
            );
        }
    );
}

Diese Ergänzung veränderte die Struktur des Projekts auf signifikante Weise.

Durch die Verwendung einer Datenbank konnten im Laufe der Zeit historische Aufzeichnungen angesammelt werden. Dadurch wurde es möglich, praktische Fragen zu beantworten wie:

  • Was hat sich geändert?
  • Was ist kürzlich aufgetaucht?
  • Was ist verschwunden?
  • Wann wurden diese Daten zuletzt erfasst?
  • Welche Quelle hat einen bestimmten Datensatz erzeugt?
  • Automatisierung wird um ein Vielfaches wertvoller, sobald sie sich daran erinnern kann, was zuvor geschehen ist.

    7. Aufbau eines wiederverwendbaren Workflow-Engines

    Zu diesem Zeitpunkt bestand das Projekt aus mehreren unabhängigen Komponenten: Browsersteuerung, HTML-Parser und Datenbankpersistenz. Was fehlte, war eine Struktur – es bestand tatsächlich die Gefahr, dass lose Funktionen überall verstreut blieben anstelle eines kohärenten Systems.

    Um das zu beheben, brachte eine Workflow-Klasse alles zusammen.

    class AutomationWorkflow {
    
        constructor(browser) {
            this.browser = browser;
            this.page = null;
        }
    
        async start() {
            this.page =
                await this.browser.newPage();
        }
    
        async visit(url) {
            await this.page.goto(url, {
                waitUntil: "domcontentloaded"
            });
        }
    
        async search(query) {
            await this.page
                .getByRole("textbox")
                .fill(query);
    
            await this.page
                .getByRole("button", {
                    name: "Search"
                })
                .click();
        }
    
        async getTitle() {
            return this.page.title();
        }
    
        async close() {
            await this.page.close();
        }
    }
    

    Sobald diese Klasse vorhanden ist, lässt sich eine vollständige Aufgabe von Anfang bis Ende leicht nachvollziehen.

    const browser =
        await chromium.launch({
            headless: false
        });
    
    const workflow =
        new AutomationWorkflow(
            browser
        );
    
    await workflow.start();
    
    await workflow.visit(
        "https://example.com"
    );
    
    await workflow.search(
        "JavaScript automation"
    );
    
    console.log(
        await workflow.getTitle()
    );
    
    await workflow.close();
    
    await browser.close();
    

    Hier zeigt sich, wo das objektorientierte Design anfängt, Vorteile zu bringen. Die Klasseninstanz modelliert den Workflow selbst, und ihre Methoden repräsentieren die einzelnen Operationen. Der Rest des Codebases muss nicht wissen, wie jeder Schritt intern umgesetzt wird.

    8. Fehlerbehandlung mit Wiederholungslogik

    Automatisierungs-Skripte neigen dazu, neunmal fehlerfrei zu laufen und dann beim zehnten Mal zu versagen. Das Netzwerk verlangsamt sich, eine Seite lädt nicht vollständig, ein Server hat Probleme oder ein Element braucht länger als erwartet, um angezeigt zu werden.

    Um damit umzugehen, wurde ein generischer Hilfsfunktion für Wiederholungen eingeführt.

    async function retry(
        operation,
        attempts = 3,
        delay = 2000
    ) {
        let lastError;
    
        for (
            let attempt = 1;
            attempt <= attempts;
            attempt++
        ) {
            try {
                return await operation();
    
            } catch (error) {
    
                lastError = error;
    
                console.log(
                    `Attempt ${attempt} failed.`
                );
    
                if (
                    attempt < attempts
                ) {
                    await new Promise(
                        resolve =>
                            setTimeout(
                                resolve,
                                delay
                            )
                    );
                }
            }
        }
    
        throw lastError;
    }
    

    Diese Hilfsfunktion ermöglichte es, kritische Schritte mit automatischen Wiederholungen zu umgeben.

    await retry(
        async () => {
            await page.goto(
                "https://example.com",
                {
                    waitUntil:
                        "domcontentloaded"
                }
            );
        },
        3,
        1500
    );
    

    Die Schlussfolgerung war eindeutig: Automatisierungen in Produktivumgebungen müssen mit Fehlern als normalen Ereignissen umgehen, nicht als Ausnahmen. Anleitungen gehen in der Regel von einem perfekt kooperativen Netzwerk aus. Reale Systeme können sich solche Annahmen nicht leisten.

    9. Einbetten in eine Kommandozeilenoberfläche

    Irgendwann wurde es mühsam, jedes Mal die Quelldatei öffnen zu müssen, nur um eine URL auszutauschen.

    Das Ziel verschob sich darauf, das Tool so zu gestalten, dass es wie eine echte Kommandozeilenanwendung funktioniert.

    Commander macht dies leicht umsetzbar.

    const { Command } = require("commander");
    
    const program = new Command();
    
    program
        .name("webpilot")
        .description(
            "JavaScript automation toolkit"
        )
        .version("1.0.0");
    
    program
        .command("visit")
        .description(
            "Open a webpage"
        )
        .argument("<url>")
        .action(async url => {
    
            const browser =
                await chromium.launch({
                    headless: false
                });
    
            const page =
                await browser.newPage();
    
            await page.goto(url);
    
            console.log(
                await page.title()
            );
    
            await browser.close();
        });
    
    program.parseAsync();
    

    Sobald das vorhanden ist, kann das Tool direkt aus einer Terminalanwendung gestartet werden.

    node webpilot.js visit https://example.com
    

    Es sieht wie eine kleine Anpassung aus, verändert aber grundlegend die Art und Weise, wie man mit der Software interagiert.

    Anstatt das Programm jedes Mal zu bearbeiten, wenn man etwas benötigt,

    läuft man es einfach aus.

    10. KI als Frontend betrachten

    Das wirft eine neue Frage auf:

    Warum sollte der Benutzer überhaupt die genaue Befehlssyntax auswendig lernen müssen?

    Anstatt etwas wie Folgendes einzugeben:

    node webpilot.js screenshot https://example.com
    

    könnte eine Person einfach den Zweck in Alltagssprache beschreiben:

    ">Nimm ein Screenshot der Startseite auf."

    Eine KI-Schicht könnte diesen Satz in ein strukturiertes Aufgabobjekt übersetzen.

    const task = {
        action: "screenshot",
        url: "https://example.com",
        output: "homepage.png"
    };
    

    Allerdings würde der KI niemals die Erlaubnis erteilt, direkt beliebigen JavaScript-Code auszuführen.

    Jede angeforderte Aktion müsste zunächst einer Validierungsstufe unterzogen werden.

    const allowedActions = new Set([
        "visit",
        "search",
        "screenshot",
        "download"
    ]);
    
    function validateTask(task) {
    
        if (
            !allowedActions.has(
                task.action
            )
        ) {
            throw new Error(
                "Unsupported action."
            );
        }
    
        if (
            task.url &&
            !task.url.startsWith("https://")
        ) {
            throw new Error(
                "Invalid URL."
            );
        }
    
        return true;
    }
    

    Dadurch entsteht eine klare Trennung der Verantwortlichkeiten:

    Die KI interpretiert, was der Benutzer möchte.

    Die JavaScript-Schicht entscheidet, was tatsächlich erlaubt ist.

    Playwright führt nur die genehmigte Aktion aus.

    Ein solches schichtweises Design ist ein viel sichererer Ansatz als die direkte, uneingeschränkte Kontrolle über die Maschine durch ein KI-Modell.

    11. Das Projekt als verkaufbares Produkt neu formulieren

    In dieser Phase richtete sich der Fokus nicht mehr auf die zugrundeliegenden Bibliotheken, sondern auf den eigentlichen Kunden.

    Der Verkaufsvorschlag sollte niemals lauten:

    "Eine Browser-Automatisierungsanwendung, die mit Playwright und JavaScript erstellt wurde."

    Niemand sucht gezielt nach Playwright – sie suchen ein Ergebnis.

    Daher wurde das Angebot selbst zum Ergebnis. Einige Beispiele:

    Webseitenüberwachung

    Unternehmen können ihre eigenen Webseiten überwachen und benachrichtigt werden, wenn wichtige Seiten sich ändern oder nicht mehr verfügbar sind.

    Automatisierte Qualitätskontrolle

    Ingenieurteams können konsistente, wiederholbare Browser-Prüfungen an ihren eigenen Anwendungen durchführen.

    Interne Workflow-Automatisierung

    Organisationen können wiederholende, browserbasierte Aufgaben in den Tools automatisieren, auf die sie bereits Zugriff haben.

    Berichtserstellungs-Automatisierung

    Eine terminierte Aufgabe kann genehmigte Daten automatisch sammeln, speichern und zu einem Bericht zusammenfassen.

    Automatisierung für Agenturen

    Eine Agentur kann maßgeschneiderte Automatisierungsworkflows für Kunden entwickeln und für die Einrichtung sowie den laufenden Support abrechnen.

    Die Preise können in verschiedenen Formen angegeben werden:

    • Eine feste Einmalkostenpauschale für die Einrichtung
    • Monatliche Wartungsgebühren
    • Preis pro individuellem Workflow
    • Lizenzierung nach Teamgröße
    • Maßgeschneiderte Integrationen
    • Hosted-Zugang auf Abonnementbasis

    Der Schlüssel besteht darin, das Angebot an ein konkretes, spezifisches Problem zu knüpfen und nicht an den Technologiestack.

    12. Kleine Skripte können zu echten Produkten heranwachsen

    Zum Schluss sah die Gesamtsystemarchitektur ungefähr so aus:

    User
                       │
                       ▼
                 CLI / AI Input
                       │
                       ▼
                 Task Validator
                       │
                       ▼
                 Workflow Engine
                       │
              ┌────────┼────────┐
              ▼        ▼        ▼
         Playwright  Cheerio  SQLite
              │        │        │
              └────────┼────────┘
                       ▼
                  Result Data
                       │
                       ▼
                 Report / API
    

    Das ist der Teil, mit dem man sich auseinandersetzen muss.

    Das zugrunde liegende Problem betraf nie wirklich die Browserautomatisierung an sich.

    Es ging darum, wiederholende manuelle Arbeiten zu beseitigen.

    Die Bibliotheken – Playwright, Cheerio, SQLite, Commander – waren lediglich die Mechanismen, um diese Wiederholungen in funktionsfähige Software umzuwandeln.

    Diese Denkweise prägt heute, wie neue JavaScript-Projekte angegangen werden.

    Sobald eine Abfolge von manuellen Schritten zum zwanzigsten Mal wiederholt wird, ist der Instinkt nicht:

    "Zeit, ein neues Framework zu nutzen.“

    Sondern:

    ">Könnte dieser Workflow in eine Funktion umgewandelt werden?"

    Falls die Antwort ja lautet, handelt es sich um den Ausgangspunkt für ein Automatisierungsprojekt.

    Und wenn diese Automatisierung jemandem genügend Zeit und Aufwand spart, kann sie zu etwas werden, das man verkaufen kann.

    Zusätzliche Literatur

  • Npm-Supply-Chain-Angriffe: Wie sie funktionieren und wie man Node.js schützt — Erklärt, wie Npm-Supply-Chain-Angriffe wie Account-Übernahmen, Typosquatting und Abhängigkeitsprobleme funktionieren, sowie konkrete Schritte zur Stärkung von Node.js-Installationen.