Node.js入门

Posted

    tags:

    篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Node.js入门 相关的知识,希望对你有一定的参考价值。

    今天拜读了Manuel Kiessling大神的《Node入门》的博客,感觉收获很多,这篇文章非常适合有javascript基础和掌握了一门后台语言(Java,Python等)的想入门node的学习者。

    文章循循善诱,一步一步升级知识,老外的文章很有思想,写的确实比国内的XX好的多。

    这里我就开一篇博客记录一下。

     

    什么是Node.js

    1.中文网官方定义:Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效,包管理器 npm,是全球最大的开源库生态系统。
    现在安装一些东西确实直接npm很方便。

    2.JS以前一直只在浏览器前端上做一些交互性的操作,现在有了Node之后,它允许在后端(脱离浏览器环境)运行JavaScript代码。

    要实现在后台运行JavaScript代码,代码需要先被解释然后正确的执行。Node.js的原理正是如此,它使用了Google的V8虚拟机(Google的Chrome浏览器使用的JavaScript执行环境),来解释和执行JavaScript代码。

    3.除此之外,伴随着Node.js的还有许多有用的模块,它们可以简化很多重复的劳作,比如向终端输出字符串。

    因此,Node.js事实上既是一个运行时环境,同时又是一个库。

    一个完整的基于Node.js的web应用

    下面我们就跟着做一个这样的应用,便于理解Node的机制。

    我们来把目标设定得简单点,不过也要够实际才行:

    • 用户可以通过浏览器使用我们的应用。
    • 当用户请求http://domain/start时,可以看到一个欢迎页面,页面上有一个文件上传的表单。
    • 用户可以选择一个图片并提交表单,随后文件将被上传到http://domain/upload,该页面完成上传后会把图片显示在页面上。

    这里先不慌具体实现确定的功能,先实现响应事件。

    我们来分解一下这个应用,为了实现上文的用例,我们需要实现哪些部分呢?

    • 我们需要提供Web页面,因此需要一个HTTP服务器
    • 对于不同的请求,根据请求的URL,我们的服务器需要给予不同的响应,因此我们需要一个路由,用于把请求对应到请求处理程序(request handler)
    • 当请求被服务器接收并通过路由传递之后,需要可以对其进行处理,因此我们需要最终的请求处理程序
    • 路由还应该能处理POST数据,并且把数据封装成更友好的格式传递给请求处理入程序,因此需要请求数据处理功能
    • 我们不仅仅要处理URL对应的请求,还要把内容显示出来,这意味着我们需要一些视图逻辑供请求处理程序使用,以便将内容发送给用户的浏览器
    • 最后,用户需要上传图片,所以我们需要上传处理功能来处理这方面的细节

    使用Node.js时,我们不仅仅在实现一个应用,同时还实现了整个HTTP服务器。事实上,我们的Web应用以及对应的Web服务器基本上是一样的。

    为了保证代码的可维护性和可扩展性,要把不同功能的代码放入不同的模块中,保持代码分离还是相当简单的。这种方法允许你拥有一个干净的主文件(main file),你可以用Node.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);

    (注意可能8888这个端口是被占用的,在命令窗口执行node server.js 会报错,我改成其他端口后即可。)

    接下来,打开浏览器访问http://localhost:8888/,你会看到一个写着“Hello World”的网页。

    这里:

    1.在JavaScript中,函数和其他变量一样都是可以被传递的。

    eg:

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

    甚至我们可以直接用匿名函数:

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

    这个其实如果刚从C/C++转过来其实是不太适应JS得这种特性的........

    2.函数传递是如何让HTTP服务器工作的,我们为什么要用这种方式呢,答案是Node.js原生的工作方式就是基于事件驱动的回调,它是事件驱动的,这也是它为什么这么快的原因。具体的深究这篇入门就先暂时不触及。

    当我们使用 http.createServer 方法的时候,我们当然不只是想要一个侦听某个端口的服务器,我们还想要它在服务器收到一个HTTP请求的时候做点什么。

      问题是,这是异步的:请求任何时候都可能到达,但是我们的服务器却跑在一个单进程中。写php应用的时候,我们一点也不为此担心:任何时候当有请求进入的时候,网页服务器(通常是Apache)就为这一请求新建一个进程,并且开始从头到尾执行相应的PHP脚本。

      那么在我们的Node.js程序中,当一个新的请求到达8888端口的时候,我们怎么控制流程呢?

      嗯,这就是Node.js/JavaScript的事件驱动设计能够真正帮上忙的地方了——虽然我们还得学一些新概念才能掌握它。

      我们创建了服务器,并且向创建它的方法传递了一个函数。无论何时我们的服务器收到一个请求,这个函数就会被调用。

      我们不知道这件事情什么时候会发生,但是我们现在有了一个处理请求的地方:它就是我们传递过去的那个函数。至于它是被预先定义的函数还是匿名函数,就无关紧要了。

      这个就是传说中的 回调 。我们给某个方法传递了一个函数,这个方法在有相应事件发生时调用这个函数来进行 回调 。

      

     

    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.");

     

      注意:在 onRequest (我们的回调函数)触发的地方,我用 console.log 输出了一段文本。在HTTP服务器开始工作之后,也输出一段文本。

      当我们与往常一样,运行它node server.js时,它会马上在命令行上输出“Server has started.”。当我们向服务器发出请求(在浏览器访问http://localhost:8888/),“Request received.”这条消息就会在命令行中出现。

      这就是事件驱动的异步服务器端JavaScript和它的回调啦!

      (我的浏览器会出现两次Request received.原因是大部分浏览器都会在你访问 http://localhost:8888/ 时尝试读取 http://localhost:8888/favicon.ico ,这是浏览器发起的,对图标文件的请求,可以url匹配下favicon.ico,匹配到直接返回)

    这里:

    1.通过response.writeHead发送一个请求状态码和内容类型,使用 response.write() 函数在HTTP响应主体中发送文本“Hello World"。

    2.response.end()这个方法告诉服务器,所有的响应头和响应体已经发送,服务器可以认为消息结束。

    3.原文想证明的是只要创建了http服务器,这个回调函数就一直是存在的,这与原生的JS就不同了,一般没有环境的纯JS代码要想一直让一个函数存在,就创建一个全局变量指向这个函数。Node环境看来是有点不同了??。。。(这里我不是太懂,记录下

    4.Node.js中自带了一个叫做“http”的模块,我们在我们的代码中请求它并把返回值赋给一个本地变量。这把我们的本地变量变成了一个拥有所有 http 模块所提供的公共方法的对象。给这种本地变量起一个和模块名称一样的名字是一种惯例,但是你也可以按照自己的喜好来。

     

    重构

    把某段代码变成模块意味着我们需要把我们希望提供其功能的部分 导出 到请求这个模块的脚本。

     目前,我们的HTTP服务器需要导出的功能非常简单,因为请求服务器模块的脚本仅仅是需要启动服务器而已。

    我们把我们的服务器脚本放到一个叫做 start 的函数里,然后我们会导出这个函数。

    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 并在其中启动我们的HTTP了,虽然服务器的代码还在 server.js 中。

    创建 index.js 文件并写入以下内容:

    var server = require("./server");
    
    server.start();

    启动:node index.js

     

    如何来进行请求的“路由”

    我们要为路由提供请求的URL和其他需要的GET及POST参数,随后路由需要根据这些数据来执行相应的代码(这里“代码”对应整个应用的第三部分:一系列在接收到请求时真正工作的处理程序)。

    因此,我们需要查看HTTP请求,从中提取出请求的URL以及GET/POST参数。这一功能应当属于路由还是服务器(甚至作为一个模块自身的功能)确实值得探讨,但这里暂定其为我们的HTTP服务器的功能。

    我们需要的所有数据都会包含在request对象中,该对象作为onRequest()回调函数的第一个参数传递。但是为了解析这些数据,我们需要额外的Node.JS模块,它们分别是urlquerystring模块。

     

     

    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;

    在我们所要构建的应用中,这意味着来自/start/upload的请求可以使用不同的代码来处理。稍后我们将看到这些内容是如何整合到一起的。

    现在我们可以来编写路由了,建立一个名为router.js的文件,添加以下内容:

    function route(pathname) {
      console.log("About to route a request for " + pathname);
    }
    
    exports.route = route;

    首先,我们来扩展一下服务器的start()函数,以便将路由函数作为参数传递过去:

    var http = require("http");
    var url = require("url");
    
    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;

    同时,我们会相应扩展index.js,使得路由函数可以被注入到服务器中:

    var server = require("./server");
    var router = require("./router");
    
    server.start(router.route);

     

    路由给真正的请求处理程序

    当然这还远远不够,路由,顾名思义,是指我们要针对不同的URL有不同的处理方式。例如处理/start的“业务逻辑”就应该和处理/upload的不同。

    应用程序需要新的部件,因此加入新的模块 -- 已经无需为此感到新奇了。我们来创建一个叫做requestHandlers的模块,并对于每一个请求处理程序,添加一个占位用函数,随后将这些函数作为模块的方法导出:

    function start() {
      console.log("Request handler \'start\' was called.");
    }
    
    function upload() {
      console.log("Request handler \'upload\' was called.");
    }
    
    exports.start = start;
    exports.upload = upload;

    .......................(省略)

    .......................(省略)

    .......................(省略)

    这里我直接给出最后的重构版本吧。。。。

    启动文件index.js:

    var server = require("./server");
    var router = require("./router");
    var requestHandlers = require("./requestHandlers");
    
    var handle = {}
    handle["/"] = requestHandlers.start;
    handle["/start"] = requestHandlers.start;
    handle["/upload"] = requestHandlers.upload;
    
    server.start(router.route, handle);

    服务器文件server.js:

    var http = require("http");
    var url = require("url");
    
    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(8001);
      console.log("Server has started.");
    }
    
    exports.start = start;

    路由文件router.js:

    function route(handle, pathname, response) {
      console.log("About to route a request for " + pathname);
      if (typeof handle[pathname] === \'function\') {
        handle[pathname](response);
      } else {
        console.log("No request handler found for " + pathname);
        response.writeHead(404, {"Content-Type": "text/plain"});
        response.write("404 Not found");
        response.end();
      }
    }
    
    exports.route = route;

    (路由目标的函数)请求处理程序:requestHandlers.js:

    var exec = require("child_process").exec;
    
    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;

    这里的问题内容很多。。:

    1.一个是阻塞与非阻塞问题,以非阻塞操作进行请求响应。这涉及到Node的机制,用Node.js就有这样一种实现方案: 函数传递,从实践角度来说,就是将response对象(从服务器的回调函数onRequest()获取)通过请求路由传递给请求处理程序。原文中讲的非常好,举了一个很好的例子,

    function start() {
      console.log("Request handler \'start\' was called.");
    
      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;

    这个模拟sleep的操作它阻塞了所有其他的处理工作,如果你同时打开两个网页,输入start和upload会发现后者也延迟了。

    因为Node一向是这样来标榜自己的:“在node中除了代码,所有一切都是并行执行的”

    这句话的意思是说,Node.js可以在不新增额外线程的情况下,依然可以对任务进行并行处理 —— Node.js是单线程的。它通过事件轮询(event loop)来实现并行操作,对此,我们应该要充分利用这一点 —— 尽可能的避免阻塞操作,取而代之,多使用非阻塞操作。

    然而,要用非阻塞操作,我们需要使用回调。

    2.我们利用回调函数后又发现一个新的问题:

    我们的代码是同步执行的,这就意味着在调用exec()之后,Node.js会立即执行 return content ;在这个时候,content仍然是“empty”,因为传递给exec()的回调函数还未执行到——因为exec()的操作是异步的。

    返回的结果是empty。

    我们这里“ls -lah”的操作其实是非常快的(除非当前目录下有上百万个文件)。这也是为什么回调函数也会很快的执行到 —— 不过,不管怎么说它还是异步的。

    解决办法就是"函数传递",这次我们将response对象作为第三个参数传递给route()函数,并且,我们将onRequest()处理程序中所有有关response的函数调都移除,因为我们希望这部分工作让route()函数来完成。同样的模式:相对此前从请求处理程序中获取返回值,这次取而代之的是直接传递response对象。

    我们的处理程序函数需要接收response参数,为了对请求作出直接的响应。

    测试:

    如果想要证明/start处理程序中耗时的操作不会阻塞对/upload请求作出立即响应的话,可以将requestHandlers.js修改为如下形式:

    var exec = require("child_process").exec;

    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;

    这样一来,当请求http://localhost:8888/start的时候,会花10秒钟的时间才载入,而当请求http://localhost:8888/upload的时候,会立即响应,纵然这个时候/start响应还在处理中。

     

     传送门:Manuel Kiessling:Node入门

    以上是关于Node.js入门 的主要内容,如果未能解决你的问题,请参考以下文章

    Node.js-入门

    Node.js 原生开发入门完全教程(上)

    (译+注解)node.js的C++扩展入门

    Node.js开发入门—HelloWorld再分析

    node.js调试入门

    前端知识总结——Node.js入门