使用Electron構建跨平臺的抓取桌面程序

使用Electron構建跨平臺的抓取桌面程序

談起桌面應用開發技術, 咱們會想到.Net下的WinForm, Java下的JavaFX以及Linux下的QT. 這些技術對於Web應用程序員來講通常比較陌生, 由於大多Web應用程序員的開發技能是前端的JavaScript和後端的Java,PHP等語言.
若是Web應用程序員想開發桌面應用怎麼辦? 主流的桌面應用開發技術的學習曲線不低, 上手比較困難. 而Electron的出現給Web應用程序員帶來了福音.html

Electron簡介:前端

Electron 是 Github 發佈跨平臺桌面應用開發工具,支持 Web 技術開發桌面應用開發,其自己是基於 C++ 開發的,GUI 核心來自於 Chrome,而 JavaScript 引擎使用 v8...node

簡單的說, Electron平臺就是用Javascript把UI和後臺邏輯打通, 後臺主進程使用NodeJs豐富的API完成複雜耗時的邏輯, 而UI進程則藉助Chrome渲染html完成交互.git

我以前使用SpringBoot開發了一套市長信箱抓取Web應用. 因爲沒服務器部署, 因此我如今想把一樣的功能移植到桌面端, 做成一個桌面應用. 對於開發平臺我有如下需求:程序員

  1. 能利用我現有的技術棧: Web前端JavaScript, 服務端的Java或者NodeJs.
  2. 能跨平臺, 既能編譯成Mac下的DMG安裝程序,又能編譯成windows平臺下的exe文件, 知足不足場景的使用.

而Electron做爲開發平臺正好能知足個人這些需求, 經過一天的摸索, 我完成了這個桌面應用, 並最終打包出Mac平臺下的DMG安裝文件.
工程代碼: https://github.com/ybak/watchergithub

輸入圖片說明

下面將介紹我是如何使用Electron平臺開發這個桌面應用.ajax

回顧: 市長信箱郵件抓取Web應用

動手以前, 我先分析一下以前所作的抓取Web應用. 它的架構以下:
輸入圖片說明
應用分可爲四部分:sql

  1. 抓取程序:使用Java的OkHttp做爲Http請求類庫獲取網頁內容,並交給Jsoup進行解析, 獲得郵件內容.
  2. 數據庫:用Mysql實現, 用來保存抓取後的網頁內容, 並提供檢索查詢服務.
  3. 靜態交互頁面:一個簡單的HTML頁面, 使用jQuery發起ajax與後端交互, 並使用handlebar做爲展現模板.
  4. 通訊: 使用SpringBoot提供了交互所需的API(搜索服務,全量抓取和更新郵件).

設計: 使用Electron構建抓取桌面應用

將要實現的桌面應用, 一樣也須要須要完成這四部分的工做. 我作了如下設計:
輸入圖片說明
Electron主進程藉助NodeJs豐富的生態系統完成網頁抓取與數據存儲與搜索的功能, UI進程則完成頁面的渲染工做.mongodb

  1. 抓取程序: 使用NodeJs的Request, cheerio, async完成.
  2. 數據庫: 使用NodeJs下的nedb存儲, 做爲應用內嵌數據庫能夠方便的集成進桌面應用.
  3. UI: 使用HTML與前端JavaScript類庫完成, 重用以前Web應用中的靜態頁面.
  4. 通訊: 使用Electron提供的IPC,完成主進程與UI進程的通訊.

實現: 使用Electron構建抓取桌面應用

1. 抓取程序的實現:

市長信箱郵件多達上萬封, JavaScript異步的特色, 會讓人不當心就寫出上千併發請求的程序, 短期內大量試圖和抓取目標服務器創建鏈接的行爲會被服務器拒絕服務, 從而形成抓取流程失敗. 因此抓取程序要作到:chrome

  1. tcp鏈接複用
  2. 併發頻率可控

我使用如下三個NodeJs組件:

  1. Request: http客戶端, 利用了底層NodeJs的Http KeepAlive特性實現了tcp鏈接的複用.
  2. async: 控制請求的併發以及異步編程的順序性.
  3. cheerio: html的解析器.

代碼: crawlService.js

