一篇关于node入门之后再回来入次门的学习
本篇文档的整理源自 https://www.jianshu.com/p/bf187fed8609,对于我个人而言,让我意识到我在学习node的时候关于一些代码的思考根本没有,学习完之后以为自己会了,其实还是什么都不明白。
本书中的代码案例都在Node.js 0.6.11版本中测试过,可以正确工作。
读完本书之后,你将完成一个完整的web应用,该应用允许用户浏览页面以及上传文件。
本书先从介绍在Node.js环境中进行JavaScript开发和在浏览器环境中进行JavaScript开发的差异开始。
紧接着,会带领大家完成一个最传统的“Hello World”应用,这也是最基础的Node.js应用。
最后,会和大家讨论如何设计一个“真正”完整的应用,剖析要完成该应用需要实现的不同模块,并一步一步介绍如何来实现这些模块。
可以确保的是,在这过程中,大家会学到JavaScript中一些高级的概念、如何使用它们以及为什么使用这些概念就可以实现而其他编程语言中同类的概念就无法实现。
该应用所有的源代码都可以通过 Github代码仓库查看,你也可以选择先将代码clone之后再继续进行下面的阅读。
一、本文档结构
目录
一、本文档结构
二、JavaScript与Node.js
- JavaScript与你
- 简短申明
- 服务器端JavaScript
- “Hello World”
三、一个完整的基于Node.js的web应用
- 用例
- 应用不同模块分析
四、构建应用的模块
- 一个基础的HTTP服务器
- 分析HTTP服务器
- 进行函数传递
- 函数传递是如何让HTTP服务器工作的
- 基于事件驱动的回调
- 服务器是如何处理请求的
- 服务端的模块放在哪里
- 如何来进行请求的“路由”
- 行为驱动执行
- 路由给真正的请求处理程序
- 让请求处理程序作出响应
①.不好的实现方式
②.阻塞与非阻塞
③.以非阻塞操作进行请求响应
五、更有用的场景
- 处理POST请求
- 处理文件上传
六、总结与展望
二、JavaScript与Node.js(4节)
1.JavaScript与你
Node.js,服务端的JavaScript。----重新认识JavaScript。
2.简短申明
本书并不是一本“从入门到精通”的书,更像是一本“从初级入门到高级入门”的书。
如果成功的话,那么本书就是我们开始学习Node.js最希望拥有的教程。
3.服务端JavaScript
要实现在后台运行JavaScript代码,代码需要先被解释然后正确的执行。Node.js的原理正是如此,它使用了Google的V8虚拟机 (Google的Chrome浏览器使用的JavaScript执行环境),来解释和执行JavaScript代码。
Node.js事实上既是一个运行时环境,同时又是一个库
4."Hello World"
打开你最喜欢的编辑器,创建一个helloworld.js文件。输出“Hello World”,如下是实现该功能的代码:
console.log("Hello World");
保存该文件,并通过Node.js来执行:
node helloworld.js
正常的话,就会在终端输出Hello World 。
第二部分结束。
三、一个完整的基于Node.js的web应用
1.用例
我们来把目标设定得简单点,不过也要够实际才行:
- 用户可以通过浏览器使用我们的应用。
- 当用户请求http://domain/start时,可以看到一个欢迎页面,页面上有一个文件上传的表单。
- 用户可以选择一个图片并提交表单,随后文件将被上传到http://domain/upload,该页面完成上传后会把图片显示在页面上。
差不多了,你现在也可以去Google一下,找点东西乱搞一下来完成功能。但是我们现在先不做这个。
更进一步地说,在完成这一目标的过程中,我们不仅仅需要基础的代码而不管代码是否优雅。我们还要对此进行抽象,来寻找一种适合构建更为复杂的Node.js应用的方式。
2.应用不同模块分析(很重要很受益的一部分)
我们来分解一下这个应用,为了实现上文的用例,我们需要实现哪些部分呢?
- 我们需要提供Web页面,因此需要一个HTTP服务器
- 对于不同的请求,根据请求的URL,我们的服务器需要给予不同的响应,因此我们需要一个路由,用于把请求对应到请求处理程序(request handler)
- 当请求被服务器接收并通过路由传递之后,需要可以对其进行处理,因此我们需要最终的请求处理程序
- 路由还应该能处理POST数据,并且把数据封装成更友好的格式传递给请求处理入程序,因此需要请求数据处理功能
- 我们不仅仅要处理URL对应的请求,还要把内容显示出来,这意味着我们需要一些视图逻辑供请求处理程序使用,以便将内容发送给用户的浏览器
- 最后,用户需要上传图片,所以我们需要上传处理功能来处理这方面的细节
现在我们就来开始实现之路,先从第一个部分--HTTP服务器着手。
四、构建应用的模块
1.一个基础的HTTP服务器
现在我们来创建一个用于启动我们的应用的主文件,和一个保存着我们的HTTP服务器代码的模块。
为便于理解,把主文件叫做index.js或多或少是个标准格式。把服务器模块放进叫server.js的文件里则很好理解。
让我们先从服务器模块开始。如果你clone了项目会发现我们将要写的server.js文件是不同的,从头开始,在你的项目的根目录下创建一个叫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);
这个时候,在终端中输入
node server
,浏览器访问http://localhost:8888,你会看到一个写着“Hello World”的网页。到这里相信大家都非常非常的熟悉,但是关于这几行代码的原理与分析你是否考虑过,或者自己明白了?到这里,我们就先来谈谈HTTP服务器的问题,而把如何组织项目的事情先放一边,我相信对于理解不深的人来说,会让你对这几行代码有一个更清楚的理解。
2.分析HTTP服务器
第一行请求(
require
)Node.js自带的 http 模块,并且把它赋值给 http 变量。
接下来我们调用http模块提供的函数: createServer 。这个函数会返回一个对象,这个对象有一个叫做 listen 的方法,这个方法有一个数值参数,指定这个HTTP服务器监听的端口号。
咱们暂时先不管 http.createServer 的括号里的那个函数定义。
我们本来可以用这样的代码来启动服务器并侦听8888端口:
var server = http.createServer();
server.listen(8888);
至此你肯定名了http是Node.js自带的模块,也知道了createServer是http模块提供的函数,并且知道了这个函数返回的对象有一个listen侦听端口的方法。接下来我们要开始对createServer函数中的内容进行一个剖析。
3.进行函数传递
我们先来看几行代码,并仔细阅读它:
function say(word){
console.log(word);
}
function execute(someFunction,value){
someFunction(value);
}
execute(say,"Hello")
代码分析(慢慢阅读,不急不急):在这里,我们把 say 函数作为execute函数的第一个变量进行了传递。这里返回的不是 say 的返回值,而是 say 本身!
这样一来, say 就变成了execute 中的本地变量 someFunction ,execute可以通过调用 someFunction() (带括号的形式)来使用 say 函数。
当然,因为 say 有一个变量, execute 在调用 someFunction 时可以传递这样一个变量。
我们可以,就像刚才那样,用它的名字把一个函数作为变量传递。但是我们不一定要绕这个“先定义,再传递”的圈子,我们可以直接在另一个函数的括号中定义和传递这个函数:
function execute(someFunction,value){
someFunction(value)
}
execute(function(word){console.log(word)},"Hello")
代码的啰嗦分析:
- 我们在 execute 接受第一个参数的地方直接定义了我们准备传递给 execute 的函数。
- 用这种方式,我们甚至不用给这个函数起名字,这也是为什么它被叫做 匿名函数 。
- 我们现在可以接受这一点:在JavaScript中,一个 函数可以作为另一个函数接收一个参数。我们可以先定义一个函数,然后传递,也可以在传递参数的地方直接定义函数。
本章总共11节,本章第三节结束。
4.函数传递是如何让HTTP服务器工作的
带着刚才我们刚学习的一点小东西(如果有丁点疑惑,返回去继续瞅一瞅,确保完全明白每个字节的意思),我们来看看我们简约而不简单的HTPP服务器:
var http = require("http");
http.createServer(function(request, response){
response.writeHead(200,{"Content-Type":"text/plain"});
response.write("Hello World");
response.end();
}).listen(8888);</pre>
是的,这个时候你已经知道,我们向createServer函数传递了一个匿名函数。我们可以将上面代码进行改观与前面的知识进行碰撞(同样的目的与作用):
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);
这个时候你有没有疑问?我们为什么要用这样的方式呢?对于刚刚开始学习看到这里的我来说,我肯定不会去想这个问题的,开玩笑,我甚至都没去想http这个东西是node自带的模块。
那么作者提出的这个为什么,到底是想要说什么?我们继续进行下一节--【基于事件驱动的回调】,什么是【基于事件驱动的回调】?这是什么东西?
这个【基于事件驱动的回调】是个什么东西?重要的文字写三遍。
本章总共十一节,下面我们开始第五节的内容。
5.基于事件驱动的回调
于是我带着【基于事件驱动的回调】这个名词来到本节的时候,作者首先说了一句“这个问题不好回答(至少对于我来说)”
然后关于这个事件驱动回调的背景知识,作者给了一篇文章的链接,感兴趣的可以去瞅瞅:http://debuggable.com/posts/understanding-node-js:4bd98440-45e4-4a9a-8ef7-0f7ecbdd56cb
这一切都归结于“Node.js是事件驱动的”这一事实。好吧,其实我也不是特别确切的了解这句话的意思。不过我会试着解释,为什么它对我们用Node.js写网络应用(Web based application)是有意义的。
- 当我们使用 http.createServer 方法的时候,我们当然不只是想要一个侦听某个端口的服务器,我们还想要它在服务器收到一个HTTP请求的时候做点什么。
问题是,这是异步的:请求任何时候都可能到达,但是我们的服务器却跑在一个单进程中.
在我们的Node.js程序中,当一个新的请求到达8888端口的时候,我们怎么控制流程呢?
- 我们创建了服务器,并且向创建它的方法传递了一个函数。无论何时我们的服务器收到一个请求,这个函数就会被调用。
- 我们不知道这件事情什么时候会发生,但是我们现在有了一个处理请求的地方:它就是我们传递过去的那个函数。至于它是被预先定义的函数还是匿名函数,就无关紧要了
- 这个就是传说中的 回调 。我们给某个方法传递了一个函数,这个方法在有相应事件发生时调用这个函数来进行 回调 。
让我们再来琢磨琢磨这个新概念。我们怎么证明,在创建完服务器之后,即使没有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.");
代码与解析分析:
当我们与往常一样,运行它node server.js
时,它会马上在命令行上输出“Server has started.”。当我们向服务器发出请求(在浏览器访问http://localhost:8888/ ),“Request received.”这条消息就会在命令行中出现。
这就是事件驱动的异步服务器端JavaScript和它的回调啦!
(请注意,当我们在服务器访问网页时,我们的服务器可能会输出两次“Request received.”。那是因为大部分服务器都会在你访问 http://localhost:8888 /时尝试读取 http://localhost:8888/favicon.ico )
本节总结:说了一大堆,通过代码我们可以简单理解,服务器启动执行后端console
方法,客户端向服务器发出请求,再执行另一个console
方法,这就是事件驱动的异步服务端JavaScript和它的回调。关于这个问题可自行多多研究。
本章总共11节,本章第五节结束。
6.服务器是如何处理请求的
好的,接下来我们简单分析一下我们服务器代码中剩下的部分,也就是我们的回调函数 onRequest() 的主体部分。
当回调启动,我们的 onRequest() 函数被触发的时候,有两个参数被传入: request 和 response 。
它们是对象,你可以使用它们的方法来处理HTTP请求的细节,并且响应请求(比如向发出请求的浏览器发回一些东西)。
所以我们的代码就是:当收到请求时,使用 response.writeHead() 函数发送一个HTTP状态200和HTTP头的内容类型(content-type),使用 response.write() 函数在HTTP相应主体中发送文本“Hello World"。
最后,我们调用 response.end() 完成响应。
目前来说,我们对请求的细节并不在意,所以我们没有使用 request 对象。
7.服务端的模块放在哪里
到这里我们开始说
如何组织应用
这个问题上。
把服务器脚本放到一个叫做start的函数里,然后导出这个函数,在主文件中使用:
#server.js
var http = require("http");
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;
#index.js
var server = require("./server");
server.start();
我们仍然只拥有整个应用的最初部分:我们可以接收HTTP请求。但是我们得做点什么——对于不同的URL请求,服务器应该有不同的反应。对于一个非常简单的应用来说,你可以直接在回调函数 onRequest() 中做这件事情,但是,当然应用不会这么简单了。
处理不同的HTTP请求在我们的代码中是一个不同的部分,叫做“路由选择”——那么,我们接下来就创造一个叫做 路由 的模块吧。
本章总共11节,本章第⑦节结束。
8.如何来进行请求的“路由”
我们要为路由提供请求的URL和其他需要的GET及POST参数,随后路由需要根据这些数据来执行相应的代码.
因此,我们需要查看HTTP请求,从中提取出请求的URL以及GET/POST参数。
我们需要的所有数据都会包含在request对象中,该对象作为onRequest()回调函数的第一个参数传递。但是为了解析这些数据,我们需要额外的Node.JS模块,它们分别是
ur
l和querystring
模块。
现在我们来给onRequest()函数加上一些逻辑,用来找出浏览器请求的URL路径:
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;
将上面代码进行运行后,输入不同的url可以看到会输出不同的
console
,我们可以通过请求的URL不同来区别不同的请求了。
现在我们可以来编写路由了,建立一个名为
router.js
的文件,添加以下内容:
function route(pathname){
console.log("About to route a request for "+ pathname);
}
exports.route = route;
这个时候,
index.js
、server.js
、rounter.js
全都有了,如何互相关联就不赘述了。直接进入下一节。
9.行为驱动执行
谈一谈函数编程:
将函数作为参数传递并不仅仅出于技术上的考量。对软件设计来说,这其实是个哲学问题。想想这样的场景:在index文件中,我们可以将router对象传递进去,服务器随后可以调用这个对象的route函数。
就像这样,我们传递一个东西,然后服务器利用这个东西来完成一些事。嗨那个叫路由的东西,能帮我把这个路由一下吗?
但是服务器其实不需要这样的东西。它只需要把事情做完就行,其实为了把事情做完,你根本不需要东西,你需要的是动作。也就是说,你不需要名词,你需要动词。
理解了这个概念里最核心、最基本的思想转换后,我自然而然地理解了函数编程。
据说这篇文章会让你对函数编程有一个更好的理解:https://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html