2.1 大话Node.js

本节我们来认识一下Node.js,了解它能帮助我们完成哪些工作。

2.1.1 Node.js是什么

Node.js是一个基于Chrome V8引擎的JavaScript运行环境。Node.js使用了一个事件驱动的、非阻塞式I/O的模型,轻量又高效,它的底层是用C/C++编写的。这是Node.js的官方描述,对前端开发人员来说,想要搞清楚其中所包含的“引擎”“运行环境”“事件驱动”以及“非阻塞I/O模型”到底是什么意思,并不是一件容易的事情。那么Node.js到底是什么?我们先用一个类比的示例来进行解释。比如,有人向你发送了一个扩展名为docx的文档,你想要查看其中的内容,于是打开记事本,把该文档拖到记事本的窗口里,然后就看到了一大堆乱码。这是因为记事本程序并不能识别这种格式的文档,你需要先安装Microsoft Office 2007以上版本的软件,然后用Word程序打开,这样才能看到正确解码的内容。如果把示例中的docx文件看作程序,那么Word就是它的运行环境,这就像JavaScript程序与浏览器的关系一样。如果你了解过现代浏览器的结构,就会知道其中包含了JavaScript引擎。以前,想要查看docx文件的内容,几乎只能依赖于Microsoft Office,后来金山公司也推出了办公软件工具WPS Office,它也能够解释和运行docx文件,于是docx文件就有了多个可运行环境,而Node.js对于JavaScript语言的意义也是如此。

拓展知识

为了更加直观地理解运行时的概念,你可以尝试一个有趣的实验,自己创造一种简单的编程语言,规定一些简易的语法,然后使用JavaScript来编写能够解释这些语法的代码。例如,用自创的语言编写一些简单的程序,最后通过Node.js运行JavaScript程序,并在程序中用Node.js提供的文件读写接口(File API)读入你用自创的编程语言编写的程序,看看它能否被正确地解释和执行。待你了解了JavaScript是如何完成对自创编程语言的解释和执行的,自然就能明白在Node.js运行环境中,C/C++对JavaScript脚本做了什么事情。当然,真实的代码解释执行过程要复杂得多,很多关键的思想和技术也被应用在前端框架的设计中,这些可以在今后的学习中慢慢消化。

相较于技术上的亮点,Node.js设计者的开发思想或许更值得学习,这一点正是大多数初级开发者所缺少的。Node.js的开发初衷是更方便地实现一个高性能的Web服务器,但当它最终问世时,并没有宣称自己是“实现高性能Web服务器的技术”,而是为开发人员提供了一个工具,这个工具的能力之一是实现高性能的Web服务器。这种思想差距在初级和高级开发人员之间表现得尤为明显:初级开发人员往往会针对具体的业务需求采用面向过程的风格进行开发,这使得他们编写的程序几乎无法灵活应对任何需求变更;而有经验的开发人员面对需求时,通常会先设计一个类,或者抽象一个与业务逻辑无关的工具方法,然后在自己的程序中调用这个方法。第5章讲解Lodash.js的相关内容时,相信读者会对这种做法的优势有更深刻的体会。要想成为优秀的程序设计师,就要不断地培养自己设计程序的能力,而不是仅仅完成语言层面的翻译工作。

2.1.2 Node.js能做什么

在Node.js的诸多功能中,与前端开发人员关系最紧密的就是创建Web服务器和本地文件的读写能力。

1. 创建高性能Web服务器

许多Node.js的初学者应该都见过那段只用了不到10行代码就建立了一个Web服务器的经典示例。尽管对于前端开发人员而言,他们依然需要学习基本的Web服务器知识,才能更加得心应手地进行服务端开发,但与配置Apache或Nginx来实现同样的功能相比,这样的学习成本已经非常低了,毕竟前端开发人员可以使用自己最熟悉的JavaScript语言来构建应用。另一方面,在Node.js中,代码可以与各类数据库进行交互,这就意味着前端工程师可以直接使用JavaScript语言编写与数据库进行交互的代码(尽管在大型应用中并不推荐这样做),且编写业务逻辑代码时,Node.js与其他后端语言没有明显的差别,因此前端开发人员不用切换开发语言就可以掌握全栈开发的技能。

由于Node.js底层使用的是异步非阻塞的I/O机制,因此它更适合于I/O密集、少量业务逻辑和计算消耗的场景。尽管解释型脚本语言本身并不适合执行计算型任务,但Node.js底层是由C/C++代码编写的,并且提供了JavaScript代码层与C/C++代码交互的接口,面对计算密集型任务时,Node.js只需要作为启动脚本调用底层C/C++程序来完成计算密集型任务就可以了。

