开发者的新装: Node.js
发布于 7个月前 作者 yingjie0904 636 次浏览

有很多人在吐槽Node.js(比如“Node.js is cancer”,一篇声名狼藉的文章),但支持者们往往误解了其中的信息,并用些不沾边的观点进行反驳,因此情况更糟糕了,因为使用Node.js的人分为两类:一类是需要能同时处理多个连接的高并发服务器;另外一类是严重依赖JavaScript,他们在浏览器,服务器,数据库甚至洗衣机上都用JS。

这里,我想对那些关于Node.js稀奇古怪、有误导性的争论一一反驳。


###Node.js速度快!

这样说其实很不准确,我们把它分成两个独立的说法:

####1. 运行在V8上的JavaScript很快!

V8的开发者值得你去称赞,因为V8让JavaScript快的让人难以置信。多快?从测试比赛上看只比Java慢1到5倍(没错是“慢”)。

如果你仔细看他们的测试,你会发现V8自带了一个很好的正则表达式引擎。结论?Node.js最适合用来完成需要大量正则表达式、CPU繁重的工作。

如果我们把那个测试比赛当作信条,那什么语言/实现通常会比JavaScript/V8快呢?一看,就是一些开发效率很低的语言:Java、Go、Erlang(HiPE)、Clojure、F#、Haskell(GHC)、OCaml、Lisp(SBCL),都是不能用来写服务器的。

更好的是Node.js不需要使用多核,因为解释器是单线程的(评论肯定会说你可以同时跑多个Node.js进程,而其他语言都不可以这么做)。

####2. Node.js是非阻塞的!并发性很好!事件驱动!

有些时候,我很怀疑人们是否真的知道他们自己在说些什么。

Node.js在这点甚是奇葩,因为你完全没得到轻量级线程所带来的便捷,而且还要自己完成轻量级线程已经帮你做好的事。因为JavaScript对任何种类的并发都没有直接支持,结果就是一堆使用回调的库函数。编程语言研究者会发现这是蹩脚版的延续传递风格(continuation-passing style (Sussman and Steele 1975)),CPS本来是用来应对递归时栈的增长,不过在Node.js里是应对语言不直接支持并发的问题。

是的,Node.js能在一个线程里高效地处理大量连接,但是它不是第一个也不是唯一能这样做的运行时系统,看看Vert.x、Erlang、Stackless Python、GHC、Go……

更重要的是,大部分人都用Node.js来实现最小化的可行产品(MVP),因为他们觉得这样可以为未来的大量用户提供一个更快的网站。(当然加载500K的Backbone.js和其他各种各样的库算不上高性能,不过不用介意的。)


###Node.js让并发变的简单!

JavaScript没有内建任何的和并发相关的语言特性,也不支持元编程,Node.js也不能化腐朽为神奇。你只好手工管理全部的延续,或者借助(很多不同的)库来把JavaScript的语法应用到极致。(顺带一提,我觉得shoud.js既可怕又方便。)这就像是现代版本的因为语言里面没有for循环,所以只好用GOTO语句。

来看一下对比吧。

在node.js里,你可能要写这样的函数:

