基于事件的Web应用
传统的Web表单提交就是典型的基于事件的模式。换句话说,在Web表单里输入了很多数据(用户输入文本框,点选复选框,从列表中选中某些项等等),之后这些数据提交给服务器。这个场景中实际是一个单一的程序事件:使用POST方式将表单数据提交。这也是基于Ajax的Web应用的工作原理。 一次性发送大量数据
对于Ajax来说,是可以和基于事件编程扯上一点关系。客户端和服务器端之间有些交互可以认为是基于事件的。典型的场景是输入一个省市代码,发送请求到服务器获得城市和省的名称。这里通过XmlHttpRequest的Ajax并不需要将很多数据一次性扔给服务器。但这并不能改变大部分web应用都是基于页面刷新这种模式的现状。Ajax已经更广泛的用于很多有意思的视觉相关的交互,快速的作表单验证,无刷新提交数据,这样就可以避免重新载入页面。因此,尽管并未通过提交表单来发起一个真正的POST请求,通过Ajax可以模拟POST表单提交。 坦率的讲,这种传统的Ajax交互方式也阻碍了Ajax程序员的创新。每次发送一个请求时(不管请求的数据多么小),都会在网络里走一个来回。服务器必须针对这个请求作出响应,通常是开辟一个新的进程。因此,如果你真正置身于一个事件模型的环境中做开发,你可能需要通过发起10到15个单独的小请求来保持你的页面和服务器之间的联系,服务器也会为之创建10到15个线程(可能更少,这取决于服务器处理新请求时分配线程池的策略),当这个数量乘以1000或者10000或者100000时(译注:每个页面需要10个请求,那么越多用户访问这个页面,所发起的请求个数就会越来越多),就会出现内存溢出、逻辑交错带来的冲突、网络瘫痪、系统崩溃这些问题。
结果是,在大多数场景中,Web应用需要保持对事件的最小依赖。有一个折衷方案,就是服务器端程序的响应返回的不是一个微小的数据片段,而是带有更多冗余数据结构的数据包,通常是JSON数据,这时就又遇到了eval()的问题。问题当然出在eval()身上,但这也和Web本身和服务器线程控制、包括页面和服务器之间的HTTP请求和响应策略(至少在这个场景下)有密不可分的关系。
或许有些人对上文提到的问题不以为然,因为你知道有很多方法来规避直接eval()带来的问题,你会使用诸如JSON.parse()来代替eval()。同样有很多令人信服的论据鼓励我们小心的使用eval()。这些东东都是值得进一步讨论的。但不管怎样,看一看eval()带来了太多类似栈溢出(Stack Overflow)这类的问题吧,你会发现大部分程序员并未正确或者安全的使用eval()。这着实是一个问题。因为太多菜鸟程序员似乎根本没有意识到eval()的问题有多严重。
不断的发送少量的数据
Node带来了架构应用的新思路,我们可以基于Node采用事件模型来架构Web应用,或者说“小型的”事件模型。换句话说,你应当基于大量的事件发送大量的请求,每个请求的数据包都很小,或者根据需要从后台抓取少量数据,而不是发送很少的请求,每次请求都带有大量的数据。在很多场景中,大多数情况下你需要唤醒GUI程序(Java Swing程序员的GUI知识储备可以派上用场了)。因此,当用户输入姓氏和名字后,移步到下一个输入框,这时就已经发起了一个请求来验证输入的用户名是否已经存在。省市代码、地址和电话号码的验证也是同理。页面上每发生一个事件,都会产生一个请求和响应。
这有什么不同吗?为什么Node可以做到,并规避了已有的线程问题?其实Node并没有这么神秘,Node官网充分解释了其哲学:
Node的目标是提供一种构建可伸缩的网络应用的方案,在hello world例子中,服务器可以同时处理很多客户端连接。Node和操作系统有一种约定,如果创建了新的链接,操作系统就将通知Node,然后进入休眠。如果有人创建了新的链接,那么它(Node)执行一个回调,每一个链接只占用了非常小的(内存)堆栈开销。 Node是无阻塞的,不会出现同源竞争线程的情况(Node非常乐于处理即时的请求,发生了什么事情,那就让他发生吧),新请求到达服务器时,不需要为这个请求单独作什么事情。Node仅仅是悠闲的坐在那里等待(请
求的发生),有请求就处理请求。用非常简单的代码就可以实现,而不用花费程序员宝贵的精力去实现一整套服务器端逻辑。
没错,混乱不可避免
值得一提的是,非阻塞系统带来的问题也会出现在这种编程模式中:一个进程(非线程)等待一个数据存储操作,这时产生了另外一个抓取与之无关的数据的操作,这个意外的操作会对现有的等待造成影响(译注:作者的意思是说多个操作同时发生或者没有按照预定顺序发生时,会产生混乱,也就是说,操作本身并不是原子性的)。但要注意,大多数基于事件的web编程模式都是“只读的”!你大概也没有遇到过通过“微请求”来修改数据的情况,或者说非常罕见。相反,通过这种请求来验证数据合法性、查询数据的情形则非常常见。这种情况下,最好直接根据请求作响应。数据库本身会作加锁操作,一般来讲,一个优秀的数据库完全可以高效的做到数据操作的加锁解锁,而不用服务器端的程序代码去多做什么。而Node又比操作系统处理线程的保持和释放更加高效,使得服务器不必单独为“web响应”开辟一个进程。
此外,Node也计划实现“进程分支”(process forking),HTML5 Web Workers API为更复杂的进程控制提供了引擎(规范)支持。同样,如果你采用基于事件的模型来架构web应用,你的程序可能至少有100多个场景需要线程的支持。最终你会发现,你的编程思路和思考问题的方式发生了改变,你的注意力将放在服务器端处理请求的逻辑上,而不必在乎Node如何工作。 Node的用武之地
这里我们讨论另外一种web开发模式,不管是不是采用了Node、或者是不是采用了基于事件的编程模式,这都无关紧要,因为这种模式实在太重要了。简言之:对症下药!概括讲就是,针对不同的问题采取不同的解决方案,而不管这种解决方案是否解决其他问题。 思维定势
不止在web设计领域,在所有编程之中都存在某种思维定势。可以这么描述这种思维定势:你学到的、掌握的越多,你能解决的问题就越多,你所掌握的技能的应用场景也就越多。这看起来理所当然,除非你在技术上钻研的更深。没错,学习新的语言和新的工具并广泛使用它总不是坏事。但往往会进入一个误区,就是,因为你了解它,所以你使用它,而不是因为你所掌握的技能和工具是“最适合”你的业务的。
我们来看一下Ajax,关于Ajax已经有太多太多的讨论了。我们知道,Ajax为无刷新的快速查询请求提供了可靠的解决方案。而如今因为Ajax的滥用以至于过分替代了传统的表单提交。我们遇到一个新技术、学习它、掌握它、应用它,然后“滥用它”。毕竟很多业务场景仅仅需要传统的表单提交,而不需要Ajax。说起来简单,实际上还有成千上万的滥用Ajax的案例场景,仅仅因为某个应用的开发工程师对Ajax的盲目尊崇。
同样的,Node也面临这样一个问题。当你初识Node发现它的种种好处,就想到处使用它。就会一股脑的将PHP或Perl程序换成Node。结果呢?糟透了。其实你已经害上了强迫症,总是想将Node用于有违其设计初衷的场景中:使用JavaScript提交大量数据给Node,或者通过Node返回给JavaScript大量的JSON数据,交给前端去作eval(),或者干脆使用Node作一个文件服务器用以返回HTML页面或做HTTP重定向。
但这些场景均不是Node所擅长的。Node更擅长处理体积小的请求以及基于事件的I/O,使用Node解决客户端和服务器之间的快速沟通,使用表单提交将大量的数据发送给服务器,使用PHP和Perl来处理重型数据库操作以及动态HTML页面的生成。使用Node运行于服务器端来处理体积不大的请求。不管是采用Rails还是Spring以及各式各样的服务端容器,只要按需索取即可。一定要明白你需要解决的问题是什么,基于此采取最佳解决方案,而不是基于你当下所掌握的技能来解决遇到的问题。 Node的简单的初衷
还有最后一点需要注意,当你越来越深入你的编程时,你会发现你不必每个工具、API和所使用的框架都达到精通。将刀用在刀刃上,不要将锤子当成钻头来使用。了解每个工具所适用的场景和能解决的问题,然后找到这个工具的最适合的应用场景。如果你想变成超人式的通才(程序员往往什么都想知道),你离“专家”也就越来越远,所谓专家,就是指在一两个方面达到非常精通。当然,每个老板都希望能找到超人式的通才,但这种人往往可遇不可求。
学习Node可能会有些吃力,但是非常值得的。为什么?因为你正在寻求基于JavaScript的web应用的解决方案。这意味着你已有的JavaScript编程技能不会丢掉,当你需要使用PHP或者Perl时,你必须重新学习一门新的语言,而Node不必如此大动干戈。学习新语言带来的问题比学习他们带来的好处要大的多。
学习Node所面临的挑战是,你需要更加活跃思维,将程序拆成低耦合的小片段,然后像组装数组一样的组装他们。但Node和基于事件的I/O并不能解决所有问题,但确定的是,很多关键问题,只能依靠Node来解决。