《Node入门》- 一个完整的基于Node.js的web应用构建详解(一)

The use cases

Let's keep it simple, but realistic:
我们来把目标设定得简单点,不过也要够实际才行:

The user should be able to use our application with a web browser
用户应能够通过浏览器使用到我们的应用。

The user should see a welcome page when requesting http://domain/start which displays a file upload form
当用户请求http://domain/start时可以看见一个欢迎页面,并且显示一个文件上传表单。

By choosing an image file to upload and submitting the form, this image should then be uploaded to http://domain/upload, where it is displayed once the upload is finished
用户可以选择一个图片文件上传提交到表单,然后这张图片应该被上传到http://domain/start,上传完成后能够显示在页面上。

Fair enough. Now, you could achieve this goal by googling and hacking together something. But that's not what we want to do here.
差不多了,现在你可以google一下来完成这个功能。但是这里我们先不做这个。

Furthermore, we don't want to write only the most basic code to achieve the goal, however elegant and correct this code might be. We will intentionally add more abstraction than necessary in order to get a feeling for building more complex Node.js applications.
进一步说,我们想要的不是仅仅写一个基础代码来完后这个功能,而不管代码的优美和正确。更多的是我们想要对此进行抽象,来寻找一种适合构建更为复杂的Node.js应用的方式。

The application stack

Let's dissect our application. Which parts need to be implemented in order to fulfill the use cases?
让我们来仔细分析一下这个应用。为了实现上文的用例,我们需要实现哪些部分呢?

We want to serve web pages, therefore we need an HTTP server
我们需要提供Web页面,因此需要一个HTTP服务器

Our server will need to answer differently to requests, depending on which URL the request was asking for, thus we need some kind of router in order to map requests to request handlers
根据不同请求的URL,我们的服务器需要给予不同的响应,因此,我们需要某种路由,用于将不同的请求分配到相应的请求处理程序。

To fulfill the requests that arrived at the server and have been routed using the router, we need actual request handlers
当请求被服务器接收并通过路由传递之后,我们需要最终的请求处理程序

The router probably should also treat any incoming POST data and give it to the request handlers in a convenient form, thus we need request data handling
路由还应该可以处理一些传进来的POST数据,并且将这些数据封装成适当的格式交给请求处理程序,因此我们需要请求数据处理功能。

We not only want to handle requests for URLs, we also want to display content when these URLs are requested, which means we need some kind of view logic the request handlers can use in order to send content to the user's browser
我们不仅想要处理URL对应的请求,还想要将其内容显示出来,这就意味着我们需要某种视图逻辑供请求处理程序使用,以便发送内容到用户的浏览器中。

Last but not least, the user will be able to upload images, so we are going to need some kind of upload handling which takes care of the details
最后,用户应能够上传图片,所以我们需要某种上传处理功能来处理这方面的细节。

Let's think a moment about how we would build this stack with PHP. It's not exactly a secret that the typical setup would be an Apache HTTP server with mod_php installed. 
我们先来想想,使用PHP的话我们会怎么构建这个结构。一般来说我们会用一个Apache HTTP服务器并配上mod_php5模块。

Which in turn means that the whole "we need to be able to serve web pages and receive HTTP requests" stuff doesn't happen within PHP itself.
从这个角度看,整个“接收HTTP请求并提供Web页面”的需求根本不需要PHP来处理。

Well, with node, things are a bit different. Because with Node.js, we not only implement our application, we also implement the whole HTTP server. In fact, our web application and its web server are basically the same.
不过对Node.js来说,概念完全不一样了。使用Node.js时,我们不仅仅在实现一个应用,同时还实现了整个HTTP服务器。事实上,我们的Web应用以及对应的Web服务器基本上是一样的。

This might sound like a lot of work, but we will see in a moment that with Node.js, it's not.
听起来好像有一大堆活要做,但随后我们会逐渐意识到,对Node.js来说这并不是什么麻烦的事。

Let's just start at the beginning and implement the first part of our stack, the HTTP server.
现在我们就来开始实现之路,先从第一个部分—HTTP服务器着手。

