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