开发者的新装: Node.js
开发者的新装: Node.js
有很多人在吐槽Node.js(比如“Node.js is cancer”,一篇声名狼藉的文章),但支持者们往往误解了其中的信息,并用些不沾边的观点进行反驳,因此情况更糟糕了,因为使用Node.js的人分为两类:一类是需要能同时处理多个连接的高并发服务器;另外一类是严重依赖JavaScript,他们在浏览器,服务器,数据库甚至洗衣机上都用JS。
这里,我想对那些关于Node.js稀奇古怪、有误导性的争论一一反驳。
<br /> ###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和其他各种各样的库算不上高性能,不过不用介意的。)
<br /> ###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);
});
});
});
}
<br /> 不理解的话,换Q promise试试:
function dostuff() {
return task1()
.then(task2)
.then(task3)
.then(function(z) {
if (z < 0) {
return 0;
} else {
return z;
});
}
<br /> 好看多了,但还是很笨。一个副作用:如果你忘记在链式调用的最后加上".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();
}
<br /> 上面的代码是有问题的的,你看出问题了吗?而且,我们还忘了异常处理,修改一下:
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();
}
<br /> 错误处理和业务交织在一起,这还有趣吗?
在Go,你的代码会写成这样:
func dostuff() int {
z := task3(task2(task1())))
if z < 0 {
return 0
}
return z
}
<br /> 或者加上错误处理:
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;
}
<br /> Go版和Node.js版的实现的功能基本等价,除了Go还处理了等待和控制权转让。在Node.js里,我们必须手工管理延续因为我们要和内建的控制流对着干。
噢,在这实现这些东西之前,你还要学会不要错误地使用process.nextTick,不然你的API的用户会很不爽。在这个讲究“精益”和MVP的时代,谁有时间去学这些建立在让人难以理解的运行时上的抽象渗漏问题。
又顺带一提,Q是很慢的(至少网上是这么说的)。看看这个测试,它对比了21种处理异步调用的方式的性能。
难怪人们喜欢Node.js,它给了你轻量级线程的性能以及x86汇编的清晰和可用性。
当人们指出Node.js手工处理控制流很麻烦,反对者就会说:“用函数库去处理,例如async.js”。于是你开始用库函数去并行执行一堆任务或者组合两个函数,这其实就你在任何多线程语言里所做的事,只是更糟糕而已。
<br /> ###LinkedIn迁移到Node.js,服务器从30台减到3台!
引用Hacker News上的一句:“我把垃圾车换成了摩托,现在快多了!”。
PayPal和沃尔玛换到Node.js之后也得到很好的收益。当然,他们是在对比两个完全不同的东西来让Node.js看起来更好。在他们好到难以置信的故事里,他们从一个庞大的企业级代码库换到一个重头开始写的Node.js应用。这有理由不变快吗?他们换到几乎任何其他东西都会得到性能提升。
在Linkedin的案例里,他们之前的代理都跑在并发度为1的Mongrel上。就像从用1个手指敲QWERTY键盘切换到10个手指敲Dvorak键盘,然后认为这全归功于Dvorak键盘布局更好。
这是一个经典的夸大的广告:真实的故事被误解,扭曲地去让不知情的人产生误解。
<br /> ###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单页面应用包装起来。根据不同的部署平台,这时在服务器和客户端共享的代码可能会成为不利因素。
<br /> ###NPM很好用!
我觉得NPM已经到了一个“不糟糕”的状态,这已经领先于很多包管理工具。就像是大多数的生态系统,NPM上有多个实现同样功能的包。例如你需要一个库来向Android推送通知。在NPM,你能找到:gcm、node-gcm、node-gcm-service、dpush、gcm4node、libgcm和ngcm,更不用提那些支持多个推送服务的库。哪个可靠?哪个已经停止开发?最后,你选了下载量最大的那个(但为什么结果不能按流行度排序呐?)。
NPM过去经常宕机,看着很多公司突然间不能部署代码也是一件好玩的事。现在NPM上线时间已经好了很多,但是谁知道它会不会打断你的部署进程。
过去,我们成功部署代码而且不用在部署阶段引入对一个新生的、由志愿者运行的、从头实现的仓库的依赖。我们甚至在本地保存一份函数库的代码。
我倒不担心NPM,某种程度上它是生态系统的一部分而不是语言的一部分,而且通常情况都能满足要求。
<br /> ###用Node.js时我效率更高!敏捷!快速!MVP!
似乎Node.js程序员心里都有一个奇怪的二分法:你在用mod_php或者可怕的JavaEE,所以是又大又慢的;你在用Node.js,所以是精益和快速的。这可能就是为什么你很少看到有人吹他怎样从Python换到Node.js*。自然,如果你来自一个到处都是AbstractFactoryFactorySingletonBean的过度工程的系统,Node.js的缺乏结构反而是清新的。但只是因为这样就说Node.js更高效是错误的——因为他们无视了全部的坑。
一个Node.js新人可能会这么做:
- 这个函数可能失败,我要抛一个异常,所以我会写
throw new Error("it broke")
; - 那个异常没有被我的
try-catch
捕获! - 用
process.on("uncaughtException")
好像可以。 - 但是得到不是想象中堆栈轨迹,StackOverflow说这可能违背了最佳实践
- 也许我要试一下domain?
- 哦,回调通常以错误作为第一个参数,我要回去改改我的函数调用
- 有人告诉我应该用promise
- 把例子看了十来二十遍,我觉得应该可以了
- 不过它还是吃了我的异常。不,我还要在最后加上.done()
<br /> 一个Python程序会这么做:
raise Exception("it broke")
;
这是一个Go程序员:
- 我要把
err
加到返回类型声明,还要在return
语句加一个返回值
Noed.js中有很多东西会阻碍你实现MVP。MVP不是担心能不能快40ms返回一个HTTP响应或者你的DigitalOcean机子能同时处理多少连接。你没有时间去成为一个并发编程的专家(而且明显你现在也不是,如果是的话你就不会用Node了)。
- 这里有一篇关于从Python换到Node.js的文章。最有价值的一句:“这种推迟的编程模式更难理解和调试。如果开发者不能完全理解Twised,他就会犯很多无知的错误,最后会死的很惨”。所以他们换到另一个困难的系统,如果你不能理解它然后又犯了些无知的错误,它一样会微妙地挂掉。
<br /> ###我热爱Node.js!Node.js就是生活!
如果你组织Node.js活动时需要演讲者,我随时都进行付费的演讲演出。详细信息请Email我。
这些观点不代表我的公司和同事,也没有经过他们的复审。也可以把全部文字都加上<sarcasm>标签。
<br />
开发者的新装: Node.js
Node.js 自发布以来,便备受争议。支持者和反对者之间的论战从未停息。然而,无论支持与否,Node.js 都有着其独特的优势和局限性。本文将对 Node.js 的一些常见说法进行分析,并提供一些示例代码来帮助读者更好地理解。
Node.js 速度快!
1. 运行在 V8 上的 JavaScript 很快!
虽然 Node.js 通常被认为是高性能的,但实际速度与 Java 相比仅相差 1 到 5 倍。尽管如此,V8 引擎的正则表达式引擎非常优秀,因此 Node.js 在需要大量正则表达式和 CPU 密集型工作时表现出色。
// 示例:使用正则表达式匹配字符串
const str = 'Hello, world!';
const regex = /world/;
console.log(regex.test(str)); // 输出 true
2. Node.js 是非阻塞的!并发性很好!事件驱动!
Node.js 虽然在处理大量连接方面表现优异,但它并不是唯一能做到这一点的运行时系统。其他如 Vert.x、Erlang 和 Go 也有类似的功能。Node.js 采用事件驱动模型,但这并不意味着它比其他语言更容易编写并发代码。
// 示例:使用回调处理异步操作
function doStuff(callback) {
setTimeout(() => {
const result = 'Done';
callback(result);
}, 1000);
}
doStuff((result) => {
console.log(result); // 输出 "Done"
});
Node.js 让并发变的简单?
Node.js 缺乏内置的并发支持,因此需要使用回调或 Promise 来管理异步操作。尽管如此,这使得代码变得复杂且难以阅读。
// 使用回调处理异步操作
function doStuff(callback) {
task1(function(x) {
task2(x, function(y) {
task3(y, function(z) {
if (z < 0) {
callback(0);
} else {
callback(z);
}
});
});
});
}
// 使用 Promise 处理异步操作
function doStuff() {
return task1()
.then(task2)
.then(task3)
.then(function(z) {
if (z < 0) {
return 0;
} else {
return z;
}
});
}
Node.js 可以让你用到你的 JavaScript 知识!
1. 前端开发者也能进行后端开发!
虽然 Node.js 允许前端开发者在后端使用相同的语言,但这也带来了额外的学习成本。前端开发者需要学习如何处理异步操作和错误处理。
// 示例:使用 async/await 处理异步操作
async function doStuff() {
try {
const x = await task1();
const y = await task2(x);
const z = await task3(y);
if (z < 0) {
return 0;
} else {
return z;
}
} catch (err) {
console.error('Error:', err);
}
}
2. 我们可以在前端和后端共享代码!
虽然可以在前后端共享代码,但这也会带来一些限制。例如,某些高级语言特性在浏览器中不可用,因此需要使用编译步骤来转换代码。
// 示例:使用 TypeScript 编写跨平台代码
class Task {
async run() {
// 代码逻辑
}
}
const task = new Task();
task.run().then(result => {
console.log(result);
}).catch(err => {
console.error('Error:', err);
});
NPM 很好用!
虽然 NPM 是一个强大的包管理工具,但也存在一些问题,例如稳定性不足和包的可靠性问题。
# 安装依赖包
npm install lodash
用 Node.js 时我效率更高!敏捷!快速!MVP!
虽然 Node.js 适合快速开发 MVP,但这也意味着开发者需要处理大量的异步操作和错误处理。
// 示例:使用 async/await 快速开发 MVP
async function createMVP() {
try {
const data = await fetchData();
render(data);
} catch (err) {
console.error('Error:', err);
}
}
总结
Node.js 无疑是一种强大的工具,尤其适用于需要处理大量连接的高并发服务器。然而,它也存在一些挑战,特别是在并发编程和错误处理方面。希望本文能帮助读者更好地理解 Node.js 的优势和局限性。
等他们去扯蛋吧,我们做我们的……
开发者的新装: Node.js
这篇文章探讨了Node.js的一些优点和缺点,并提出了一些对Node.js常见的误解。以下是对文章主要内容的总结和补充:
Node.js速度快!
JavaScript执行速度
虽然V8引擎使得JavaScript执行速度很快,但与一些高性能语言相比,如Java、Go等,Node.js的速度仍然有一定差距。
// 示例:使用V8引擎处理大量数据
function processData(data) {
return data.map(item => item * 2); // 简单的映射操作
}
const data = Array(1000000).fill().map((_, i) => i);
console.time('processing');
processData(data);
console.timeEnd('processing');
单线程并发
Node.js利用事件循环和非阻塞I/O模型来高效处理大量连接,但这并不是唯一的解决方案,如Erlang、Vert.x等也有类似特性。
// 示例:使用Node.js处理大量连接
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World\n');
});
server.listen(3000, () => {
console.log('Server running at http://localhost:3000/');
});
Node.js并发性
Node.js的非阻塞I/O模型导致了大量的回调函数,这使得代码可读性和维护性降低。
// 示例:使用回调函数处理异步操作
function dostuff(callback) {
task1(function(x) {
task2(x, function(y) {
task3(y, function(z) {
if (z < 0) {
callback(0);
} else {
callback(z);
}
});
});
});
}
使用Promise可以改善这种情况:
// 示例:使用Promise简化异步操作
function dostuff() {
return task1()
.then(task2)
.then(task3)
.then(function(z) {
if (z < 0) {
return 0;
} else {
return z;
}
});
}
NPM包管理
NPM是一个强大的包管理工具,但它也存在一些问题,如重复的包、可靠性问题等。
// 示例:安装和使用NPM包
// 在package.json中添加依赖
{
"dependencies": {
"lodash": "^4.17.21"
}
}
// 使用NPM安装依赖
npm install
// 使用lodash
const _ = require('lodash');
console.log(_.join(['Hello', 'World'], ' '));
结论
Node.js确实有一些优势,特别是在高并发场景下。然而,它也有一些明显的缺点,如复杂的回调函数、非阻塞I/O带来的复杂性等。开发者在选择技术栈时应权衡利弊,选择最适合项目需求的技术。