A basic HTTP server

When I arrived at the point where I wanted to start with my first "real" Node.js application, I wondered not only how to actually code it, but also how to organize my code. 
当我准备开始写我的第一个“真正的”Node.js应用的时候,我不但不知道怎么写Node.js代码,也不知道怎么组织这些代码。

Do I need to have everything in one file? Most tutorials on the web that teach you how to write a basic HTTP server in Node.js have all the logic in one place. What if I want to make sure that my code stays readable the more stuff I implement?
我需要将所有东西放在一个文件里吗?网上的大多数教程都教你如何把所有逻辑放进用Node.js写的一个基础HTTP服务中,但是如果我想要添加更多的东西,还要确保我的代码的可读性呢?

Turns out, it's relatively easy to keep the different concerns of your code separated, by putting them in modules.
实际上,只要把不同功能的代码放入不同的模块中,保持代码分离还是相当简单的。

This allows you to have a clean main file, which you execute with Node.js, and clean modules that can be used by the main file and among each other.
这种方法允许你拥有一个干净的主文件(main file),你可以用Node.js执行它;同时你可以拥有干净的模块,它们可以被主文件和其他的模块调用。

So, let's create a main file which we use to start our application, and a module file where our HTTP server code lives.
所以呢,让我们创建一个用于启动我们的应用的主文件,和一个存放HTTP服务器代码的模块文件。

My impression is that it's more or less a standard to name your main file index.js. It makes sense to put our server module into a file named server.js.
在我印象里这个名叫index.js的文件或多或少是个标准格式。把服务器模板放在server.js文件中是说得通的。

Let's start with the server module. Create the file server.js in the root directory of your project, and fill it with the following code:
让我们先从服务器模块开始。在你的项目的根目录下创建一个叫server.js的文件,并写入以下代码:

var http = require("http");

http.createServer(function(request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("Hello World");
  response.end();
}).listen(8888);

That's it! You just wrote a working HTTP server. Let's prove it by running and testing it. First, execute your script with Node.js:
就是这样!你刚刚写完了一个可以工作的HTTP服务。为了证明这一点,我们来运行并且测试这段代码。首先,用Node.js执行你的脚本:

node server.js

Now, open your browser and point it at http://localhost:8888/. This should display a web page that says "Hello World".
接下来,打开浏览器访问http://localhost:8888/,你会看到一个写着“Hello World”的网页。

That's quite interesting, isn't it. How about talking about what's going on here and leaving the question of how to organize our project for later? I promise we'll get back to it.
这很有趣,不是吗?让我们先来谈谈HTTP服务器的问题,把如何组织项目的事情先放一边吧,你觉得如何?我保证之后我们会解决那个问题的。

Analyzing our HTTP server

Well, then, let's analyze what's actually going on here.
那么接下来,让我们分析一下这个HTTP服务器的构成。

The first line requires the http module that ships with Node.js and makes it accessible through the variable http.
第一行导入Node.js自带的http模板,并把它赋值给变量http。

We then call one of the functions the http module offers: createServer. This function returns an object, and this object has a method named listen, and takes a numeric value which indicates the port number our HTTP server is going to listen on.
接下来调用http模板提供的一个函数:createServer。这个函数会返回一个对象,并且这个对象有一个方法叫做listen,方法还带有一个数值参数,用来指定HTTP服务器监听的端口号。

Please ignore for a second the function definition that follows the opening bracket of http.createServer.
咱们暂时先忽略 http.createServer 的括号里的那个函数定义。

We could have written the code that starts our server and makes it listen at port 8888 like this:
我们本来可以用这样的代码来启动服务器并侦听8888端口:

var http = require("http");

var server = http.createServer();
server.listen(8888);

That would start an HTTP server listening at port 8888 and doing nothing else (not even answering any incoming requests).
这段代码只会启动一个侦听8888端口的服务器,它不做任何别的事情,甚至连请求都不会应答。

