Skip to content

Latest commit

 

History

History
612 lines (403 loc) · 34.9 KB

readme.zh-cn.md

File metadata and controls

612 lines (403 loc) · 34.9 KB

Node的艺术

Node.js入门

本文档假定读者已经懂了以下的两样东西:

  • 懂得至少一种编程语言。例如:JavaScript,Ruby,Python,Perl或其他编程语言。如果你还不是程序员,你不懂编程语言,你可以阅读JavaScript for Cats。:cat2:
  • git和github。这是一个开源的协作工具,Node社区的用户使用git共享模块。你需要懂得基本操作就能了。这里有三篇很好的入门教程:1, 2, 3

This short book is a work in progress + I don't have a job right now (if I did I wouldn't have the time to write this). If you like it then please consider donating via gittip so that I can write more!

译者: 上面这段我没有翻译,因为我希望保持原文。上面作者提到,目前他还没找到工作。如果你喜欢这个文档,希望你可以通过gittip乐捐给作者。这样作者才能够写更多。

donate

目录

了解Node

Node.js是一个开源项目,目的是让你通过编写JavaScript的程序进行网络、文件系统或其他I/O源的沟通。就这些!它只是一个简单而稳定的I/O平台,你可以在这个平台上架构模块。

有没有I/O出的例子? 我这里有一张图,上面是我用Node.js制作的程序,你可以看到上面有很多I/O源:

server diagram

如果你无法明白上图显示的所有东西,这是没问题的。重点是你看到一个Node的运作(在中间六边形那个),它就像经纪人,管理全部I/O的端口(橙色和紫色的线条代表I/O)。

一般上我们编写的程序可以分为以下两类:

  • 很难编写,但是效率超高(就像用C从零开始编写一个Web服务器)
  • 很简单编写,但是不够效率/强大(就像有人上传5GB的文件去你服务器,但是服务器宕机了)

Node试图做到平衡在这两者之间:在大多数用列做到高效运行,而且容易明白和开发。

Node不是以下两样东西:

  • 不是Web框架 (不像Rails或Django,尽管它可以被用来做这样的事情)
  • 不是编程语言(Node是使用JavaScript编程,它没有自己的编程语言)

相反,Node是:

  • 设计上简单,而且容易明白和使用的平台
  • 适合那些需要快速和处理很多I/O链接的程序

在基层,Node可以作为一种工具,并编写出以下两类程序:

  • 需要使用到Web协议(如:HTTP、TCP、UDP、DNS和SSL)的网络程序
  • 需要对文件系统或者本地进程/内存进行读入和读出操作的程序

什么是“I/O程序”? 这里有一些常见的I/O源:

  • 资料库 (如:MySQL、PostgreSQL、MongoDB、Redis、CouchDB)
  • APIs(如:Twitter、Facebook、Apple Push Notifications)
  • HTTP/WebSocket的链接(从用户的Web应用程序)
  • 文件档(图像尺寸伸缩软件、视频编辑软件、网络收音机)

Node能够异步处理多个不同种类的I/O源。比如说,假设你来到快餐店,你向店员要了一个芝士汉堡,他们会马上为你下单和准备汉堡。然后,他们会要求你在旁边等汉堡完成。在你等待这段时间,他们可以接受其他订单和帮其他人准备汉堡。试想下,如果你站在柜台前面,一直等到你的芝士汉堡完成,那么你就阻碍了后面的人下订单,厨师也不能帮其他人准备汉堡!我们称这个为阻塞I/O,因为一次只能处理一个I/O操作(厨师一次只能准备一个汉堡)。Node,不是这样的,它是非阻塞性质,就是说它能一次准备很多汉堡。

多谢Node非阻塞的性质,让我们可以实现以下这么有趣事情:

核心模块

首先,你需要在电脑上安装Node。Node安装很简单,只需浏览nodejs.org和点击Install.

Node拥有一组核心模块(通常被称为Node核心)提供公共 API 让你编程时候调用。我们可以调用fs模块来操作文件系统。当我们要进行网络操作时候,我们会调用网络模块,例如:net(TCP),httpdgram(UDP)。

除了fs和网络模块之外,Node核心还有很多其他的核心模块。如dns模块用来异步解析DNS查询。os模块可以用来收集操作系统的资讯,如tempdir的路径。buffer模块可以处理二进制数据。还有些模块可以处理URL和路径,如:urlquerystringpath等等。大部分的核心模块都支持Node的主要使用目标:快速编写能够进行文件或网络操作的程序。

