首页 / 文章 / 从零开始构建模块化 JavaScript 自动化工具包

从零开始构建模块化 JavaScript 自动化工具包

了解如何将 Playwright、Cheerio、SQLite 和 Commander 结合起来,打造出一个可重复使用的 Node.js 工作流引擎,从而将简单的脚本发展成可销售的自动化产品。

2080 词

1. 一个反复出现的问题成为起点

在某个时刻,你会发现自己一直在重复做同样的任务。

打开一个页面。

在其中查找有用的信息。

提取相关的内容。

将它们存储起来以备后用。

接着打开下一页。

再次开始这个循环。

这些步骤单独来看并不困难,但如此频繁的重复实在荒谬。

因此,与其为了构建另一个控制面板而再学习一个 JavaScript 框架,不如打造真正能派上用场的工具:一款JavaScript 自动化与数据收集工具

其背后的理念很简单:

将任务交给程序 → 让 JavaScript 处理重复性工作 → 获得结构化的结果。

这类工具的早期版本仅需寥寥几项技术:

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

仅凭这些技术,就能将一个小脚本发展成近乎真正产品的工具。

2. Playwright作为首个构建模块

我们的目标是让JavaScript能够控制真实的浏览器。

Playwright让这一目标比预期更容易实现。

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"
);

第一次运行类似代码时,会感觉简单得不可思议。

JavaScript打开浏览器。

JavaScript导航至指定页面。

JavaScript读取页面内容。

JavaScript关闭浏览器。

仅凭这些步骤,就已经为浏览器测试、监控、重复性工作流程以及通用自动化奠定了基础。

3. 从坐标定位转向元素定位

从一开始就应避免的是脆弱的自动化方案。

如果让程序执行类似这样的指令:

在确切的此位置点击。

那么一旦界面布局有丝毫变化,自动化流程就会失效。

更好的方法是编写描述用户实际操作内容的代码,而非仅指定屏幕上元素的位置。

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

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

    await page.waitForLoadState(
        "domcontentloaded"
    );
}

这样长期维护起来要容易得多。

代码并非在指示:

点击坐标为742、381处的任意元素。

而是在指示:

找到文本框和搜索按钮。

这虽只是一个小小的设计选择,却能大幅降低日后进行浏览器自动化的难度。

4. 从页面中提取数据

一旦浏览器能够自行在页面中导航,下一步就是让它收集信息。

比如,想象一个满是产品卡片的页面。

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
                };
            });
        });
}

此时浏览器不再只是浏览页面而已。

它会将页面上的内容转换为 JavaScript 对象。

这样数据就可以用于进一步处理了。

const products =
    await extractProducts(page);

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

一旦信息具备了结构,就可以存储它、对比它、对其进行分析,或者将其传递到应用程序的其他部分。

5. 使用 Cheerio 进行 HTML 解析

当确实需要运行浏览器时,Playwright 才能大显身手。

但很多时候你已经有了 HTML 文件,根本无需启动 Chromium。

这时 Cheerio 就派上用场了。

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;
}

同时拥有这两种工具确实具有很大优势。

Playwright负责处理浏览器交互。

Cheerio则负责轻量级的HTML解析。

这样,只有在真正需要时才会使用完整的浏览器实例,其余情况则由简单的解析器处理。

6. 使用SQLite为自动化流程提供内存支持

存储问题是接下来需要解决的难题。

如果脚本今天收集了数据,那么这些数据明天会存放在哪里?

解决方案就是引入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
    )
