Pinvon's Blog

所见, 所闻, 所思, 所想

Node.js 学习

安装

nvm

nvm 安装后, 在终端临时换源的办法:

export NVM_NODEJS_ORG_MIRROR=https://npm.taobao.org/mirrors/node/
nvm install v8.9.0

# 设置默认版本
nvm alias default v8.9.0

npm是Node.js的包管理工具, 在Node.js上开发时,会用到很多别人写的JavaScript代码。如果我们要使用别人写的某个包,每次都根据名称搜索一下官方网站,下载代码,解压,再使用,非常繁琐。于是一个集中管理的工具应运而生:大家都把自己开发的模块打包后放到npm官网上,如果要使用,直接通过npm安装就可以直接用,不用管代码存在哪,应该从哪下载。

npm 换源

打开 ~/.npmrc, 编辑内容如下(如果不存在该文件, 则新建一个):

registry = https://registry.npm.taoba.org

这种办法会在每次使用 npm 命令时, 都使用配置文件里的源. 据说, 在 node 7.1.1 之后, 会有一些模块搜索不到, 要改回原来的源才能搜索成功(这边在终端指定源, 因为一般情况下用国内源会更快, 而搜索不到的情况较少):

npm install --registry=https://registry.npmjs.org

Hello World

创建名为helloworld.js的文件, 输入如下语句:

'use strict';
console.log('Hello World');

第一行的'use strick'表示以严格模式运行JavaScript代码, 避免各种潜在错误.

如果文件很多, 每个都写上'use strict'则稍显麻烦, 可以使用 node --use_strick xxx.js 来以严格模式执行此文件.

执行:

node helloworld.js

完整的Web应用程序

目标

用户请求某网址, 进入欢迎界面, 页面上有一个文件上传的表单, 用户可以选择一个图片并提交表单, 随后文件被上传, 上传完成后将图片显示在页面上.

分析

  • 需要Web页面, 因此要有一个HTTP服务器.
  • 对于不同的请求, 根据不同的URL, 服务器需要给予不同的响应, 因此需要一个路由, 把请求对应到请求处理程序.
  • 当请求被路由传递之后, 需要对其进行处理, 因此要有一个请求处理程序.
  • 路由要能处理POST数据, 并且能把数据封装成更友好的格式传递给请求处理程序, 因此需要请求数据处理功能.
  • 要显示上传的内容, 因此需要一些视图逻辑供请求处理程序使用, 以便将内容发送给用户的浏览器.
  • 需要上传处理功能.

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

//进入命令行执行:
node server.js

然后打开浏览器输入http://localhost:8888 即可访问.

分析: 首先请求Node.js自带的http模块, 赋给http变量. 调用http模块提供的函数createServer, 该函数返回一个对象, 该对象有个listen方法, 参数指定了http服务器监听的端口号. 向createServer函数传递了一个匿名函数.

回调函数

我们给某个方法A传递了一个参数B, 该参数是一个函数名, 这个方法在有相应事件发生时调用这个函数来进行回调. 方法A是提前写好的, 而方法B是后期根据实际需求写的.

代码的组织

通常使用index.js的文件来作为主模块, 调用其他的模块, 如server.js.

但是之前的server.js并不是一个模块, 把某段代码变成模块, 意味着我们需要把我们希望提供其功能的部分导出到请求这个模块的脚本.

可以把服务器脚本放到一个叫做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.js");
server.start();

然后执行命令 node index.js 即可.

路由

对于不同的URL请求, 服务器应该有不同的反应.如果是非常简单的应用, 可以直接在回调函数onRequest()中做这件事. 但是对于稍微复杂一些的应用, 还是要有组织较好.

我们需要为路由提供请求的URL和其他需要的GET及POST参数, 随后路由需要根据这些数据来执行相应的代码.

查看HTTP请求, 提取URL及GET/POST参数, 可以由路由提供或者服务器提供, 此处, 我们暂定其为HTTP服务器的功能.

所需要的数据都包含在request对象中, 该对象作为onRequest()回调函数的第一个参数进行传递. 但是为了解析这些数据, 需要使用url模块和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, 根据不同的URL, 来区别不同的请求, 并以此为基础映射到相应的处理程序上.

编写路由, 新建一个名为route.js的文件:

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

这段代码没做什么具体的事, 应该现在的重点是如何把路由和服务器整合起来, 然后才开始编写处理程序.

