从零开始构建模块化 JavaScript 自动化工具包
了解如何将 Playwright、Cheerio、SQLite 和 Commander 结合起来,打造出一个可重复使用的 Node.js 工作流引擎,从而将简单的脚本发展成可销售的自动化产品。
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次重复时,人们的本能反应并不是:
“是时候换用新框架了。”
而是:
“这个工作流程能否被转化为一个函数?”
如果答案是肯定的,那便是自动化项目的起点。
而当这种自动化能为某人节省足够的时间和精力时,它便有可能发展成值得出售的产品。
相关阅读
- 分层式 Node.js API 设计:从臃肿的控制器到整洁架构 — 了解如何将 Node.js API 重构为控制器、服务层和数据访问层,从而解决复杂的业务逻辑、不一致的错误以及扩展难题。