Node通过回调,事件,数据流和模块来控制I/O。如果你学会了这四样东西如何工作,那么你就能够灵活使用任何核心模块,而且你还会懂得模块的基本接口。

回调函数

如果想真的弄明白怎么使用Node,回调函数是你需要了解的东西中最重要的,没有之一。回调函数倒不是有了Node后才有的,只不过这功能是JavaScript中尤其好用的一个。

回调函数是指非同步执行的,或者是在将来某个时间才会被执行的函数。同步代码运行的顺序是从上至下,而非同步的程序却是在不同的时间运行不同的函数,这些事件都基于某些某同步函数的顺序和运行速度,包括HTTP请求和从文件系统里读取内容等等。

这种同步和非同步之间的差异可能会让人比较困惑,因为看一个函数是不是非同步,很大程度上取决于具体的情况。下面是一个很简单的同步函数的例子:

var myNumber = 1
function addOne() { myNumber++ } // 定义函数
addOne() // run the function
console.log(myNumber) // 结果显示2

上面的代码定义了一个函数,然后调用了它,之间没有任何停留。当该函数被调用时,它立即把那个数字加上1,所以我们可以预见到,调用过该函数后,那个数字的值会变成2。

现在假设我们把数字存在一个叫number.text的文件里:

var fs = require('fs') // require是Node提供的一个特别函数
var myNumber = undefined // 数字被存在文件里,因此我们并不知道它的值

function addOne() {
  fs.readFile('./number.txt', function doneReading(err, fileContents) {
    myNumber = parseInt(fileContents)
    myNumber++
  })
}

addOne()

console.log(myNumber) // 结果显示undefined

为什么这些显示出来的值是undefined?因为在上面的代码中,我们用了fs.readFile这个方法,而它恰好是个非同步方法。一般来说,需要和硬盘沟通或是从通信网络获得数据的,都是非同步的。只是需要从内存里或CPU里读些东西的话,就是同步的。这是因为I/O(输入输出)是非常非常非常慢的。如果要大概形容一下,从硬盘里读取大概比从内存里读取慢了10万倍。

当这个程序运行的时候,所有的函数都马上被定义,但它们不是马上都被执行的。这是非同步编程的一个基础概念。当addOne被调用的时候,Node执行readFile这个方法,但不等到readFile结束,它就继续进行下一个不需要等待就能执行的函数了。如果没有可以执行的东西了,Node要么会停下来,等待文件读取或是网络通讯结束,要么就结束运行,返回到命令行。

readFile终于把文件读完的时候(需要的时间从几毫秒到几秒到几分钟不等,要看硬盘有多快),Node会执行doneReading这个函数,并把报的错(如果读文件的时候有报错的话)和文件的内容传给它。

在上面的程序中,之所以会显示undefined,是因为我们的代码并没有在任何地方注明了要在文件读取完成后再console.log出数字。

如果你有一些想要反复执行的代码,你应该做的第一件事就是把这些代码放在一个函数里。然后,在你需要执行那些代码的时候,调用这个函数就好了。你给函数起的名字最好能让人一看就知道这个函数是做什么的。

回调函数,不过是在将来某个时间被执行的函数。要理解回调函数,很关键的一点是它被使用的时机。你使用回调函数的前提是,你不知道什么时候某个非同步进程会结束,但知道这个进程会在哪里结束————就在那个非同步函数的最后一行!你在什么地方声明这些函数并不重要,重要的是这些函数之间的逻辑顺序。把代码分装进各个函数之后,如果一个函数的执行取决于另一个函数何时结束,就该使用回调函数了。

上面代码中的fs.readFile方法是Node自带的,这个方法是非同步的,而且要花费很长时间。想想看它要做多少事情:它要进入操作系统,进入文件系统,文件系统可是在硬盘上的,硬盘可能转得飞快,也可能根本就不转。然后它要用激光读出数据,并把数据传回你的JavaScript程序。当你给了它一个回调函数后,它就可以在成功从文件系统中取得数据以后,调用那个回调函数。它会把数据放在一个变量里,交给你给的回调函数,我们给这个变量起的名字叫做fileContents,因为变量中包含的是读取到的文件内容。

