├── .gitignore ├── app ├── index.js ├── libs │ ├── crawlService.js │ └── dbService.js ├── package.json └── static │ ├── css │ └── index.css │ ├── index.html │ └── js │ ├── app.js │ └── preload.js ├── docs ├── app.png ├── layout.png ├── springboot.png └── watcher.png ├── license ├── package.json └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .DS_Store 4 | .idea/ 5 | .electronapp/ 6 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const electron = require('electron'); 3 | const app = electron.app; 4 | const CrawlService = require('./libs/crawlService'); 5 | 6 | //start crawl service 7 | CrawlService.start(); 8 | 9 | // adds debug features like hotkeys for triggering dev tools and reload 10 | require('electron-debug')(); 11 | 12 | // prevent window being garbage collected 13 | let mainWindow; 14 | 15 | function onClosed() { 16 | // dereference the window 17 | // for multiple windows store them in an array 18 | mainWindow = null; 19 | } 20 | 21 | function createMainWindow() { 22 | const win = new electron.BrowserWindow({ 23 | width: 1200, 24 | height: 800 25 | }); 26 | 27 | win.loadURL(`file://${__dirname}/static/index.html`); 28 | win.openDevTools(); 29 | win.on('closed', onClosed); 30 | 31 | return win; 32 | } 33 | 34 | app.on('window-all-closed', () => { 35 | if (process.platform !== 'darwin') { 36 | app.quit(); 37 | } 38 | }); 39 | 40 | app.on('activate', () => { 41 | if (!mainWindow) { 42 | mainWindow = createMainWindow(); 43 | } 44 | }); 45 | 46 | app.on('ready', () => { 47 | mainWindow = createMainWindow(); 48 | }) 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/libs/crawlService.js: -------------------------------------------------------------------------------- 1 | const request = require('request'), 2 | async = require('async'), 3 | ipcMain = require('electron').ipcMain, 4 | db = require('./dbService'), 5 | cheerio = require('cheerio'); 6 | 7 | const CrawlService = { 8 | start: function () { 9 | ipcMain.on('search-keyword', function (event, keyword) { 10 | console.log('channel "search-keyword" on msg:' + keyword); 11 | 12 | let match = {$regex: eval('/' + keyword + '/')}; 13 | var query = keyword ? {$or: [{title: match}, {content: match}]} : {}; 14 | db.find(query).sort({publishDate: -1}).limit(100).exec(function (err, mails) { 15 | event.sender.send('search-reply', {mails: mails}); 16 | }); 17 | }); 18 | 19 | ipcMain.on('start-crawl', (event, arg) => { 20 | console.log('channel "start-crawl" on msg:' + arg); 21 | var updater = { 22 | sender: event.sender, 23 | channel: arg, 24 | updateProgress: function (progress) { 25 | this.sender.send(this.channel, {progress: progress}); 26 | } 27 | }; 28 | crawler(updater); 29 | }); 30 | } 31 | }; 32 | 33 | function UrlCrawler(targetUrl) { 34 | return { 35 | targetUrl: targetUrl, 36 | startCrawl: function (processDom) { 37 | request(this.targetUrl, (err, response, body) => { 38 | if (err) throw err; 39 | var $ = cheerio.load(body); 40 | processDom($) 41 | }); 42 | } 43 | }; 44 | } 45 | 46 | function pageCrawl(page, totalPage, updater, crawlNextPage, crawProgress) { 47 | new UrlCrawler('http://12345.chengdu.gov.cn/moreMail?page=' + page).startCrawl(($) => { 48 | var $pageMails = $('div.left5 ul li.f12px'), 49 | sameMailsInPage = 0; 50 | 51 | async.eachOfLimit($pageMails, 10, function iteratee(item, key, nextMail) { 52 | if(crawProgress.skip){ 53 | return nextMail(); 54 | } 55 | let $item = $(item), 56 | mailDetailUrl = $item.find('a').prop('href'), 57 | divs = $item.find('div'); 58 | var mail = { 59 | _id: mailDetailUrl.match(/\d+/g)[0], 60 | title: $(divs[0]).text().trim(), 61 | sender: $(divs[1]).text().trim(), 62 | receiveUnit: $(divs[2]).text().trim(), 63 | status: $(divs[3]).text().trim(), 64 | category: $(divs[4]).text().trim(), 65 | views: $(divs[5]).text().trim() 66 | }; 67 | 68 | new UrlCrawler('http://12345.chengdu.gov.cn/' + mailDetailUrl).startCrawl(($) => {// crawl mail detail 69 | mail.content = $($('.rightside1 td.td2')[1]).text().trim(); 70 | mail.result = $('.rightside1 tbody tr:last-child').text().trim(); 71 | mail.publishDate = $($('.rightside1 td.td32')[0]).text().trim() || $($('.rightside1 td.td32')[1]).text().trim(); 72 | 73 | console.log(mail._id); 74 | 75 | db.update({_id: mail._id}, mail, {upsert: true, returnUpdatedDocs: true}, function (err, numReplaced, affectedDocuments, upsert) { 76 | if (err) { 77 | throw err; 78 | } 79 | if(!upsert && affectedDocuments.result == mail.result){//if a mail are not update 80 | if(++sameMailsInPage == 15){ //if all mails in one page are note update. 81 | crawProgress.skip = true; 82 | } 83 | } 84 | }); 85 | 86 | nextMail(); 87 | }); 88 | }, function done() { 89 | crawlNextPage(); 90 | updater.updateProgress(Math.floor(page * 100 / totalPage)); 91 | }); 92 | }); 93 | } 94 | 95 | /** 96 | * 1. get total page size 97 | * 2. iterator from page 1 to totalSize 98 | * 2.1 fetch mails summary list on 1 page 99 | * 2.2 iterator from mails 1 to maxItems mails summary in 1 page 100 | * 2.2.1 fetch mails detail from url 101 | * 2.2.2 save mail to db 102 | * 2.3 test if none of mails in current page updated? if none, stop crawling or continue step 2. 103 | * 104 | * @param url 105 | */ 106 | function crawler(updater) { 107 | new UrlCrawler('http://12345.chengdu.gov.cn/moreMail').startCrawl(($) => { 108 | var totalSize = $('div.pages script').html().match(/iRecCount = \d+/g)[0].match(/\d+/g)[0], 109 | totalPageSize = Math.ceil(totalSize / 15), 110 | pagesCollection = [], 111 | crawProgress = {skip: false}; 112 | for (let i = 1; i <= totalPageSize; i++) { 113 | pagesCollection.push(i); 114 | } 115 | async.eachSeries(pagesCollection, function (page, crawlNextPage) { 116 | pageCrawl(page, totalPageSize, updater, crawlNextPage, crawProgress); 117 | }) 118 | }); 119 | } 120 | 121 | module.exports = CrawlService; 122 | -------------------------------------------------------------------------------- /app/libs/dbService.js: -------------------------------------------------------------------------------- 1 | const Datastore = require('nedb'); 2 | 3 | function getUserHome() { 4 | return process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME']; 5 | } 6 | 7 | const db = new Datastore({filename: getUserHome()+'/.electronapp/watcher/12345mails.db', autoload: true}); 8 | 9 | module.exports = db; 10 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "watcher", 3 | "productName": "Watcher", 4 | "version": "0.0.1", 5 | "description": "watch some websites", 6 | "license": "MIT", 7 | "repository": "ybak/watcher", 8 | "author": { 9 | "name": "ybakswu", 10 | "email": "ybakswu@gmail.com", 11 | "url": "test.com" 12 | }, 13 | "scripts": { 14 | "start": "electron ." 15 | }, 16 | "dependencies": { 17 | "async": "^2.0.1", 18 | "cheerio": "^0.22.0", 19 | "electron-debug": "^1.0.0", 20 | "nedb": "^1.8.0" 21 | }, 22 | "devDependencies": { 23 | "devtron": "^1.1.0", 24 | "electron": "^1.3.5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/static/css/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | body { 8 | font-family: -apple-system, 'Helvetica Neue', Helvetica, sans-serif; 9 | } 10 | 11 | header { 12 | position: absolute; 13 | width: 500px; 14 | height: 250px; 15 | top: 50%; 16 | left: 50%; 17 | margin-top: -125px; 18 | margin-left: -250px; 19 | text-align: center; 20 | } 21 | 22 | header h1 { 23 | font-size: 60px; 24 | font-weight: 100; 25 | margin: 0; 26 | padding: 0; 27 | } 28 | -------------------------------------------------------------------------------- /app/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 邮件查询 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