The really interesting (and, if your background is a more conservative language like PHP, odd looking) part is the function definition right there where you would expect the first parameter of the createServer() call.
最有趣(而且,如果你之前习惯使用一个更加保守的语言,比如PHP,它还很奇怪)的部分是 createServer() 的第一个参数,一个函数定义。

Turns out, this function definition IS the first (and only) parameter we are giving to the createServer() call. Because in JavaScript, functions can be passed around like any other value.
实际上,这个函数定义是 createServer() 的第一个也是唯一一个参数。因为在JavaScript中,函数和其他变量一样都是可以被传递的。

Passing functions around

You can, for example, do something like this:
例如,你可以这样做:

function say(word) {
  console.log(word);
}

function execute(someFunction, value) {
  someFunction(value);
}

execute(say, "Hello");

Read this carefully! We pass the function say as the first parameter to the execute function. Not the return value of say, but say itself!
仔细阅读以上代码!在这里,我们把 say 函数作为execute函数的第一个变量进行了传递。这里传递的不是 say 的返回值,而是 say 本身!

Thus, say becomes the local variable someFunction within execute, and execute can call the function in this variable by issuing someFunction() (adding brackets).
这样一来, say 就变成了execute 中的本地变量 someFunction ,execute可以通过调用 someFunction() (带括号的形式)来使用 say 函数。

Of course, because say takes one parameter, execute can pass such a parameter when calling someFunction.
当然,因为 say 有一个变量, execute 在调用 someFunction 时可以传递这样一个变量。

We can, as we just did, pass a function as a parameter to another function by its name. But we don't have to take this indirection of first defining, then passing it - we can define and pass a function as a parameter to another function in-place:
我们可以,就像刚才那样,用它的名字把一个函数作为变量传递。但是我们不一定要绕这个“先定义,再传递”的圈子,我们可以直接在另一个函数的括号中定义和传递这个函数:

function execute(someFunction, value) {
  someFunction(value);
}

execute(function(word){ console.log(word) }, "Hello");

We define the function we want to pass to execute right there at the place where execute expects its first parameter.
我们在 execute 接受第一个参数的地方直接定义了我们准备传递给 execute 的函数。

This way, we don't even need to give the function a name, which is why this is called an anonymous function.
用这种方式,我们甚至不用给这个函数起名字,这也是为什么它被叫做 匿名函数。

This is a first glimpse at what I like to call "advanced" JavaScript, but let's take it step by step. For now, let's just accept that in JavaScript, we can pass a function as a parameter when calling another function. We can do this by assigning our function to a variable, which we then pass, or by defining the function to pass in-place.
这是我们和我所认为的“进阶”JavaScript的第一次亲密接触,不过我们还是得循序渐进。现在,我们先接受这一点:在JavaScript中,一个函数可以作为另一个函数接收一个参数。我们可以先定义一个函数,然后传递,也可以在传递参数的地方直接定义函数。

How function passing makes our HTTP server work

With this knowledge, let's get back to our minimalistic HTTP server:
带着这些知识,我们来看一看我们极简主义的HTTP服务:

var http = require("http");

http.createServer(function(request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("Hello World");
  response.end();
}).listen(8888);

By now it should be clear what we are actually doing here: we pass the createServer function an anonymous function.
现在它看上去应该清晰了很多:我们向 createServer 函数传递了一个匿名函数。

We could achieve the same by refactoring our code to:
用这样的代码也可以达到同样的目的:

var http = require("http");

function onRequest(request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("Hello World");
  response.end();
}

http.createServer(onRequest).listen(8888);

Maybe now is a good moment to ask: Why are we doing it that way?
也许现在我们该问这个问题了:我们为什么要用这种方式呢?

Event-driven asynchronous callbacks

To understand why Node.js applications have to be written this way, we need to understand how Node.js executes our code. Node’s approach isn’t unique, but the underlying execution model is different from runtime environments like Python, Ruby, PHP or Java.
为了理解为什么Node.js应用可以以这种方式书写,我们需要理解Node.js是如何执行我们的代码的。Node的这种方式并不是独有的,像Python, Ruby, PHP 或者 Java这类语言的运行环境下的基础执行模式都是不同的。(巨不顺)