想想看这个教程刚开始时的那个餐厅的例子。在很多餐厅,在你点的菜上来之前,服务生会放一个数字牌在你桌上。这个和回调函数很类似。回调函数的作用就是告诉服务器在你的芝士汉堡好了后要做些什么。

现在,让我们把console.log放进一个函数里作回调函数使用吧。

var fs = require('fs')
var myNumber = undefined

function addOne(callback) {
  fs.readFile('./number.txt', function doneReading(err, fileContents) {
    myNumber = parseInt(fileContents)
    myNumber++
    callback()
  }
}

function logMyNumber() {
  console.log(myNumber)
}

addOne(logMyNumber)

现在logMyNumber这个函数可以被传给addOne作为回调函数了。在readFile完成后,callback这个变量会被执行(也就是callback())。只有函数才能被执行,所以如果你提供一个不是函数的东西,程序会出错。

在JavaScript里,当函数被调用,其包含的代码会立刻被执行。在这个例子里,console.log会被执行,因为callback其实就是logMyNumber。要记得,你定义了一个函数,不代表它会执行!你一定得调用它才行。

如果要更细地分析一下这个例子,下面是按时间顺序排列的所有发生的事件:

  • 1: 代码被分析,这时,如果有任何语法错误,程序会停止并报错。
  • 2: addOne被调用,以logMyName作为它的回调函数,也就是我们想在addOne结束后执行的函数。接下来,非同步的fs.readFile马上开始运行。这个部分要花上点时间。
  • 3: Node暂时没事做的,于是它就闲下来等待着readFile结束。
  • 4: readFile结束了,doneReading这个函数被调用,它把数字加上1然后马上调用回调函数————也就是我们传给addOnelogMyNumber

也许关于回调函数最难理解的部分是,为什么函数可以被存在变量里被传来传去,而且还有着变来变去的名字。要让你的代码更容易被看懂,给你的函数起简单明了的名字是很重要的一部分。总的来说,在使用Node时,如果你看见一个变量叫做callback或是它的缩写cb,你差不多可以确定它就是一个函数。

你可能听过一个术语叫“事件驱动式编程”,或者叫“事件循环”。readFile这类的函数就利用了“事件循环”。Node首先开始运行readFile,并等待着readFile发回一个事件。在Node等待的这段时间,它可以继续运行其他的东西。在Node里有一个列表,里面记下了所有开始运行却还没有发回结束信号的事,Node就一遍遍循环检查这个列表,看看有没有事情完成了。它们运行完之后,就会被Node处理掉,也就是说,需要运行的回调函数会被运行。

下面是上面例子的伪代码写法:

function addOne(thenRunThisFunction) {
  waitAMinuteAsync(function waitedAMinute() {
    thenRunThisFunction()
  })
}

addOne(function thisGetsRunAfterAddOneFinishes() {})

假设你有三个非同步函数:ab,和c。它们要花上一分钟来运行,运行完了之后会调用一个回调函数(函数以第一个参数的形式被传进函数)。如果你想让Node先运行a,a运行完后运行b,b运行完后再运行c,那么程序是下面这样的:

a(function() {
  b(function() {
    c()
  })
})

当这段代码被运行时,a马上就会被运行,一分钟后a结束运行,b开始执行,再一分钟后,b结束运行,c开始运行。最后,也就是三分钟后,Node会停止运行,因为所有事都运行完了。上面的代码可能看起来没那么漂亮,但重点是,如果有些代码需要在某些非同步的事情运行完了之后再运行,你需要做的是把那些代码放进一个函数,当作回调函数传给非同步函数,以表示回调函数中的代码要依赖非同步的部分运行结束才能运行。

Node要求你用非线性的思维思考。看看下面这两件事:

read a file
process that file

如果你只是不假思索地把这两件事改成伪代码,你会这么写:

var file = readFile()
processFile(file)

这种线性的代码不是Node的风格。(线性是指一步接一步、按照顺序地)。如果上面的代码被运行了。那么readFileprocessFile会同时被调用。这根本说不通,因为reafFile要花上一阵子时间才能运行结束。正确的做法是,表达清楚processFile是要依赖readFile结束才能运行的。这就是回调函数的作用了!因为JavaScript的特点,有好几种方法可以表达这种依赖性:

var fs = require('fs')
fs.readFile('movie.mp4', finishedReading)

function finishedReading(error, movieData) {
  if (error) return console.error(error)
  // do something with the movieData
}

不过你这样写也可以,照样会成功运行:

var fs = require('fs')

function finishedReading(error, movieData) {
  if (error) return console.error(error)
  // do something with the movieData
}

fs.readFile('movie.mp4', finishedReading)

甚至像下面这样:

var fs = require('fs')

fs.readFile('movie.mp4', function finishedReading(error, movieData) {
  if (error) return console.error(error)
  // do something with the movieData
})

事件

在Node中如果你加载了events模块, 就可以用被称作event emitter(事件分发器)的功能。 Node在它的API中使用这一功能分发事件。

在编程中运用事件是一种常见的方法。它还有一个我们更为熟知的名字观察者模式,或者发布/监听模式。在回调函数的模式中,调用回调函数的命令与等待回调函数的命令间的关系是一一对应的,而在事件模式中这两种命令的关系可以是多对多的。

理解事件最简单的方式,就是把它当成一个你监听的东西。如果说在回调函数里面我们的逻辑是先做X,再做Y,那么在事件中我们的逻辑是当X发生时,做Y

以下是一些常见的用事件取代回调函数的例子:

  • 需要向所有听众广播的聊天室
  • 需要及时了解玩家上线、下线、运动、设计、跳跃等动作的游戏服务器
  • 需要能让开发者执行.on('jump', function() {})这种命令的游戏引擎
  • 能够执行.on('incomingRequest').on('serverError')这一API的低端web服务器。

如果我们想只用回调函数写一个连接聊天服务器的模块的话,代码会长这样:

var chatClient = require('my-chat-client')

function onConnect() {
  // have the UI show we are connected
}

function onConnectionError(error) {
  // show error to the user
}

function onDisconnect() {
 // tell user that they have been disconnected
}

function onMessage(message) {
 // show the chat room message in the UI
}

chatClient.connect(
  'http://mychatserver.com',
  onConnect,
  onConnectionError,
  onDisconnect,
  onMessage
)

正如你所见,用回调函数写会变得十分笨拙。你需要把所有的功能函数按特定的顺序传给.connect来执行。但是将上面所写的功能用事件来实现,就会变成这样:

var chatClient = require('my-chat-client').connect()

chatClient.on('connect', function() {
  // have the UI show we are connected
}) 

chatClient.on('connectionError', function() {
  // show error to the user
})

chatClient.on('disconnect', function() {
  // tell user that they have been disconnected
})

chatClient.on('message', function() {
  // show the chat room message in the UI
})

这种写法和回调函数很像,但是运用了高大上的.on功能,它会让一个回调函数‘监听’一个事件。 这意味着你可以在chatClient中选择任意一个想要监听的事件。 你甚至可以为多个回调函数监听同一个事件:

var chatClient = require('my-chat-client').connect()
chatClient.on('message', logMessage)
chatClient.on('message', storeMessage)

function logMessage(message) {
  console.log(message)
}

function storeMessage(message) {
  myDatabase.save(message)
}

在早期的node项目中,文件系统和网络API有各自处理I/O流的方式。比如,在文件系统中,文件有一个‘文件描述器’的东西,因此fs模块需要调用额外的逻辑来跟踪这个东西。然而在网络模块中根本没有’xx描述器‘这样的概念。尽管在语义上有像这样较小的区别,在最底层这两种模块(文件系统、网络模块)在重复着同样的数据读写操作。Node的维护们很快意识到这样的重复很容易迷惑开发者,于是他们造了这么个叫(Stream)的东西,使网络与文件系统的代码可以同样工作。

Node的理念就是以更简单的方式来处理文件系统和网络,所有理所应当的应该有一个通用的模式,可以在不同的场景中运用。好消息是,类似的大多数模式(尽管数量很少)现在已经被认为node在未来不会去更改。

已经有两个很棒的资源可以用来学习node的流对象。一个叫‘stream-adventure’(参考‘了解Node’部分),另一个叫‘Stream Handbook’。

Stream Handbook

stream-handbook 是一个与本项目相似的,包含所有你需要、想要了解的有关流对象的内容的教程。

stream-handbook

模块

Node的核心是由许多模块(modules)组成,像底层的事件,高一些层次的httpcrypto

Node有意被设计成这样,使它的核心模块轻量化,并注重于提供跨平台的处理普通I/O协议和类型的最基本工具。

除此之外,你可以在npm上找到其它需要了解的东西。任何人都可以创建一个新的模块,添加一些功能,并发布到npm上。到目前为止,npm上已经有196,950个模块可供下载。

如何找到心怡的模块

想象一下你在试图把一个PDF文件转换成一个TXT文本。最好的方式就是执行这样一个搜索命令npm search pdf

pdfsearch

这里有数以千计的结果! npm十分热门,所以通常你都可以找到许多可能的解决方案。 如果你把以上的搜索结果浓缩一下(比如过滤掉PDF生成模块),你会得到这样的一些结果:

在这之中许多模块都有重复的功能,并且使用了不同的API。很多模块可能会依赖外部的库,你需要先安装这些库(比如 apt-get install poppler)才能使用这些模块。

以下是对上述这些模块的一些说明:

  • pdf2json是唯一一个用纯JavaScript写的模块,所以他没有依赖并且很容易安装。特别是在一些低功耗的设备上,像树莓派,或者像Windoes这样没有跨平台库支持的操作系统。
  • mimeograph, hummuspdf-extract ,这几个模块集合了许多底层的模块,并抽象出高层的API
  • 许多模块实际上都是在unix命令后工具pdftotext/poppler上搭建的

让我们来比较一下pdftotextjspdf-text-extract这两个工具,他们都是在pdftotext的基础上打包而成的。

pdf-modules

这两个模块:

  • 最近都有更新
  • 有github的项目链接(这一点很重要!)
  • 有说明文档
  • 每周都有一定的新安装用户
  • 非常宽松的使用许可(所有人都可以使用)

仅依靠package.json文件和模块的统计数据很难说哪一个最正确的选择。所以我们来对比一下说明文档吧:

pdf-readmes

两个文档都有简单的介绍,CI编译通过的标志,安装命令,清晰的例子和一些测试命令。赞!但是我们要选哪一个呢?我们来对比一下代码吧:

pdf-code

pdftotextjs 有110行代码,而pdf-text-extract则只有40行。其实这两个模块最核心的操作可以归结为这一行代码:

var child = shell.exec('pdftotext ' + self.options.additional.join(' '));

通过这一点能判断出哪一个更好吗?很难说诶!所以代码再下结论是很重要的。如果你找到了想要的模块,执行npm star modulename来给你喜欢的模块一个正面的反馈信息吧。

模块开发流程

npm和大多数的包管理软件不同,它会将模块安装在另一个已有模块的目录中。这句话可能很难以理解,但知道这是npm成功的关键就好。

许多包管理软件会全局安装。比如你在Debian系统上执行apt-get install couchdb,apt-get会试图安装最新的CouchDB。如果你再试图安装一个依赖旧版本CouchDB的软件,你就得卸载掉新的版本,再安装旧版本的CouchDB。你无法同时保留新旧两个版本的CouchDB,因为Debian(apt-get)只知道将软件安到同一个位置。

当然这不是Debian一个系统的错,绝大多数语言的包管理软件都这样。 为了解决这种全局依赖的问题,已经有了许多虚拟环境的项目被创建出来。比如针对Python的 virtualenv,或者针对Ruby的bundler。然而这些只是把你的环境配置划分成不同的虚拟环境,每个工程对应一个,但实际上每个环境配置依旧是全局安装的。而且虚拟环境不总是能解决问题,有时候只是增加了多一层的复杂度。

用npm来安装全局模块是反人类的。就像你不应该在你的JavaScript代码中使用全局变量一样。(除非你需要一个可执行的二进制文件集成进PATH中,但你不总需要这样做--在后面我们会解释这一点)。

require命令是如何工作的

当我们加载一个模块的时候,我们调用require('some_module'),以下是在node中会发生的事情:

  1. 如果some_module.js文件在当前目录下,node会加载它,否则
  2. node会在当前目录下寻找 node_modules 文件夹,然后在其中找some_module
  3. 如果还没找到,node会跳到上一层文件夹,然后重复步骤2

这一操作会不断循环直到node找到根目录是还没有找的这个模块,在那之后node回去找全局安装时的文件夹(比如Mac OS系统上的 /usr/local/node_modules),如果还没有找到这个some_module,node会报错。

这里有一个上述操作的可视化说明:

mod-diagram-01

当前的工作目录为subsubfolder,并且require('foo')被执行时,node会查找 subsubsubfolder/node_modules这个子目录。在这个例子中,由于这个子目录被错误地命名为my_modules了,因而node找不到它,只好跳到subsubfolder的上一级目录subfolder_B寻找subfolder_B/node_modules,然而这个文件夹不存在;于是node再往上一级目录寻找,在subfolder_B的上一级目录folder中找到了folder/node_modules并且foo文件夹在其中。至此搜索便结束了,但如果foo并不在那个目录里,node会继续往上一层目录搜索。

注意这点,我们在subfolder_B中没找到foo模块并向上一级目录寻找的时候,并不会向同一级的 subfolder_A/node_modules中寻找。在它的搜索树中只有 folder/node_modules

使用npm的一个好处就是,模块可以安装自己依赖的特定版本模块。 在这个例子中,foo模块特别流行,以至于我们将三个版本安装在不同位置。这样做的原因是调用它们的模块依赖特定版本的foo,比如folder依赖[email protected], subfolder_A 依赖 [email protected] 等等.

如果我们把刚才的那个错误的文件夹名称改过来,从my_modules改成node_modules,那么搜索过程就会变成这样:

mod-diagram-02

为了测试node到底加载了哪个模块,可以执行require.resolve('some_module') 命令,这会告诉你哪个文件路径下的模块被node找到并调用了。require.resolve 非常有用,尤其是在确认你认为被夹在的模块是实际上被加载的模块的时候--有时候一个不同版本的模块可能被存在了被更先查找的位置,导致你的代码调用了错误版本的模块。

如何写一个模块

现在你已经知道了如何找一个模块了,在这之后你就可以开始开发自己的模块了!

The simplest possible module

Node的模块十分的轻量化。这里有一个最简单的node模块:

package.json:

{
  "name": "number-one",
  "version": "1.0.0"
}

index.js:

module.exports = 1

默认情况下,当你调用require('module')时node会试图加载module/index.js,除非你在package.json中设定了main一项内容指向你的代码,不然用的名称的文件无法被node识别。

把这两个文件放到number-one目录下(package.json中的id一项必须和目录的名称相同),然后你就可以加载他们了。

调用require('number-one') 这一命令会返回你在模块中module.exports输出的内容:

simple-module

一个更快捷的创建模块的方法是,执行以下命令:

mkdir my_module
cd my_module
git init
git remote add [email protected]:yourusername/my_module.git
npm init

执行npm init会生成一个package.json,如果你是在一个git项目里执行,它还会在package.json中自动帮你把repositories设成你的git repo地址!

添加依赖项

一个模块可以添加其它在npm上或是在Github上的模块到他的配置文件package.json中的dependencies项。如果你想安装一个新的依赖项,并把它自动添加到package.json中,在你的模块的根目录中执行这个命令:

npm install --save request

这个命令会安装request模块到最近的node_modules文件夹中,并会把package.json改成这样:

{
  "id": "number-one",
  "version": "1.0.0",
  "dependencies": {
    "request": "~2.22.0"
  }
}

默认情况下 npm install会安装模块的最新版本。

用npm在客户端开发

人们对npm有一个常见的错误观念,认为npm的名字中有一个Node,所以只能用于服务器端的JS模块。一派胡言!npm的全称是Node Packaged Modules,是由node为你打包过的模块。而模块本身可以是任何东西--本质上只是一个被打包成.tar.gz的文件夹,和一个声明了模块版本和模块依赖项的配置文件package.json (也包括依赖项的版本,这样对应版本的依赖项会被自动安装)。这是无穷无尽的--模块可以有依赖,模块的依赖项也可以有依赖,依赖项的依赖项也可以有依赖。。。

browserify 是一个用Node写的实用工具,可以讲任何node模块转换成可以在浏览器上运行的代码。当然,并不是所有模块都能工作(比如浏览器无法搭一个HTTP服务器),但是很多NPM上的模块可以

你可以用RequireBin来尝试在浏览器上使用npm的模块,这是一个原作者写的应用,它在Browserify-CDN的基础上完成。原作在RequireBin中使用了browserify,并通过HTTP返回输出结果(而不是通过命令后--browserify通常都是用来干这个)

试着将下面的代码粘贴到RequireBin并点preview按钮:

var reverse = require('ascii-art-reverse')

// makes a visible HTML console
require('console-log').show(true)

var coolbear =
  "    ('-^-/')  \n" +
  "    `o__o' ]  \n" +
  "    (_Y_) _/  \n" +
  "  _..`--'-.`, \n" +
  " (__)_,--(__) \n" +
  "     7:   ; 1 \n" +
  "   _/,`-.-' : \n" +
  "  (_,)-~~(_,) \n"

setInterval(function() { console.log(coolbear) }, 1000)

setTimeout(function() {
  setInterval(function() { console.log(reverse(coolbear)) }, 1000)
}, 500)

或者看这个更复杂的例子(可以随意改变它的颜色):

requirebin

析薪杝矣

原文的标题是Going with the Grain,大意是顺应着木材的纹理(刨木),不违背它
此处的'析薪杝矣'出自詩·小雅:
	伐木掎矣,析薪杝矣
大意为,砍伐树木时,要撑住使大树不致突然倒下;劈木材,要依循木材的纹理,才比较容易

像任意一个顺手的工具一样,node非常强大,但也只适用于特定的应用场景。比如,Rails这个网络架构,非常适合做一些复杂的框架,比如用代码来构建生活中的业务对象:帐户、借贷、流程图、存货清单等等。虽然从技术上讲,用node可以完成同样的工作,但这并不是node的强项,node更适合去做一些处理I/O问题的工作。希望这个教程能够帮你获得对node适用方案的直觉。

node外的世界

node只是一个处理文件系统和网络I/O的工具,它把更多有趣的功能留给第三方模块来处理。以下是node核心模块之外奇妙世界的一些介绍:

网络框架

有许多搭建在node之上的网络框架(框架是一种解决特定高层应用问题的功能集合),但是node自身并不是一个网络框架。一些搭建在node之上的网络框架有自己的特性、抽象和权衡,这些和node自身的理念与开发优先级不一定相同。

编程语法

Node适用Javascript的语法并且没有加以修饰。 Felix Geisendörfer针对node的风格有一篇很棒的介绍

语言的抽象

node用最简单的方式来完成任务。在Javascirpt中,你想把它做的越有趣,就会带来更大的复杂度。编程是有难度的,尤其是在写js的时候更有这种体会,因为你应对的每一个问题都可能有1000种解决方案。正是因为如此,node试图用最简单、通用的方式来解决问题。如果你在处理一个很复杂的问题,并且你并不满意node应用的‘vanilla JS’解决方案,你大可不用它,并且自己写一个模块,用你自己喜欢的方法来解决它。

一个很棒的例子就是node中的回调函数。 早期node的一些实验中,有一个特性叫做‘promises’。它被用来使异步运行的代码看上去更线性。但是出于以下原因,这个特性后来被移除了:

  • 它比回调函数更复杂
  • 它可以让用户来选择应用(在npm上以第三方模块的形式发布)

试着考虑node处理的最基本最通用的事情:读取一个文件,当你读一个文件的时候,你希望在诸如硬盘错误这种事件发生的时候能及时知道。如果node用了上述的’promises‘特性,那么每个人的代码就会变成这样:

fs.readFile('movie.mp4')
  .then(function(data) {
    // do stuff with data
  })
  .error(function(error) {
    // handle error
  })

这添加了复杂度,而且并不是所有人都想要这个特性。 node会用一个简单的回调函数来完成这两个独立的功能。其它的诸如此的规则还有:

  • 当没有错误的时候,对第一个参数返回null
  • 当有错误的时候,对第一个参数返回错误代码
  • 其它的变量可以用来做任何事情(node多数情况下在读写东西,所以这些变量通常被用来传数据或响应)

基于上述规则写出来的回调函数则应是这样的:

fs.readFile('movie.mp4', function(err, data) {
  // handle error, do stuff with data
})

线程/纤程/非事件的并发处理

注意:如果你并不知道这些词的含义,你可能会学Node学的更轻松一些。

Node内部使用线程来加速操作,但是这些部分并不会暴露给用户。如果你是专业人员,并且对node的设计理念十分好奇的话,推荐你阅读这篇the design of libuv,这个是node使用的C++ I/O层。

使用许可

CCBY

原文适用知识共享许可协议 http://creativecommons.org/licenses/by/2.0/

捐款图标来源于 Noun Project