修改server.js:

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.js");
var router = require("./router.js");
server.start(router.route);

执行 node index.js 后, 随便输入一个路由, 即可在命令行看到输出信息.

真正的请求处理程序

在将服务器模块与路由模块整合之后, 开始编写真正的请求处理程序.

创建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;

这样可以把请求处理程序和路由模块连接起来, 让路由"有路可寻".

JavaScript的对象可以看成是一个键为字符串类型的字典, 值可以是字符串, 数字或函数.

修改index.js:

var server = require("./server.js");
var router = require("./router.js");
var requestHandlers = require("./requestHandlers");

var handle = {}
handle["/"] = requestHandlers.start;
handle["/start"] = requestHandlers.start;
handlex["/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.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()函数里添加了handle参数, 并且把handle对象作为第一个参数传递给route()回调函数.

修改router.js:

function route(handle, pathname) {
    console.log("About to route a request for " + pathname);
    if(typeof handle[pathname] === 'function') {
        handle[pathname]();
    } else {
        console.log("No request handler found for " + pathname);
    }
}
exports.route = route;

首先检查给定的路径对应的请求处理程序是否存在, 如果存在的话直接调用相应的函数.

阻塞与非阻塞

阻塞

在JavaScript中没有sleep()函数, 可以使用其他形式来实现.

修改requestHandlers.js:

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;

修改router.js:

function route(handle, pathname) {
  console.log("About to route a request for " + pathname);
  if (typeof handle[pathname] === 'function') {
    return handle[pathname]();
  } else {
    console.log("No request handler found for " + pathname);
    return "404 Not found";
  }
}

exports.route = route;

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

        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;

此时, 如果我们先开start界面, 再开upload界面, 则start和upload都会加载10秒, 因为start中的sleep()函数阻塞了upload界面的加载.

Node.js可以在不新增额外线程的情况下, 依然可以对任务进行并行处理----Node.js是单线程的. 它通过事件轮询来实现并行操作, 对此, 我们应该要充分利用这一点----尽可能的避免阻塞操作, 取而代之, 多使用非阻塞操作. 要使用非阻塞操作, 我们需要使用回调, 通过将函数作为参数传递给其他需要花时间做处理的函数.

非阻塞的错误用法

修改requestHandlers.js:

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

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;

child_process模块可以实现一个既简单又实用的非阻塞操作exec(). exec()在Node.js中执行一个shell命令, 在这个例子中, 我们用它来获取当前目录下所有的文件, 然后, 当/start URL请求的时候将文件信息输出到浏览器中.

但是, 实际运行的结果却是输出"empty". 因为shell操作是个耗时操作, 而非阻塞时, 浏览器显示content的信息并不需要先停下来等待shell操作, 因此, 还没来得及将值赋给content, 浏览器就进行显示了.

出现这种非阻塞的问题在于, exec()使用了回调函数. 这个回调函数就是exec()的第2个参数. 当exec()在后台执行的时候, Node.js自身会继续执行后面的代码.

非阻塞的正确用法

正确的方式是函数传递.

之前的传递方式: 请求处理函数->请求路由->服务器->返回内容到HTTP服务器.

为了正确实现非阻塞, 将"将内容传递给服务器"改成"将服务器传递给内容". 也就是说, 将response对象(从服务器的回调函数onRequest()获取)通过请求路由传递给请求处理程序. 随后, 处理程序就可以采用该对象上的函数来对请求作出响应.

修改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(8888);
    console.log("Server has started.");
}
exports.start = start;

相对于之前从route()函数获取返回值的做法, 这次是将response对象作为第三个参数传递给了route()函数. 然后, 与response有关的函数调用都移除, 由route()来完成.

修改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;

修改requestHandler.js:

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

处理POST请求

显示一个文本区供用户输入内容, 然后通过POST请求提交给服务器. 服务器收到请求, 通过处理程序将输入的内容展示到浏览器.

修改requestHandlers.js:

