Home > Article > Web Front-end > Node.js introductory tutorial mini book, complete example of node.js introductory web application development_Basic knowledge
本书状态
你正在阅读的已经是本书的最终版。因此,只有当进行错误更正以及针对新版本Node.js的改动进行对应的修正时,才会进行更新。
本书中的代码案例都在Node.js 0.6.11版本中测试过,可以正确工作。
读者对象
本书最适合与我有相似技术背景的读者: 至少对一门诸如Ruby、Python、PHP或者Java这样面向对象的语言有一定的经验;对JavaScript处于初学阶段,并且完全是一个Node.js的新手。
这里指的适合对其他编程语言有一定经验的开发者,意思是说,本书不会对诸如数据类型、变量、控制结构等等之类非常基础的概念作介绍。要读懂本书,这些基础的概念我都默认你已经会了。
然而,本书还是会对JavaScript中的函数和对象作详细介绍,因为它们与其他同类编程语言中的函数和对象有很大的不同。
本书结构
读完本书之后,你将完成一个完整的web应用,该应用允许用户浏览页面以及上传文件。
当然了,应用本身并没有什么了不起的,相比为了实现该功能书写的代码本身,我们更关注的是如何创建一个框架来对我们应用的不同模块进行干净地剥离。 是不是很玄乎?稍后你就明白了。
本书先从介绍在Node.js环境中进行JavaScript开发和在浏览器环境中进行JavaScript开发的差异开始。
紧接着,会带领大家完成一个最传统的“Hello World”应用,这也是最基础的Node.js应用。
最后,会和大家讨论如何设计一个“真正”完整的应用,剖析要完成该应用需要实现的不同模块,并一步一步介绍如何来实现这些模块。
可以确保的是,在这过程中,大家会学到JavaScript中一些高级的概念、如何使用它们以及为什么使用这些概念就可以实现而其他编程语言中同类的概念就无法实现。
该应用所有的源代码都可以通过 本书Github代码仓库:https://github.com/ManuelKiessling/NodeBeginnerBook/tree/master/code/application.
JavaScript与Node.js
JavaScript与你
抛开技术,我们先来聊聊你以及你和JavaScript的关系。本章的主要目的是想让你看看,对你而言是否有必要继续阅读后续章节的内容。
如果你和我一样,那么你很早就开始利用HTML进行“开发”,正因如此,你接触到了这个叫JavaScript有趣的东西,而对于JavaScript,你只会基本的操作——为web页面添加交互。
而你真正想要的是“干货”,你想要知道如何构建复杂的web站点 —— 于是,你学习了一种诸如PHP、Ruby、Java这样的编程语言,并开始书写“后端”代码。
与此同时,你还始终关注着JavaScript,随着通过一些对jQuery,Prototype之类技术的介绍,你慢慢了解到了很多JavaScript中的进阶技能,同时也感受到了JavaScript绝非仅仅是window.open() 那么简单。 .
不过,这些毕竟都是前端技术,尽管当想要增强页面的时候,使用jQuery总让你觉得很爽,但到最后,你顶多是个JavaScript用户,而非JavaScript开发者。
然后,出现了Node.js,服务端的JavaScript,这有多酷啊?
于是,你觉得是时候该重新拾起既熟悉又陌生的JavaScript了。但是别急,写Node.js应用是一件事情;理解为什么它们要以它们书写的这种方式来书写则意味着——你要懂JavaScript。这次是玩真的了。
问题来了: 由于JavaScript真正意义上以两种,甚至可以说是三种形态存在(从中世纪90年代的作为对DHTML进行增强的小玩具,到像jQuery那样严格意义上的前端技术,一直到现在的服务端技术),因此,很难找到一个“正确”的方式来学习JavaScript,使得让你书写Node.js应用的时候感觉自己是在真正开发它而不仅仅是使用它。
因为这就是关键: 你本身已经是个有经验的开发者,你不想通过到处寻找各种解决方案(其中可能还有不正确的)来学习新的技术,你要确保自己是通过正确的方式来学习这项技术。
当然了,外面不乏很优秀的学习JavaScript的文章。但是,有的时候光靠那些文章是远远不够的。你需要的是指导。
本书的目标就是给你提供指导。
简短申明
业界有非常优秀的JavaScript程序员。而我并非其中一员。
I am the person described in the previous section. I was familiar with developing backend web applications, but I was new to "real" JavaScript and Node.js. I have only recently learned some advanced concepts of JavaScript and have no practical experience.
Therefore, this book is not a "from entry to master" book, but more like a "from beginner to advanced entry" book.
If successful, then this book will be the tutorial I most wanted to have when I started learning Node.js.
Server-side JavaScript
JavaScript first ran in the browser. However, the browser only provided a context, which defined what can be done using JavaScript, but did not "say" much about what the JavaScript language itself can do. In fact, JavaScript is a "complete" language: it can be used in different contexts and its capabilities are as good as those of other similar languages.
Node.js is actually another context that allows JavaScript code to be run on the backend (out of the browser environment).
To run JavaScript code in the background, the code needs to be interpreted first and then executed correctly. This is the principle of Node.js. It uses Google's V8 virtual machine (the JavaScript execution environment used by Google's Chrome browser) to interpret and execute JavaScript code.
In addition, Node.js comes with many useful modules that can simplify many repetitive tasks, such as outputting strings to the terminal.
Therefore, Node.js is actually both a runtime environment and a library.
To use Node.js, you first need to install it. Regarding how to install Node.js, I won’t go into details here. You can directly refer to the official installation guide. After the installation is complete, continue to come back and read the rest of this book.
“Hello World”
Okay, no more "nonsense", let's start our first Node.js application: "Hello World".
Open your favorite editor and create a helloworld.js file. What we need to do is to output "Hello World" to STDOUT. The following is the code to implement this function:
Save the file and execute it through Node.js:
If normal, Hello World will be output in the terminal.
Okay, I admit that this application is a bit boring, so let’s get some “dry stuff”.
A complete web application based on Node.js
Use Cases
Let’s make the goal simple, but realistic enough:
1. Users can use our application through the browser.
2. When the user requests http://domain/start, they can see a welcome page with a file upload form.
3. The user can select an image and submit the form, and then the file will be uploaded to http://domain/upload. After the page completes the upload, the image will be displayed on the page.
That’s it. You can also go to Google now and find something to mess around with to complete the function. But let’s not do that right now.
Furthermore, in the process of accomplishing this goal, we need more than just basic code regardless of whether the code is elegant or not. We also need to abstract this to find a way to build more complex Node.js applications.
Apply different modules for analysis
Let’s break down this application. In order to implement the above use cases, what parts do we need to implement?
1. We need to provide Web pages, so we need an HTTP server
2. For different requests, our server needs to give different responses according to the requested URL, so we need a route to route the requests Corresponds to the request handler
3. After the request is received by the server and passed through the route, it needs to be processed, so we need the final request handler
4. The route should also be able to handle it POST data, and encapsulate the data into a more friendly format and pass it to the request processing program, so you need to request the data processing function
5. We not only have to process the request corresponding to the URL, but also display the content, which means We need some view logic for the request handler to send content to the user's browser
6. Finally, the user needs to upload images, so we need the upload handling function to handle the details of this
Let's do it first Think about how we would build this structure using PHP. Generally we will use an Apache HTTP server with the mod_php5 module.
From this perspective, the entire "receiving HTTP request and serving a web page" requirement does not need to be handled by PHP at all.
But for Node.js, the concept is completely different. When using Node.js, we are not only implementing an application, but also implementing an entire HTTP server. In fact, our web applications and corresponding web servers are basically the same.
It sounds like a lot of work to do, but then we will gradually realize that it is not a troublesome thing for Node.js.
Now let’s start the road to implementation, starting with the first part-HTTP server.
Modules for building applications
A basic HTTP server
When I was about to start writing my first "real" Node.js application, I not only didn't know how to write Node.js code, but I also didn't know how to organize it.
Should I put everything into one file? There are many tutorials on the Internet that will teach you to put all the logic into a basic HTTP server written in Node.js. But what if I want to add more content while still keeping the code readable?
In fact, it is quite simple to keep code separation as long as you put the code for different functions into different modules.
This approach allows you to have a clean main file that you can execute with Node.js; and you can have clean modules that can be called by the main file and other modules.
So, now let’s create a main file that starts our application, and a module that holds our HTTP server code.
In my mind, calling the main file index.js is more or less a standard format. Putting the server module in a file called server.js is easy to understand.
Let’s start with the server module. Create a file called server.js in the root directory of your project and write the following code:
http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World") ;
response.end();
}).listen(8888);
node server.js
Next, open the browser and visit http://localhost:8888/, you will see a web page with "Hello World" written on it.
That’s interesting, isn’t it? Let’s talk about the HTTP server first and leave aside how to organize the project. What do you think? I promise we'll figure that out later.
Analyze HTTP server
So next, let us analyze the composition of this HTTP server.
The first line requests the http module that comes with Node.js and assigns it to the http variable.
Next we call the function provided by the http module: createServer. This function will return an object. This object has a method called listen. This method has a numeric parameter that specifies the port number that the HTTP server is listening on.
Let’s ignore the function definition in the brackets of http.createServer for now.
We could have started the server and listened on port 8888 with code like this:
var server = http.createServer();
server.listen(8888);
The most interesting (and, if you're used to a more conservative language like PHP, weird) part is the first argument to createSever(), a function definition.
In fact, this function definition is the first and only parameter of createServer(). Because in JavaScript, functions can be passed like other variables.
Perform function transfer
For example, you can do this:
function execute(someFunction, value) {
someFunction(value);
}
execute(say, "Hello");
Please read this code carefully! Here, we pass the say function as the first variable of the execute function. What is returned here is not the return value of say, but say itself!
In this way, say becomes the local variable someFunction in execute. execute can use the say function by calling someFunction() (with parentheses).
Of course, since say has a variable, execute can pass such a variable when calling someFunction.
We can, like just did, pass a function as a variable by its name. But we don’t have to go around this “define first, then pass” circle. We can directly define and pass this function in the brackets of another function:
execute(function(word){ console.log(word) }, "Hello");
We directly define the function we are going to pass to execute where execute accepts the first parameter.
In this way, we don’t even need to give the function a name, which is why it is called an anonymous function.
This is our first close encounter with what I consider “advanced” JavaScript, but we still have to take it step by step. For now, let's accept this: in JavaScript, a function can receive a parameter as another function. We can define a function first and then pass it, or we can define the function directly where the parameters are passed.
How function passing makes HTTP servers work
With this knowledge, let’s take a look at our simple but not simple HTTP server:
http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World") ;
response.end();
}).listen(8888);
The same purpose can be achieved with code like this:
function onRequest(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
Event-driven callbacks
This question is not easy to answer (at least for me), but this is the native way Node.js works. It's event driven, which is why it's so fast.
You may want to take a moment to read Felix Geisendörfer’s masterpiece Understanding node.js, which provides some background knowledge.
It all comes down to the fact that "Node.js is event driven". Well, actually I don’t know exactly what this sentence means. But I will try to explain why it makes sense for us to write web based applications with Node.js.
When we use the http.createServer method, of course we don’t just want a server that listens on a certain port, we also want it to do something when the server receives an HTTP request.
The problem is, this is asynchronous: requests can arrive at any time, but our server is running in a single process.
When writing PHP applications, we don’t worry about this at all: any time a request comes in, the web server (usually Apache) creates a new process for this request and starts executing the corresponding processes from beginning to end. PHP script.
So in our Node.js program, when a new request arrives at port 8888, how do we control the process?
Well, this is where the event-driven design of Node.js/JavaScript can really help - although we still have to learn some new concepts to master it. Let's see how these concepts apply to our server code.
We created the server and passed a function to the method that created it. Whenever our server receives a request, this function is called.
We don’t know when this will happen, but we now have a place to handle the request: it’s the function we passed it to. It doesn't matter whether it is a predefined function or an anonymous function.
This is the legendary callback. We pass a function to a method, and this method calls this function to perform a callback when a corresponding event occurs.
For me at least, it took some work to figure it out. If you're still not sure, read Felix's blog post.
Let’s consider this new concept again. How do we prove that after creating the server, our code continues to be valid even if no HTTP request comes in and our callback function is not called? Let’s try this:
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.");
When we run node server.js as usual, it will immediately output "Server has started." on the command line. When we make a request to the server (visit http://localhost:8888/ in the browser), the "Request received." message will appear on the command line.
This is event-driven asynchronous server-side JavaScript and its callbacks!
(Please note that when we access the web page on the server, our server may output "Request received." twice. That is because most servers will try to read when you access http://localhost:8888/ Get http://localhost:8888/favicon.ico )
How the server handles requests
Okay, let’s briefly analyze the remaining part of our server code, which is the main part of our callback function onRequest().
When the callback starts and our onRequest() function is triggered, two parameters are passed in: request and response.
They are objects whose methods you can use to handle the details of an HTTP request and respond to the request (such as sending something back to the requesting browser).
So our code is: when receiving a request, use the response.writeHead() function to send an HTTP status 200 and the content-type of the HTTP header (content-type), and use the response.write() function to add the HTTP corresponding body Send the text "Hello World".
Finally, we call response.end() to complete the response.
For now, we don’t care about the details of the request, so we don’t use the request object.
Where to place the server module
OK, like I promised, we can now get back to how we organize our applications. We now have a very basic HTTP server code in the server.js file, and I mentioned that usually we have a file called index.js that calls other modules of the application (such as the HTTP server module in server.js). Boot and launch the application.
Let’s now talk about how to turn server.js into a real Node.js module so that it can be used by our (not yet started) index.js main file.
Maybe you have noticed that we have used modules in the code. Like this:
...
http.createServer(...);
This turns our local variable into an object with all the public methods provided by the http module.
It is a convention to give such local variables the same name as the module name, but you can also use it to your liking:
...
foo.createServer(...);
You’ll figure it out when we turn server.js into a real module.
In fact, we don’t have to make too many changes. Turning a piece of code into a module means that we need to export the portion of the functionality we want to provide to the script that requests the module.
Currently, the functions that our HTTP server needs to export are very simple, because the script that requests the server module only needs to start the server.
We put our server script into a function called start, which we will then export.
function start() {
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.");
}
exports.start = start;
Create the index.js file and write the following content:
server.start();
Okay. We can now launch our application from our main script, which is still the same as before:
We still only have the initial part of the entire application: we can receive HTTP requests. But we have to do something - the server should respond differently to different URL requests.
For a very simple application, you can do this directly in the callback function onRequest(). But like I said, we should add some abstract elements to make our example a little more interesting.
Handling different HTTP requests is a different part in our code, called "routing" - so, let's create a module called routing next.
How to "route" requests
We need to provide the requested URL and other required GET and POST parameters for the route. Then the route needs to execute the corresponding code based on these data (the "code" here corresponds to the third part of the entire application: a series of requests are received the handler that actually works).
Therefore, we need to look at the HTTP request and extract the requested URL and GET/POST parameters. Whether this function belongs to routing or server (or even as a function of the module itself) is indeed worthy of discussion, but here it is tentatively considered as the function of our HTTP server.
All the data we need will be contained in the request object, which is passed as the first parameter of the onRequest() callback function. But in order to parse this data, we need additional Node.JS modules, which are the url and querystring modules.
Now let’s add some logic to the onRequest() function to find out the URL path requested by the browser:
var http = require("http");
var url = require("url");
function start() {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " pathname " received .");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
Okay, our application can now distinguish between different requests by the URL path of the request - this allows us to use routing (not yet completed) to route the request to the URL path. Baselines are mapped onto handlers.
In the application we are building, this means that requests from /start and /upload can be handled with different code. We'll see how this fits together later.
Now we can write the route. Create a file named router.js and add the following content:
function route(pathname) {
console.log("About to route a request for " pathname);
}
exports.route = route;
As you can see, this code does nothing, but for now it is as it should be. Before adding more logic, let's first look at how to integrate routing and servers.
Our servers should be aware of the existence of routes and use them effectively. We could of course hard-code this dependency to the server, but experience with programming in other languages tells us that this would be a pain, so we will use dependency injection to loosely add the route. Modules (you can read Martin Fowlers' masterpiece on dependency injection for background knowledge).
First, let’s extend the server’s start() function to pass the routing function as a parameter:
function start(route) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " pathname " received.");
route(pathname);
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
server.start(router.route);
If you start the application now (node index.js, always remember this command line) and then request a URL, you will see the application output the corresponding information, which indicates that our HTTP server is already using the routing module, and The requested path will be passed to the route:
Behavior-Driven Execution
Please allow me to deviate from the topic again and talk about functional programming here.
Passing functions as arguments isn’t just for technical reasons. For software design, this is actually a philosophical question. Think about this scenario: In the index file, we can pass the router object in, and the server can then call the route function of this object.
Just like this, we pass something, and then the server uses this thing to complete something. Hi, that thing called routing, can you help me route this?
But the server doesn’t actually need such a thing. It just needs to get things done. In fact, in order to get things done, you don't need things at all, you need actions. That is, you don’t need a noun, you need a verb.
After understanding the most core and basic ideological transformation in this concept, I naturally understood functional programming.
I understood functional programming after reading Steve Yegge’s masterpiece Death Penalty in the Kingdom of Nouns. You should read this book too, really. This is one of the books about software that has ever given me the joy of reading.
Routed to the real request handler
Back to the topic, now our HTTP server and request routing module can communicate with each other as we expected, like a pair of close brothers.
Of course this is not enough. Routing, as the name suggests, means that we have to handle different URLs in different ways. For example, the "business logic" for processing /start should be different from that for processing /upload.
Under the current implementation, the routing process "ends" in the routing module, and the routing module is not the module that really "takes action" on the request, otherwise when our application becomes more complex, it will not be able to Scales well.
We temporarily refer to the function as the routing target as the request handler. Let's not rush into developing the routing module now, because there is little point in improving the routing module if the request handler is not ready.
Applications require new widgets, so add new modules -- there's no need to be novel about it anymore. Let’s create a module called requestHandlers, and for each request handler, add a placeholder function, and then export these functions as module methods:
function upload() {
console.log("Request handler 'upload' was called.");
}
exports.start = start;
exports.upload = upload;
Here we have to make a decision: should we hardcode the requestHandlers module into the route for use, or add a little dependency injection? Although like other patterns, dependency injection should not be used just for usage, in this case, using dependency injection can make the coupling between the route and the request handler looser, and therefore make the route more reusable. .
This means we have to pass request handlers from the server to the route, but this feels even more outrageous. We have to pass a bunch of request handlers all the way from our main file to the server, and then pass them Passed from server to route.
So how do we pass these request handlers? Don't look at it now that we only have 2 handlers. In a real application, the number of request handlers will continue to increase. We certainly don't want to have to complete the request in the route every time there is a new URL or request handler. Mapping to handlers and tossing it over and over again. In addition, there are a lot of if request == x then call handler y in the routing, which also makes the system ugly.
Think about it carefully, there are a lot of things, each of which needs to be mapped to a string (that is, the requested URL)? It seems that an associative array works perfectly.
But the result is a bit disappointing, JavaScript does not provide associative arrays -- can you say that it does? In fact, in JavaScript, it is its objects that can really provide such functionality.
In this regard, http://msdn.microsoft.com/en-us/magazine/cc163419.aspx has a good introduction, I will excerpt it here:
In C or C#, when we talk about objects, we refer to instances of classes or structures. Objects have different properties and methods depending on the template they are instantiated from (the so-called class). But objects are not this concept in JavaScript. In JavaScript, an object is a collection of key/value pairs -- you can think of a JavaScript object as a dictionary with string keys.
But if a JavaScript object is just a collection of key/value pairs, how can it have methods? Well, the value here can be a string, a number, or... a function!
Okay, let’s finally get back to the code. Now that we have decided to pass a series of request handlers through an object, we need to inject this object into the route() function in a loosely coupled way.
We first introduce this object into the main file index.js:
var handle = {}
handle["/"] = requestHandlers.start;
handle["/start"] = requestHandlers.start;
handle["/upload"] = requestHandlers. upload;
server.start(router.route, handle);
As you can see, mapping different URLs to the same request handler is easy: just add a property with the key "/" to the object, corresponding to requestHandlers.start, and we can clean Requests for concisely configured /start and / are handled by the start handler.
After completing the definition of the object, we pass it to the server as an additional parameter. For this purpose, server.js is modified as follows:
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " pathname " received.");
route(handle, pathname);
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
Then we modify the route() function in the route.js file accordingly:
exports.route = route;
With this, we have the server, route and request handler together. Now we start the application and visit http://localhost:8888/start in the browser. The following log can show that the system calls the correct request handler:
Let the request handler respond
Very good. But now it would be nice if the request handler could return some meaningful information to the browser instead of just "Hello World".
What should be remembered here is that the "Hello World" information obtained and displayed after the browser makes a request still comes from the onRequest function in our server.js file.
In fact, "processing a request" simply means "responding to a request." Therefore, we need to enable the request handler to "talk" to the browser like the onRequest function.
Bad implementation
For developers like us with PHP or Ruby technical background, the most straightforward implementation method is actually not very reliable: it seems effective, but in fact it may not be so.
What I mean here by "straightforward implementation" is to let the request handler directly return (return()) the information they want to display to the user through the onRequest function.
Let’s implement it like this first, and then look at why this is not a good way to implement it.
Let’s start by having the request handler return the information that needs to be displayed in the browser. We need to modify requestHandler.js to the following form:
function upload() {
console.log("Request handler 'upload' was called.");
return "Hello Upload";
}
exports.start = start;
exports.upload = upload;
exports.route = route;
Finally, we need to refactor our server.js so that it responds to the browser with the content returned by the request handler via the request route, like this:
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " pathname " received.");
response.writeHead(200, {"Content-Type": "text/plain"});
var content = route(handle, pathname)
response.write(content);
response .end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
Okay, so what’s the problem? To put it simply: when a request handler needs to perform non-blocking operations in the future, our application will "hang".
Don’t understand? It doesn’t matter, let’s explain it in detail below.
Blocking and non-blocking
As mentioned before, problems arise when including non-blocking operations in request handlers. However, before talking about this, let's first take a look at what blocking operations are.
I don’t want to explain the specific meaning of “blocking” and “non-blocking”, let’s just look at what happens when blocking operations are added to the request handler.
Here, let’s modify the start request handler. We let it wait for 10 seconds before returning “Hello Start”. Because there is no operation like sleep() in JavaScript, we can only use a little Hack to simulate the implementation.
Let us modify requestHandlers.js to the following form:
function sleep(milliSeconds) {
var startTime = new Date().getTime();
while (new Date().getTime() < startTime milliSeconds);
}
sleep(10000);
return "Hello Start";
}
function upload() {
console.log("Request handler 'upload' was called.");
return "Hello Upload";
}
exports.start = start;
exports.upload = upload;
(Of course, this is just a simulation of sleeping for 10 seconds. In actual scenarios, there are many such blocking operations, such as some long-term calculation operations.)
Let’s take a look at what changes our changes have brought.
As usual, we need to restart the server first. In order to see the effect, we need to perform some relatively complicated operations (follow me): First, open two browser windows or tabs. Enter http://localhost:8888/start in the address bar of the first browser window, but don't open it yet!
Enter http://localhost:8888/upload in the address bar of the second browser window. Again, don’t open it yet!
Next, do the following: press Enter in the first window ("/start"), then quickly switch to the second window ("/upload") and press Enter.
Notice what happened: the /start URL took 10 seconds to load, which is what we expected. However, the /upload URL actually took 10 seconds, and it did not have an operation similar to sleep() in the corresponding request handler!
Why is this? The reason is that start() contains blocking operations. To put it figuratively, "it blocks all other processing work."
This is obviously a problem, because Node has always advertised itself like this: "In node, except for the code, everything is executed in parallel."
What this sentence means is that Node.js can still process tasks in parallel without adding additional threads - Node.js is single-threaded. It implements parallel operations through event loop, and we should make full use of this - avoid blocking operations as much as possible, and instead use non-blocking operations.
However, to use non-blocking operations, we need to use callbacks by passing the function as a parameter to other functions that take time to process (for example, sleeping for 10 seconds, or querying the database, or performing a large number of calculations ).
For Node.js, it is handled like this: "Hey, probablyExpensiveFunction() (Translator's Note: This refers to functions that take time to process), you continue to deal with your things, I (Node. js thread) I won’t wait for you for now. I will continue to process the code behind you. Please provide a callbackFunction(). I will call the callback function after you finish processing. Thank you! ”
(If you want to know more details about event polling, you can read Mixu’s blog post - Understanding node.js event polling.)
Next, we will introduce a wrong way to use non-blocking operations.
Like last time, we modified our application to expose the problem.
This time we still use the start request handler to "operate". Modify it to the following form:
function start() {
console.log("Request handler 'start' was called.");
var content = "empty";
exec("ls -lah", function (error, stdout, stderr) {
content = stdout;
});
return content;
}
function upload() {
console.log("Request handler 'upload' was called.");
return "Hello Upload";
}
exports.start = start;
exports.upload = upload;
What does exec() do? It executes a shell command from Node.js. In the above example, we use it to get all the files in the current directory ("ls -lah"), and then output the file information to the browser when /startURL is requested.
The above code is very intuitive: Create a new variable content (initial value is "empty"), execute the "ls -lah" command, assign the result to content, and finally return content.
As usual, we start the server and access "http://localhost:8888/start".
A beautiful web page will be loaded with the content "empty". What's going on?
At this time, you may have roughly guessed that exec() plays a magical role in non-blocking. It's actually a good thing, with it, we can perform very time-consuming shell operations without forcing our application to stop and wait for the operation.
(If you want to prove this, you can replace "ls -lah" with a more time-consuming operation such as "find /" to achieve the effect).
However, judging from the results displayed by the browser, we are not satisfied with our non-blocking operation, right?
Okay, next, let’s fix this problem. While we're at it, let's take a look at why the current approach doesn't work.
The problem is that in order to perform non-blocking work, exec() uses a callback function.
In our case, the callback function is the anonymous function passed as the second parameter to exec():
The operation of "ls -lah" here is actually very fast (unless there are millions of files in the current directory). This is why the callback function will be executed quickly - but it is still asynchronous anyway.
To make the effect more obvious, let's imagine a more time-consuming command: "find /", which takes about 1 minute to execute on my machine. However, although in the request handler, I put "ls - lah" is replaced with "find /", when opening the /start URL, you can still get the HTTP response immediately - obviously, when exec() is executed in the background, Node.js itself will continue to execute the following code. And we assume here that the callback function passed to exec() will only be called after the "find /" command is executed.
So how can we display the file list in the current directory to the user?
Okay, now that we understand this bad implementation, let’s introduce how to make the request handler respond to browser requests in the correct way.
Response to requests with non-blocking operations
I just mentioned the phrase – “the right way”. In fact, the "right way" is usually not simple.
However, there is such an implementation solution using Node.js: function passing. Let's take a look at how to implement this in detail.
So far, our application has been able to pass the content returned by the request handler (the request handler will eventually display content to the user) is passed to the HTTP server.
Now we adopt the following new implementation method: instead of passing the content to the server, this time we use the method of "passing" the server to the content. From a practical perspective, the response object (obtained from the server's callback function onRequest()) is passed to the request handler through the request routing. The handler can then respond to the request using functions on that object.
That’s the principle, let’s implement this solution step by step.
Start with server.js:
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " pathname " received.");
route(handle, pathname, response);
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
Let’s take a look at our router.js:
exports.route = route;
If there is no corresponding request handler, we will directly return a "404" error.
Finally, we modify requestHandler.js to the following form:
function start(response) {
console.log("Request handler 'start' was called.");
exec("ls -lah", function (error, stdout, stderr) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write (stdout);
response.end();
});
}
function upload(response) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"} );
response.write("Hello Upload");
response.end();
}
exports.start = start;
exports.upload = upload;
The start handler does the request response operation in the anonymous callback function of exec(), while the upload handler still simply replies "Hello World", but this time it uses the response object.
Now we start the application (node index.js) again and everything will work fine.
If you want to prove that the time-consuming operations in the /start handler will not block the immediate response to the /upload request, you can modify requestHandlers.js to the following form:
function start(response) {
console.log("Request handler 'start' was called.");
exec("find /",
{ timeout: 10000, maxBuffer: 20000*1024 },
function (error, stdout, stderr) {
response.writeHead(200, {"Content- Type": "text/plain"});
response.write(stdout);
response.end();
});
}
function upload(response) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"} );
response.write("Hello Upload");
response.end();
}
exports.start = start;
exports.upload = upload;
더 유용한 시나리오
지금까지 우리는 잘 해왔지만 우리의 애플리케이션은 실용성이 없습니다.
서버, 요청 라우팅 및 요청 핸들러가 완료되었습니다. 이전 사용 사례에 따라 웹사이트에 상호 작용을 추가해 보겠습니다. 사용자가 파일을 선택하고 파일을 업로드한 다음 업로드된 파일을 브라우저에서 확인합니다. 간단하게 유지하기 위해 사용자가 이미지만 업로드하고 앱이 해당 이미지를 브라우저에 표시한다고 가정합니다.
자, 이제 차근차근 구현해 보겠습니다. 이전에 이미 많은 JavaScript 원리와 기술 내용을 소개했으므로 이번에는 속도를 조금 높여보겠습니다.
이 기능을 구현하려면 다음과 같은 두 단계가 있습니다. 먼저 POST 요청(파일이 아닌 업로드)을 처리하는 방법을 살펴보겠습니다. 그런 다음 파일 업로드를 위해 Node.js의 외부 모듈을 사용합니다. 이 구현에는 두 가지 이유가 있습니다.
첫째, Node.js에서 기본 POST 요청을 처리하는 것은 비교적 간단하지만 그 과정에서 많은 것을 배울 수 있습니다.
둘째, Node.js를 사용하여 파일 업로드(멀티파트 POST 요청)를 처리하는 것은 상대적으로 복잡하며 이 책의 범위를 벗어납니다. 그러나 외부 모듈을 사용하는 방법은 이 책의 범위에 속합니다.
POST 요청 처리
간단한 예를 생각해 보겠습니다. 사용자가 콘텐츠를 입력할 수 있는 텍스트 영역을 표시한 다음 POST 요청을 통해 서버에 제출합니다. 마지막으로 서버는 요청을 수신하고 핸들러를 통해 입력 내용을 브라우저에 표시합니다.
/start 요청 핸들러는 텍스트 영역이 있는 양식을 생성하는 데 사용되므로 requestHandlers.js를 다음 형식으로 수정합니다.
function start(response) {
console.log("요청 핸들러 'start'가 호출되었습니다.");
var body = ''
'
response.write(body);
response.end();
}
console.log("요청 핸들러 'upload'가 호출되었습니다.");
response.writeHead(200, {"Content-Type": "text/plain"} );
response.write("안녕하세요 업로드");
response.end();
}
exports.upload = upload;
자, 이제 우리의 신청서가 매우 완성되어 Webby Awards를 수상할 수도 있습니다. 하하. (번역자 주: 웨비 어워드는 국제디지털예술과학아카데미(International Academy of Digital Arts and Sciences)가 후원하는 세계 최고의 웹사이트를 선정하는 상입니다. 자세한 내용은 상세 설명을 참조하세요.) http://localhost를 방문하시면 보실 수 있습니다: 8888/start in your browser 간단한 양식입니다. 서버를 다시 시작하는 것을 잊지 마세요!
나머지 공간에서는 더 흥미로운 문제에 대해 논의하겠습니다. 사용자가 양식을 제출하면 /upload 요청 핸들러가 트리거되어 POST 요청을 처리합니다.
이제 우리는 초보자 중 전문가이므로 비동기 콜백을 사용하여 POST 요청 데이터를 비차단 방식으로 처리하는 것을 생각하는 것이 당연합니다.
여기에서는 비차단이 사용됩니다.