因为工作原因接触到了 Node,与之前练习的 Demo 不同,此次 Node 项目不是单纯的进行接口编写,更加准确的来说是一个自动化工具,里面包含了公司自己的业务网站截图、获取当前用户的环境信息:当前用户操作的系统信息、时间、文件 hash 等,不走常规接口路由,而是通过 Rabbitmq 消息中间件进行监听和发送数据进行一系列动作。

node 环境准备

node 版本选择的是 LTS 版本的16.18.1

PM2 的安装

一般需要运行 js 文件,在终端输入对应的命令则可,例:node xxx.js,如果需要额外加自定义参数的话,则需要在 package.json 文件的 scripts 对象中编写对应的keyvalue格式的命令。但是这些方法有以下缺陷:

1、窗口关闭后服务自动停止。

2、文件变化后需要手动重启。

这时候我们则可使用 pm2 进行进程守护、监听文件变化自动重启项目。

1
2
3
4
5
6
7
8
9
10
11
12
// pnpm 全局安装PM2(注:yarn|npm|pnpm 都可以,只是包管理选择而已)
pnpm add pm2 -g

// 检查是否安装成功
pm2 -v

// 输出以下信息则成功
-----------------------------------------------------------------------------------------------------------------------------------------
[PM2] Spawning PM2 daemon with pm2_home=C:\Users\xxx\.pm2
[PM2] PM2 Successfully daemonized
-----------------------------------------------------------------------------------------------------------------------------------------

pm2 常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 可查看指定进程的log日志,后面的number指的是需要打印的行数,默认是最后15行。
pm2 log --lines <行数:number>

// 查看所有进程列表
pm2 list
-----------------------------------------------------------------------------------------------------------------------------------------
┌────┬───────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
└────┴───────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
-----------------------------------------------------------------------------------------------------------------------------------------

// 暂停进程
pm2 stop <进程id:number>

// 关闭指定id进程
pm2 delete <进程id:number>

// 杀死所有进程
pm2 kill

// 重启指定id进程
pm2 restart <进程id:number>

创建 pm2 配置文件:pm2.config.json

在开发过程中,不同环境的对应的变量也不同,例如 RabbitMQ 的路由键、交换机、队列名称在开发测试生产环境下都不一样,所以需要像普通前端项目一样在输入启动命令时注入对应的自定义参数,使其能映射到本地已配置好的文件对象里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{
"apps": [
{
// 应用配置
"name": "node-evidence-server", // 应用的名称,PM2 将使用该名称来管理进程
"args": "", // 启动应用时的命令行参数,这里为空,表示没有额外参数
"script": "./index.js", // 指定应用的入口脚本,这里是 index.js 文件
"exec_interpreter": "node", // 指定应用使用的解释器,这里是 Node.js(即使用 node 命令来启动脚本)
"exec_mode": "fork", // 指定进程模式,"fork" 模式表示 PM2 将启动一个新的进程来运行应用,不会启动集群模式(如多进程模式)

// 日志配置
"error_file": "./logs/error.log", // 错误日志的文件路径,PM2 会将错误日志输出到该文件
"out_file": "./logs/combined.log", // 普通输出日志的文件路径,PM2 会将普通的控制台输出写入该文件
"log_date_format": "YYYY-MM-DD HH:mm:ss", // 日志时间戳的格式,指定日志中时间的显示方式

// 监控文件配置
"ignore_watch": [
"tmp",
".git",
"puppeteer-core",
"setup-sandbox.js",
"logs"
], // 忽略监视的文件或目录,指定这些文件/目录的变化不触发应用重启
// 例如,`tmp` 目录、`.git` 目录、`puppeteer-core` 库等

// 环境变量配置
"env": {
"NODE_CONFIG_ENV": "default", // 设置应用的配置环境为 "default"
"NODE_ENV": "dev", // 设置 Node.js 环境为开发环境(dev)
"NODE_APP_INSTANCE": "dev" // 设置应用实例的名称为 "dev",通常用于标识多个应用实例
},

// 测试环境的环境变量配置
"env_test": {
"NODE_CONFIG_ENV": "test", // 配置为测试环境
"NODE_ENV": "test", // 设置 Node.js 环境为测试环境(test)
"NODE_APP_INSTANCE": "test" // 设置应用实例的名称为 "test"
},

// 生产环境的环境变量配置
"env_prod": {
"NODE_CONFIG_ENV": "prod", // 配置为生产环境
"NODE_ENV": "prod", // 设置 Node.js 环境为生产环境(prod)
"NODE_APP_INSTANCE": "prod" // 设置应用实例的名称为 "prod"
}
}
]
}

主角 Puppeteer