//使用request獲取頁面內容
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控制請求併發, 順序的抓取郵件分頁內容
    async.eachSeries(pagesCollection, function (page, crawlNextPage) {
        pageCrawl(page, totalPageSize, updater, crawlNextPage);
    })
});

2. 數據庫的實現:

抓取後的內容存儲方式有較多選擇:

  1. 文本文件
  2. 搜索引擎
  3. 數據庫

文本文件雖然保存簡單, 但不利於查詢和搜索, 顧不採用.
搜索引擎通常須要獨立部署, 不利於桌面應用的安裝, 這裏暫不採用.
獨立部署的數據庫有和搜索引擎一樣的問題, 因此像鏈接外部Mysql的方式這裏也不採用.

綜合考慮, 我須要一種內嵌數據庫. 幸虧NodeJs的組件很是豐富, nedb是一個不錯的方案, 它能夠將數據同時保存在內存和磁盤中, 同時是文檔型內嵌數據庫, 使用mongodb的語法進行數據操做.
代碼: dbService.js

//創建數據庫鏈接
const db = new Datastore({filename: getUserHome()+'/.electronapp/watcher/12345mails.db', autoload: true});
......
//使用nedb插入數據
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的實現:

桌面應用的工程目錄如圖:
輸入圖片說明
我將UI頁面放到static文件夾下. 在Electron的進行前端UI開發和普通的Web開發方式同樣, 由於Electron的UI進程就是一個Chrome進程. Electron啓動時, 主進程會執行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頁面開發的過程當中, 有一點須要注意的是: 默認狀況下頁面會出現jQuery, require等組件加載失敗的狀況, 這是由於瀏覽器window加載了NodeJs的一些方法, 和jQuery類庫的方法衝突. 因此咱們須要作些特別的處理, 在瀏覽器window中把這些NodeJs的方法刪掉:
代碼:preload.js

// 解決require衝突致使jQuery等組件不可用的問題
window.nodeRequire = require;
delete window.require;
delete window.exports;
delete window.module;
// 解決chrome調試工具devtron不可用的問題
window.__devtron = {require: nodeRequire, process: process}

4. 通訊的實現:

在Web應用中, 頁面和服務的通訊都是經過ajax進行, 那咱們的桌面應用不是也能夠採用ajax的方式通訊? 這樣理論雖然上可行, 但有一個很大弊端: 咱們的應用須要打開一個http的監聽端口, 一般我的操做系統都禁止軟件打開http80端口, 而打開其餘端口也容易和別的程序形成端口衝突, 因此咱們須要一種更優雅的方式進行通訊.
Electron提供了UI進程和主進程通訊的IPC API, 經過使用IPC通訊, 咱們就能實現UI頁面向NodeJs服務邏輯發起查詢和抓取請求,也能實現NodeJs服務主動向UI頁面通知抓取進度的更新.
使用Electron的IPC很是簡單.
首先, 咱們須要在UI中使用ipcRenderer, 向自定義的channel發出消息. 代碼: app.js

const ipcRenderer = nodeRequire('electron').ipcRenderer;

//提交查詢表單
$('form.searchForm').submit(function (event) {
    $('#waitModal').modal('show');
    event.preventDefault();
    ipcRenderer.send('search-keyword', $('input.keyword').val());//發起查詢請求
});
ipcRenderer.on('search-reply', function(event, data) {//監聽查詢結果
    $('#waitModal').modal('hide');
    if (data.mails) {
        var template = Handlebars.compile($('#template').html());
        $('div.list-group').html(template(data));
    }
});

而後, 須要在主進程執行的NodeJs代碼中使用ipcMain, 監聽以前自定義的渠道, 就能接受UI發出的請求了.
代碼: crawlService.js

const ipcMain = require('electron').ipcMain;

ipcMain.on('search-keyword', (event, arg) => {
  ....//處理查詢邏輯
});

ipcMain.on('start-crawl', (event, arg) => {
  ....//處理抓取邏輯
});

桌面應用打包

解決完以上四個方面的問題後, 剩下的程序寫起來就簡單了. 程序調試完後, 使用electron-builder, 就能夠編譯打包出針對不一樣平臺的可執行文件了.

相關文章
相關標籤/搜索