var exec = require("child_process").exec;
function start(response) {
    console.log("Request handler 'start' was called.");
    var body = '<html>' +
        '<head>' +
        '<meta http-equiv="Content-Type" content="text/html; ' + 'charset=UTF-8" />' +
        '</head>' +
        '<body>' +
        '<form action="/upload" method="post">' +
        '<textarea name="text" rows="20" cols="60"></textarea>' +
        '<input type="submit" value="Submit text" />' +
        '</form>' +
        '</body>' +
        '</html>';
    response.writeHead(200, {"Content-Type": "text/plain"});
    response.write(body);
    response.end();
}
function upload(response) {
    console.log("Request handler 'upload' was called.");
    response.writeHead(200, {"Content-Type": "text/html"});
    response.write("hello upload");
    response.end();
}
exports.start = start;
exports.upload = upload;

当用户提交输入的数据时, 触发/upload请求处理程序处理POST请求的问题.

可以在服务器中处理POST数据, 然后将最终的数据传递给请求路由和请求处理器, 让他们来进行进一步的处理.

Node.js会将POST数据拆分成很多小的数据块, 然后通过触发特定的事件, 将这些小数据块传递给回调函数. 特定的事件由data事件表示新的小数据块到达了, 由end事件表示所有的数据都已经接收完毕.

我们需要告诉Node.js, 当这些事件触发的时候, 回调哪些函数. 通过在request对象上注册监听器来实现. 如下所示:

request.addListener("data", function(chunk){
    ...
});
request.addListener("end", function(){
    ...
});

实现: 修改server.js:

var http = require("http");
var url = require("url");

function start(route, handle) {
    function onRequest(request, response) {
        var postData  = "";
        var pathname = url.parse(request.url).pathname;
        console.log("Request for " + pathname + " received.");
        request.setEncoding("utf8");
        request.addListener("data", function(postDataChunk){
            postData += postDataChunk;
            console.log("Received POST data chunk ' " + postDataChunk + " '.");
        });
        request.addListener("end", function(){
            route(handle, pathname, response, postData);
        });
    }
    http.createServer(onRequest).listen(8888);
    console.log("Server has started.");
}
exports.start = start;

首先, 设置接收数据的编码格式为UTF-8, 然后注册了"data"事件的监听器, 最后将请求路由的调用移到end事件处理程序中, 以确保它只会当所有数据接收完毕后才触发, 且仅触发一次.

修改router.js:

function route(handle, pathname, response, postData) {
    console.log("About to route a request for " + pathname);
    if(typeof handle[pathname] === 'function') {
        handle[pathname](response, postData);
    } 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 querystring = require("querystring");
function start(response, postData) {
    console.log("Request handler 'start' was called.");
    var body = '<html>' +
        '<head>' +
        '<meta http-equiv="Content-Type" content="text/html; ' + 'charset=UTF-8" />' +
        '</head>' +
        '<body>' +
        '<form action="/upload" method="post">' +
        '<textarea name="text" rows="20" cols="60"></textarea>' +
        '<input type="submit" value="Submit text" />' +
        '</form>' +
        '</body>' +
        '</html>';
    response.writeHead(200, {"Content-Type": "text/html"});
    response.write(body);
    response.end();
}
function upload(response, postData) {
    console.log("Request handler 'upload' was called.");
    response.writeHead(200, {"Content-Type": "text/plain"});
    response.write("You've sent: " + querystring.parse(postData).text);
    response.end();
}
exports.start = start;
exports.upload = upload;

处理文件上传

允许用户上传图片, 并将图片在浏览器中显示出来.

外部模块node-formidable对解析上传的文件数据做了很好的抽象, 可以用这个模块来处理上传的文件数据.

安装模块:

npm install formidable

这个模块可以对提交的表单进行抽象表示, 然后用它解析request对象, 获取表单中所需要的数据字段.

先通过官方例子展示如何使用formidable:

var formidable = require('formidable'),
    http = require('http'),
    util = require('util');

http.createServer(function(req, res) {
    if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
        var form = new formidable.IncomingForm();
        form.parse(req, function(err, fields, files) {
            res.writeHead(200, {'content-type': 'text/plain'});
            res.write('received upload:\n\n');
            res.end(util.inspect({fields: fields, files: files}));
        });
        return;
    }
    res.writeHead(200, {'content-type': 'text/html'});
    res.end(
        '<form action="/upload" enctype="multipart/form-data" '+
            'method="post">' +
            '<input type="text" name="title"><br>' +
            '<input type="file" name="upload" multiple="multiple"><br>' +
            '<input type="submit" value="Upload">' +
            '</form>'
    );
}).listen(8888);