Puppeteer 是一款非常强大的 Node 库,它以提供一系列高级 API 通过谷歌开发者协议来控制 Chromium 或者是 Chrome。以下是它能完成的功能,能手动完成的基本都可以用它替代:

  • 生成页面 PDF。
  • 抓取 SPA(单页应用)并生成预渲染内容(即“SSR”(服务器端渲染))。
  • 自动提交表单,进行 UI 测试,键盘输入等。
  • 创建一个时时更新的自动化测试环境。 使用最新的 JavaScript 和浏览器功能直接在最新版本的 Chrome 中执行测试。
  • ···
Chromium 和 Chrome的区别
    Chromium 和 Chrome 都是由 Google 开发的浏览器,但它们在代码基础、功能、更新策略等方面有所不同。下面是它们的主要区别:
  • 1. 代码基础 Chromium 是一个开源项目,所有的源代码都是公开的,任何人都可以查看、修改和分发。
  • Chrome 是基于 Chromium 构建的,但它添加了 Google 的一些专有功能和代码,因此它不是完全开源的。
  • 2. 更新和发布
  • Chromium 的更新频率通常比 Chrome 更快,因为它是开源项目,开发者可以自由地提交新特性和修复。更新过程也比较灵活,通常不稳定。
  • Chrome 由 Google 维护,并通过正式渠道发布,提供稳定的版本,每个版本会经过严格的测试,确保安全性和兼容性。
  • 3. 功能差异
  • Chrome 包含一些 Chromium 没有的专有功能,例如:
  • 自动更新机制:Chrome 会自动更新,确保你始终使用最新版本。
  • Google 服务集成:如同步 Google 账户、Google 推送通知、Google 安全保护等。
  • Flash Player 和 PDF 阅读器:在某些版本中,Chrome 自带 Flash 插件和 PDF 阅读器,而 Chromium 默认没有这些功能。
  • 商店扩展和特性:Chrome 支持通过 Chrome 网上应用商店下载和安装一些特定的扩展和功能。
  • Chromium 没有一些这些附加功能,通常更多适合开发者或对隐私和开源有特别关注的用户。
  • 4. 隐私和安全性
  • Chrome 作为一个由 Google 运营的浏览器,用户数据和活动可能会被用于改善广告投放和其他商业化目的,因此在隐私方面相比 Chromium 更加依赖于 Google 的数据政策。
  • Chromium 是完全开源的,因此没有集成 Google 的跟踪和数据收集工具,但这也意味着 Chromium 自身可能需要更多的安全配置(如没有自动更新机制)。
  • 5. 安装包和大小
  • Chromium 的安装包通常比 Chrome 更小,因为它不包含 Chrome 的一些额外组件(如自动更新器、插件等)。
  • Chrome 的安装包比较大,因为它集成了更多的附加功能和服务。
  • 总结
  • Chromium 是一个开源项目,适合开发者或者有特定需求(例如隐私保护、定制化)的人群。它没有 Google 的专有功能,也没有自动更新机制。
  • Chrome 则是 Chromium 的稳定版本,包含了更多的功能、自动更新和安全机制,适合普通用户和追求更高稳定性的用户。
  • 如果你只是日常使用浏览器,Chrome 更适合。如果你更关注开源、隐私或定制化,Chromium 可能是更好的选择。

通过从多方面考虑、评估,最终选择 Chromium 来作为实现自动化的工具容器。并且用puppeteer-core替代了Puppeteer,因为Puppeteer会自动下载一个与其版本匹配的 Chromium 浏览器,因此包的体积比较大。而puppeteer-core则不会。

首先定义一个浏览器类,以下示例代码有所简略,删除了跟业务有关的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
const puppeteer = require("puppeteer-core");
const fs = require("fs");
const path = require("path");

// 通过环境变量或者配置文件设定浏览器路径
const executablePath =
process.env.CHROME_EXEC_PATH ||
path.join(
__dirname,
"./puppeteer-core/.local-chromium/linux-982053/chrome-linux/chrome"
);

const args = [
"--disable-setuid-sandbox",
"--disable-infobars", // 去掉多余的空格
"--disable-dev-shm-usage",
"--disable-gpu",
"--no-first-run",
"--no-sandbox",
"--no-zygote",
"--window-size=1920,1080",
"--lang=zh-CN",
"--ignore-certificate-errors", // 去掉空格
"--ignore-ssl-errors", // 去掉空格
];