`);

之后,有一个专门的函数负责保存每条结果。

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

这一添加从本质上改变了项目的结构。

有了数据库之后,历史记录就可以随着时间不断积累。这为回答以下实际问题提供了可能:

  • 有什么变化?
  • 最近出现了什么?
  • 有什么消失了?
  • 这些数据最后是在何时收集的?
  • 某条记录是由哪个来源生成的?
  • 一旦系统能够记住之前发生的事情,自动化功能的价值就会大幅提升。

    7. 构建可复用的工作流引擎

    到这个阶段,该项目已经有了几个独立的组件:浏览器控制、HTML解析以及数据库持久化功能。但它缺乏结构——存在函数分散在各处而无法形成完整系统的风险。

    为了解决这个问题,一个工作流类将所有组件整合在了一起。

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

    有了这个类之后,整个任务的执行流程就从开始到结束都变得清晰易懂了。

    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();
    

    这就是面向对象设计开始发挥作用的时刻。类实例用于模拟工作流程本身,其方法则代表各个具体操作。代码库的其他部分无需了解每一步在内部是如何实现的。

    8. 使用重试逻辑处理故障

    自动化脚本常常在前九次运行时一切正常,却在第十次出现故障。可能是网络速度变慢、页面未能完全加载、服务器出现异常,或是某个元素渲染所需时间超出预期。

    为了解决这个问题,人们引入了一个通用的重试辅助工具。

    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;
    }
    

    该工具使得可以用自动重试机制来包裹那些关键的步骤。

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

    核心结论很明确:生产级自动化系统必须将故障视为正常现象而非异常情况来处理。教程往往假设网络始终处于完美协作状态,而实际系统无法承受这样的假设。

    9. 将其封装为命令行界面

    每次只需更换一个URL就不得不打开源文件,这种做法渐渐变得繁琐。

    目标转变为让该工具具备标准命令行工具的功能。

    Commander使得这一目标很容易实现。

    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();
    

    有了这样的设计,该工具就可以直接从终端启动了。

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

    这看似只是微小的调整,但实际上彻底改变了用户与软件的交互方式。

    无需每次需要功能时都去编辑程序,

    只需直接运行它即可。

    10. 将人工智能视为前端

    这引出了一个新问题:

    为什么用户必须记住确切的命令语法呢?

    与其输入类似这样的内容:

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

    人们可以直接用通俗语言描述意图:

    “截取主页的屏幕截图。”

    人工智能层可以将这句话转换为结构化的任务对象。

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

    不过,人工智能永远不会被允许直接运行任意的 JavaScript 代码。

    每一个请求的操作都会先经过验证步骤。

    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;
    }
    

    这样就能实现职责的清晰分离:

    人工智能负责理解用户的意图。

    JavaScript 层则决定实际允许什么操作。

    Playwright仅执行已获授权的操作。

    这种分层设计方式比让AI模型直接、无限制地控制机器要安全得多。

    11. 将项目重构为可销售的产品

    此时,关注点已从底层库转向真正的客户。

    宣传内容绝不会是:

    “一个用Playwright和JavaScript开发的浏览器自动化应用。”

    没人会专门去寻找Playwright——他们追求的是成果。

    因此,产品本身就变成了那个成果。以下是一些例子:

    网站监控

    企业可以监控自己的网站,当关键页面发生变化或无法访问时及时收到通知。

    自动化质量检测

    工程团队可以对自己的应用程序进行一致且可重复的浏览器测试。

    内部工作流程自动化

    各组织可以利用已有的工具自动完成那些重复性的浏览器相关任务。

    报告自动化

    定时任务可以自动收集已确认的数据、将其保存并整合成报告。

    服务机构自动化服务

    服务机构可以为客户设计定制的自动化流程,并就设置及后续支持收取费用。

    收费方式可能包括:

    • 一次性固定设置费
    • 按月收取的维护费
    • 按单个工作流程计费
    • 团队许可制
    • 定制集成服务
    • 基于订阅的托管访问服务

    关键在于将解决方案聚焦于具体、明确的问题,而非技术栈本身。

    12. 小脚本也能发展成真正的产品

    最终,整个系统架构大致如下所示:

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

    这才是值得深入研究的部分。

    根本问题其实并不在于浏览器自动化本身。

    关键在于消除重复的手动工作

    Playwright、Cheerio、SQLite、Commander这些库只是将重复性工作转化为可用软件的工具而已。

    这种思维方式如今影响着新JavaScript项目的开发方式。

    每当一系列手动操作开始第20次重复时,人们的本能反应并不是:

    “是时候换用新框架了。”

    而是:

    “这个工作流程能否被转化为一个函数?”

    如果答案是肯定的,那便是自动化项目的起点。

    而当这种自动化能为某人节省足够的时间和精力时,它便有可能发展成值得出售的产品。

    相关阅读

  • Npm供应链攻击:其运作原理及防御Node.js的方法 — 阐述了诸如账户劫持、域名抢注和依赖混淆等Npm供应链攻击的运作方式,同时提供了强化Node.js安装安全的具体措施。