拓展知识

服务端执行的任务大体可分为读写密集型任务和计算密集型任务。对于读写密集型任务而言,CPU更多的时间是在等待磁盘读写,使用率并不高,在Web服务器上进行的网络通信、信息传输和磁盘读写等都属于读写操作,它对磁盘的响应速度和传输效率有着更高的需求。相较而言,计算密集型任务对CPU的运算能力要求更高,但对磁盘读写造成的性能负担很小,计算过程中通常也不需要与I/O接口进行交互,可直接、高效地在内存中执行,这类任务的计算过程通常比较复杂,例如需要实现某些加密算法或者矩阵计算等。

大型架构的后端技术选型需要考虑的因素更为复杂,Node.js设计之初并没有准备承担这项任务,就连Node.js之父Ryan Dhal自己也说,在面对大型服务端应用开发时,Node.js的开发体验不如Go语言。但是,全世界目前有600多种编程语言,没有任何一种语言能够解决所有问题,语言只是承载和传递程序设计思想的媒介,如何为目标场景选择一项合适的技术,或许是开发人员更应该关注的问题。当你在前端领域有一定的积累时,很多前辈都会推荐你继续学习Java或C++等更为完备也更为复杂的语言,这样做的目的并不仅仅是扩展能力边界,更多的是希望你能够跳出一种编程语言的束缚,学习和体会编程语言背后的思想。

2. 本地文件的读写功能

文件读写功能的底层所要解决的问题其实有很多。如果文件里的内容比较多,读入内存的过程比较耗时,应该怎么处理呢?是等待读入操作完成还是先去执行其他任务?如果客户端请求的资源是一部高清电影,文件比程序可用的总内存还大,那么该文件是否就一定无法读取了呢?Node.js的fs模块几乎为每个文件操作接口都提供了同步和异步两种方法,同时也支持以流的方式对读写过程实现更细粒度的控制,甚至还可以监测指定文件或文件夹的变动。文件可读写意味着开发人员可以通过程序分析另一个程序中文件的内容,并对其进行检查和纠错,甚至可将其编译成另一种语言,这便是前端工程化的能力基石。

2.1.3 招黑的JavaScript全栈工程师

Node.js凭借创建高性能Web服务器以及与数据库通信的能力,为前端开发人员提供了服务端开发的机会。早在几年之前,开发人员就可以使用MEAN(MongoDB + Express + Angular.js + Node.js)这种纯JavaScript技术栈完成闭环的业务逻辑开发,很多前端工程师也因此自诩为全栈工程师。不可否认当年这样的技术栈确实可以使许多中小型团队以更少的人力和时间就把产品从创意阶段推进到线上,但这也使得JavaScript开发人员成为业内最招黑的全栈工程师,因为业务逻辑的实现并不足以撑起全栈工程师进行后端开发。

在企业级开发中,后端开发仍然以Java工程师为主力军。由于Java本身具有强类型和完整的面向对象的特性,因此后端工程师的编码质量和程序设计意识整体要高于前端工程师,再加上与Java开发体验非常相似的Angular技术栈的支持(Angular本身就是一项由Google的Java工程师开发和维护的技术),后端工程师很容易就能编写出规范性和可维护性都不输于前端开发人员的代码,这大大提升了Java全栈工程师的竞争力。如果不是工程化配置和CSS实战经验形成的门槛,前端工程师在面对后端全栈工程师时很难体现其自身价值。然而,前端开发出身的工程师在使用Node.js技术栈进行服务端开发时却没有那么顺利,最流行的Express和Koa框架,仅仅提供了框架和基本中间件,要想实现更多的功能,还需要引入或者自行开发大量中间件。这时开发人员之间的差别就会表现得非常明显,即使完全不懂Node.js,后端开发人员也很清楚自己应该寻找具备日志记录、错误追踪、会话管理、安全校验、性能监控、对象关系映射(ORM)、数据库连接等功能的模块或中间件,而普通的前端开发人员却除了业务逻辑的增删改查外,往往连使用“try...catch...”语句捕获运行时错误的意识都没有。前端工程师很容易只考虑业务逻辑一切正常的情况,只要主流程能够正常运行,就觉得万事大吉了,然而在真实的开发过程中,往往是那些没有覆盖到的边界情况需要花费更多的精力。

把后端开发等同于编写业务逻辑代码,就好像把前端开发等同于编写静态页面代码一样。如果真的想成为全栈工程师,需要用一颗谦虚求知的心,踏踏实实地去学习那些陌生的知识,学习的过程可能充满艰辛,但你一定会受益于所学的结果。