因为工作原因接触到了 Node,与之前练习的 Demo 不同,此次 Node 项目不是单纯的进行接口编写,更加准确的来说是一个自动化工具,里面包含了公司自己的业务网站截图、获取当前用户的环境信息:当前用户操作的系统信息、时间、文件 hash 等,不走常规接口路由,而是通过 Rabbitmq 消息中间件进行监听和发送数据进行一系列动作。
node 环境准备
node 版本选择的是 LTS 版本的16.18.1
PM2 的安装
一般需要运行 js 文件,在终端输入对应的命令则可,例:node xxx.js,如果需要额外加自定义参数的话,则需要在 package.json 文件的 scripts 对象中编写对应的key
、value
格式的命令。但是这些方法有以下缺陷:
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" , "args" : "" , "script" : "./index.js" , "exec_interpreter" : "node" , "exec_mode" : "fork" , "error_file" : "./logs/error.log" , "out_file" : "./logs/combined.log" , "log_date_format" : "YYYY-MM-DD HH:mm:ss" , "ignore_watch" : [ "tmp" , ".git" , "puppeteer-core" , "setup-sandbox.js" , "logs" ], "env" : { "NODE_CONFIG_ENV" : "default" , "NODE_ENV" : "dev" , "NODE_APP_INSTANCE" : "dev" }, "env_test" : { "NODE_CONFIG_ENV" : "test" , "NODE_ENV" : "test" , "NODE_APP_INSTANCE" : "test" }, "env_prod" : { "NODE_CONFIG_ENV" : "prod" , "NODE_ENV" : "prod" , "NODE_APP_INSTANCE" : "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 , dumpio: true , }); } catch (error) { console .error("Browser init error:" , error); throw error; } } 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" , 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, fullPage: true , }); const txtName = base_path + "/" + uploadTitle.split("." )[0 ] + ".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; 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); } 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
对我来说这次也算是一个较大挑战,面对未知的领域,还是会有点担心、紧张,但是回头看,也摸索前进了不少。