Tworzenie modułowego narzędzia do automatyzacji w JavaScript od zera
Dowiedz się, jak połączyć Playwright, Cheerio, SQLite i Commander w wielokrotnie używalny silnik procesów Node.js, który rozwija się z prostego skryptu w produkt automatyzacji nadający się do sprzedaży.
1. Powtarzający się problem stał się punktem wyjścia
W pewnym momencie zauważasz, że raz po raz wykonywasz ten sam rodzaj zadania.
Otwierasz stronę.
Szukasz na niej czegoś przydatnego.
Wybierasz istotne informacje.
Zapisujesz je gdzieś na później.
Przechodzisz do następnej strony.
Ponawiasz ten cykl.
Żaden z tych kroków sam w sobie nie jest trudny, ale sama ich powtarzalność jest absurdalna.
Zamiast więc bierać kolejną ramę JavaScript tylko po to, by stworzyć kolejny panel sterowania, bardziej przydatnym rozwiązaniem jest stworzenie czegoś, co faktycznie można wykorzystać: narzędzia do automatyzacji w JavaScript i zbierania danych.
Idea jest prosta:
Poddaj programowi zadanie → pozwól JavaScript zająć się powtarzalną częścią → otrzymaj ustrukturyzowane wyniki.
Wcześniejsza wersja takiego narzędzia wymaga zaledwie kilku technologii:
- Node.js
- Playwright
- Cheerio
- SQLite
- Commander
To połączenie wystarcza, by przekształcić mały skrypt w coś, co zaczyna przypominać prawdziwy produkt.
2. Playwright jako pierwszy element budulcowy
Celem było umożliwienie JavaScriptowi sterowania rzeczywistym przeglądarką.
Playwright ułatwia to znacznie bardziej, niż się spodziewano.
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"
);
Kiedy po raz pierwszy uruchamiasz coś takiego, wydaje się to niemal zbyt proste.
JavaScript otwiera przeglądarkę.
JavaScript przechodzi na określoną stronę.
JavaScript odczytuje jej zawartość.
JavaScript zamyka przeglądarkę.
Samo to daje podstawę, na której można budować narzędzia do testowania przeglądarek, monitoringu, wykonywania powtarzalnych zadań oraz ogólnej automatyzacji.
3. Przejście od współrzędnych do elementów
Jedną rzeczą, której należy unikać od samego początku, jest krucha automatyzacja.
Mówienie programowi czegoś w stylu:
Kliknij dokładnie w to miejsce.
Oznacza, że automatyzacja może przestać działać w momencie najmniejszej zmiany układu.
Lepszym podejściem jest pisanie kodu, który opisuje to, z czym faktycznie interaguje użytkownik, a nie to, gdzie co znajduje się na ekranie.
async function searchPage(page, query) {
await page
.getByRole("textbox")
.fill(query);
await page
.getByRole("button", {
name: "Search"
})
.click();
await page.waitForLoadState(
"domcontentloaded"
);
}
To o wiele łatwiejsze do utrzymania z biegiem czasu.
Kod nie mówi:
Kliknij to, co znajduje się w współrzędnych 742, 381.
Mówi natomiast:
Znajdź pole tekstowe i przycisk wyszukiwania.
To niewielki wybór projektowy, ale znacznie ułatwia późniejszą automatyzację przeglądarki.
4. Wyodrębnianie danych z strony
Gdy przeglądarka potrafi sama poruszać się po stronie, kolejnym krokiem jest pozyskiwanie przez nią informacji.
Wyobraźmy sobie na przykład stronę wypełnioną kartami produktów.
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
};
});
});
}
W tym momencie przeglądarka już tylko nie odwiedza stron.
Przekształca to, co znajduje się na stronie, w obiekty JavaScript.
Dzięki temu dane stają się przydatne do dalszej obróbki.
const products =
await extractProducts(page);
console.log(
JSON.stringify(
products,
null,
2
)
);
Gdy informacje mają już określoną strukturę, można je przechowywać, porównywać, analizować lub przekazywać do innej części aplikacji.
5. Wykorzystanie Cheerio do parsowania HTML
Playwright sprawdza się, gdy naprawdę potrzebujesz działającej przeglądarki.
Często jednak masz już HTML pod ręką i w ogóle nie musisz uruchamiać Chromium.
Wtedy właśnie Cheerio odgrywa kluczową rolę.
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;
}
Posiadanie obu tych narzędzi jest prawdziwą zaletą.
Playwright zajmuje się interakcją z przeglądarką.
Cheerio służy do lekkiego parsowania HTML.
Dzięki temu pełna instancja przeglądarki jest używana tylko wtedy, gdy jest to rzeczywiście konieczne, a prosty parser zajmuje się resztą.
6. Zapewnienie pamięci dla automatyzacji za pomocą SQLite
Kolejnym wyzwaniem do rozwiązania była przechowywanie danych.
Jeśli skrypt zbiera dane dzisiaj, gdzie one będą znajdować się jutro?
Rozwiązaniem okazało się wprowadzenie SQLite.
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
)
`);
Następnie dedykowana funkcja zajmowała się zapisywaniem każdego wyniku.
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();
}
);
}
);
}
To dodatek znacząco zmienił strukturę projektu.
Dzięki bazie danych dane historyczne mogły się z czasem gromadzić. To otworzyło drogę do odpowiadania na praktyczne pytania takie jak:
- Co się zmieniło?
- Co pojawiło się ostatnio?
- Co zniknęło?
Automatyzacja staje się znacznie bardziej przydatna, gdy potrafi pamiętać to, co wydarzyło się wcześniej.
7. Budowa wielokrotnie używalnego silnika procesów pracy
W tym etapie projekt składał się z kilku niezależnych elementów: sterowania przeglądarką, parsowania HTML oraz przechowywania danych w bazie. Brakowało mu jednak struktury — istniało realne ryzyko, że funkcje będą rozproszone wszędzie zamiast tworzyć spójny system.
Aby to naprawić, klasa procesu pracy połączyła wszystko razem.
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();
}
}
Gdy ta klasa została wprowadzona, realizacja pełnego zadania staje się prosta i przejrzysta od początku do końca.
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();
To jest moment, w którym projektowanie obiektowe zaczęło przynosić efekty. Instancja klasy modeluje sam proces pracy, a jej metody reprezentują poszczególne operacje. Reszta bazy kodu nie musi wiedzieć, jak każdy krok jest implementowany wewnętrznie.
8. Radzenie sobie z błędami za pomocą logiki ponawiania
Skrypty automatyzacyjne mają tendencję do bezbłędnego działania przez dziewięć razy, a potem zawalenia się przy dziesiątym. Sieć zwalnia, strona nie jest w pełni załadowana, serwer ma problemy lub element potrzebuje więcej czasu niż oczekiwano na wyświetlenie.
Aby temu zaradzić, wprowadzono uniwersalną funkcję pomocniczą do ponawiania prób.
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;
}
To narzędzie umożliwiło otoczenie kluczowych kroków automatycznym ponawianiem prób.
await retry(
async () => {
await page.goto(
"https://example.com",
{
waitUntil:
"domcontentloaded"
}
);
},
3,
1500
);
Wniosek był prosty: automatyzacja na poziomie produkcyjnym musi uwzględniać awarie jako zjawisko normalne, a nie wyjątek. Tutorials zazwyczaj zakładają idealnie współpracującą sieć. Prawdziwe systemy nie mogą sobie pozwolić na takie założenia.
9. Umieszczenie w interfejsie linii poleceń
W pewnym momencie otwieranie pliku źródłowego za każdym razem tylko po to, by zmienić URL, stało się męczące.
Cel przesunął się w kierunku sprawienia, by narzędzie zachowywało się jak prawdziwa aplikacja linii poleceń.
Commander ułatwił osiągnięcie tego celu.
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();
Gdy to zostało zaimplementowane, narzędzie można było uruchomić bezpośrednio z terminala.
node webpilot.js visit https://example.com
Wygląda to na niewielką korektę, ale fundamentalnie zmienia sposób interakcji z oprogramowaniem.
Zamiast edytować program za każdym razem, gdy czegoś potrzebujesz,
wystarczy go po prostu uruchomić.
10. Traktowanie AI jako interfejsu użytkownika
To rodzi nowe pytanie:
Dlaczego użytkownik w ogóle musi zapamiętywać dokładną składnię poleceń?
Zamiast wpisywać coś takiego jak:
node webpilot.js screenshot https://example.com
osoba może po prostu opisać intencję w zwykłym języku:
">Zrób zrzut ekranu strony głównej."
Szara warstwa AI może przetłumaczyć to zdanie na strukturalny obiekt zadania.
const task = {
action: "screenshot",
url: "https://example.com",
output: "homepage.png"
};
Jednak AI nigdy nie otrzyma pozwolenia na bezpośrednie uruchamianie dowolnego JavaScriptu.
Każda żądana akcja najpierw przechodzi przez krok walidacji.
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;
}
To zapewnia czyste rozdzielenie obowiązków:
AI interpretuje to, czego chce użytkownik.
Warstwa JavaScriptu decyduje, co jest faktycznie dozwolone.
Playwright wykonywaje tylko zatwierdzoną akcję.
Taki układ warstwowy to znacznie bezpieczniejsze podejście niż przekazanie modelowi AI bezpośredniej, nieograniczonej kontroli nad maszyną.
11. Przekształcenie projektu w produkt sprzedażowy
Na tym etapie uwaga przeniosła się z podstawowych bibliotek na rzecz rzeczywistego klienta.
Prezentacja nigdy nie miała brzmieć tak:
">Aplikacja do automatyzacji przeglądarki stworzona przy użyciu Playwright i JavaScript."
Nikt nie szuka konkretnie Playwright – szukają rezultatu.
Dlatego ofertą stał sam rezultat. Kilka przykładów:
Nadzór nad stronami internetowymi
Firmy mogą śledzić swoje strony internetowe i otrzymywać powiadomienia, gdy kluczowe strony ulegną zmianie lub przestaną działać.
Automaticzne testy jakości
Zespoły inżynieryjne mogą przeprowadzać spójne, powtarzalne testy przeglądarek w odniesieniu do własnych aplikacji.
Autoryzacja procesów wewnętrznych
Organizacje mogą zautomatyzować powtarzalne zadania oparte na przeglądarce w narzędziach, do których już mają dostęp.
Autoryzacja raportowania
Zadanie zaplanowane może automatycznie zbierać zatwierdzone dane, je przechowywać i kompilować je w raport.
Autoryzacja dla agencji
Agencja może projektować dostosowane do potrzeb klientów ścieżki automatyzacji oraz naliczać opłaty za konfigurację i ciągłą obsługę.
Ceny mogą przybierać kilka form:
- Jednorazowa stała opłata za konfigurację
- Miesięczne opłaty za utrzymanie
- Ceny za poszczególne procesy
- Licencjonowanie oparte na zespołach
- Dostosowane prace integracyjne
- Dostęp oparty na subskrypcji i hostowaniu
Kluczem jest powiązanie oferowanego rozwiązania z konkretnym, określonym problemem, a nie z zestawem technologii.
12. Małe skrypty mogą przerodzić się w prawdziwe produkty
Na końcu architektura całego systemu wyglądała mniej więcej w ten sposób:
User
│
▼
CLI / AI Input
│
▼
Task Validator
│
▼
Workflow Engine
│
┌────────┼────────┐
▼ ▼ ▼
Playwright Cheerio SQLite
│ │ │
└────────┼────────┘
▼
Result Data
│
▼
Report / API
To jest ta część, nad którą warto się skupić.
Prawdziwy problem nigdy nie dotyczył samej automatyzacji przeglądarki.
Chodziło o eliminację powtarzalnej pracy ręcznej.
Biblioteki — Playwright, Cheerio, SQLite, Commander — były jedynie mechanizmami służącymi do przekształcenia tej powtarzalności w działający oprogramowanie.
Takie podejście kształtuje teraz sposób traktowania nowych projektów w JavaScript.
Każdy raz, gdy sekwencja kroków ręcznych zaczyna się powtarzać po raz dwudziesty, instynkt nie jest taki:
">Czas poszukać nowego frameworku."
Jest taki:
">Czy ten proces może zostać przekształcony w funkcję?"
Jeśli odpowiedź brzmi tak, to jest to zalążek projektu automatyzacji.
A gdy ta automatyzacja zaoszczędzi komuś wystarczająco dużo czasu i wysiłku, może rozwinąć się w coś, co warto sprzedać.
Powiązane materiały
- Projektowanie API Node.js w warstwach: od grubych kontrolerów do czystej architektury — Dowiedz się, jak przerrefaktoryzować API Node.js na warstwy kontrolerów, usług i dostępu do danych, aby rozwiązać problemy z splątaną logiką biznesową, niejednolitymi błędami oraz trudnościami w skalowaniu.