使用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.github

  2. 能跨平台, 既能编译成Mac下的DMG安装程序,又能编译成windows平台下的exe文件, 知足不足场景的使用.ajax

而Electron做为开发平台正好能知足个人这些需求, 经过一天的摸索, 我完成了这个桌面应用, 并最终打包出Mac平台下的DMG安装文件. 工程代码: https://github.com/ybak/watchersql

逻辑图

下面将介绍我是如何使用Electron平台开发这个桌面应用.mongodb

回顾: 市长信箱邮件抓取Web应用

动手以前, 我先分析一下以前所作的抓取Web应用. 它的架构以下:
图片描述
应用分可为四部分:chrome

  1. 抓取程序:使用Java的OkHttp做为Http请求类库获取网页内容,并交给Jsoup进行解析, 获得邮件内容.

  2. 数据库:用Mysql实现, 用来保存抓取后的网页内容, 并提供检索查询服务.

  3. 静态交互页面:一个简单的HTML页面, 使用jQuery发起ajax与后端交互, 并使用handlebar做为展现模板.

  4. 通讯: 使用SpringBoot提供了交互所需的API(搜索服务,全量抓取和更新邮件).

设计: 使用Electron构建抓取桌面应用

将要实现的桌面应用, 一样也须要须要完成这四部分的工做. 我作了如下设计:
图片描述
Electron主进程借助NodeJs丰富的生态系统完成网页抓取与数据存储与搜索的功能, UI进程则完成页面的渲染工做.

  1. 抓取程序: 使用NodeJs的Request, cheerio, async完成.

  2. 数据库: 使用NodeJs下的nedb存储, 做为应用内嵌数据库能够方便的集成进桌面应用.

  3. UI: 使用HTML与前端JavaScript类库完成, 重用以前Web应用中的静态页面.

  4. 通讯: 使用Electron提供的IPC,完成主进程与UI进程的通讯.

实现: 使用Electron构建抓取桌面应用

1. 抓取程序的实现:

市长信箱邮件多达上万封, JavaScript异步的特色, 会让人不当心就写出上千并发请求的程序, 短期内大量试图和抓取目标服务器创建链接的行为会被服务器拒绝服务, 从而形成抓取流程失败. 因此抓取程序要作到:

  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, 就能够编译打包出针对不一样平台的可执行文件了.

相关文章
相关标签/搜索