内容检索

18 | 28 |
29 |
30 | 31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 | 52 | 66 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /app/static/js/app.js: -------------------------------------------------------------------------------- 1 | const ipcRenderer = nodeRequire('electron').ipcRenderer; 2 | 3 | Handlebars.registerHelper('formatDate', function(date) { 4 | return moment(date).format('YYYY-MM-DD hh:mm:ss'); 5 | }); 6 | 7 | $(function () { 8 | //提交查询表单 9 | $('form.searchForm').submit(function (event) { 10 | $('#waitModal').modal('show'); 11 | event.preventDefault(); 12 | ipcRenderer.send('search-keyword', $('input.keyword').val()); 13 | }); 14 | ipcRenderer.on('search-reply', function(event, data) { 15 | $('#waitModal').modal('hide'); 16 | if (data.mails) { 17 | var template = Handlebars.compile($('#template').html()); 18 | $('div.list-group').html(template(data)); 19 | } 20 | }); 21 | 22 | //增量更新全部邮件 23 | $('button.update-all').click(function (e) { 24 | e.preventDefault(); 25 | 26 | let channel = 'crawl-job-' + Date.now(); 27 | ipcRenderer.on(channel, function(event, msg) { 28 | console.log('Msg Received: ' + JSON.stringify(msg)); 29 | let progress = msg.progress; 30 | $('#progressModal .progress-bar').width(progress + '%'); 31 | if (progress >= 100) { 32 | $('#progressModal').modal('hide'); 33 | } 34 | }); 35 | ipcRenderer.send('start-crawl', channel); 36 | 37 | $('#progressModal .progress-bar').width('0%'); 38 | $('#progressModal').modal('show'); 39 | 40 | }); 41 | 42 | //更新当前邮件 43 | $(document).on('click', '.updateBtn', function (e) { 44 | $('#waitModal').modal('show'); 45 | e.preventDefault(); 46 | var $target = $(this); 47 | var id = $target.data('id'), 48 | url = $target.data('url'); 49 | $.post('/update', {'id': id, 'url': url}, function (data) { 50 | $('#waitModal').modal('hide'); 51 | $target.closest('.mail-item').find('.result').text(data.result); 52 | }); 53 | }); 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /app/static/js/preload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by ybak on 16/9/15. 3 | */ 4 | // 解决require冲突导致jQuery等组件不可用的问题 5 | window.nodeRequire = require; 6 | delete window.require; 7 | delete window.exports; 8 | delete window.module; 9 | // 解决chrome调试工具devtron不可用的问题 10 | window.__devtron = {require: nodeRequire, process: process} 11 | -------------------------------------------------------------------------------- /docs/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ybak/watcher/c82a210fa36ab30cb431d412d72ecc3ddbdec31c/docs/app.png -------------------------------------------------------------------------------- /docs/layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ybak/watcher/c82a210fa36ab30cb431d412d72ecc3ddbdec31c/docs/layout.png -------------------------------------------------------------------------------- /docs/springboot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ybak/watcher/c82a210fa36ab30cb431d412d72ecc3ddbdec31c/docs/springboot.png -------------------------------------------------------------------------------- /docs/watcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ybak/watcher/c82a210fa36ab30cb431d412d72ecc3ddbdec31c/docs/watcher.png -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) ybakswu (http://test.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "postinstall": "install-app-deps", 5 | "start": "electron ./app", 6 | "compile": "rimraf app/out && tsc", 7 | "pack": "npm run compile && build --dir", 8 | "dist": "npm run compile && build", 9 | "release": "npm run compile && build" 10 | }, 11 | "build": { 12 | "asar": false, 13 | "appId": "org.ybak", 14 | "category": "public.app-category.graphics-design", 15 | "dmg": { 16 | "contents": [ 17 | { 18 | "x": 410, 19 | "y": 150, 20 | "type": "link", 21 | "path": "/Applications" 22 | }, 23 | { 24 | "x": 130, 25 | "y": 150, 26 | "type": "file" 27 | } 28 | ] 29 | }, 30 | "win": { 31 | "publish": ["github", "bintray"] 32 | }, 33 | "linux": { 34 | "publish": null, 35 | "target": ["deb", "AppImage"] 36 | } 37 | }, 38 | "devDependencies": { 39 | "devtron": "^1.1.0", 40 | "electron-builder": "next", 41 | "electron": "1.3.5", 42 | "rimraf": "^2.5.4", 43 | "typescript": "^1.8.10" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ### Run 2 | ``` 3 | $ npm install && npm start 4 | ``` 5 | ### Build 6 | ``` 7 | $ npm run build 8 | ``` 9 | 10 | 11 | # 使用Electron构建跨平台的抓取桌面程序 12 | 13 | 谈起桌面应用开发技术, 我们会想到.Net下的WinForm, Java下的JavaFX以及Linux下的QT. 这些技术对于Web应用程序员来说一般比较陌生, 因为大多Web应用程序员的开发技能是前端的JavaScript和后端的Java,PHP等语言. 14 | 如果Web应用程序员想开发桌面应用怎么办? 主流的桌面应用开发技术的学习曲线不低, 上手比较困难. 而Electron的出现给Web应用程序员带来了福音. 15 | 16 | Electron简介: 17 | > Electron 是 Github 发布跨平台桌面应用开发工具,支持 Web 技术开发桌面应用开发,其本身是基于 C++ 开发的,GUI 核心来自于 Chrome,而 JavaScript 引擎使用 v8... 18 | 19 | 简单的说, Electron平台就是用Javascript把UI和后台逻辑打通, 后台主进程使用NodeJs丰富的API完成复杂耗时的逻辑, 而UI进程则借助Chrome渲染html完成交互. 20 | 21 | 我之前使用SpringBoot开发了一套[市长信箱抓取Web应用][1]. 由于没服务器部署, 所以我现在想把同样的功能移植到桌面端, 作成一个桌面应用. 对于开发平台我有以下需求: 22 | 1. 能利用我现有的技术栈: Web前端JavaScript, 服务端的Java或者NodeJs. 23 | 2. 能跨平台, 既能编译成Mac下的DMG安装程序,又能编译成windows平台下的exe文件, 满足不足场景的使用. 24 | 25 | 而Electron作为开发平台正好能满足我的这些需求, 通过一天的摸索, 我完成了这个桌面应用, 并最终打包出Mac平台下的DMG安装文件. 26 | ![逻辑图](./docs/app.png) 27 | 28 | 下面将介绍我是如何使用Electron平台开发这个桌面应用. 29 | 30 | ### 回顾: 市长信箱邮件抓取Web应用 31 | 动手之前, 我先分析一下之前所做的抓取Web应用. 它的架构如下: 32 | ![逻辑图](./docs/springboot.png) 33 | 应用分可为四部分: 34 | 1. 抓取程序:使用Java的OkHttp作为Http请求类库获取网页内容,并交给Jsoup进行解析, 得到邮件内容. 35 | 2. 数据库:用Mysql实现, 用来保存抓取后的网页内容, 并提供检索查询服务. 36 | 3. 静态交互页面:一个简单的HTML页面, 使用jQuery发起ajax与后端交互, 并使用handlebar作为展示模板. 37 | 4. 通信: 使用SpringBoot提供了交互所需的API(搜索服务,全量抓取和更新邮件). 38 | 39 | ### 设计: 使用Electron构建抓取桌面应用 40 | 将要实现的桌面应用, 同样也需要需要完成这四部分的工作. 我做了以下设计: 41 | ![逻辑图](./docs/watcher.png) 42 | Electron主进程借助NodeJs丰富的生态系统完成网页抓取与数据存储与搜索的功能, UI进程则完成页面的渲染工作. 43 | 44 | 1. 抓取程序: 使用NodeJs的[Request][2], [cheerio][3], [async][4]完成. 45 | 2. 数据库: 使用NodeJs下的[nedb][5]存储, 作为应用内嵌数据库可以方便的集成进桌面应用. 46 | 3. UI: 使用HTML与前端JavaScript类库完成, 重用之前Web应用中的静态页面. 47 | 4. 通信: 使用Electron提供的IPC,完成主进程与UI进程的通信. 48 | 49 | ### 实现: 使用Electron构建抓取桌面应用 50 | 51 | #### 1. 抓取程序的实现: 52 | 市长信箱邮件多达上万封, JavaScript异步的特点, 会让人不小心就写出上千并发请求的程序, 短时间内大量试图和抓取目标服务器建立连接的行为会被服务器拒绝服务, 从而造成抓取流程失败. 所以抓取程序要做到: 53 | 1. tcp连接复用 54 | 2. 并发频率可控 55 | 56 | 我使用以下三个NodeJs组件: 57 | 1. [Request][2]: http客户端, 利用了底层NodeJs的Http KeepAlive特性实现了tcp连接的复用. 58 | 2. [async][4]: 控制请求的并发以及异步编程的顺序性. 59 | 3. [cheerio][3]: html的解析器. 60 | 61 | *代码: crawlService.js* 62 | ```JavaScript 63 | //使用request获取页面内容 64 | request('http://12345.chengdu.gov.cn/moreMail', (err, response, body) => { 65 | if (err) throw err; 66 | //使用cheerio解析html 67 | var $ = cheerio.load(body), 68 | totalSize = $('div.pages script').html().match(/iRecCount = \d+/g)[0].match(/\d+/g)[0]; 69 | ...... 70 | //使用async控制请求并发, 顺序的抓取邮件分页内容 71 | async.eachSeries(pagesCollection, function (page, crawlNextPage) { 72 | pageCrawl(page, totalPageSize, updater, crawlNextPage); 73 | }) 74 | }); 75 | ``` 76 | 77 | #### 2. 数据库的实现: 78 | 抓取后的内容存储方式有较多选择: 79 | 1. 文本文件 80 | 2. 搜索引擎 81 | 3. 数据库 82 | 83 | 文本文件虽然保存简单, 但不利于查询和搜索, 顾不采用. 84 | 搜索引擎一般需要独立部署, 不利于桌面应用的安装, 这里暂不采用. 85 | 独立部署的数据库有和搜索引擎同样的问题, 所以像连接外部Mysql的方式这里也不采用. 86 | 87 | 综合考虑, 我需要一种内嵌数据库. 幸好NodeJs的组件非常丰富, [nedb][5]是一个不错的方案, 它可以将数据同时保存在内存和磁盘中, 同时是文档型内嵌数据库, 使用mongodb的语法进行数据操作. 88 | *代码: dbService.js* 89 | ```JavaScript 90 | //建立数据库连接 91 | const db = new Datastore({filename: getUserHome()+'/.electronapp/watcher/12345mails.db', autoload: true}); 92 | ...... 93 | //使用nedb插入数据 94 | db.update({_id: mail._id}, mail, {upsert: true}, function (err, newDoc) {}); 95 | ...... 96 | //使用nedb进行邮件查询 97 | let match = {$regex: eval('/' + keyword + '/')}; //关键字匹配 98 | var query = keyword ? {$or: [{title: match}, {content: match}]} : {}; 99 | db.find(query).sort({publishDate: -1}).limit(100).exec(function (err, mails) { 100 | event.sender.send('search-reply', {mails: mails});//处理查询结果 101 | }); 102 | ``` 103 | #### 3. UI的实现: 104 | 桌面应用的工程目录如图: 105 | ![逻辑图](./docs/layout.png) 106 | 我将UI页面放到static文件夹下. 在Electron的进行前端UI开发和普通的Web开发方式一样, 因为Electron的UI进程就是一个Chrome进程. Electron启动时, 主进程会执行index.js文件, index.js将初始化应用的窗口, 设置大小, 并在窗口加载UI入口页面index.html. 107 | *代码:index.js* 108 | ```JavaScript 109 | function createMainWindow() { 110 | const win = new electron.BrowserWindow({ 111 | width: 1200, 112 | height: 800 113 | });//初始应用窗口大小 114 | win.loadURL(`file://${__dirname}/static/index.html`);//在窗口中加载页面 115 | win.openDevTools();//打开chrome的devTools 116 | win.on('closed', onClosed); 117 | return win; 118 | } 119 | ``` 120 | 在UI页面开发的过程中, 有一点需要注意的是: 默认情况下页面会出现jQuery, require等组件加载失败的情况, 这是因为浏览器window加载了NodeJs的一些方法, 和jQuery类库的方法冲突. 所以我们需要做些特别的处理, 在浏览器window中把这些NodeJs的方法删掉: 121 | *代码:preload.js* 122 | ```JavaScript 123 | // 解决require冲突导致jQuery等组件不可用的问题 124 | window.nodeRequire = require; 125 | delete window.require; 126 | delete window.exports; 127 | delete window.module; 128 | // 解决chrome调试工具devtron不可用的问题 129 | window.__devtron = {require: nodeRequire, process: process} 130 | ``` 131 | #### 4. 通信的实现: 132 | 在Web应用中, 页面和服务的通信都是通过ajax进行, 那我们的桌面应用不是也可以采用ajax的方式通信? 这样理论虽然上可行, 但有一个很大弊端: 我们的应用需要打开一个http的监听端口, 通常个人操作系统都禁止软件打开http80端口, 而打开其他端口也容易和别的程序造成端口冲突, 所以我们需要一种更优雅的方式进行通信. 133 | Electron提供了UI进程和主进程通信的**IPC** API, 通过使用**IPC**通信, 我们就能实现UI页面向NodeJs服务逻辑发起查询和抓取请求,也能实现NodeJs服务主动向UI页面通知抓取进度的更新. 134 | 使用Electron的**IPC**非常简单. 135 | 首先, 我们需要在UI中使用ipcRenderer, 向自定义的channel发出消息. 136 | *代码: app.js* 137 | ```JavaScript 138 | const ipcRenderer = nodeRequire('electron').ipcRenderer; 139 | 140 | //提交查询表单 141 | $('form.searchForm').submit(function (event) { 142 | $('#waitModal').modal('show'); 143 | event.preventDefault(); 144 | ipcRenderer.send('search-keyword', $('input.keyword').val());//发起查询请求 145 | }); 146 | ipcRenderer.on('search-reply', function(event, data) {//监听查询结果 147 | $('#waitModal').modal('hide'); 148 | if (data.mails) { 149 | var template = Handlebars.compile($('#template').html()); 150 | $('div.list-group').html(template(data)); 151 | } 152 | }); 153 | ``` 154 | 然后, 需要在主进程执行的NodeJs代码中使用ipcMain, 监听之前自定义的渠道, 就能接受UI发出的请求了. 155 | *代码: crawlService.js* 156 | ```JavaScript 157 | const ipcMain = require('electron').ipcMain; 158 | 159 | ipcMain.on('search-keyword', (event, arg) => { 160 | ....//处理查询逻辑 161 | }); 162 | 163 | ipcMain.on('start-crawl', (event, arg) => { 164 | ....//处理抓取逻辑 165 | }); 166 | ``` 167 | #### 桌面应用打包 168 | 解决完以上四个方面的问题后, 剩下的程序写起来就简单了. 程序调试完后, 使用[electron-builder][6], 就可以编译打包出针对不同平台的可执行文件了. 169 | 170 | [1]: https://segmentfault.com/a/1190000005183675 171 | [2]: https://github.com/request/request 172 | [3]: https://github.com/cheeriojs/cheerio 173 | [4]: https://github.com/caolan/async 174 | [5]: https://github.com/louischatriot/nedb 175 | [6]: https://github.com/electron-userland/electron-builder 176 | --------------------------------------------------------------------------------