class Browser {
static INSTANCE;

constructor() {}

// 初始化浏览器
async init() {
console.log("==========>", "Init Chrome browser with headless");
try {
// 检查浏览器路径是否存在
const browserPath = fs.existsSync(executablePath)
? executablePath
: undefined;

Browser.INSTANCE = await puppeteer.launch({
executablePath: browserPath, // 仅在浏览器路径存在时传递
headless: true, // 默认为无头模式
args,
defaultViewport: { width: 1920, height: 1080 },
ignoreHTTPSErrors: true, // 忽略 HTTPS 错误
dumpio: true, // 输出浏览器日志
});
} catch (error) {
console.error("Browser init error:", error);
throw error; // 确保在失败时抛出异常
}
}

// 浏览指定的 URL
async browse(url, fn) {
if (!Browser.INSTANCE) {
throw new Error("Browser is not initialized. Please call init() first.");
}

let page;
try {
page = await Browser.INSTANCE.newPage();
console.log("==========>", "Browser is browsing", url);

await page.goto(url, {
timeout: 600000, // 增加时间,防止页面加载超时
waitUntil: "domcontentloaded", // 等待 DOM 加载完成
referer:
"https://www.sz.msa.gov.cn/iframe/fljsList_15.jhtml?contentType=%E8%A7%84%E8%8C%83%E6%80%A7%E6%96%87%E4%BB%B6",
});

await fn(Browser.INSTANCE, page);
} catch (error) {
console.error("Error while browsing:", error);
} finally {
// 无论如何都要关闭页面以释放资源
if (page) await page.close();
}
}

// 清理浏览器实例
static async close() {
if (Browser.INSTANCE) {
await Browser.INSTANCE.close();
Browser.INSTANCE = null;
}
}
}

module.exports = Browser;

监听到队列消息后,初始化自定义浏览器实例并根据参数打开对应的网址页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 打开浏览器
async function openBrowse(opt) {
return new Promise(async (resolve) => {
const { url } = opt;
const browseInstance = new Browser();
await browseInstance.init();
console.log("==========>", "Ready to automate screenshot url", url);
browseInstance.browse(url, async (instance, page) => {
const params = {
...opt,
};

const result = await pageScrollable({ page, params });
resolve(result);
});
});
}

对页面进行滚动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 滚动页面
async function pageScrollable({ page, params }) {
try {
await autoScroll(page);
await sleep(300);
const title = await page.title();
await page.screenshot({
path: base_path + uploadTitle, // 图片输出的文件路径地址
// quality: 90, //图片质量 0-100
fullPage: true, //是否包含滚动页面
});
const txtName = base_path + "/" + uploadTitle.split(".")[0] + ".txt";
// 保存txt环境信息
await saveTxt(await page.url(), txtName);
} catch (e) {
console.error("->screenshotPageScrollable error", e);
}
}

为了防止无限瀑布流导致页面截图不全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 防止无限瀑布流
async function autoScroll(page) {
try {
await page.evaluate(async () => {
const scrollDistance = 100;
const maxScrollHeight = 1080 * 10; // 最大滚动高度
let totalHeight = 0;
let scrollHeight = document.body.scrollHeight;

// 通过 requestAnimationFrame 进行滚动控制
const scroll = async () => {
if (totalHeight >= scrollHeight || totalHeight >= maxScrollHeight)
return;

window.scrollBy(0, scrollDistance);
totalHeight += scrollDistance;

// 使用递归的方式,等待下一帧的执行
await new Promise(requestAnimationFrame);
scrollHeight = document.body.scrollHeight; // 重新获取页面高度,避免动态加载内容
scroll(); // 继续滚动
};

// 开始滚动
await scroll();
});

// 回到页面顶部
await sleep(500);
await page.evaluate(() => {
window.scroll(0, 0);
});
} catch (error) {
console.error("autoScroll error:", error);
}
}

写入对应的环境信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function saveTxt(url, txtName) {
const urlInfo = URL.parse(url);
return new Promise((resolve, reject) => {
dns.resolve(urlInfo.hostname, (err, addresses) => {
let htmlContent = `写入时间:${format("YYYY/MM/DD HH:mm:ss")}
ip地址:${[addresses]}
URL为:${urlInfo.href}
协议为:${urlInfo.protocol.slice(0, urlInfo.protocol.length - 1)}
验证信息:${urlInfo.hostname}
主机名:${urlInfo.hostname}
路径为:${urlInfo.path}
端口:${urlInfo.port}`;
fs.writeFile(txtName, htmlContent, function (err) {
if (err) {
console.log("数据写入异常");
reject(err);
}
// r;
resolve();
console.log("数据写入成功");
});
});
});
}

ncc 构建

ncc 是一个简单易用的命令行工具,它的目标是将 Node.js 模块编译成单个文件,如同 GCC 一样,将所有依赖一并打包。这个项目旨在简化发布流程,尤其是针对服务器端环境和无服务器架构的应用场景。ncc 无需配置,内置 TypeScript 支持,并且全面兼容 Node.js 的各种模式和 npm 模块。

安装

1
2
3
4
pnpm add -g @vercel/ncc

// 查看是否安装成功
ncc -v

打包

1
2
// 简易打包命令,可添加参数进行打包配置,如:是否生成源码映射sourceMap文件
ncc build index.js

启动

1
pm2 start pm2.config.json --env <dev|test|prod> --watch
对我来说这次也算是一个较大挑战,面对未知的领域,还是会有点担心、紧张,但是回头看,也摸索前进了不少。