当前位置: 首页 > 后端技术 > Node.js

使用Angular和TypeScript构建Electron应用(四)

时间:2023-04-03 15:22:30 Node.js

这一节我们只做两件事。第一是构建相应的爬虫系统,从网页链接中提取合适的信息,第二是将这些信息存储在数据库中,当需要展示render的时候,可以查询展示。在我们开始构建代码之前,让我们考虑一下这样做的好处是什么。简介在新闻源应用程序中,我们将爬虫逻辑放在客户端应用程序而不是服务器中。这是对的。考虑到用户的增加,我们承担不起所有的爬虫任务。如果我们使这些任务合理分配是最佳的,利用一些客户端资源。在生产环境中,也可以考虑用户每次爬取后将处理后的字符串返回给服务器存储,甚至可以考虑根据服务器返回的资源不同,返回不同的任务给用户。虽然我们不会在news-feed中做这些事情,但我们不妨考虑一下这样一个系统是如何工作的:应用程序内部存储了一个映射表,可以更新它作为当前应用程序的基本爬行任务。根据用户下载的应用IP不同,分发不同的应用包,基础数据库的标识有些不同。根据用户请求的标识+IP地址,返回给用户不同的爬虫任务。经过短暂的工作后,数据返回到服务器。用户每次查看的新闻一部分是自己客户端抓取的,另一部分是从服务器下载的。这样的系统非常有趣。积累了很多格式化数据资源后,甚至可以转化为开发好的新闻API供大家使用,但是非常复杂(大家可以自己试试)。目前我们希望应用的所有数据都可以自己完成。为此,我们至少需要一个数据库来存储格式化的数据,以及一个可配置的代码来爬取和分析数据。在做所有事情之前,我要为爬取任务添加一个新的语法糖。配置Asyncasync是ES7的新语法。简单的说,async就是在Generator的基础上加了一个语法糖。如果你还不了解Generator,建议先学习一些ES6基础知识。爬虫任务可能会涉及到很多异步任务,但是大多数时候我们更希望它们可以同步执行(如果并发量过大,网站容易封IP),async函数可以帮到我们轻松地以同步函数的形式编写异步逻辑,而且非常简单,学习起来很容易,这是javascript的趋势之一。首先我们需要安装一些必要的npm包:npmi--savetransform-async-to-generatorsyntax-async-functionstransform-regeneratornpmi--savebabel-corebabel-polyfillbabel-preset-es2016这里我希望代码不频繁转码后,应用程序可以忽略兼容性,所以我添加了一些shim来让语法糖正常工作。在根文件夹下创建一个.babelrc文件:{"presets":["es2016"],"plugins":["transform-async-to-generator","syntax-async-functions","transform-regenerator"]}并在根文件夹中创建一个main.js,组装这些文件:require('babel-core/register');require("babel-polyfill");require("./index");从现在开始我们每次运行electronmain.js就可以轻松启动一个富含ES7语法糖的应用程序。当然,你可以包含任何语法,甚至Gulp/Webpack编译代码,只要你喜欢。安装数据库作为桌面应用程序,数据存储是必不可少的部分,但是已经自带的浏览器存储这里就不使用了:浏览器的各种存储总是有限的。它们很难存储结构复杂的数据,你需要为此做很多转换。最大的限制就是window对象不能随意释放,会造成很多存储丢失的问题,势必会影响以后的扩展。此外,我们还可以选择一些流行的云存储、远程数据库等,但希望应用在离线时也能正常工作。为此,我们需要一个易于安装、本地实时编译的轻量级数据库。这里我选择了比较流行的nedb,它的社区环境足够好,用户多(保证库能及时更新,解决各种问题),能和electron很好的结合。安装nedb:npmi--savenedb在根目录的index.js中启动数据库:constDatastore=require('nedb')global.Storage=newDatastore({filename:`${__dirname}/.database/news-feed.db`,autoload:true})nedb有多种存储方式,包括内存。这里的autoload是指每次更新都会更新数据库的本地文件,将数据写入硬盘。也可以选择每次使用loadDatabase时手动触发写入硬盘的动作。在构建爬虫代码之前,我们先来尝试分析一下爬虫代码的逻辑:这里至少需要一个实际工作的爬虫函数,从http请求中获取数据并开始分析html,最后存储数据。不同的网站结构意味着需要不同的解析函数,但至少可以提取出基本的http服务(它们总是一样的)。以后我们可以从服务端获取一些解析码填入这里。手动发起http请求和处理字符串的工作量非常大。我们可以使用库来完成这些任务:*https://github.com/request/requestnpmi--saverequest*https://github.com/cheeriojs/cheerionpmi--savecheerio1。新建一个http请求函数,在/browser/task下新建一个base.js:constreq=require('request')module.exports=classBase{constructor(){}staticmakeOptions(url){return{url:url,port:8080,method:'GET',headers:{'User-Agent':'nodejs','Content-Type':'application/json'}}}静态请求(url){returnnewPromise((resolve,reject)=>{req(Base.makeOptions(url),(err,response,body)=>{if(err)returnreject(err)resolve(body)})})}}Base类有两个静态方法。makeOptions负责根据url生成option对象,并为每个请求设置配置项。以后需要验证token/cookie的时候,我们会扩展这个方法。request返回一个Promise对象,明明会发起一个request,但是更多的功能是在使用的时候返回body而不是response。这个非常重要。可能你开始注意到,这两个静态函数根本不依赖于this,它们只是类的静态方法,不用实例化就可以使用,也可以被继承。这样做的目的是暗示这些函数是完全不依赖于状态的纯函数。它们总是返回相同的结果并且没有副作用。这样的功能以后可以更好的阅读和扩展。2.新建一个爬虫文件。假定该文件只负责单个网站(如凤凰网)的功能。当然,以后这样的文件会越来越多。现在为这些函数文件创建一个集合文件,负责导出:///browser/task/index.jsmodule.exports={ifeng:require('./ifeng')}在task文件夹下再创建一个ifeng.js:constcheerio=require('cheerio')constBase=require('./base')module.exports=newclassSelfextendsBase{constructor(){super()this.url='http://news.ifeng.com/xijinping/'}start(){global.Storage.count({},(err,c)=>{if(c||c>0)return;this.request().then(res=>{console.log('Allstored!');global.Storage.loadDatabase()}).catch(err=>{console.log(err);})})}asyncrequest(){try{constbody=awaitSelf.request(this.url)letlinks=awaitthis.parseLink(body)for(letindex=1;indexa').map((i,el)=>$(el).attr('href'))}parseContent(html){if(!html)返回;const$=cheerio.load(html)consttitle=$('title').text()constcontent=$('.yc_con_txt').html()return{title:title,content:content}}saveContent(文章){如果(!article||!article.title)返回;returnglobal.Storage.insert(article)}}()ifeng.js的主体是request函数,主要做了以下几件事:try代码块,捕获await可能抛出的错误使用继承的request静态方法获取基本列表文件,使用parseLink解析html获取链接数组。cheerio是一个类似JQuery的库,可以帮助我们解析这些html文件。在循环体中,单独请求文章正文,使用parseContent解析文章,组装成对象。如果对象获取成功,将为该文章对象合并一个序号,方便后续查询/分类。每个循环插入数据库一次。这是因为当单次数据插入失败时,neDB会回滚所有数据。当然,这个的大小取决于你。在较大的应用程序中,您可以抽象出一层类似ORM的服务,专用于高效快速的存储查询,甚至提供一些语法糖。这里的global.Storage.count是权宜之计,等前端代码完全完成后再回来解决。目前我们只需要在根目录下的index.js中添加require('./browser/task/index').ifeng.start()即可:OK,本节目标全部完成,下一节我们开始讨论如何在Angular中构建合理的展示模块,并与数据库进行通信。