Let’s take a very simple piece of code like this:
让我们以下面这段简单的代码为例:

var result = database.query("SELECT * FROM hugetable");
console.log("Hello World");

Please ignore for now that we haven’t actually talked about connecting to databases before - it’s just an example. The first line queries a database for lots of rows, the second line puts ”Hello World” to the console.
在我们还没有讨论到连接数据库时先忽略这部分内容—这仅仅是个列子。第一行是从数据库中查询很多行,第二行意思是输出“Hello World”到控制台。

Let’s assume that the database query is really slow, that it has to read an awful lot of rows, which takes several seconds.
先假定这个数据库查询是非常缓慢的,需要耗费几秒钟来读取很多行。

The way we have written this code, the JavaScript interpreter of Node.js first has to read the complete result set from the database, and then it can execute the console.log() function.
我们编写的这段代码的方式是,node.js先要从数据库中读取完整的结果集,然后才可以执行console.log()函数。

If this piece of code actually was, say, PHP, it would work the same way: read all the results at once, then execute the next line
of code. If this code would be part of a web page script, the user would have to wait several seconds for the page to load.
实际上如果是PHP来执行这段代码,会以相同的方式来运作:先读取所有的结果集,然后再执行下一行代码。如果这段代码是网页脚本的一部分,那么用户就不得不等待几秒来加载页面。

However, in the execution model of PHP, this would not become a ”global” problem: the web server starts its own PHP process for every HTTP request it receives. If one of these requests results in the execution of a slow piece of code, it results in a slow page load for this particular user, but other users requesting other pages would not be affected.
然而,在PHP的执行模型中,这不会成为一个“全局”问题:Web服务器为它接收到的每个HTTP请求启动自己的PHP进程。如果其中一个请求是执行一段缓慢的代码,那么会导致该特定用户的页面加载缓慢,但其他用户请求的页面不会受到影响。

The execution model of Node.js is different - there is only one single process. If there is a slow database query somewhere in this process, this affects the whole process - everything comes to a halt until the slow query has finished.
而Node.js的执行模式是不同的,它只有一个单进程。如果在这个过程中的某个地方有一个缓慢的数据库查询,这会影响整个过程——在缓慢的查询完成之前,所有的操作都会停止。

To avoid this, JavaScript, and therefore Node.js, introduces the concept of event-driven, asynchronous callbacks, by utilizing an
event loop.
为了避免这种情况,javascript和node.js通过使用事件循环引入了事件驱动的异步回调的概念。

We can understand this concept by analyzing a rewritten version of our problematic code:
我们可以通过分析问题代码的重写版本来理解这个概念:

database.query("SELECT * FROM hugetable", function(rows) {
    var result = rows;
});
console.log("Hello World");

Here, instead of expecting database.query() to directly return a result to us, we pass it a second parameter, an anonymous function.
这段代码里,我们不希望database.query()直接返回结果,而是将第二个参数(匿名函数)传递给它。

In its previous form, our code was synchronous: first do the database query, and only when this is done, then write to the
console.
在之前的形式中,我们的代码是同步的:先进行数据库查询,只有在完成查询之后,才能写入控制台。

Now, Node.js can handle the database request asynchronously. Provided that database.query() is part of an asynchronous library, this is what Node.js does: just as before, it takes the query and sends it to the database. But instead of waiting for it to be finished, it makes a mental note that says ”When at some point in the future the database server is done and sends the result of the query, then I have to execute the anonymous function that was passed to database.query().”
现在,node.js可以异步处理数据库请求。如果database.query() 是异步库的一部分,这就是node.js所做的:和以前一样,它接受查询并将其发送到数据库。但它并没有等待它完成,而是在脑海中记下“将来某个时候数据库服务器完成并发送查询结果时,我再执行传递给database.query()的匿名函数。”

