使用Electron構(gòu)建跨平臺(tái)的抓取桌面程序
談起桌面應(yīng)用開(kāi)發(fā)技術(shù), 我們會(huì)想到.Net下的WinForm, Java下的JavaFX以及Linux下的QT. 這些技術(shù)對(duì)于Web應(yīng)用程序員來(lái)說(shuō)一般比較陌生, 因?yàn)榇蠖郬eb應(yīng)用程序員的開(kāi)發(fā)技能是前端的JavaScript和后端的Java,PHP等語(yǔ)言.
如果Web應(yīng)用程序員想開(kāi)發(fā)桌面應(yīng)用怎么辦? 主流的桌面應(yīng)用開(kāi)發(fā)技術(shù)的學(xué)習(xí)曲線(xiàn)不低, 上手比較困難. 而Electron的出現(xiàn)給Web應(yīng)用程序員帶來(lái)了福音.
Electron簡(jiǎn)介:
Electron 是 Github 發(fā)布跨平臺(tái)桌面應(yīng)用開(kāi)發(fā)工具,支持 Web 技術(shù)開(kāi)發(fā)桌面應(yīng)用開(kāi)發(fā),其本身是基于 C 開(kāi)發(fā)的,GUI 核心來(lái)自于 Chrome,而 JavaScript 引擎使用 v8…
簡(jiǎn)單的說(shuō), Electron平臺(tái)就是用Javascript把UI和后臺(tái)邏輯打通, 后臺(tái)主進(jìn)程使用NodeJs豐富的API完成復(fù)雜耗時(shí)的邏輯, 而UI進(jìn)程則借助Chrome渲染html完成交互.
我之前使用SpringBoot開(kāi)發(fā)了一套市長(zhǎng)信箱抓取Web應(yīng)用. 由于沒(méi)服務(wù)器部署, 所以我現(xiàn)在想把同樣的功能移植到桌面端, 作成一個(gè)桌面應(yīng)用. 對(duì)于開(kāi)發(fā)平臺(tái)我有以下需求:
- 能利用我現(xiàn)有的技術(shù)棧: Web前端JavaScript, 服務(wù)端的Java或者NodeJs.
- 能跨平臺(tái), 既能編譯成Mac下的DMG安裝程序,又能編譯成windows平臺(tái)下的exe文件, 滿(mǎn)足不足場(chǎng)景的使用.
而Electron作為開(kāi)發(fā)平臺(tái)正好能滿(mǎn)足我的這些需求, 通過(guò)一天的摸索, 我完成了這個(gè)桌面應(yīng)用, 并最終打包出Mac平臺(tái)下的DMG安裝文件. 工程代碼: https://github.com/ybak/watcher
下面將介紹我是如何使用Electron平臺(tái)開(kāi)發(fā)這個(gè)桌面應(yīng)用.
回顧: 市長(zhǎng)信箱郵件抓取Web應(yīng)用
動(dòng)手之前, 我先分析一下之前所做的抓取Web應(yīng)用. 它的架構(gòu)如下:
應(yīng)用分可為四部分:
- 抓取程序:使用Java的OkHttp作為Http請(qǐng)求類(lèi)庫(kù)獲取網(wǎng)頁(yè)內(nèi)容,并交給Jsoup進(jìn)行解析, 得到郵件內(nèi)容.
- 數(shù)據(jù)庫(kù):用Mysql實(shí)現(xiàn), 用來(lái)保存抓取后的網(wǎng)頁(yè)內(nèi)容, 并提供檢索查詢(xún)服務(wù).
- 靜態(tài)交互頁(yè)面:一個(gè)簡(jiǎn)單的HTML頁(yè)面, 使用jQuery發(fā)起ajax與后端交互, 并使用handlebar作為展示模板.
- 通信: 使用SpringBoot提供了交互所需的API(搜索服務(wù),全量抓取和更新郵件).
設(shè)計(jì): 使用Electron構(gòu)建抓取桌面應(yīng)用
將要實(shí)現(xiàn)的桌面應(yīng)用, 同樣也需要需要完成這四部分的工作. 我做了以下設(shè)計(jì):
Electron主進(jìn)程借助NodeJs豐富的生態(tài)系統(tǒng)完成網(wǎng)頁(yè)抓取與數(shù)據(jù)存儲(chǔ)與搜索的功能, UI進(jìn)程則完成頁(yè)面的渲染工作.
- 抓取程序: 使用NodeJs的request, cheerio, async完成.
- 數(shù)據(jù)庫(kù): 使用NodeJs下的nedb存儲(chǔ), 作為應(yīng)用內(nèi)嵌數(shù)據(jù)庫(kù)可以方便的集成進(jìn)桌面應(yīng)用.
- UI: 使用HTML與前端JavaScript類(lèi)庫(kù)完成, 重用之前Web應(yīng)用中的靜態(tài)頁(yè)面.
- 通信: 使用Electron提供的IPC,完成主進(jìn)程與UI進(jìn)程的通信.
實(shí)現(xiàn): 使用Electron構(gòu)建抓取桌面應(yīng)用
1. 抓取程序的實(shí)現(xiàn):
市長(zhǎng)信箱郵件多達(dá)上萬(wàn)封, JavaScript異步的特點(diǎn), 會(huì)讓人不小心就寫(xiě)出上千并發(fā)請(qǐng)求的程序, 短時(shí)間內(nèi)大量試圖和抓取目標(biāo)服務(wù)器建立連接的行為會(huì)被服務(wù)器拒絕服務(wù), 從而造成抓取流程失敗. 所以抓取程序要做到:
- tcp連接復(fù)用
- 并發(fā)頻率可控
我使用以下三個(gè)NodeJs組件:
- Request http客戶(hù)端, 利用了底層NodeJs的Http KeepAlive特性實(shí)現(xiàn)了tcp連接的復(fù)用.
- async 控制請(qǐng)求的并發(fā)以及異步編程的順序性.
- cheerio html的解析器.
代碼: crawlService.js
//使用request獲取頁(yè)面內(nèi)容request(\’http://12345.chengdu.gov.cn/moreMail\’, (err, response, body) => { if (err) throw err; //使用cheerio解析html var $ = cheerio.load(body), totalSize = $(\’div.pages script\’).html().match(/iRecCount = d /g)[0].match(/d /g)[0]; …… //使用async控制請(qǐng)求并發(fā), 順序的抓取郵件分頁(yè)內(nèi)容 async.eachSeries(pagesCollection, function (page, crawlNextPage) { pageCrawl(page, totalPageSize, updater, crawlNextPage); })});
2. 數(shù)據(jù)庫(kù)的實(shí)現(xiàn):
抓取后的內(nèi)容存儲(chǔ)方式有較多選擇:
- 文本文件
- 搜索引擎
- 數(shù)據(jù)庫(kù)
文本文件雖然保存簡(jiǎn)單, 但不利于查詢(xún)和搜索, 顧不采用.
搜索引擎一般需要獨(dú)立部署, 不利于桌面應(yīng)用的安裝, 這里暫不采用.
獨(dú)立部署的數(shù)據(jù)庫(kù)有和搜索引擎同樣的問(wèn)題, 所以像連接外部Mysql的方式這里也不采用.
綜合考慮, 我需要一種內(nèi)嵌數(shù)據(jù)庫(kù). 幸好NodeJs的組件非常豐富, nedb是一個(gè)不錯(cuò)的方案, 它可以將數(shù)據(jù)同時(shí)保存在內(nèi)存和磁盤(pán)中, 同時(shí)是文檔型內(nèi)嵌數(shù)據(jù)庫(kù), 使用mongodb的語(yǔ)法進(jìn)行數(shù)據(jù)操作.
代碼: dbService.js
//建立數(shù)據(jù)庫(kù)連接const db = new Datastore({filename: getUserHome() \’/.electronapp/watcher/12345mails.db\’, autoload: true});……//使用nedb插入數(shù)據(jù)db.update({_id: mail._id}, mail, {upsert: true}, function (err, newDoc) {});……//使用nedb進(jìn)行郵件查詢(xún)let match = {$regex: eval(\’/\’ keyword \’/\’)}; //關(guān)鍵字匹配var query = keyword ? {$or: [{title: match}, {content: match}]} : {};db.find(query).sort({publishDate: -1}).limit(100).exec(function (err, mails) { event.sender.send(\’search-reply\’, {mails: mails});//處理查詢(xún)結(jié)果});
3. UI的實(shí)現(xiàn):
桌面應(yīng)用的工程目錄如圖:
我將UI頁(yè)面放到static文件夾下. 在Electron的進(jìn)行前端UI開(kāi)發(fā)和普通的Web開(kāi)發(fā)方式一樣, 因?yàn)镋lectron的UI進(jìn)程就是一個(gè)Chrome進(jìn)程. Electron啟動(dòng)時(shí), 主進(jìn)程會(huì)執(zhí)行index.js文件, index.js將初始化應(yīng)用的窗口, 設(shè)置大小, 并在窗口加載UI入口頁(yè)面index.html.
代碼:index.js
function createMainWindow() { const win = new electron.BrowserWindow({ width: 1200, height: 800 });//初始應(yīng)用窗口大小 win.loadURL(`file://${__dirname}/static/index.html`);//在窗口中加載頁(yè)面 win.openDevTools();//打開(kāi)chrome的devTools win.on(\’closed\’, onClosed); return win;}
在UI頁(yè)面開(kāi)發(fā)的過(guò)程中, 有一點(diǎn)需要注意的是: 默認(rèn)情況下頁(yè)面會(huì)出現(xiàn)jQuery, require等組件加載失敗的情況, 這是因?yàn)闉g覽器window加載了NodeJs的一些方法, 和jQuery類(lèi)庫(kù)的方法沖突. 所以我們需要做些特別的處理, 在瀏覽器window中把這些NodeJs的方法刪掉:
代碼:preload.js
// 解決require沖突導(dǎo)致jQuery等組件不可用的問(wèn)題window.nodeRequire = require;delete window.require;delete window.exports;delete window.module;// 解決chrome調(diào)試工具devtron不可用的問(wèn)題window.__devtron = {require: nodeRequire, process: process}
4. 通信的實(shí)現(xiàn):
在Web應(yīng)用中, 頁(yè)面和服務(wù)的通信都是通過(guò)ajax進(jìn)行, 那我們的桌面應(yīng)用不是也可以采用ajax的方式通信? 這樣理論雖然上可行, 但有一個(gè)很大弊端: 我們的應(yīng)用需要打開(kāi)一個(gè)http的監(jiān)聽(tīng)端口, 通常個(gè)人操作系統(tǒng)都禁止軟件打開(kāi)http80端口, 而打開(kāi)其他端口也容易和別的程序造成端口沖突, 所以我們需要一種更優(yōu)雅的方式進(jìn)行通信.
Electron提供了UI進(jìn)程和主進(jìn)程通信的IPC API, 通過(guò)使用IPC通信, 我們就能實(shí)現(xiàn)UI頁(yè)面向NodeJs服務(wù)邏輯發(fā)起查詢(xún)和抓取請(qǐng)求,也能實(shí)現(xiàn)NodeJs服務(wù)主動(dòng)向UI頁(yè)面通知抓取進(jìn)度的更新.
使用Electron的IPC非常簡(jiǎn)單.
首先, 我們需要在UI中使用ipcRenderer, 向自定義的channel發(fā)出消息.
代碼: app.js
const ipcRenderer = nodeRequire(\’electron\’).ipcRenderer;//提交查詢(xún)表單$(\’form.searchForm\’).submit(function (event) { $(\’#waitModal\’).modal(\’show\’); event.preventDefault(); ipcRenderer.send(\’search-keyword\’, $(\’input.keyword\’).val());//發(fā)起查詢(xún)請(qǐng)求});ipcRenderer.on(\’search-reply\’, function(event, data) {//監(jiān)聽(tīng)查詢(xún)結(jié)果 $(\’#waitModal\’).modal(\’hide\’); if (data.mails) { var template = Handlebars.compile($(\’#template\’).html()); $(\’div.list-group\’).html(template(data)); }});
然后, 需要在主進(jìn)程執(zhí)行的NodeJs代碼中使用ipcMain, 監(jiān)聽(tīng)之前自定義的渠道, 就能接受UI發(fā)出的請(qǐng)求了.
代碼: crawlService.js
const ipcMain = require(\’electron\’).ipcMain;ipcMain.on(\’search-keyword\’, (event, arg) => { ….//處理查詢(xún)邏輯});ipcMain.on(\’start-crawl\’, (event, arg) => { ….//處理抓取邏輯});
桌面應(yīng)用打包
解決完以上四個(gè)方面的問(wèn)題后, 剩下的程序?qū)懫饋?lái)就簡(jiǎn)單了. 程序調(diào)試完后, 使用electron-builder, 就可以編譯打包出針對(duì)不同平臺(tái)的可執(zhí)行文件了.
版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶(hù)自發(fā)貢獻(xiàn),該文觀(guān)點(diǎn)僅代表作者本人。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請(qǐng)發(fā)送郵件至 舉報(bào),一經(jīng)查實(shí),本站將立刻刪除。