function dostuff(callback) {
  task1(function(x) {
    task2(x, function(y) {
      task3(y, function(z) {
        if (z < 0) {
          callback(0);
        } else {
          callback(z);
        });
    });
  });
}


不理解的话,换Q promise试试:

function dostuff() {
  return task1()
    .then(task2)
    .then(task3)
    .then(function(z) {
      if (z < 0) {
        return 0;
      } else {
        return z;
      });
}


好看多了,但还是很笨。一个副作用:如果你忘记在链式调用的最后加上".done()",Q会把你的异常都吞了,而且还有其他不太明显的问题。当然了,大部分Node.js的库都不用Q,所以你是要老老实实地用回调。如果take2不返回Q promise会怎样?

function dostuff() {
  return task1()
    .then(function(x) {
      var deferred = Q.defer();
      task2(x, deferred.resolve);
      return deferred;
    })
    .then(task3)
    .then(function(z) {
      if (z < 0) {
        return 0;
      } else {
        return z;
      }
    })
    .done();
}


上面的代码是有问题的的,你看出问题了吗?而且,我们还忘了异常处理,修改一下:

function dostuff() {
  return task1()
    .then(function(x) {
      var deferred = Q.defer();
      task2(x, function(err, res) {
        if (err) {
          deferred.reject(err);
        } else {
          deferred.resolve(res);
        }
      });
      return deferred.promise;
    },
    function(e) {
      console.log("Task 1 failed.");
    })
    .then(task3, function(e) {
      console.log("Task 2 failed.");
    })
    .then(function(z) {
      if (z < 0) {
        return 0;
      } else {
        return z;
      }
    },
    function(e) {
      console.log("Task 3 failed.");
    })
    .done();
}


错误处理和业务交织在一起,这还有趣吗?

在Go,你的代码会写成这样:

func dostuff() int {
  z := task3(task2(task1())))
  if z < 0 {
    return 0
  }
  return z
}


或者加上错误处理:

func dostuff() int, err {
  x, err := task1();
  if err != nil {
    log.Print("Task 1 failed.")
    return 0, err
  }
  y, err := task2(x);
  if err != nil {
    log.Print("Task 2 failed.")
    return 0, err
  }
  z, err := task3(y);
  if err != nil {
    log.Print("Task 3 failed.")
    return 0, err
  }
  if z < 0 {
    return 0;
  }
  return z;
}


Go版和Node.js版的实现的功能基本等价,除了Go还处理了等待和控制权转让。在Node.js里,我们必须手工管理延续因为我们要和内建的控制流对着干。

噢,在这实现这些东西之前,你还要学会不要错误地使用process.nextTick,不然你的API的用户会很不爽。在这个讲究“精益”和MVP的时代,谁有时间去学这些建立在让人难以理解的运行时上的抽象渗漏问题。

又顺带一提,Q是很慢的(至少网上是这么说的)。看看这个测试,它对比了21种处理异步调用的方式的性能

难怪人们喜欢Node.js,它给了你轻量级线程的性能以及x86汇编的清晰和可用性。

当人们指出Node.js手工处理控制流很麻烦,反对者就会说:“用函数库去处理,例如async.js”。于是你开始用库函数去并行执行一堆任务或者组合两个函数,这其实就你在任何多线程语言里所做的事,只是更糟糕而已。


###LinkedIn迁移到Node.js,服务器从30台减到3台!

引用Hacker News上的一句:“我把垃圾车换成了摩托,现在快多了!”。

PayPal和沃尔玛换到Node.js之后也得到很好的收益。当然,他们是在对比两个完全不同的东西来让Node.js看起来更好。在他们好到难以置信的故事里,他们从一个庞大的企业级代码库换到一个重头开始写的Node.js应用。这有理由不变快吗?他们换到几乎任何其他东西都会得到性能提升。

在Linkedin的案例里,他们之前的代理都跑在并发度为1的Mongrel上。就像从用1个手指敲QWERTY键盘切换到10个手指敲Dvorak键盘,然后认为这全归功于Dvorak键盘布局更好。

这是一个经典的夸大的广告:真实的故事被误解,扭曲地去让不知情的人产生误解。


###Node.js可以让你用到你的JavaScript知识!

为了更准确,我们也要把它分成两点:

####1. 前端开发者也能进行后端开发! 以前JavaScript被用在哪里?主要是浏览器端的前端代码,让按钮加上动画,把JSON变成精美的用户界面等等。在后端用JavaSctipt,你可以你的UI开发者去hack后端关键的网络代码,因为两边都是JS,没什么东西要学的!(对吧?)

直到他们发现不能像平常那样使用return(因为并发),不能像平常那样throw/catch(因为并发),而且全部东西都是基于回调的,会返回Q promise,会返回原生的promise、genator、pipe或其他的奇怪东西,因为这是Node.js。(记得告诉他们要检查类型声明)

你要相信前端开发者学习后端开发的能力。如果不同语言就是一个障碍,那么要明白怎样正确结合各种回调/promise/generator也不是一件简单的事。

####2. 我们可以在前端和后端共享代码! 那你就要把服务器端能使用的语言特性限制到浏览器所支持的特性。例如你的代码不能用JS 1.7生成器,直到浏览器也支持,而且我们也知道等它普及还要好几年。

事实上,如果不远离浏览器的JS,Node根本就没办法从本质上得到提高。Node.js有很多坑需要用库去填,但是因为它和一个叫JavaScript的语言绑定在一起,它不能直接在语言层面上出处理这些问题。

这是很尴尬的情况,语言本来就没给你到来太多东西,而你又不能改变这个语言,所以你只好一直npm install band-aid

通过执行某些编译步骤来把新的语言特性转换到旧的语言,这种情况可以得到改善,你也就可以在服务器写新的代码,同时可以运行在正常的JavaScript上。你的选择可能是95%都是JavaScript的新语言(TypeSctipt、CoffectSctipt)或者完全不是JavaSctipt的语言(ClojureSctipt)。

更值得担心的是这意味这你实际上是混淆前端和后台的职责。事实上,你的后台成为了是一个囊括验证、处理等等功能的JSON API,而且这个API会有多个消费者(包括第三方)。例如,当你要造个iPhone和Android应用,你必须决定是用Obj-C、Java或者C#实现一个原生应用还是用Phonegap/Cordova把你的Backbone.js/Angular.js单页面应用包装起来。根据不同的部署平台,这时在服务器和客户端共享的代码可能会成为不利因素。


###NPM很好用!

我觉得NPM已经到了一个“不糟糕”的状态,这已经领先于很多包管理工具。就像是大多数的生态系统,NPM上有多个实现同样功能的包。例如你需要一个库来向Android推送通知。在NPM,你能找到:gcm、node-gcm、node-gcm-service、dpush、gcm4node、libgcm和ngcm,更不用提那些支持多个推送服务的库。哪个可靠?哪个已经停止开发?最后,你选了下载量最大的那个(但为什么结果不能按流行度排序呐?)。

NPM过去经常宕机,看着很多公司突然间不能部署代码也是一件好玩的事。现在NPM上线时间已经好了很多,但是谁知道它会不会打断你的部署进程

过去,我们成功部署代码而且不用在部署阶段引入对一个新生的、由志愿者运行的、从头实现的仓库的依赖。我们甚至在本地保存一份函数库的代码。

我倒不担心NPM,某种程度上它是生态系统的一部分而不是语言的一部分,而且通常情况都能满足要求。


###用Node.js时我效率更高!敏捷!快速!MVP!

似乎Node.js程序员心里都有一个奇怪的二分法:你在用mod_php或者可怕的JavaEE,所以是又大又慢的;你在用Node.js,所以是精益和快速的。这可能就是为什么你很少看到有人吹他怎样从Python换到Node.js*。自然,如果你来自一个到处都是AbstractFactoryFactorySingletonBean的过度工程的系统,Node.js的缺乏结构反而是清新的。但只是因为这样就说Node.js更高效是错误的——因为他们无视了全部的坑。

一个Node.js新人可能会这么做:

  1. 这个函数可能失败,我要抛一个异常,所以我会写 throw new Error("it broke");
  2. 那个异常没有被我的try-catch捕获!
  3. process.on("uncaughtException")好像可以。
  4. 但是得到不是想象中堆栈轨迹,StackOverflow说这可能违背了最佳实践
  5. 也许我要试一下domain?
  6. 哦,回调通常以错误作为第一个参数,我要回去改改我的函数调用
  7. 有人告诉我应该用promise
  8. 把例子看了十来二十遍,我觉得应该可以了
  9. 不过它还是吃了我的异常。不,我还要在最后加上.done()


一个Python程序会这么做:

  1. raise Exception("it broke");

这是一个Go程序员:

  1. 我要把err加到返回类型声明,还要在return语句加一个返回值

Noed.js中有很多东西会阻碍你实现MVP。MVP不是担心能不能快40ms返回一个HTTP响应或者你的DigitalOcean机子能同时处理多少连接。你没有时间去成为一个并发编程的专家(而且明显你现在也不是,如果是的话你就不会用Node了)。

  1. 这里有一篇关于从Python换到Node.js的文章。最有价值的一句:“这种推迟的编程模式更难理解和调试。如果开发者不能完全理解Twised,他就会犯很多无知的错误,最后会死的很惨”。所以他们换到另一个困难的系统,如果你不能理解它然后又犯了些无知的错误,它一样会微妙地挂掉。


###我热爱Node.js!Node.js就是生活!

如果你组织Node.js活动时需要演讲者,我随时都进行付费的演讲演出。详细信息请Email我。

这些观点不代表我的公司和同事,也没有经过他们的复审。也可以把全部文字都加上<sarcasm>标签。


From:http://notes.ericjiang.com/posts/751

1 回复

等他们去扯蛋吧,我们做我们的……

回到顶部