使用该代码, 可以实现文件上传, 除此之外, 我们还要另外实现如何将文件显示在浏览器中.

首先解决后一个问题, 要将文件显示在浏览器中, 可以先使用fs模块, 将文件读取到服务器中. 添加/showURL的请求处理程序, 该处理程序直接硬编码将文件/tmp/test.png内容展示到浏览器中.

修改requestHandlers.js:

var querystring = require("querystring"),
    fs = require("fs");
function start(response, postData) {
    console.log("Request handler 'start' was called.");
    var body = '<html>' +
        '<head>' +
        '<meta http-equiv="Content-Type" content="text/html; ' + 'charset=UTF-8" />' +
        '</head>' +
        '<body>' +
        '<form action="/upload" method="post">' +
        '<textarea name="text" rows="20" cols="60"></textarea>' +
        '<input type="submit" value="Submit text" />' +
        '</form>' +
        '</body>' +
        '</html>';
    response.writeHead(200, {"Content-Type": "text/html"});
    response.write(body);
    response.end();
}
function upload(response, postData) {
    console.log("Request handler 'upload' was called.");
    response.writeHead(200, {"Content-Type": "text/plain"});
    response.write("You've sent: " + querystring.parse(postData).text);
    response.end();
}

function show(response, postData) {
    console.log("Request handler 'show' was called.");
    fs.readFile("/tmp/test.png", "binary", function(error, file) {
        if (error) {
            response.writeHead(500, {"Content-Type": "text/plain"});
            response.write(error + "\n");
            response.end();
        } else {
            response.writeHead(200, {"Content-Type": "image/png"});
            response.write(file, "binary");
            response.end();
        }
    });
}
exports.start = start;
exports.upload = upload;
exports.show = show;

修改index.js:

var server = require("./server.js");
var router = require("./router.js");
var requestHandlers = require("./requestHandlers");

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

然后, 在/start表单中添加一个文件上传元素, 将formidable模块整合到upload请求处理程序中, 用于将上传的图片保存到/tmp/test.png, 最后将上传的图片内嵌到/uploadURL输出的HTML中.

如果要在upload处理程序中对上传的文件进行处理, 则需要将request对象传递给formidable的form.parse(). 但是, 我们只有response对象和postData数组, 所以request对象只能从服务器开始传递给请求路由, 再传递给请求处理程序.

修改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, request);
    }
    http.createServer(onRequest).listen(8888);
    console.log("Server has started.");
}
exports.start = start;

修改router.js:

function route(handle, pathname, response, request) {
    console.log("About to route a request for " + pathname);
    if(typeof handle[pathname] === 'function') {
        handle[pathname](response, request);
    } 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;

到此, request对象就可以在upload请求处理程序中使用了.

修改requestHandlers.js:

var querystring = require("querystring"),
    fs = require("fs"),
    formidable = require("formidable");
function start(response) {
    console.log("Request handler 'start' was called.");
    var body = '<html>' +
        '<head>' +
        '<meta http-equiv="Content-Type" content="text/html; ' + 'charset=UTF-8" />' +
        '</head>' +
        '<body>' +
        '<form action="/upload" enctype="multipart/form-dta" method="post">' +
        '<input type="file" name="upload">' +
        '<input type="submit" value="Upload file" />' +
        '</form>' +
        '</body>' +
        '</html>';
    response.writeHead(200, {"Content-Type": "text/html"});
    response.write(body);
    response.end();
}
function upload(response, request) {
    console.log("Request handler 'upload' was called.");
    var form = new formidable.IncomingForm();
    form.parse(request, function(error, fields, files) {
        console.log("parsing done");
        fs.renameSync(files.upload.path, "/tmp/test.png");
        response.writeHead(200, {"Content-Type": "text/plain"});
        response.write("received image:<br/>");
        response.write("<img src='/show' />");
        response.end();
    });
}

function show(response, postData) {
    console.log("Request handler 'show' was called.");
    fs.readFile("/tmp/test.png", "binary", function(error, file) {
        if (error) {
            response.writeHead(500, {"Content-Type": "text/plain"});
            response.write(error + "\n");
            response.end();
        } else {
            response.writeHead(200, {"Content-Type": "image/png"});
            response.write(file, "binary");
            response.end();
        }
    });
}
exports.start = start;
exports.upload = upload;
exports.show = show;

Comments

使用 Disqus 评论
comments powered by Disqus