Then, it immediately executes console.log(), and afterwards, it enters the event loop. Node.js continuously cycles through this loop again and again whenever there is nothing else to do, waiting for events. Events like, e.g., a slow database query finally delivering its results.
然后,它立即执行console.log(),然后进入事件循环。每当没有其他事情可做时,node.js会一次又一次地循环通过这个循环,等待事件发生。例如,像慢数据库查询这样的事件最终会传递其结果。

This also explains why our HTTP server needs a function it can call upon incoming requests - if Node.js would start the server and then just pause, waiting for the next request, continuing only when it arrives, that would be highly inefficent. If a second user requests the server while it is still serving the first request, that second request could only be answered after the first one is done - as soon as you have more than a handful of HTTP requests per second, this wouldn’t work at all.
这也解释了为什么我们的HTTP服务器需要一个可以对传入请求进行调用的函数 — 如果node.js启动服务器,然后只是暂停,等待下一个请求,直到这个请求到达时才继续,这将是非常无效的。如果第二个用户在当服务器仍在服务第一个请求时请求了服务器,那么第二个请求只能在第一个请求完成后才能得到响应-只要您每秒有多个HTTP请求,这就根本不起作用。

It’s important to note that this asynchronous, single-threaded, event-driven execution model isn’t an infinitely scalable performance unicorn with silver bullets attached. It is just one of several models, and it has its limitations, one being that as of now, Node.js is just one single process, and it can run on only one single CPU core. Personally, I find this model quite approachable, because it allows to write applications that have to deal with concurrency in an efficient and relatively straightforward manner.
需要注意的是,这个异步、单线程、事件驱动的执行模型并不是一个带有银色子弹的无限可扩展性能独角兽(what?)。它只是几个模型中的一个并且有其自身的局限性,其中一个局限性是到目前为止,node.js还只是一个单进程,它只能在一个CPU核心上运行。就我个人而言,我发现这个模型非常容易获得的,因为它允许编写以一种高效且相对简单的方式处理并发性的应用程序。

You might want to take the time to read Felix Geisendoerfer’s excellent post Understanding node.js³ for additional background explanation.
你也许会想花点时间读一下 Felix Geisendörfer 的大作 Understanding node.js,它介绍了一些背景知识。

Let’s play around a bit with this new concept. Can we prove that our code continues after creating the server, even if no HTTP request happened and the callback function we passed isn’t called? Let’s try it:
让我们再来琢磨琢磨这个新概念。我们怎么证明,在创建完服务器之后,即使没有 HTTP 请求进来、我们的回调函数也没有被调用的情况下,我们的代码还继续有效呢?我们试试这个:

var http = require("http");
function onRequest(request, response) {
    console.log("Request received.");
    response.writeHead(200, {"Content-Type": "text/plain"});
    response.write("Hello World");
    response.end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");

Note that I use console.log to output a text whenever the onRequest function (our callback) is triggered, and another text right after starting the HTTP server.
注意:在 onRequest(我们的回调函数)触发的地方,我用 console.log 输出了一段文本。在 HTTP 服务器开始工作之后,也输出一段文本。

When we start this (node server.js, as always), it will immediately output ”Server has started.” on the command line. Whenever we request our server (by opening http://localhost:8888/ in our browser), the message ”Request received.” is printed on the command line.
当我们与往常一样,运行它 node server.js 时,它会马上在命令行上输出“Server has started.”。当我们向服务器发出请求(在浏览器访问http://localhost:8888/),“Request received.”这条消息就会在命令行中出现。

Event-driven asynchronous server-side JavaScript with callbacksin action :-)
这就是事件驱动的异步服务器端 JavaScript 和它的回调啦!

(Note that our server will probably write ”Request received.” to STDOUT two times upon opening the page in a browser. That’s because most browser will try to load the favicon by requesting http://localhost:8888/favicon.ico whenever you open http://localhost:8888/).
(请注意,当我们在服务器访问网页时,我们的服务器可能会输出两次“Request received.” 。那是因为大部分服务器都会在你访问http://localhost:8888 / 时尝试读取http://localhost:8888/favicon.ico )

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值