Promise方式实现Node.js应用的实践①
2017-05-17邓森泉杨海波中国科学院沈阳计算技术研究所沈阳0168中国科学院大学北京100049
邓森泉, 杨海波(中国科学院 沈阳计算技术研究所, 沈阳 0168)(中国科学院大学, 北京 100049)
Promise方式实现Node.js应用的实践①
邓森泉1,2, 杨海波11(中国科学院 沈阳计算技术研究所, 沈阳 110168)2(中国科学院大学, 北京 100049)
Node.js是目前非常火热的技术之一, 它是运行在服务器端的JavaScript执行环境. Node.js借助JavaScript的事件驱动机制加上V8高性能引擎, 使得编写高性能Web服务轻而易举. Node.js在处理异步问题时一般采用的是callback回调的方式, 但callback回调的方式存在Callback Hell的问题, 无论是阅读还是调试都很不方便, 甚至无法获取代码的堆栈. 基于Node.js平台, 采用Promise方式, 编写了一套网络爬虫的应用, 在编写过程中详细的描述了如何使用Promise方式处理异步回调问题.
Node.js; Promise; Web应用
Node.js是建立在Chrome V8引擎的javaScript运行时之上的平台, 用于构建快速、可扩展的Web应用程序. Node.js采用单线程、事件驱动、非阻塞的I/O模型, 这些特性不仅带来了巨大的性能提升, 还减少了多线程程序设计的复杂性, 进而提高了开发效率,使其轻量又高效. 传统的Node.js在处理异步问题时,一般采用的是callback回调的方式. callback回调存在一个很严重的金字塔问题——大量的回调函数慢慢向右侧屏幕延伸的一种状态.
Promise是异步编程的一种解决方案, 比传统的解决方案——回调函数和事件, 更合理和强大. 它最早由javascript社区提出和实现, 目前最新的JavaScript语言标准ES6已将其写进了标准中, 统一了用法, 原生提供了Promise对象. 借助Promise对象, 可以将异步操作以同步操作的流程表达出来, 避免了层层嵌套的回调函数.
本文就是采用Promise方式在Node.js平台上搭建了一个网络爬虫的应用. 本文首先介绍了Node.js平台以及其相关的一些特点和概念, 然后在此基础上, 针对其传统的callback的回调方式的“回调地狱”等问题,引入了Promise对象来处理这种异步回调的问题. 通过深入分析Promise对象的理论知识以及规范, 将其合理地运用到网络爬虫的应用中去. 最后通过爬取一个课程网站的视频课程信息, 充分展示了Node.js平台的强大和方便, 以及Promise对象在处理异步回调问题上的优越性以及新思路.
1 Node.js平台介绍
Node.js是一位叫Ryan Dahl的程序员发明的. 他的工作是用C/C++写高性能Web服务. 对于高性能,异步IO、事件驱动是基本原则, 但是用C/C++写就太痛苦了. 于是Ryan开始设想用高级语言开发Web服务.他评估了很多种高级语言, 发现很多语言虽然同时提供了同步IO和异步IO, 但是开发人员一旦用了同步IO, 他们就再也懒得写异步IO了, 所以, 最终, Ryan瞄向了JavaScript. 因为JavaScript是单线程执行, 根本不能进行同步IO操作, 所以, JavaScript的这一“缺陷”导致了它只能使用异步IO.
选定了开发语言, 还要有运行时引擎. Ryan曾考虑过自己写一个, 不过明智地放弃了, 因为V8就是开源的JavaScript引擎. 让Google投资去优化V8, 我们只管拿过来用就好了.
于是在2009年, Ryan正式推出了基于JavaScript语言和V8引擎的开源Web服务器项目, 命名为Node.js. Node第一次把JavaScript带入到后端服务器开发, 加上世界上已经有无数的JavaScript开发人员,所以Node.js一下子就火了起来.
Node.js架构如图1所示.
图1 Node.js架构
Node.js主要特点是(1)时间驱动、异步编程; (2)单进程单线程.
1.1 事件驱动、异步编程
事件驱动并不是Node.js专属, 在某些传统语言的网络编程中, 我们会用到回调函数, 比如当socket资源达到某种状态时, 注册的回调函数就会执行. Node.js的设计思想中以事件驱动为核心, 它提供的绝大多数API都是基于事件的、异步的风格. 以Net模块为例, 其中的net.Socket对象就有以下事件: connect、data、end、timeout、drain、error、close等, 使用Node.js的开发人员需要根据自己的业务逻辑注册相应的回调函数. 这些回调函数都是异步执行的, 这意味着虽然在代码结构中, 这些函数看似是依次注册的, 但是它们并不依赖于自身出现的顺序, 而是等待相应的事件触发. 事件驱动、异步编程的设计重要的优势在于, 充分利用了系统资源, 执行代码无须阻塞等待某种操作完成, 有限的资源可以用于其他的任务.此类设计非常适合于后端的网络服务编程, Node.js的目标也在于此. 在服务器开发中, 并发的请求处理是个大问题, 阻塞式的函数会导致资源浪费和时间延迟.通过事件注册、异步函数, 开发人员可以提高资源的利用率, 性能也会改善.
从Node.js提供的支持模块中, 我们可以看到包括文件操作在内的许多函数都是异步执行的, 这和传统语言存在区别, 而且为了方便服务器开发, Node.js的网络模块特别多, 包括HTTP、DNS、NET、UDP、HTTPS、TLS等, 开发人员可以在此基础上快速构建Web服务器.
比如搭建一个简单的http服务器:
1.2 单进程单线程
1.2.1 高性能
Node.js单线程模式避免了传统php那样频繁创建、切换线程的花销, 执行速度更快. 而且, 资源占用小, Node.js在大负荷下对内存占用任然很低.
1.2.2 线程安全
单线程的node.js还保证了绝对的线程安全, 不用担心统一变量同时被多个线程进行读写而造成程序崩溃. 线程安全的同时也解放了开发人员, 免去了多线程编程中忘记对变量加锁或者解锁造成的隐患.
2 Promise
Promise主要解决JavaScript中异步的场景. Promise是个对象, 同JavaScript中其它对象没什么区别, 但同时它也是一个规范, 针对异步操作约定了统一的接口, 表示一个一步操作最终的结果, 以同步的方式来写代码, 执行的操作是异步的, 但是又保证程序的执行顺序是同步的. 这原本是JavaScript社区的一个规范的构想, 现在已经被加入到了ES6的语言标准中, Firefox和Chrome等浏览器已经对它进行了实现. 2.1同步与异步
JS引擎是单线程的. 这意味着在任何环境中, 只有一段JS代码会被执行. 每个函数是一个不可分割的片段或者代码块. 当JS引擎开始执行一个函数(比如回调函数)时, 它就会把这个函数执行完, 只有执行完这段代码才会继续执行后面的代码. 这就是JS中的同步. Promise对象的then()方法就是同步处理每个Promise对象.
异步是指在执行一段代码时, 这段代码依赖一些其他的操作或者数据, 这时就不用等待数据或者操作的返回, 直接执行下一段代码, 当有数据或操作返回时再去响应之前的代码, 从而提高代码执行的效率.
2.2 Promise对象的状态
Promise对象只有三种状态:
(1) Pending: 初始状态, 进行中.
(2) Resolved(或Fulfilled): 成功的操作.
(3) Rejected: 失败的操作.
(1) Promise对象的状态不受外界影响.
Promise对象代表一个异步操作, 有三种状态: Pending(进行中)、Resolved(已完成, 又称Fulfilled)和Rejected(已失败). 只有异步操作的结果, 可以决定当前是哪一种状态, 任何其他操作都无法改变这个状态.
(2) Promise对象一旦状态改变, 就不会再变, 任何时候都可以得到这个结果.
Promise对象的状态改变, 只有两种可能: 从Pending变为Resolved和从Pending变为Rejected. 只要这两种情况发生, 状态就凝固了, 不会再变了, 会一直保持这个结果. 就算改变已经发生了, 再对Promise对象添加回调函数, 也会立即得到这个结果.
2.3 Promise的核心方法
Promise对象的核心部件是它的then方法, 它的作用是为Promise实例添加状态改变时的回调函数. then方法接受两个回调函数作为参数. 第一个回调函数是Promise对象的状态变为Resolved时调用, 第二个回调函数是Promise对象的状态变为Rejected时调用. 其中,第二个函数是可选的, 不一定要提供. 这两个函数都接受Promise对象传出的值作为参数.
Promise对象另一个核心方法是它的catch方法,用于指定发生错误时的回调函数, 是then(null, rejection)的别名. catch方法可以捕捉promise实例中的异常还能捕获在它之前太狠方法中发生的异常, 所以在实际的使用中, 多用catch方法来取代then(null, rejection)处理异常.
3 爬虫应用设计与实现
3.1 模块加载
新建一个promise_crawler.js文件, 首先把需要的相应的模块加载进来.
http模块: 主要用于搭建 HTTP 服务端和客户端,使用 HTTP 服务器或客户端功能必须调用 http 模块;
bluebird模块: Promise类库(在最新的Node.js里已经引入了Promise模块, 可直接使用, 但考虑到兼容性问题, 本例中采用bluebird模块);
cheerio模块: 类似于前端的jQuery, 能够简单方便地操作装在后台的html.
代码如下:
3.2 组织数据结构
首先在chrome浏览器中打开需要爬取的网页, 同时打开控制台查看网页html DOM结构, 分析出所需信息, 组织好数据结构, 然后根据DOM结构去获取所需信息. 如图2所示.
图2 网页及DOM结构
分析所需数据, 组织好数据结构:
3.3 Promise主要流程
本例中完成的主要功能是, 同时爬取一个课程网站的多个页面, 获取相关信息, 然后将数据按照组织好的数据结构打印出来.
核心代码如下:
代码中所用到的Promise.all方法用于将多个Promise实例, 包装成一个新的Promise实例.
该方法接收一个Promise对象数组作为参数, p1、p2、p3都是Promise对象的实例.
p的状态由p1、p2、p3决定, 分成两种情况.
(1) 只有p1、p2、p3的状态都变成Resolved, p的状态才会变成Resolved, 此时p1、p2、p3的返回值组成一个数组, 传递给p的回调函数.
(2) 只要p1、p2、p3之中有一个被rejected, p的状态就变成Rejected, 此时第一个被Rejected的实例的返回值, 会传递给p的回调函数.
3.4 相关函数实现
3.4.1 爬取页面getPageAsync(url)
通过http模块的get方法爬取页面数据, 最后返回一个Promise对象, 方便异步处理.
核心代码如下:
3.4.2 过滤数据filterChapters(html)
过滤出每个页面所需的数据, 然后按一定的数据结构组织起来.
核心代码如下:
3.4.3 打印数据printCourseInfo(coursesData)
将爬取到的数据, 按照组织好的数据结构打印出来.
核心代码如下:
3.4 实验结果
执行promise_crawler.js文件, 即可看到输出的相关信息如图3.
图3 输出的相关信息
实验中同爬取了4个页面, 可以看到, 实验结果是按照代码中设定好的数据结构爬取并打印出来的,符合实验预期. Promise对象是基于异步的方式来处理程序的. 爬取每个页面时, 不用等待页面的数据处理完毕再去爬取下一个页面, 而是无阻塞不间断的去爬取每个页面, 当有异步的数据返回时调用Promise对象的resolve()方法去处理, 出现错误异常时调用reject()方法去解决. 当有多个Promise对象时, 调用then(onFulfilled)方法, 同步处理每个Promise对象, 一旦处理哪个Promise对象出错时, 可以立即调用catch方法处理异常, 中止程序往下执行, 及时发现错误.而且onFulfilled()方法每次返回的是新的Promise对象,这样保证了then()可以一直链式调用下去, 提高了程序的效率和可靠性.
4 结语
Node.js作为一门新兴的技术, 打通了前后端的界限. 由于采用事件驱动和无阻塞模型, 可以很方便的构建高效、可扩展的网络应用, 这是Node.js最大的一个优点, 同时也是最大的一个缺点, 由于事件驱动和无阻塞模型是建立在callback这种回调方式上的, 随着回调的增加, 代码嵌套的层次就会增加, 这样很容易陷入“回调地狱”, 这种代码难以编写, 难以理解而且难以维护.
Promise对象是解决Node.js中异步回调的一种很有效的方式. 借助Promise对象, 可以将异步操作以同步操作的流程表达出来, 避免了层层嵌套的回调函数.在保证异步回调的基础上又实现了多个promise对象之间的同步顺序, 使程序能快速高效的执行下去, 给我们的开发带来很大的便利.
1 顾宁,刘家茂,柴晓路.Web Services原理与研发实践.北京:机械工业出版社,2006.
2 朴灵.深入浅出Node.js.北京:人民邮电出版社,2013.
3 赵昆.改变Web开发格局的新技术node.js.程序员, 2011,(7):124–125.
4 Burnhamt. Javascript异步编程:设计快速响应的网络应用.北京:人民邮电出版社.
5 Node.js官方网站.http://www.nodejs.org.
6 Getify. Promise: The Inversion Problem(part 2), https://blog. getify.com/promise-part-2/. [2014-5-19].
7 Brett M. What is node? California: O’Reily Media, 2011.
Web Application Based on Node.js in the Way of Promise
DENG Sen-Quan1,2, YANG Hai-Bo112(Shenyang Institute of Computing Technology, Chinese Academy of Sciences, Shenyang 110168, China) (University of Chinese Academy of Sciences, Beijing 100049, China)
Node.js is one of the most popular technologies at present, and it is the JavaScript execution environment running on the server. With event-driven mechanism Node.js JavaScript plus high-performance V8 engine, it’s easy to achieve high-performance Web services. When Node.js deals with the problem of asynchronous, it generally uses callback method, but there are Callback Hell problems in the way of callback. Whether reading or debugging is very inconvenient, it is even impossible to get the code stack. Based on Node.js platform, using Promise method, we realize a Web crawler application. We describe in detail how to use the Promise approach to deal with the problem of asynchronous callback during the application process.
Node.js; Promise; Web application
2016-07-26;收到修改稿时间:2016-08-25
10.15888/j.cnki.csa.005700