node.js 教程_node.js教程 - CSDN
精华内容
参与话题
  • 一、安装 ... node -v 出现相关信息,即证明安装成功 ...新建一个web-server.js文件 var http = require('http'); var serv = http.createServer(function (req,res) { res.writeHead(200,{'Content-Ty...

    一、安装

    1、去官网下载下来点击安装即可

    node -v

    出现相关信息,即证明安装成功

     

    2、在node中执行文件

    新建一个web-server.js文件

    var http = require('http');

    var serv = http.createServer(function (req,res) {

        res.writeHead(200,{'Content-Type':'text/html'});

        res.end('<marquee> Smashing krysliang!</marquee>');

    });

    serv.listen(3000)

    上面代码表示创建了一个http服务器托管一个简单的html文档。并且监听3000断开。

    在命令行中执行该文件

    node web-server.js

    然后在浏览器中输入localhost:3000,就可以看到下面的页面。

    二、node.js中的JavaScript基础知识

    这个部分分为两个部分,一部分是JavaScript基础(这部分我只会记一些我不太懂的,很基础的麻烦大家自己另外学习),一部分是V8中的JavaScript。

    1、什么是V8?

    V8 是 Google 开发的开源的 JavaScript 引擎,用于 Google Chrome 及 Chromium 中。

    也就是说V8中的JavaScript的某些特性并不是所有浏览器都会支持。

    2、函数中有一个属性length,此字段用来记录函数参数的个数。比如

    function a(b,c) {}

    console.log(a.length)//2

    一些node.js的框架就是通过此属性来根据不同的参数个数提供不同的功能的。

    3、reduce方法

    刚开始觉得这个方法就是一个累加器,将数组中的值全部加起来,比如下面的用法

    let arr = [2098,6679,45687,2340]

    let reducer = function (prev,next) {

        return prev+next

    }

    let total = arr.reduce(reducer,0)//表示初始值为0

    console.log(total)//56804

     

    其中reduce接收两个参数,第二个参数为可选的。第一个参数为必须的。

    其第一个参数为一个函数,这个函数拥有下面四个参数

    • previousValue上一次函数执行返回的结果

    • currentValue当前元素的值

    • index当前元素的索引值

    • array调用reduce的数组

     

     

    4、_proto_指向创建该对象的构造函数的原型。

    比如,这样Dog就继承了Animal了。

    function Animal(){}

    function Dog(){}

    Dog.prototype._proto_ = Animal.prototype

     

    let Arr = {}

    console.log(Arr.__proto__ == Object.prototype)//true

     

     

    三、阻塞和非阻塞IO

    1、了解一个概念

    node.js是单线程的,也就是说服务器中无论有多少个请求,也是由一个主线程去处理的,所以如果在某些时候改变了全局变量的值,那么会影响之后请求的处理结果的。所以一定要慎重!

     

    2、阻塞与非阻塞

    看下面的php的代码以及对应的js的代码

    print('hello')

    sleep(5)

    print('world')

     

    console.log('hello')

    this.timer = setTimeout(funtion(){console.log('world)},5000)

    console.log('hhh')

     

    这两段代码中,php中遇到了sleep(5)阻塞了进程的执行,也就是说进程在这里暂停了后面的代码不会继续执行,除非过了5秒。

     

    而第二段代码中,因为事件轮询,且setTimeout是非阻塞的,所以后面的代码也会执行的。

     

    而什么是事件轮询呢?从本质上来说,node会先注册事件,随后不停的去询问内核这些事件是否已经分发。当事件分发时,对应的回调函数就会被触发,然后继续执行下去。如果事件没有触发,则会继续执行其他代码,直到有新事件时再去执行对应的回调函数。

    也就是说setTimeout仅仅只是注册了一个事件,而程序继续执行。所以这是异步的。相反,php的sleep是 同步的。

     

        node的并发实现爱你也采用了事件轮询,与timeout一样,所有像http、net这样的原声模块中的IO部分也都采用了事件轮询技术。node使用事件轮询,触发一个和文件描述符相关的通知。

     

    文件描述符是抽象的句柄、存有对打开的文件、socket、管道等的引用。本质上说。当node接收到从浏览器发送的http请求的时候,底层的tcp连接会分配一个文件描述符。随后,如果客户端向服务器发送数据,node就会收到该文件描述符上的通知,然后触发JavaScript的回调函数。

     

    3、刚刚说过,node.js是单线程的。所以在引入事件轮询的时候,对应的事件触发不一定会准时执行。

    比如

    var start = new Date()

        setTimeout(function () {

            console.log(new Date() - start)

            for (var i = 0;i<1000000000;i++){}

        },1000)

    setTimeout(function () {

        console.log(new Date() - start)

    },2000)

     

     

    可以看到这两个timeout事件都不是准时的,这是因为这两个事件都是在执行完主事件之后才去执行的,而第一个timeout,因为有一个很大的for循环,故会影响后一个timeout事件触发的时间。因此,JavaScript的timeout并不能严格遵守时钟设置。

     

    所以很多优秀的node模块都不会采取类似于这样阻塞的方式(很大的for),执行任务也都采取了异步的方式。

     

     

    四、node中的JavaScript

    (一)global对象

    在浏览器中,全局对象指的就是window对象。在window对象上定义的任何内容都可以被全局访问到。比如,setTimeout其实就是window.setTimeout,document其实是window的document。

     

    而node中的有两个类似的代表着全局的的对象,

    • global:跟window类似,任何global对象的属性都可以被全局访问到。

    • process,所有全局执行上下文中的内容都在process对象。在node中,只有一个process对象。进程的名字是process.title。

     

     

    1、process.nextTick,这个函数可以将一个函数的执行时间规划到下一个事件循环中:

    console.log(1);

    process.nextTick(function(){

        console.log(3)

    });

    console.log(2);

    可以把它看做,在执行完当前的其它代码,然后再执行指定的函数。

    其实就等于下面这样的代码

    console.log(1);

    setTimeout(function(){

        console.log(3)

    },0);

    console.log(2);

     

    通过异步的方法在最近的将来调用指定的函数。

     

    (二)、模块系统(这里不懂可以看我的另外一篇文章从模块化开始到webpack入门

    node引入了一种模块系统,该模块系统有三个核心的全局对象:require、module、exports

    1、绝对和相对模块

    绝对模块是只在node内部的node_modules这里查找的模块,或者是node内置的http这样的模块,绝对模块可以直接通过名字来require这个模块,无须添加路径名。

    var http = require('http');

     

    相对模块就是自定义的文件,比如同目录下的modulea.js文件就要像这样引入

    require('./modulea.js')

     

    2、暴露API

    在模块内部使用exports来暴露模块内部的内容,将需要暴露的内容,赋给exports对象。

    exports其实就是对module.exports的引用,默认情况下是一个对象。

    比如modulea.js内部

    exports.name = 'krysliang'

    var age = 22

    exports.getAge = () =>{

        return age

    }

    或者像下面这样,将整个对象赋给module.exports

    module .exports = {

        name:'krysliang',

        age:'22',

        getAge:()=>{

            return this.age

        }

    }

    那么在index.js中就可以像下面这样使用

    var modulea =require('./modulea.js')

    console.log(modulea.name)

    console.log(modulea.getAge())

     

    还可以像下面这样

    //module_a.js

    function krysliang() {

        this.name = 'krysliang'

    }

    krysliang.prototype.getAge = function () {

        console.log('我的年龄是',22)

    }

    krysliang.prototype.getName = function () {

        console.log('我的名字是',this.name)

    }

    module.exports = krys

     

    //index.js

    var Krysliang =require('./module_a.js')

    var lwl = new Krysliang()

    lwl.getName()

    lwl.getAge()

     

    这就是典型的OOP写法了。

     

    (三)、事件

    node.js中基础的API之一就是EventEmitter,也就是事件分发器,用它来分发事件处理,就是相当于一个监听器。node暴露了 Event EmitterAPI,在该API中定义了on、emit以及removeListener方法。它以process.EventEmitter形式暴露出来

    var EventEmitter = require('events').EventEmitter,a = new EventEmitter;

    a.on('event',funtion(){})

    e.emit('event')

    事件时Node非阻塞设计的重要体现。node通常不会直接返回数据(因为这样可能会在等待某个资源的时候发生线程阻塞),而是采用分发事件来传递数据。

    比如像HTTP服务,当请求到达的时候,Node会调用一个回调函数,这个时候数据有可能还没有完全到达

    http.Server(funtion(req,res){

        var buf = '';

        req.on('data',funtion(){buf+=data })

        req.on('end'.funtion(){ console.log('数据接收完毕')})

    })

    将请求数据内容进行缓存(data事件),等到所有数据都接收完毕后(end事件)在对数据进行处理。不管是否所有的数据都已经到达,node为了让你尽快知道请求已经到达服务器,都需要分发事件出来。

    有些API会分发error事件。

    不管某事件在将来会被触发多少次,我都希望只调用一次回调函数,这样的分发方法叫做once,比如

    a.once('某个事件',function(){

    })

     

     

    (四)buffer

    buffer是一个表示固定内存分配的全局对象(也就是说,要放到缓冲区中的字节数需要提前定下),可以有效在JavaScript中表示二进制数据。

    该功能的一部分中功能就是可以对数据进行编码转换。

    比如

    var myBuffer = new Buffer('==iiij2i3h1i23h','base64');

    将第一个数据作为base编码方式进行缓存

     

     

    五、Node中重要的API。

    node通过回调和事件机制来实现并发。

    构建一个简单的命令行文件浏览器,其功能是允许用户读取和创建文件。

    需求:

        (1)程序需要在命令行运行,也就是程序要么通过node命令来执行或者是直接执行后通过终端提供交互给用户进行输入输出。

    (2)程序启动后,需要显示当前目录下列表

    (3)选择某个文件时,程序需要显示该文件内容。

    (4)选择一个目录时,程序需要显示该目录下的信息

    (5)运行结束后,程序退出。

     

    步骤

    (1)创建模块

    (2)决定采用同步的fs还是异步的fs

    (3)理解什么是流(stream)

    (4)实现输入输出

    (5)重构

    (6)使用fs进行文件交互

    (7)完成

     

    1、创建模块

        在文件目录下面创建一个package.json的文件,然后执行npm install

    {

      "name": "file-explorer",

      "version": "0.0.1",

      "description": "a command-file file explorer"

    }

     

    2、fs模块

    需要注意的是,在node中,唯一一个提供同步和异步Api的模块就是fs模块。

    比如,获取当前目录的文件列表中,同步的方法是readdirSync,异步的方法是readdir

    //同步

    console.log(require('fs').readdirSync(__dirname))

    //异步

    require('fs').readdor('.',async)

     

    首先在node.js中, 标准的输入/输出管理模块stdioApi是全局对象process的一部分,所以在这里我们唯一的依赖就是fs模块,

    //引入fs模块

    var fs = require('fs')

     

    为了获取文件列表,采用fs.readdir(此处采用异步)

    fs.readdir(__dirname,function (err,files) {

        //回调函数中的files是一个数组,用来显示当前目录下的文件列表

        console.log(files)

    })

    然后执行文件,可以看到,当前目录下的文件名被打印出来。

     

    3、理解什么是流

    console.log会输出到控制台,事实上,console.log内部做了这样的事情:它在指定的字符串后面加上\n换行符,并将其写到stdout流中。

    console.log可以使用process.stdout.write来替代,但是需要注意的是。process.stdout.write是不会在原来的字符串后面加上换行符的。

    process全局对象中包含了三个流对象,分别对应三个UNIX标准流

    • stdin标准输入(可读流)

    • stdout标准输出(可写流)

    • stderr标准错误(可写流)

     

    stdin流默认的状态是paused(暂停的。

    此外,流的另一个属性是它默认的编码,如果在流上面设置了编码,那么就会得到编码后的字符串而不是原始的Buffer作为事件参数。

    在node中,会接触到各种类型的流,比如TCP套接字、http请求等。简而言之,当涉及持续不断地对数据进行读写的时候,流就出现了。

     

    4、输入和输出

    这里用到的输入输出函数有如下几个

    stdout.write('  \033[33mEnter your choice: \033[39m');//输出

            stdin.resume();//等待命令行输入

            stdin.setEncoding('utf8');//设置编码

            stdin.on('data',option);//监听数据的输入,然后会调用回调函数

       stdin.pause();//退出输入

     

    //引入fs模块

    var fs = require('fs'),stdin = process.stdin,stdout=process.stdout;

    fs.readdir(process.cwd(),function (err,files) {

        //回调函数中的files是一个数组,用来显示当前目录下的文件列表

        // console.log(files)

        if (!files.length){

            //如果当前目录下面没有文件,则返回并告知没有文件可以选择

            return console.log('    \033[31m No files to show!\033[39m\n');

     

     

        }

        console.log('   Select which file or directory you want to see \n');

        var stats = [];

        function file(i) {

            var filename = files[i];

            //fs.stat会给出文件或者目录的元数据

            fs.stat(__dirname+'/'+filename,function (err,stat) {

                stats[i] = stat;

                if (stat.isDirectory()){

                    console.log('   '+i+'   \033[36m]'+filename+'/\033[39m');

                }else {

                    console.log('   '+i+'   \033[90m'+filename+'\033[39m');

                }

                if (++i == files.length){

                    read();

                }else {

                    file(i);

                }

            });

        }

        function read() {

            console.log('');

            stdout.write('  \033[33mEnter your choice: \033[39m');//输出

            stdin.resume();//等待命令行输入

            stdin.setEncoding('utf8');//设置编码

            stdin.on('data',option);//监听数据的输入,然后会调用回调函数

        }

        function option(data) {

            var filename = files[Number(data)];

            if (!filename){

                stdout.write('  \033[33mEnter your choice: \033[39m');

            }else {

                stdin.pause();//退出输入

                //如果是目录则读出目录下面的文件,如果是文件则输出文件内容

                if (stats[Number(data)].isDirectory()){

                    fs.readdir(__dirname+'/'+filename,function (err,files) {

                        console.log('');

                        console.log('   ('+files.length+')'+files);

                        files.forEach(function (file) {

                            console.log('   -   '+file);

                        });

                        console.log('');

                    })

                }else {

                    fs.readFile(__dirname+'/'+filename,'utf8',function (err,data) {

                        console.log('');

                        console.log('\033[90m'+data.replace(/(.*)/g,'   $1')+'\033[93m');

                    })

                }

            }

        }

        file(0)

    });

     

    5、一些命令行常用的变量

    (1)argv,process.argv包含了所有node程序运行时的参数值。

    (2)工作目录

    使用__dirname来获取当前文件在文件系统下的路径,而使用process.cwd来获取程序运行时的当前工作目录,还可以使用process.chdir('路径')来改变当前工作的目录。

    (3)环境变量

    node允许通过proces.env变量来轻松访问shell环境下的变量。使用NODE_ENV来控制程序是运行在开发模式下还是产品模式下。

    (4)让应用退出使用process.exit()并提供一个错误信息,比如

    当发生错误的时候,要退出程序,这个时候最好是使用退出代码1

    console.error('an error occurred');

    process.exit(1)

    (5)信号,进程和操作系统进行通信的其中一种方式就是通过信号。比如,要让进程终止,可以发送SIGKILL信号。

    node是通过在process对象上以时间分发的形式来发送信号的

    process.on('SIGKILL',function(){

        //进程杀掉的信息已收到

    })

    (6)ANSII转义码

    要在文本终端下控制格式、颜色以及其他输出选项,可以使用ANSI转义码。

    在文本周围添加的明显不用于输出的字符,成为非打印字符。

    console.log('\033[90m'+data+'\033[39m);

    在这里,\033代表转义序列的开始,[表示开始颜色设置,90表示前景色为亮灰色,m表示颜色设置结束,最后使用39表示将颜色再设置回去。

     

    6、stream

    使用fs.readFile这个函数的时候,回调函数必须要等到整个文件读取完毕,载入到RAM中、可用的情况下才会触发。

    而使用stream,每次会读取可变大小的内容块,并且每次读取完毕后会触发回调函数。比如

    var stream = fs.createReadStream('file.txt');

    stream.on('data',function(chunk){

        //    c处理一部分的文件

    })

    stream.on('end',function(){

    //读取完毕

    })

     

    7、监视

    node允许监视文件或者目录是否发生变化。监视意味着,当文件系统中的文件(或则目录)发生变化的时候,会分发一个事件,然后触发指定的回调函数。

     

     

    六、TCP(稍后会开一个详解TCP的系列再仔细了解TCP)

    TCP是一个面向连接的协议,它保证了两台计算机之间数据传输的可靠性和顺序,换句话说,TCP是一种传输层协议,它可以让你将数据从一台计算机完整有序地传输到另一台计算机。

    Node HTTP服务器是构建于Node TCP服务器之上的,也就是说Node中的http.Server继承自net.Server(net是TCP模块)

    TCP有如下几个特征

    (1)面向字节:TCP对字符以及字符编码是完全无知的,所以TCP会允许数据以ASCII字符(一个字符一个字节)或者是Unicode(每个字符四个字节)进行传输,这使得TCP有很好的灵活性

    (2)可靠性,TCP有一些列确认和超时实现的机制来实现可靠性的要求。

    当数据发送出去,发送方就会等到一个确认消息(表示数据包已经收到的简短的确认消息),如果在指定的窗口时间还没有收到确认消息,发送方就会对数据进行重发。这种机制有效的解决了很多不可预测的网络出错的情况。

    (3)流控制:TCP通过一种叫做流控制的方式来确保两点之间传输数据的平衡。

    (4)拥堵控制:TCP有一种内置的机制能够控制数据包的延迟率以及丢包率不会太高,以此来确保服务的质量。举例来说,和流控制能够避免发送方压垮接收方一样,TCP会通过控制数据包的传输速率来避免拥堵的情况。

     

    1、套接字Socket=ip:端口、

    2、基于TCP的聊天程序

     

    下面来创建一个基本的TCP服务器,任何人都可以连接到该服务器,无须实现任何协议或者指令:

    • 成功连接到服务器后,服务器会显示欢迎信息,并要求输入用户名。同时还会告诉你当前还有多少其他客户端也连接到了该服务器。

    • 输入用户名,按下回车键,就认为成功连接上了。

    • 连接后就可以通过输入信息再按下回车键,来向客户端进行消息的收发。

    按下回车键是因为Telnet中输入的任何信息都会立刻发送到服务器。按下回车键是为了输入\n字符。在node服务器端中,通过\n来判断消息是否已完全到达。所以,这其实是作为一个分隔符在使用。

     

     

    var net = require('net');

    var count = 0;//连接数

    var user = [];//连接的用户

    /**

    * 创建tcp服务器,每次有新的链接建立的时候都会执行后面的回调函数.createServer回调函数会接收一个对象,该对象是node中很常见的实例:流,在

    * 这里,它传递的是net.stream。

    */

    var server = net.createServer(function (conn) {

        conn.write(

            '\n>Welcome to krys-node-chat'+ '\n > '+count +'other people are connected at this time'+ '\n> please write your name and press enter'

        )

        var nickname;//当前连接的用户名

        count++;

        conn.on('data',function (data) {

            //获取到的数据是Buffer数据,也就对应了TCP是面向字节的协议。可以调用BUffer对象上的toString('utf8')来转换成utf8编码的字符串

            //也可以通过conn.setEncoding('utf8‘)来进行编码

          data = data.replace('\r\n','');//将回车键清除

            console.log(data);

            //接收到的第一份数据应当是用户输入的昵称

            if (!nickname){

                if (user[data]){

                    conn.write('\033[93m>nikename is already in use\033[39m');

     

     

                }else {

                    nickname = data;

                    user[nickname] = conn;

                    broadcast('\033[90m> '+nickname+'join in the room\033[39m\n');

     

     

                }

            }else {

                //用户输入的聊天信息

                broadcast('\033[90m> '+nickname+':\033[39m'+data+'\n',true);

            }

     

     

        })

        function broadcast(msg,exceptMyself) {

            for (var i in user){

                if (!exceptMyself || i != nickname){

                    user[i].write(msg)

                }

            }

     

     

        }

        conn.on('close',function () {

            //有人断开连接的时候就需要清除users数组中对应的元素,并且通知其他用户

            count--;//有连接关闭的时候就减少一个

            delete user[nickname];

            broadcast('\033[90m> '+nickname+':left the room\033[39m'+'\n')

        })

        conn.setEncoding('utf8');

    });

     

     

     

     

    /**

    * 监听

    */

    server.listen(3000,function () {

        console.log('\033[96m SERVER LISTEN ON 3000 \033[90m');

    })

     

    七、http

    1、在node.js中,使用http.ServerRequest来创建请求,使用http.ServerResponse来创建响应。

    2、创建一个http服务器的例子如下

    3、node天生是异步机制,所以所有的http响应头中都会带有Transfer-Encoding头的值是chunked,这样响应就可以逐步产生了。

    4、在调用res.end方法之前,我们可以多次调用write方法来发送数据。为了尽可能快地响应客户端,在首次调用write的时候,node就能把所有的响应头信息以及第一块数据发送出去。当遇到end方法的时候,node就不再允许网响应中发送数据了。

    var http = require('http');

    http.createServer(function (req,res) {

        res.writeHead(200,{'Content-Type':'text/html'});//向响应头中注入文件类型

        res.end('hello'+'<b>world</b>')//返回文档

     

     

    }).listen(3000);

    var http = require('http');

    http.createServer(function (req,res) {

        res.writeHead(200,{'Content-Type':'image/png'});//向响应头中注入文件类型

        var steam = require('fs').createReadStream('image.png');//创建文件流

        stream.on('data',function (data) {//一旦有数据读到了就传给response返回

            res.write(data)

        })

        stream.on('end',function () {

            res.end()

        })

        // 另一种写法

        require('fs').createReadStream('image.png').pipe(res);

    }).listen(3000);

    5、连接

    在net中,回调函数中对象的类型是一个连接对象,而在http中,是请求和响应对象。这样的区分是因为

    • node在内部做了很多的事情,它拿到了浏览器发送的数据后,对其进行分析,然后构造了一个JavaScript对象方便我们在脚本中使用,

    • 浏览器在访问站点的时候不会就只用一个连接,很多主流的浏览器为了更快加载网站内容,能向一个主机打开八个不同的链接并发送请求。

    var http = require('http');

    var qs = require('querystring');

    http.createServer(function (req,res) {

        if (req.url == '/'){

            res.writeHead(200,{'Content-Type':'text/html'});//向响应头中注入文件类型

            res.end([

                '<form method="POST" action = "/url">' +

                '<h1>My form</h1>' +

                +'<filedset>' +

                '<label>personal information</label>' +

                '<p>what s your name</p>' +

                '<input type="text" name="name"/>'+

                '<button>submit</button>'+

                '</filedset>'+

                '</form>'

            ].join(''))

        }else if (req.url == '/url' && req.method == "POST"){

            var body = '';

            req.on('data',function (chunk) {

                body +=chunk

            })

            req.on('end',function () {

                res.writeHead(200,{'Content-Type':'text/html'});

                res.end('<p>your name is </p>'+qs.parse(body).name)//解析发送进来的数据,使用qs.parse对请求内容进行解析,然后从解析生成的对象中获取name的值。

            })

        }else {

            //没有匹配任何其他的数据就返回404

            res.writeHead(404);

            res.end('Not Found');

        }

     

     

    }).listen(3000);

     

    八、connect

    connect是一个基于HTTP服务器的工具集,它提供了一种新的组织代码的方式来与请求、响应对象进行交互,称为中间件。

    假如我们有一个站点,其目录结构如下所示

    $ls website/

    index.html images/

     

    然后再images目录下面有四个图片文件

    1.png 2.png 3.png 4.png

    index中的内容如下

    <h1>krys website</h1>

    <img src='/images/1.png'/>

    <img src='/images/2.png'/>

    <img src='/images/3.png'/>

    <img src='/images/4.png'/>

     

    那么我们的服务器端要怎么写呢?

    // 模块依赖

    var http = require('http'),

    fs = require('fs');

    //创建服务器

    var server = http.createServer(function(req, res) {

    //检查url是否和服务器目录下的文件匹配,如果匹配,则读取该文件并展示出来。

    if (req.method == 'GET' && req.url.substr(0, 7) == '/images' && req.url.substr(-4) == '.png') {

    fs.stat(__dirname + req.url, function(err, stat) {

    //检查文件是否存在

    if (err || !stat.isFile()) {

    //如果发生错误,则终止进程并返回404

    res.writeHead(404);

    res.end('Not Found');

    return;

    }

    })

    server(__dirname + req.url, 'application/png');

     

     

     

     

    } else if (req.method == 'GET' && req.url.substr(0, 7) == '/') {

    server(__dirname + '/index.html', 'text/html');

    } else {

    res.writeHead(404);

    res.end('Not Found');

    }

     

     

     

     

    function server(path, type) {

    //根据文件路径来获取文件内容,并添加content-type头信息

    res.writeHead(200, { 'Content-Type': type });

    res.createReadStream(path).pipe(res)

    }

     

     

    });

    //监听

    server.listen(3000);

      

    那如果是通过connect来实现呢?

    创建网站时一些常见的任务

    • 托管静态文件

    • 处理错误以及损坏或者不存在的URL

    • 处理不同类型的请求

     

    基于http模块API之上的connect,提供了一些工具方法能够让这些重复性的处理便于实现,以至于让开发者能够更加专注在应用本身。

    如果要使用connect的话,就要在package.json文件中写明对其的依赖

    {

    "name": "test",

    "version": "0.0.1",

    "dependencies": {

    "connect": "1.8.7"

    }

    }

     

     

     

    中间件其实就是一个简单的JavaScript函数。使用use方法来添加static中间件。在这里,我们这样配置static中间间-通过传递一些参数connect.static方法,该方法本身会返回一个方法

    server.user(connect.static(__dirname+'/website))

    将index.html以及Image目录放在/website下。确保没有将不想托管的文件放进去。

    然后调用listen方法。

     

    //模块依赖

    var connect = require('connect');

    //创建服务器

    var server = connect.createServer();

    //处理静态文件

    server.use(connect.static(__dirname + '/website'));

    //监听

    server.listen(3000);

     

    那么,什么是中间件呢?

    简单的来说,中间件由函数组成,它除了处理req和res对象之外,还接收一个next函数来做流控制,也就是相当于一个过滤器。

     

    connect中自带的中间件

    1、static中间件

    static中间件是使用node开发web的时候最常用的中间件之一了。

    它有如下几种功能

    • 挂载,挂载的意思就是相关联,比如static允许将任意一个URL匹配到文件系统中的任意一个目录

    举例说,假设要让/my-imagesURL和名为/images的目录对应起来,可以通过以下的写法

    server.use('images',connect.static('/path/to/myimages'));

    • 设置文件缓存时间maxAge 

    static 中间件接收一个名为maxAge的选项,这个选项代表一个资源在客户端缓存的时间。

    server.use('/js',connect.static('/path/to/bundles',{maxAge:1000000}))

    • 隐藏在UNIX文件系统中被认为·是·隐藏的文件(以.开始的文件)

    server.use(connect.static('path/to/resource',{hidden:true}))

     

    2、query中间件

    有时要发送给服务器的数据会以查询字符串的形式,作为URL的一部分。比如url/bl;og-posts?page=5。当在浏览器中访问改URL的时候,node会以字符串的形式将该URL存储在req.url的变量中

    server.use(function(req){

    //req.url == '/blog-posts?page=5'

    })

     

    而大部分的时候,我们都是需要获取查询字符串这部分的数据,使用query中间件,就能通过req.query对象自动获取这些数据

    server.use(connect.query)

    server.use(function(req,res){

        //req.query.page == 5

    })

     

    3、logger中间件

    logger中间件是一个对web应用非常有用的诊断工具,它将发送进来的请求消息和发送出去的响应消息打印在终端。

    它提供了以下四种日志格式

    • default

    • dev

    • short

    • tiny

     

    4、body parse中间件

    在一个使用http模块的例子,如果有post的请求发送过来的时候,我们可以使用这个中间件来获取

    server.use(connect.bodyParse());

    server.use(function(req,res){

        req.body.input

    })

    此外,这个中间件还可以用来使用formidable模块,用来处理用户上传的文件。

     

     

    5、cookie

    当浏览器发送cookie数据的时候,会将其写到cookie头信息中,其数据格式和URL的查询字符串类似。

    那么·如果想使用这些值,就可以使用cookieParse中间件。

    server.use(connect.cookieParse())

    server.use(function(req,res){

        //req.cookirs.select == 'value'

    })

     

    6、session

    在绝大多数数的web应用中,多个请求间共享“用户会话”的概念是非常必要的。“登录”一个网站的时候,多多少少会使用某种形式的会话系统,它主要通过在浏览器中设置cookie来实现,该cookie信息会在随后所有的请求头信息中被带回到服务器中。

    connect为此也提供了简单的实现方式。

     

    7、redis session

    session默认的存储方式是在内存,这也就意味着session数据都是存储在内存中的,当进程退出后,session数据自然也就丢失了。

    redis是一个既小又快的数据库,有一个connect-redis模块使用redis来持久化session数据,这样就让session驻扎到了node进程之外。

    首先要先安装好redis

    vart connect = require('connect'),

    RedisStore = require('connect-redis')(connect);

    然后将其作为store选项的值传递给session中间件

    server.use(connect.session({store:new RedisStore,secret:'my-secrect'}))

     

    九、Express

    connect是基于http模块提供了开发web常用的功能,而Express是基于connect为构建整个网站以及web应用提供了更为方便的API。

     

    在Express中有一个很重要的概念,就是路由,它是有URL和Method组成的,

    Express基于connect构建而成,因此,它保持了重用中间件来完成基础任务的想法。这就意味着,通过Expres的API方便地构建web用用的同时,又不失构建于http模块之上的高可用中间件的生态系统。

     

    一个小型的Express应用

    package.json

    {

      "name": "test9",

      "version": "0.0.1",

      "description": "my first tcp client",

      "dependencies": {

        "express": "^2.5.9",

        "ejs": "^0.4.2",

        "superagent": "^0.3.0"

      }

    }

     

    创建HTML模板

    为了避免将HTML代码嵌入到应用逻辑中,这次我们要使用一个简单的模板语言来处理。模板语言的名字叫EJS(内嵌的js),跟在html中内嵌php类似。

     

    index.ejs

    提供一个入口给用户提交搜索的关键字:

    <h1>krys app</h1>

    <p>please enter youe search term:</p>

    <form action="/serch" method="GET">

        <input type="text" name="q">

        <button>Search</button>

    </form>

     

    search.ejs

    用来显示搜索结果

    <h1>results for <%=search%></h1>

    <%if (results.length){%>

    <ul>

        <% for (var i = 0; i< results.length; i++){%>

        <li> <%= results[i].text%> - <em><%=results[i].from_user%></em></li>

        <% }%>

    </ul>

    <% } else {%>

    <p> NO resultes</p>

    <%}%>

     

     

    server.js

     

     

    var express = require('express');

    var search = require('./search')

    var app = express.createServer();

    //配置

    app.set('view engine','ejs');//指定使用的模板引擎

    app.set('views',__dirname+'/views');//指定视图文件所在的目录

    app.set('view options',{layout:false});//指定在渲染视图的时候传递给每个模板

     

     

    //定义首页的路由,处理get方法的请求

    /**

    * express为response对象提供了render方法,该方法完成如下三件事

    * 初始化模块引擎

    * 读取视图文件并将其传递给模板引擎

    * 获取解析后的HTML文件页面并作为响应发送给客户端

    */

    app.get('/',function (req,res) {

        res.render('index');

    })

     

     

    app.get('/search',function (req,res,next) {

        search(req.query.q,function (err,twwets) {

            if (err) return next(err);

            res.render('search',{results:twwets,search:req.query.q})

        });

    })

    app.listen(3000)

     

    search.js

    var request = require('superagent')

    module.exports = function  search(query,fn) {

        request.get('http://search.twitter.com/search.json?q='+query)

            .end(function (res) {

                console.log(res.body)

                if (res.body && Array.isArray(res.body.results)){

                    return fn(null,res.body.results);

                }

                fn(new Error('Bad response'));

            })

    }

    运行,查看3000端口

     

     

    对express进行设置

     

    1、让express对模板进行缓存

    app.configure('production',function(){

        app.enable('view cache')

    })

    //或者这样写

    app.set('view cache',true);

    2、开启大小写严格

    当开启这个选项之后,就会变成严格的路由,就是匹配到的路由必须一模一样。

     

    3、模板引擎

    要使用模板引擎,必须要在npm安装ejs模块,并且生命view engine 为ejs

     

    4、错误处理

    绝大多数的web应用都会自定义错误页面,或者自建一套后台的错误处理机制。我们可以通过app.error方法定义一个特殊的错误处理器作为错误处理的中间件

    app.error(function(err,req,res,next){

        if(err.messag == 'Bad response'){

            res.render('error-page')

            }else{

            next()

    }

     

     

    })

     

     

     

     

     

     

     

    十、WebSocket

     

    (一)ajax

    ajax的弊端

    当执行代码发出请求时,浏览器会使用可用的socket来进行数据发送,为了提高心梗,浏览器会和服务器之间建立多个socket通道。举例来说,在下载图片的时候,还是可以发送ajax请求。要是浏览器只有一个socket通道,那么网站渲染和使用都会非常慢。若三个请求分别通过三个不同的socket通道发送,就无法抱枕服务器端接收的顺序了,所以我们需要在服务器接受到一个请求之后再接着发送第二个请求,这样就能保证接收的顺序了。

    var sending = false;

    $(docement),mosemove(function(env){

        if(sending) return;

        sending =  true;

        $.Post('/position',{x:ev.clientX,y:ev.clientY},function(){

            sending = false

    })

    })

     

    如果是这样的话,就会发送大量的请求以及接受大量的响应,但是这里的响应式我们不需要的。这样就造成了资源的浪费了。

     

    (二)WebSocket

    websocket是web下的TCP,它是双向的socket。websocket有两部分,一部分是浏览器实现的websocket API,另一部分是服务器端实现的websocket协议。这两部分是随着HTML5的推动一起被设计和开发的。

    在浏览器中实现的代码如下

    <!DOCTYPE html>

    <html lang="en">

    <head>

        <meta charset="UTF-8">

        <title>Title</title>

    </head>

    <body>

    <h2> latency <span id="latency"></span></h2>

    <script>

        var lastmessage;

        window.onload = function () {

            var ws = new WebSocket('ws://localhost:3000');

            ws.onopen = function () {

                ping();

            }

            ws.onmessage = function (ev) {

                console.log('client got   :'+ev.data);

                document.getElementById('latency').innerHTML = new Date() - lastmessage;

            }

            function ping() {

                lastmessage = +new Date();

                console.log('client send ping')

                ws.send('ping')

            }

        }

     

     

    </script>

     

     

    </body>

    </html>

     

    服务器端

    package.json

    {

      "name": "test10",

      "version": "0.0.1",

      "description": "my first ws",

      "dependencies": {

        "express": "^2.5.9",

        "nodejs-websocket": "^1.7.2"

      }

    }

     

    //创建服务器

    // 引入WebSocket模块

    var ws = require('nodejs-websocket')

    var PORT = 3000

     

     

    // on就是addListener,添加一个监听事件调用回调函数

    // Scream server example:"hi"->"HI!!!",服务器把字母大写

    var server = ws.createServer(function(conn){

        console.log('New connection')

        conn.on("text",function(str){

            console.log("Server Received "+str)

            conn.sendText("Server Received "+str) //大写收到的数据

            //conn.sendText(str)  //收到直接发回去

        })

        conn.on("close",function(code,reason){

            console.log("connection closed")

        })

        conn.on("error",function(err){

            console.log("handle err")

            console.log(err)

        })

    }).listen(PORT)

     

     

     

    十一、另一种实现websocket的方法--Socket.IO

    因为websocket是新出的标准,所以会有一些浏览器不支持。而Socke.IO的传递是基于传输的,也就是说Socket.IO可以在绝大部分的浏览器和设备上运行,从IE6和IOS都支持。

    1、Socket.IO提供了可靠的事件机制,如果客户端停止传输数据,但在一定的时间内又没有正常的关闭连接,Socket.IO就认为它是断开连接了。而当连接丢失的时候,会自动重连。

     

    2、在实时web世界中,都是基于事件传输的。Socket.IO仍然允许你想websocket那样传输简单文本信息,除此之外,它还支持通过分发、和监听事件来进行Json数据的收发。

    io.sockets.on('connection',function(socket){

        socket.send('a');

        socket.on('message',function(msg){

            console.log(msg)

        })

    })

     

    3、Socket.IO还提供了另一个强大的特性,它允许在单个连接中利用命名空间来将消息彼此区分开来。

    有的时候,应用程序需要进行逻辑拆分,但考虑到性能、速度之类的原因,使用同一个连接还是可以接受的。因此,Socket.IO允许监听多个命名空间中的Connection事件。

    io.sockets.on('connection');

    io.of('/some/namespace').on('connection')

    io.of('/some/other/namespace').on('connection')

    尽管这样可以获取到不同的连接对象,但是通常只会使用一个传输通道。

     

    4、Socket.IO 总会尝试U型安泽对用户来说速度最快、对服务器性能来说最好的方法来建立连接,要是条件达不到,那么首先会保证连接正常。

     

    一、MongoDB

    1、MangoDB 是一个面向文档,schema-less的数据库,它可以将任意类型的文档数据存储到集合中(与schema无关)。

    //package.json

    {

    "name": "user-auth-example",

    "version": "0.0.1",

    "dependencies": {

    "express": "2.5.8",

    "mongodb": "^2.2.26",

    "jade": "0.20.3"

    }

    }

     

     

     

    //server.js

    //模块依赖

    var express = require('express'),

    mongodb = require('mongodb');

     

     

    /**

    * 创建数据库

    * 提供ip和端口来初始化服务器

    */

    var server = new mongodb.Server('127.0.0.1', 27017);

     

     

    /**

    * 告诉驱动器去连接数据库,比如取一个叫krys的数据库名字,在这里如果指定名字的数据库不存在,就会创建一个数据库

    */

    var db = new mongodb.Db('krys', server);

    db.open(function(err, client) {

    //要是连接数据库失败,就终止进程

    if (err) throw err;

    //要是连接成功,则打印成功的消息

    console.log('connected to mongodb!');

    //让Express服务器监听端口准备操作集合

    //创建app

    var app = express.createServer();

    app.use(express.bodyParser());

    app.use(express.cookieParser());

    app.use(express.session({

    secret: 'my secret'

    }));

    //设置模板引擎

    app.set('view engine', 'jade');

    app.set('view options', { layout: false });

    //定义路由

    app.get('/', function(req, res) {

    //默认路由

    res.render('index', {

    authenticated: false

    })

    });

    app.get('/login', function(req, res) {

    //登录路由

    res.render('login');

    });

    app.get('/signup', function(req, res) {

    //注册路由

    res.render('signup');

    });

     

     

    app.get("/login/:signupEmail", function(req, res) {

    //处理登录路由

    console.log(req.params.signupEmail)

    res.render('login');

    })

     

     

    //MongoDB有一个ensureIndex命令,调用这个命令来确保在查询前建立了索引

    client.ensureIndex('user', 'email', function(err) {

    if (err) throw err;

    client.ensureIndex('user', 'password', function(err) {

    if (err) throw err;

    console.log('ensure index');

    //监听

    app.listen(3000, function() {

    console.log('app listen on 3000');

    });

    })

    })

    //处理路由

    app.post("/signup", function(req, res, next) {

    //处理注册路由

    //插入文档,参数是需要插入的数据以及一个回调函数,回调函数中的的参数,第一个是错误对象,第二个是插入的文档数组。

    client.collection('user', function(err, collection) {

    collection.insert(req.body.user, function(err, doc) {

    if (err) return next();

    console.log(doc)

    res.redirect('/login/' + doc.ops[0].email);

    })

    })

    })

     

     

    app.post('/login', function(req, res) {

    client.collection('user', function(err, collection) {

    collection.findOne({ email: req.body.user.email, password: req.body.user.password }, function(err, doc) {

    if (err) return next(err);

    console.log(doc)

    if (!doc) return res.send('<p>user not found</p>');

     

     

    res.redirect('/')

    })

    })

     

     

    })

     

     

     

     

    })

     

    //layout.jade

    doctype 5

    html

    head

    title Mongodb example

    body

    h1 my first mongodb

    hr

    block body

    //index.jade

     

    if(authenticated)

    p welcome back #{me.first}

    a(href="/logout") Logout

    else

    p welcome new vistor!

    a(href='login') Login

    a(href='signup') Signup

     

    //signup.jade

    form(action="/signup",method="POST")

    fieldset

    legend Sign up

    p

    label First

    input(name="user[first]",type='text')

    p

    label Last

    input(name="user[last]",type='text')

    p

    label Email

    input(name="user[email]",type='text')

    p

    label Password

    input(name="user[password]",type='text')

    p

    button Submit

    p

    a(href="/") Go Back

    //login.jade

     

    form(action="/login",method="POST")

    fieldset

    legend Log in

    P

    label Email

    input(name="user[email]",type='text')

    p

    label Password

    input(name="user[password]",type='text')

    p

    button Submit

    p

    a(href="/") Go Back

     

     

    1、原子性

      假设我们基于Express和MongoDB写了一个博客。有两个人,用户A想要编辑文档标题,与此同时用户B想要添加一个标签。如果两个用户都传递了一份完整的万当拷贝来进行更新操作,那么只会有一个胜出。另外一个将无法成功完成对文档的修改。

    要确保某个操作的原子性,MongoDB提供了$set 和$push这样不同的操作符。

    collection.updat({_id:id},{$set:{title:'my title'}})

    collection.updat({_id:id},{tag:{$push:'my tag'}})

     

    2、安全模式

    在操作文档的时候提供了一个可短的option参数。其中一个选项叫safe,它会在对数据库进行修改的时候启动安全模式。

     

    3、Mongoose 

    相比于原生的驱动器,Mongoos做的第一个简化的事情就是它会假定绝大部分的应用程序都是用一个数据库,这大大简化了使用方式。要连接数据库,只需要调用mongoose.connect并提供mongodb://URI即可。

    另外,使用mongoose就无须关心连接是否真的已经建立了,因为,它会先把数据库操作指令缓存起来,在连接上数据库后就会把这些操作发送给MongoDB。这就意味着,我们无须监听Connection的回调函数。

     

    5、定义嵌套的键

    var BlogPost = new Schema ({

    author: ObjectId,

    title: String,

    body: String,

    meta: {

    votes: Number,

    favs: NUmber

    }

     

     

    })

    像上面这样的文档是被允许的,那么如果要查找拥有指定投票数的博文,可以通过如下方式来实现

    db.blogposts.find({'mets.votes':5})

     

    6、定义嵌套文档,在MongoDB中,文档可以很大,也可以层次很深,也就是说,如果博文有留言的话,可以直接将留言定义在博文中,而不需要将其定义为单独的集合。

    var Comments = new Schema({

    title: String,

    body: String,

    date: Date

    })

    var BlogPost = new Schema({

    author: ObjectId,

    title: String,

    body: String,

    buf: Buffer,

    meta: {

    votes: Number,

    favs: NUmber

    },

    commets: [Comments]

    })

     

     

     

    7、构建索引

    索引是在MongoDB数据库中确保快速查询的重要因素。要对指定的键做索引,需要传递一个index选项,并将值设置为true。

    比如,要对title键做索引,并将uid键设置为唯一,可以这样

    var BlogPost = new Schema({

        author: ObjectId,

        title:{type:String,index:true},

        uid:{type:String,unique:true}

        body: String,

        buf: Buffer,

        meta: {

        votes: Number,

        favs: NUmber

        },

        commets: [Comments]

    })

     

    如果要设置更浮渣的索引(如组合索引),可以使用静态的index方法

    BlogPost.index({ket:-1,otherKey:1});

     

    8、中间件

    在相当一部分应用中,有的时候会在不同的地方以不同的方式对同样的数据进行修改。通过模型接口将这部分对数据库的交互集中处理是避免代码重复很有效的方法。Mongoose通过引入中间件来实现。Mongoose中间件的工作方式和Express中间件非常相似。定义一些方法,在某些特定动作前执行:save和remove

    比如,要在博文删除的时候,发送电子邮件给作者:

    BlogPost.pre('remove', function(next) {

    emailAuthor(this.email, 'Blog Post removed!');

    next();

    })

     

     

     

    9、扩展查询

    如果对某个查询不提供回调函数,那么知道调用run它才会执行:

    Post.find({

    author: '12345600'

    }).where('title', 'my title').sort('content', -1).limit(5).run(function(err, post) {

     

     

    })

    如果查询的文档很大,而想要的知识部分指定的键,那么就可以调用select方法

    Post.find().select('field','field2');

     

    如果要跳过指定数量的文档数据,可以通过如下方式

    query.skip(10);

    这个功能结合Model#count对做分页非常有用

    Post.count(funtion(err,totalPosts){

        var numPages = Math.ceil(totalPosts/10)

    })

     

    10、转换

    因为Mongoose提前知道需要什么样的数据类型,所以它总是会尝试做类型转换。

     

    //模块依赖

    var express = require('express'),

    mongoose = require('mongoose');

    var app = express.createServer();

    app.use(express.bodyParser());

    app.use(express.cookieParser());

    app.use(express.session({

    secret: 'my secret'

    }));

    //设置模板引擎

    app.set('view engine', 'jade');

    app.set('view options', {

    layout: false

    });

    //定义路由

    app.get('/', function(req, res) {

    //默认路由

    res.render('index', {

    authenticated: false

    })

    });

    app.get('/login', function(req, res) {

    //登录路由

    res.render('login');

    });

    app.get('/signup', function(req, res) {

    //注册路由

    res.render('signup');

    });

     

     

     

     

    app.get("/login/:signupEmail", function(req, res) {

    //处理登录路由

    console.log(req.params.signupEmail)

    res.render('login');

    })

    /**

    * 创建数据库

    * 提供ip和端口来初始化服务器

    */

    // var server = new mongodb.Server('127.0.0.1', 27017);

    //连接数据库

    mongoose.connect('mongodb://127.0.0.1/my-website');

    app.listen(3000, function() {

    console.log('app listen on 3000');

    })

     

     

    /**

    * 定义模型

    */

    var Schema = mongoose.Schema;

    var User = mongoose.model('User', new Schema({

    first: String,

    last: String,

    email: {

    type: String,

    unique: true

    },

    password: {

    type: String,

    index: true

    }

    }))

    //身份认证中间件

    app.use(function(req, res, next) {

    if (req.session.loggedIn) {

    res.local('authenticated', true);

    User.findById(req.session.loggedIn, function(err, doc) {

    if (err) return next(err);

    res.local('me', doc);

    next();

    })

    } else {

    res.local('authenticated', false);

    next();

    }

    })

     

     

    //处理登录路由

    app.post('/login', function(req, res) {

    User.findOne({

    email: req.body.user.email,

    password: req.body.user.password

    }, function(err, doc) {

    if (err) return next(err);

    if (!doc) return res.send('<p>user not found.go back and try agin</p>');

    req.session.loggedIn = doc._id.toString();

    res.redirect('/');

    })

    })

    //处理注册路由

    app.post('/signup', function(req, res) {

    var user = new User(req.body.user).save(function(err) {

    if (err) return next(err);

    res.redirect('/login/' + user.email)

    })

     

     

    })

     

     

     

    二、MySQL

    node-mysql

    1、创建数据库

    //添加对node-mysql的依赖

    var mysql = require('mysql');

    //连接mysql

    var db = mysql.createClient(config);

     

    2、表操作,使用mysql.createClient创建了数据库后,使用db.query()来执行sql语句,值得注意的是,如果sql语句中的值用?号来代替,那么query中第二个参数是一个数组,分别对应其值。第三个才是回调函数

    /创建数据库

    db.query('CREATE DATABASE IF NOT EXITS `car-example`');

    db.query('USE `car-example`');

     

     

    //创建表

    db.query('DROP TABLE IF EXITS item');

    db.query('CREATE TABLE item(id INT(11) AUTO_INCREMENT,title VARCHAT(255),description TEXT,PRIMARY KEY(id))');

     

     

    db.query('DROP TABLE IF EXITS review');

    db.query('CREATE TABLE review(id INT(11) AUTO_INCREMENT,item_id INT(11),text TEXT,stars INT(1),created DATETIME,PRIMARY KEY(id))');

     

     

    //关闭客户端

    db.end(function() {

    process.exit();

    });

     

    3、当执行的命令是select的时候,回调函数会接收一个包含查询结果对象的数组。数组中的对象包含了指定返回的字段。

    db.query('SELECT id,title,description FROM item', function(err, results) {

     

     

    });

     

    三、Redis

    1、redis中的查询

     

    KEYS接收一个匹配键的模式,并返回匹配到的键。*表示匹配所有键

    KEYS *

     

    使用SET来设置一个键值对,第一个是键,第二个是值

    SET my.key test

     

    使用GET来获取

    GET my.key

     

    2、哈希

    设置一个值

    HSET profile.1 name krys

    获取一个指定哈希中所有的键值

    HSETALL profile.1

    删除一个键(删除哈希 profile1中的name键值

    HDEL profile1. name

     

    检查指定的字段是否存在

    HEXISTS profile1. name

     

    3、列表

    redis中的列表等同于JavaScript中的字符串数组。

    对于列表,redis也有两个基本的操作命令是RPUSH(push到右边,也就是列表的尾端)和LPUSH(push到左边,也就是列表的顶端)

    RPUSH profile.1.jobs  "job1"

    RPUSH profile.1.jobs "job2"

     

    获取指定返回的数组(后面是范围,0 -1 表示返回所有)

    LRANGE profile.1 jobs 0 -1

     

    4、数据集

    数据集处于列表和哈希之间。

    它拥有哈希的属性,即数据集的每一项(或者说哈希中的键)都是唯一不重复的。既然是等同于哈希中的键,操作数据集中的元素都只需要固定时长(也就是说,无论数据集多大,删除、添加、查找数据集中的元素都只需同等的时间。)

    类似列表的是,数据集保存的是单个值,没有键。不过数据集还有它专属的有意思的特性。Redis允许在数据集、联合、获取到的随机元素等之间做交集操作。

     

    添加一个元素到数据集中,使用SADD

    SADD myset "a member"

    获取数据集中的所有元素,可以使用SMEMBERS:

    SMEMBERS myset

    移除数据集中的某个元素,可以使用SREM

    SERM myset "a member"

     

     

     

    展开全文
  • NodeJS教程

    千次阅读 2019-06-17 10:46:55
    简单的说 Node.js 就是运行在服务端的 JavaScriptNode.js 是一个基于Chrome JavaScript 运行时建立的一个平台。 Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度...

    简单的说 Node.js 就是运行在服务端的 JavaScript。

    Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台。

    Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好。

    Node.js 安装配置

    你可以根据不同平台系统选择你需要的 Node.js 安装包。

    检测PATH环境变量是否配置了Node.js
    输入"cmd" => 输入命令"path"

    Package.json 属性说明
    name - 包名。

    version - 包的版本号。

    description - 包的描述。

    homepage - 包的官网 url 。

    author - 包的作者姓名。

    contributors - 包的其他贡献者姓名。

    dependencies - 依赖包列表。如果依赖包没有安装,npm 会自动将依赖包安装在 node_module 目录下。

    repository - 包代码存放的地方的类型,可以是 git 或 svn,git 可在 Github 上。

    main - main 字段指定了程序的主入口文件,require(‘moduleName’) 就会加载这个文件。这个字段的默认值是模块根目录下面的 index.js。

    keywords - 关键字

    Node.js REPL(交互式解释器)
    Node.js REPL(Read Eval Print Loop:交互式解释器) 表示一个电脑的环境,类似 Window 系统的终端或 Unix/Linux shell,我们可以在终端中输入命令,并接收系统的响应。

    读取 - 读取用户输入,解析输入了Javascript 数据结构并存储在内存中。

    执行 - 执行输入的数据结构

    打印 - 输出结果

    循环 - 循环操作以上步骤直到用户两次按下 ctrl-c 按钮退出。

    下划线()变量
    你可以使用下划线(
    )获取上一个表达式的运算结果

    REPL 命令
    ctrl + c - 退出当前终端。

    ctrl + c 按下两次 - 退出 Node REPL。

    ctrl + d - 退出 Node REPL.

    向上/向下 键 - 查看输入的历史命令

    tab 键 - 列出当前命令

    .help - 列出使用命令

    .break - 退出多行表达式

    .clear - 退出多行表达式

    .save filename - 保存当前的 Node REPL 会话到指定文件

    .load filename - 载入当前 Node REPL 会话的文件内容。

    Node.js 是单进程单线程应用程序,但是因为 V8 引擎提供的异步执行回调接口,通过这些接口可以处理大量的并发,所以性能非常高。

    Node.js 几乎每一个 API 都是支持回调函数的。

    Node.js 基本上所有的事件机制都是用设计模式中观察者模式实现。

    Node.js 单线程类似进入一个while(true)的事件循环,直到没有事件观察者退出,每个异步事件都生成一个事件观察者,如果有事件发生就调用该回调函数.

    fs.readFile() 是异步函数用于读取文件。 如果在读取文件过程中发生错误,错误 err 对象就会输出错误信息。

    如果没发生错误,readFile 跳过 err 对象的输出,文件内容就通过回调函数输出。

    Node.js EventEmitter
    Node.js 所有的异步 I/O 操作在完成时都会发送一个事件到事件队列。

    events 模块只提供了一个对象: events.EventEmitter。EventEmitter 的核心就是事件触发与事件监听器功能的封装。

    你可以通过require(“events”);来访问该模块。

    EventEmitter 对象如果在实例化时发生错误,会触发 error 事件。当添加新的监听器时,newListener 事件会触发,当监听器被移除时,removeListener 事件被触发。

    //event.js 文件
    var EventEmitter = require('events').EventEmitter; 
    var event = new EventEmitter(); 
    event.on('some_event', function() { 
        console.log('some_event 事件触发'); 
    }); 
    setTimeout(function() { 
        event.emit('some_event'); 
    }, 1000);
    

    当事件触发时,注册到这个事件的事件监听器被依次调用,事件参数作为回调函数参数传递。

    //event.js 文件
    var events = require('events'); 
    var emitter = new events.EventEmitter(); 
    emitter.on('someEvent', function(arg1, arg2) { 
        console.log('listener1', arg1, arg2); 
    }); 
    emitter.on('someEvent', function(arg1, arg2) { 
        console.log('listener2', arg1, arg2); 
    }); 
    emitter.emit('someEvent', 'arg1 参数', 'arg2 参数'); 
    

    on 函数用于绑定事件函数,emit 属性用于触发一个事件。

    addListener(event, listener)
    为指定事件添加一个监听器到监听器数组的尾部。

    on(event, listener)
    为指定事件注册一个监听器,接受一个字符串 event 和一个回调函数。

    once(event, listener)
    为指定事件注册一个单次监听器,即 监听器最多只会触发一次,触发后立刻解除该监听器。

    removeAllListeners([event])
    移除所有事件的所有监听器, 如果指定事件,则移除指定事件的所有监听器。

    setMaxListeners(n)
    默认情况下, EventEmitters 如果你添加的监听器超过 10 个就会输出警告信息。 setMaxListeners 函数用于提高监听器的默认限制的数量。

    listeners(event)
    返回指定事件的监听器数组。

    emit(event, [arg1], [arg2], […])
    按监听器的顺序执行执行每个监听器,如果事件有注册监听返回 true,否则返回 false。

    events.EventEmitter.listenerCount(emitter, eventName) //已废弃,不推荐
    events.emitter.listenerCount(eventName) //推荐
    
    //event.js 文件
    var events = require('events'); 
    var emitter = new events.EventEmitter(); 
    emitter.on('someEvent', function(arg1, arg2) { 
        console.log('listener1', arg1, arg2); 
    }); 
    emitter.on('someEvent', function(arg1, arg2) { 
        console.log('listener2', arg1, arg2); 
    }); 
    emitter.emit('someEvent', 'arg1 参数', 'arg2 参数'); 
    

    newListener
    event - 字符串,事件名称

    listener - 处理事件函数

    该事件在添加新监听器时被触发。

    var events = require('events'); 
    var emitter = new events.EventEmitter(); 
    emitter.emit('error'); 
    

    Node.js Buffer(缓冲区)

    JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。


    请点赞!因为你的鼓励是我写作的最大动力!

    官方微信公众号

    吹逼交流群:711613774

    吹逼交流群

    展开全文
  • nodejs安装与配置+初学实例详解

    万次阅读 多人点赞 2019-11-14 22:49:36
    Nodejs安装与配置: 1.下载对应的nodejs安装包 ... 2.运行安装包,选择相关的路径,主要注意点如下选择add to path,因为这样会自动给你配置对应的环境变量,...检验是否安装成功,执行两个命令node -v 和npm -v(...

    Nodejs安装与配置:

    1.下载对应的nodejs安装包

    https://nodejs.org/en/download/

     

       2.运行安装包,选择相关的路径,主要注意点如下选择add to path,因为这样会自动给你配置对应的环境变量,其余的都是直接下一步下一步然后install

     

             3.检验是否安装成功,执行两个命令node -v 和npm -v(这个主要高版本的nodejs会在安装的过程中自动帮你安装的,主要的作用是对Node.js依赖的包进行管理,也可以理解为用来安装/卸载Node.js需要装的东西) 分别查看版本信息

     

     

    Nodejs使用前的准备工作

    1.在安装目录D:\Program Files\nodejs下创建两个文件夹node_global和node_cache,主要防止执行其他安装命令时候将东西安装在C盘里面,希望将全模块所在路径和缓存路径放在我node.js安装的文件夹中。

     

     2.新建文件后在执行命令行cmd,然后执行下面两个语句

            npm config set prefix "D:\Program Files\nodejs\node_global"

            npm config set cache "D:\Program Files\nodejs\node_cache"

     3.接下来设置环境变量,关闭cmd窗口,“我的电脑”-右键-“属性”-“高级系统设置”-“高级”-“环境变量”,系统变量下新建NODE_PATH,填写好对应的路径

     

     

    修改默认的用户变量D:\Program Files\nodejs\node_global

     

     

    测试一下

    配置完后,安装个module测试下,我们就安装最常用的express模块,打开cmd窗口,
    输入如下命令进行模块的全局安装:

    npm install express -g     # -g是全局安装的意思

     

     

    Nodejs的第一个应用实例:

    主要下面俩步骤:

    Step1:引入required模块,var http = require(“HTTP”);

    Step2:创建服务器,使用http.createServer()方法创建服务器,并使用listen方法绑定8888端口,函数通过request,response参数来接收和相应数据。

    例如下:

    var http = require('http');
    http.createServer(function (request, response) {
    response.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
    if(request.url!=="/favicon.ico"){ //清除第2此访问 node.js bug,第二次访问/favicon.ico
    console.log('访问');
    response.write('hello,world 世界,一点意思都没有');
    response.end();//不写则没有http协议尾
    }
    }).listen(8888);
    console.log('Server running at http://127.0.0.1:8888/');
    /*
    启动服务
    cmd下执行:
    node first_hello.js
    浏览器访问:http://localhost:8888
    
    */

    具体实现过程,先在本地创建一个存放代码的目录,里面写一个js文件(first_hello.js),代码如上所示,然后再在cmd下找到文件目录并运行 node first_hello.js

     

    再在浏览器中输入:http://localhost:8888即可

     

     

    觉得对大家有帮助的话麻烦给点赏钱,10元即可享受专人代写的情诗一首(q.2393703536)

    加入扶贫大军吧:

    展开全文
  • Node.js原生开发入门完全教程

    千次阅读 2017-07-15 15:01:02
    Node.js原生开发入门完全教程一、关于本篇文章致力于教会你如何用Node.js来开发应用,过程中会传授你所有所需的“高级”JavaScript知识。Node入门看这篇文章就够了。二、代码状态所有代码为春哥亲测,全部正确通过。...

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

    一、关于

    本篇文章致力于教会你如何用Node.js来开发应用,过程中会传授你所有所需的“高级”JavaScript知识。Node入门看这篇文章就够了。

    二、代码状态

    所有代码为春哥亲测,全部正确通过。

    三、阅读文章的对象

    1.有编程基础
    2.想转向Node.js后端的技术爱好者
    3.Node.js新手

    四、进入正题

    1.环境安装

    请直接移步Node.js官网,如下图所示,直接点击最新版下载并进行安装。

    Node.js安装完毕后,打开终端,在终端分别输入如下命令,检测是否安装成功。

    Last login: Tue Jun 27 09:19:38 on console
    liyuechun:~ yuechunli$ node -v
    v8.1.3
    liyuechun:~ yuechunli$ npm -v
    5.0.3
    liyuechun:~ yuechunli$ 

    如果能够正确显示node和npm的版本,说明Node.js安装成功。

    2.”Hello World”

    • 第一种输出方式

    好了,“废话”不多说了,马上开始我们第一个Node.js应用:“Hello World”。

    liyuechun:~ yuechunli$ node
    > console.log("Hello World!");
    Hello World!
    undefined
    > console.log("从零到壹全栈部落!");
    从零到壹全栈部落!
    undefined
    > process.exit()
    liyuechun:~ yuechunli$ 

    在终端里面直接输入命令node,接下来输入一句console.log("Hello World!"); ,回车,即可输出Hello World

    简单解释一下为什么每一次打印后面都出现了一个undefined,原因是因为你输入js代码并按下回车后,node会输出执行完该代码后的返回值,如果没有返回值,就会显示undefined,这个跟Chrome的调试工具相似。

    如上代码所示,当输入process.exit()并回车时,即可退出node模式

    • 第二种输出方式
    Last login: Thu Jun 29 18:17:27 on ttys000
    liyuechun:~ yuechunli$ ls
    Applications        Downloads       Pictures
    Creative Cloud Files    Library         Public
    Desktop         Movies
    Documents       Music
    liyuechun:~ yuechunli$ cd Desktop/
    liyuechun:Desktop yuechunli$ mkdir nodejs入门
    liyuechun:Desktop yuechunli$ pwd
    /Users/liyuechun/Desktop
    liyuechun:Desktop yuechunli$ cd nodejs入门/
    liyuechun:nodejs入门 yuechunli$ pwd
    /Users/liyuechun/Desktop/nodejs入门
    liyuechun:nodejs入门 yuechunli$ vi helloworld.js
    liyuechun:nodejs入门 yuechunli$ cat helloworld.js 
    console.log("Hello World!");
    liyuechun:nodejs入门 yuechunli$ node helloworld.js 
    Hello World!
    liyuechun:nodejs入门 yuechunli$ 

    命令解释:
    ls:查看当前路径下面的文件和文件夹。
    pwd:查看当前所在路径。
    cd Desktop:切换到桌面。
    mkdir nodejs入门:在当前路径下面创建nodejs入门文件夹。
    cd nodejs入门:进入nodejs入门文件夹。
    vi helloworld.js:创建一个helloworld.js文件,并在文件里面输入console.log("Hello World!"),保存并退出。
    cat helloworld.js:查看helloworld.js文件内容。
    node helloworld.js:在当前路径下面执行helloworld.js文件。

    PS:如果对命令行不熟悉的童鞋,可以用其他编辑器创建一个helloworld.js文件,在里面输入console.log("Hello World!"),将文件保存到桌面,然后打开终端,直接将helloworld.js文件拖拽到终端,直接在终端中执行node helloworld.js即可在终端输出Hello World!

    好吧,我承认这个应用是有点无趣,那么下面我们就来点“干货”。

    下面我们将通过VSCode来进行Node.js的编码。

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

    1.用例

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

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

    差不多了,你现在也可以去Google一下,找点东西乱搞一下来完成功能。但是我们现在先不做这个。

    更进一步地说,在完成这一目标的过程中,我们不仅仅需要基础的代码而不管代码是否优雅。我们还要对此进行抽象,来寻找一种适合构建更为复杂的Node.js应用的方式。

    2.应用不同模块分析

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

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

    现在我们就来开始实现之路,先从第一个部分–HTTP服务器着手。

    六、构建应用的模块

    1.一个基础的HTTP服务器

    用VSCode创建一个server.js的文件,将文件保存到桌面的nodejs入门文件夹里面。

    server.js文件里面写入以下内容:

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

    上面的代码就是一个完整的Node.js服务器,如下图所示,点击VSCode左下脚按钮,打开VSCode终端,在终端中输入node server.js来进行验证。

    如上图所示,一个基础的HTTP服务器搞定。

    2.HTTP服务器原理解析

    上面的案例中,第一行请求(require)Node.js自带的 http 模块,并且把它赋值给 http 变量。

    接下来我们调用http模块提供的函数: createServer 。这个函数会返回一个对象,这个对象有一个叫做 listen 的方法,这个方法有一个数值参数,指定这个HTTP服务器监听的端口号。

    咱们暂时先不管 http.createServer 的括号里的那个函数定义。

    我们本来可以用这样的代码来启动服务器并侦听8888端口:

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

    这段代码只会启动一个侦听8888端口的服务器,它不做任何别的事情,甚至连请求都不会应答。

    3.进行函数传递

    举例来说,你可以这样做:

    Last login: Thu Jun 29 20:03:25 on ttys001
    liyuechun:~ yuechunli$ node
    > function say(word) {
    ...   console.log(word);
    ... }
    undefined
    > 
    > function execute(someFunction, value) {
    ...   someFunction(value);
    ... }
    undefined
    > 
    > execute(say, "Hello");
    Hello
    undefined
    > 

    请仔细阅读这段代码!在这里,我们把 say 函数作为execute函数的第一个变量进行了传递。这里传递的不是 say 的返回值,而是 say 本身!

    这样一来, say 就变成了execute 中的本地变量 someFunction ,execute可以通过调用 someFunction() (带括号的形式)来使用 say 函数。

    当然,因为 say 有一个变量, execute 在调用 someFunction 时可以传递这样一个变量。

    我们可以,就像刚才那样,用它的名字把一个函数作为变量传递。但是我们不一定要绕这个“先定义,再传递”的圈子,我们可以直接在另一个函数的括号中定义和传递这个函数:

    Last login: Thu Jun 29 20:04:35 on ttys001
    liyuechun:~ yuechunli$ node
    > function execute(someFunction, value) {
    ...   someFunction(value);
    ... }
    undefined
    > 
    > execute(function(word){ console.log(word) }, "Hello");
    Hello
    undefined
    > 

    我们在 execute 接受第一个参数的地方直接定义了我们准备传递给 execute 的函数。

    用这种方式,我们甚至不用给这个函数起名字,这也是为什么它被叫做 匿名函数 。

    这是我们和我所认为的“进阶”JavaScript的第一次亲密接触,不过我们还是得循序渐进。现在,我们先接受这一点:在JavaScript中,一个函数可以作为另一个函数接收一个参数。我们可以先定义一个函数,然后传递,也可以在传递参数的地方直接定义函数。

    4.函数传递是如何让HTTP服务器工作的

    带着这些知识,我们再来看看我们简约而不简单的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);
    
    console.log("请在浏览器中打开 http://127.0.0.1:8888...");

    现在它看上去应该清晰了很多:我们向 createServer 函数传递了一个匿名函数。

    用这样的代码也可以达到同样的目的:

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    //请求(require)Node.js自带的 http 模块,并且把它赋值给 http 变量。
    let http = require("http");
    
    //箭头函数
    let onRequest = (request, response) => {
        response.writeHead(200, {"Content-Type": "text/plain"});
        response.write("Hello World");
        response.end();
    }
    //把函数当作参数传递
    http.createServer(onRequest).listen(8888);
    
    console.log("请在浏览器中打开 http://127.0.0.1:8888...");

    也许现在我们该问这个问题了:我们为什么要用这种方式呢?

    5.基于事件驱动的回调

    事件驱动是Node.js原生的工作方式,这也是它为什么这么快的原因。

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

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

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

    我们试试下面的代码:

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    //请求(require)Node.js自带的 http 模块,并且把它赋值给 http 变量。
    let http = require("http");
    
    //箭头函数
    let onRequest = (request, response) => {
        console.log("Request received.");
        response.writeHead(200, {"Content-Type": "text/plain;charset=utf-8"});
        response.write("添加小精灵微信(ershiyidianjian),加入全栈部落");
        response.end();
    }
    //把函数当作参数传递
    http.createServer(onRequest).listen(8888);
    
    console.log("Server has started.");
    console.log("请在浏览器中打开 http://127.0.0.1:8888...");

    在上图中,当我们执行node server.js命令时,Server has started.正常往下执行。

    我们看看当我们在浏览器里面打开http://127.0.0.1:8888时会发生什么。


    大家会发现在浏览器中打开http://127.0.0.1:8888时,在终端会输出Request received.,浏览器会输出添加小精灵微信(ershiyidianjian),加入全栈部落这一句话。

    请注意,当我们在服务器访问网页时,我们的服务器可能会输出两次“Request received.”。那是因为大部分浏览器都会在你访问 http://localhost:8888/ 时尝试读取 http://localhost:8888/favicon.ico )

    6.服务器是如何处理请求的

    好的,接下来我们简单分析一下我们服务器代码中剩下的部分,也就是我们的回调函数onRequest()的主体部分。

    当回调启动,我们的onRequest()函数被触发的时候,有两个参数被传入:requestresponse

    它们是对象,你可以使用它们的方法来处理HTTP请求的细节,并且响应请求(比如向发出请求的浏览器发回一些东西)。

    所以我们的代码就是:当收到请求时,使用response.writeHead()函数发送一个HTTP状态200和HTTP头的内容类型(content-type),使用response.write()函数在HTTP相应主体中发送文本添加小精灵微信(ershiyidianjian),加入全栈部落

    最后,我们调用 response.end() 完成响应。

    目前来说,我们对请求的细节并不在意,所以我们没有使用 request 对象。

    7.服务端模块化

    • 何为模块
    let http = require("http");
    ...
    http.createServer(...);

    在上面的代码中,Node.js中自带了一个叫做“http”的模块,我们在我们的代码中请求它并把返回值赋给一个本地变量。

    这把我们的本地变量变成了一个拥有所有 http 模块所提供的公共方法的对象。

    给这种本地变量起一个和模块名称一样的名字是一种惯例,但是你也可以按照自己的喜好来:

    var foo = require("http");
    ...
    foo.createServer(...);
    • 如何自定义模块

    server.js文件的内容改成下面的内容。

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    //请求(require)Node.js自带的 http 模块,并且把它赋值给 http 变量。
    let http = require("http");
    
    //用一个函数将之前的内容包裹起来
    let start = () => {
            //箭头函数
        let onRequest = (request, response) => {
            console.log("Request received.");
            response.writeHead(200, {"Content-Type": "text/plain;charset=utf-8"});
            response.write("添加小精灵微信(ershiyidianjian),加入全栈部落");
            response.end();
        }
        //把函数当作参数传递
        http.createServer(onRequest).listen(8888);
    
        console.log("Server has started.");
        console.log("请在浏览器中打开 http://127.0.0.1:8888...");
    }
    
    //导出`server`对象,对象中包含一个start函数
    //对象格式为
    /**
     * {
     *    start
     * }
     */
    
    //这个对象导入到其他文件中即可使用,可以用任意的名字来接收这个对象
    
    exports.start = start;

    server.js当前的文件路径下新建一个index.js文件。内容如下:

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    //从`server`模块中导入server对象
    
    let server = require('./server');
    
    //启动服务器
    server.start();

    如下图所示运行index.js文件。



    一切运行正常,上面的案例中,server.js就是自定义的模块。

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

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

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

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

                                   url.parse(string).query
                                               |
               url.parse(string).pathname      |
                           |                   |
                           |                   |
                         ------ -------------------
    http://localhost:8888/start?foo=bar&hello=world
                                    ---       -----
                                     |          |
                                     |          |
                  querystring.parse(string)["foo"]    |
                                                |
                             querystring.parse(string)["hello"]
    

    当然我们也可以用querystring模块来解析POST请求体中的参数,稍后会有演示。

    现在我们来给onRequest()函数加上一些逻辑,用来找出浏览器请求的URL路径:

    接下来我在终端执行node index.js命令,如下所示:

    bogon:如何来进行请求的“路由” yuechunli$ node index.js
    Server has started.
    请在浏览器中打开 http://127.0.0.1:8888...

    我先在Safari浏览器中打开http://127.0.0.1:8888,浏览器展示效果如下:

    控制台效果如下:

    bogon:如何来进行请求的“路由” yuechunli$ node index.js
    Server has started.
    请在浏览器中打开 http://127.0.0.1:8888...
    Request for / received.

    接着我在Google浏览器里面打开 http://127.0.0.1:8888… ,浏览器效果图如下:

    控制台效果如下:

    为什么在Safari浏览器中进行请求时,只打印了一个Request for / received.,而在Google浏览器中访问时,会多打印一个Request for /favicon.ico received.,如上图所示,原因是因为在Google浏览器中,浏览器的原因会去尝试请求favicon.ico小图标。

    为了演示效果,还有不受Google浏览器的favicon.ico请求的干扰,我接着在Safari里面请求http://127.0.0.1:8888/starthttp://127.0.0.1:8888/upload,我们看看控制台展示的内容是什么。

    bogon:如何来进行请求的“路由” yuechunli$ node index.js
    Server has started.
    请在浏览器中打开 http://127.0.0.1:8888...
    Request for /start received.
    Request for /upload received.

    好了,我们的应用现在可以通过请求的URL路径来区别不同请求了–这使我们得以使用路由(还未完成)来将请求以URL路径为基准映射到处理程序上。

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

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

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    function route(pathname) {
      console.log("About to route a request for " + pathname);
    }
    
    exports.route = route;

    如你所见,这段代码什么也没干,不过对于现在来说这是应该的。在添加更多的逻辑以前,我们先来看看如何把路由和服务器整合起来。

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

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    //请求(require)Node.js自带的 http 模块,并且把它赋值给 http 变量。
    let http = require("http");
    
    let url = require("url");
    
    
    //用一个函数将之前的内容包裹起来
    let start = (route) => {
            //箭头函数
        let onRequest = (request, response) => {
    
            let pathname = url.parse(request.url).pathname;
            console.log("Request for " + pathname + " received.");
            route(pathname);
    
            response.writeHead(200, {"Content-Type": "text/plain;charset=utf-8"});
            response.write("添加小精灵微信(ershiyidianjian),加入全栈部落");
            response.end();
        }
        //把函数当作参数传递
        http.createServer(onRequest).listen(8888);
    
        console.log("Server has started.");
        console.log("请在浏览器中打开 http://127.0.0.1:8888...");
    }
    
    exports.start = start;

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

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    //从`server`模块中导入server对象
    
    let server = require('./server');
    let router = require("./router");
    
    //启动服务器
    server.start(router.route);

    在这里,我们传递的函数依旧什么也没做。

    如果现在启动应用(node index.js,始终记得这个命令行),随后请求一个URL,你将会看到应用输出相应的信息,这表明我们的HTTP服务器已经在使用路由模块了,并会将请求的路径传递给路由:

    bogon:如何来进行请求的“路由” v2.0 yuechunli$ node index.js
    Server has started.
    请在浏览器中打开 http://127.0.0.1:8888...
    Request for / received.
    About to route a request for /

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

    现在我们的HTTP服务器和请求路由模块已经如我们的期望,可以相互交流了,就像一对亲密无间的兄弟。

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

    在现在的实现下,路由过程会在路由模块中“结束”,并且路由模块并不是真正针对请求“采取行动”的模块,否则当我们的应用程序变得更为复杂时,将无法很好地扩展。

    我们暂时把作为路由目标的函数称为请求处理程序。现在我们不要急着来开发路由模块,因为如果请求处理程序没有就绪的话,再怎么完善路由模块也没有多大意义。

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

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    function start() {
      console.log("Request handler 'start' was called.");
    }
    
    function upload() {
      console.log("Request handler 'upload' was called.");
    }
    
    exports.start = start;
    exports.upload = upload;

    现在我们将一系列请求处理程序通过一个对象来传递,并且需要使用松耦合的方式将这个对象注入到route()函数中。

    我们先将这个对象引入到主文件index.js中:

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    //从`server`模块中导入server对象
    
    let server = require('./server');
    let router = require("./router");
    let requestHandlers = require("./requestHandlers");
    
    //对象构造
    var handle = {}
    handle["/"] = requestHandlers.start;
    handle["/start"] = requestHandlers.start;
    handle["/upload"] = requestHandlers.upload;
    
    //启动服务器
    server.start(router.route, handle);

    虽然handle并不仅仅是一个“东西”(一些请求处理程序的集合),我还是建议以一个动词作为其命名,这样做可以让我们在路由中使用更流畅的表达式,稍后会有说明。

    正如所见,将不同的URL映射到相同的请求处理程序上是很容易的:只要在对象中添加一个键为"/"的属性,对应requestHandlers.start即可,这样我们就可以干净简洁地配置/start"/"的请求都交由start这一处理程序处理。

    在完成了对象的定义后,我们把它作为额外的参数传递给服务器,为此将server.js修改如下:

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    //请求(require)Node.js自带的 http 模块,并且把它赋值给 http 变量。
    let http = require("http");
    
    let url = require("url");
    
    
    //用一个函数将之前的内容包裹起来
    let start = (route,handle) => {
            //箭头函数
        let onRequest = (request, response) => {
    
            let pathname = url.parse(request.url).pathname;
            console.log("Request for " + pathname + " received.");
            route(handle,pathname);
    
            response.writeHead(200, {"Content-Type": "text/plain;charset=utf-8"});
            response.write("添加小精灵微信(ershiyidianjian),加入全栈部落");
            response.end();
        }
        //把函数当作参数传递
        http.createServer(onRequest).listen(8888);
    
        console.log("Server has started.");
        console.log("请在浏览器中打开 http://127.0.0.1:8888...");
    }
    
    exports.start = start;

    这样我们就在start()函数里添加了handle参数,并且把handle对象作为第一个参数传递给了route()回调函数。

    然后我们相应地在route.js文件中修改route()函数:

    有了这些,我们就把服务器、路由和请求处理程序在一起了。现在我们启动应用程序并在浏览器中访问http://127.0.0.1:8888/start,以下日志可以说明系统调用了正确的请求处理程序:

    bogon:路由给真正的请求处理程序 yuechunli$ node index.js
    Server has started.
    请在浏览器中打开 http://127.0.0.1:8888...
    Request for /start received.
    About to route a request for /start
    Request handler 'start' was called.

    并且在浏览器中打开http://127.0.0.1:8888/可以看到这个请求同样被start请求处理程序处理了:

    bogon:路由给真正的请求处理程序 yuechunli$ node index.js
    Server has started.
    请在浏览器中打开 http://127.0.0.1:8888...
    Request for / received.
    About to route a request for /
    Request handler 'start' was called.

    10.让请求处理程序作出响应

    很好。不过现在要是请求处理程序能够向浏览器返回一些有意义的信息而并非全是添加小精灵微信(ershiyidianjian),加入全栈部落,那就更好了。

    这里要记住的是,浏览器发出请求后获得并显示的添加小精灵微信(ershiyidianjian),加入全栈部落信息仍是来自于我们server.js文件中的onRequest函数。

    其实“处理请求”说白了就是“对请求作出响应”,因此,我们需要让请求处理程序能够像onRequest函数那样可以和浏览器进行“对话”。

    11.不好的实现方式

    • 修改requestHandler.js文件内容如下:
    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    function start() {
      console.log("Request handler 'start' was called.");
      return "Hello Start";
    }
    
    function upload() {
      console.log("Request handler 'upload' was called.");
      return "Hello Upload";
    }
    
    exports.start = start;
    exports.upload = upload;

    好的。同样的,请求路由需要将请求处理程序返回给它的信息返回给服务器。因此,我们需要将router.js修改为如下形式:

    
    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    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进行重构以使得它能够将请求处理程序通过请求路由返回的内容响应给浏览器,如下所示:

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    //请求(require)Node.js自带的 http 模块,并且把它赋值给 http 变量。
    let http = require("http");
    
    let url = require("url");
    
    //用一个函数将之前的内容包裹起来
    let start = (route,handle) => {
            //箭头函数
        let onRequest = (request, response) => {
    
            let pathname = url.parse(request.url).pathname;
            console.log("Request for " + pathname + " received.");
            route(handle,pathname);
    
            response.writeHead(200, {"Content-Type": "text/plain;charset=utf-8"});
            var content = route(handle, pathname)
            response.write(content);
            response.end();
        }
        //把函数当作参数传递
        http.createServer(onRequest).listen(8888);
    
        console.log("Server has started.");
    }
    
    exports.start = start;

    如果我们运行重构后的应用,一切都会工作的很好:

    • 请求http://localhost:8888/start,浏览器会输出Hello Start
    • 请求http://localhost:8888/upload会输出Hello Upload,
    • 而请求http://localhost:8888/foo 会输出404 Not found

    好,那么问题在哪里呢?简单的说就是: 当未来有请求处理程序需要进行非阻塞的操作的时候,我们的应用就“挂”了。

    没理解?没关系,下面就来详细解释下。

    12.阻塞与非阻塞

    我们先不解释这里阻塞非阻塞,我们来修改下start请求处理程序,我们让它等待10秒以后再返回Hello Start。因为,JavaScript中没有类似sleep()这样的操作,所以这里只能够来点小Hack来模拟实现。

    让我们将requestHandlers.js修改成如下形式:

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    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;

    上述代码中,我先调用了upload(),会和此前一样立即返回。当函数start()被调用的时候,Node.js会先等待10秒,之后才会返回“Hello Start”。如下图所示,等待中:

    (当然了,这里只是模拟休眠10秒,实际场景中,这样的阻塞操作有很多,比方说一些长时间的计算操作等。)

    接下来就让我们来看看,我们的改动带来了哪些变化。

    如往常一样,我们先要重启下服务器。为了看到效果,我们要进行一些相对复杂的操作(跟着我一起做): 首先,打开两个浏览器窗口或者标签页。在第一个浏览器窗口的地址栏中输入http://localhost:8888/start, 但是先不要打开它!

    在第二个浏览器窗口的地址栏中输入http://localhost:8888/upload, 同样的,先不要打开它!

    接下来,做如下操作:在第一个窗口中(“/start”)按下回车,然后快速切换到第二个窗口中(“/upload”)按下回车。

    注意,发生了什么: /start URL加载花了10秒,这和我们预期的一样。但是,/upload URL居然也花了10秒,而它在对应的请求处理程序中并没有类似于sleep()这样的操作!

    这到底是为什么呢?原因就是start()包含了阻塞操作。形象的说就是“它阻塞了所有其他的处理工作”。

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

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

    然而,要用非阻塞操作,我们需要使用回调,通过将函数作为参数传递给其他需要花时间做处理的函数(比方说,休眠10秒,或者查询数据库,又或者是进行大量的计算)。

    对于Node.js来说,它是这样处理的:“嘿,probablyExpensiveFunction()(译者注:这里指的就是需要花时间处理的函数),你继续处理你的事情,我(Node.js线程)先不等你了,我继续去处理你后面的代码,请你提供一个callbackFunction(),等你处理完之后我会去调用该回调函数的,谢谢!”

    (如果想要了解更多关于事件轮询细节,可以阅读Mixu的博文——理解node.js的事件轮询。)

    接下来,我们会介绍一种错误的使用非阻塞操作的方式。

    和上次一样,我们通过修改我们的应用来暴露问题。

    这次我们还是拿start请求处理程序来“开刀”。将其修改成如下形式:

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    
    //我们引入了一个新的Node.js模块,child_process。之所以用它,是为了实现一个既简单又实用的非阻塞操作:exec()。
    var exec = require("child_process").exec;
    
    function start() {
      console.log("Request handler 'start' was called.");
    
      /**
       * exec()做了什么呢?
       * 它从Node.js来执行一个shell命令。
       * 在本例子中,我们用它来获取当前目录下所有的文件(“ls -lah”)
       * 然后,当`/start` URL请求的时候将文件信息输出到浏览器中。
       * 下面的代码非常直观的: 
       * 创建了一个新的变量content(初始值为“empty”)。
       * 执行“ls -lah”命令,将结果赋值给content,最后将content返回。
       */
      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;

    和往常一样,我们启动服务器,然后访问“http://localhost:8888/start” 。

    载入一个漂亮的web页面,其内容为“empty”。怎么回事?

    如果想要证明这一点,可以将“ls -lah”换成比如“find /”这样更耗时的操作来效果。

    然而,针对浏览器显示的结果来看,我们并不满意我们的非阻塞操作,对吧?

    好,接下来,我们来修正这个问题。在这过程中,让我们先来看看为什么当前的这种方式不起作用。

    问题就在于,为了进行非阻塞工作,exec()使用了回调函数。

    在我们的例子中,该回调函数就是作为第二个参数传递给exec()的匿名函数:

    function (error, stdout, stderr) {
      content = stdout;
    }

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

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

    为了让效果更加明显,我们想象一个更耗时的命令: “find /”,它在我机器上需要执行1分钟左右的时间,然而,尽管在请求处理程序中,我把“ls -lah”换成“find /”,当打开/start URL的时候,依然能够立即获得HTTP响应 —— 很明显,当exec()在后台执行的时候,Node.js自身会继续执行后面的代码。并且我们这里假设传递给exec()的回调函数,只会在“find /”命令执行完成之后才会被调用。

    那究竟我们要如何才能实现将当前目录下的文件列表显示给用户呢?

    好,了解了这种不好的实现方式之后,我们接下来来介绍如何以正确的方式让请求处理程序对浏览器请求作出响应。

    13.以非阻塞操作进行请求响应

    我刚刚提到了这样一个短语 —— “正确的方式”。而事实上通常“正确的方式”一般都不简单。

    不过,用Node.js就有这样一种实现方案: 函数传递。下面就让我们来具体看看如何实现。

    到目前为止,我们的应用已经可以通过应用各层之间传递值的方式(请求处理程序 -> 请求路由 -> 服务器)将请求处理程序返回的内容(请求处理程序最终要显示给用户的内容)传递给HTTP服务器。

    现在我们采用如下这种新的实现方式:相对采用将内容传递给服务器的方式,我们这次采用将服务器“传递”给内容的方式。 从实践角度来说,就是将response对象(从服务器的回调函数onRequest()获取)通过请求路由传递给请求处理程序。 随后,处理程序就可以采用该对象上的函数来对请求作出响应。

    原理就是如此,接下来让我们来一步步实现这种方案。

    先从server.js开始:

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    //请求(require)Node.js自带的 http 模块,并且把它赋值给 http 变量。
    let http = require("http");
    
    let url = require("url");
    
    //用一个函数将之前的内容包裹起来
    let start = (route,handle) => {
            //箭头函数
        let onRequest = (request, response) => {
    
            let 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()函数,并且,我们将onRequest()处理程序中所有有关response的函数调都移除,因为我们希望这部分工作让route()函数来完成。

    下面就来看看我们的router.js:

    同样的模式:相对此前从请求处理程序中获取返回值,这次取而代之的是直接传递response对象。

    如果没有对应的请求处理器处理,我们就直接返回“404”错误。

    最后,我们将requestHandler.js修改为如下形式:

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    
    //我们引入了一个新的Node.js模块,child_process。之所以用它,是为了实现一个既简单又实用的非阻塞操作:exec()。
    var exec = require("child_process").exec;
    
    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;

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

    start处理程序在exec()的匿名回调函数中做请求响应的操作,而upload处理程序仍然是简单的回复“Hello World”,只是这次是使用response对象而已。

    这时再次我们启动应用(node index.js),一切都会工作的很好。

    在浏览器中打开 http:127.0.0.0:8888/start 效果图如下所示:

    在浏览器中打开 http:127.0.0.0:8888/upload 效果图如下所示:

    如果想要证明/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响应还在处理中。

    14.更有用的场景

    到目前为止,我们做的已经很好了,但是,我们的应用没有实际用途。

    服务器,请求路由以及请求处理程序都已经完成了,下面让我们按照此前的用例给网站添加交互:用户选择一个文件,上传该文件,然后在浏览器中看到上传的文件。 为了保持简单,我们假设用户只会上传图片,然后我们应用将该图片显示到浏览器中。

    好,下面就一步步来实现,鉴于此前已经对JavaScript原理性技术性的内容做过大量介绍了,这次我们加快点速度。

    要实现该功能,分为如下两步: 首先,让我们来看看如何处理POST请求(非文件上传),之后,我们使用Node.js的一个用于文件上传的外部模块。之所以采用这种实现方式有两个理由。

    第一,尽管在Node.js中处理基础的POST请求相对比较简单,但在这过程中还是能学到很多。
    第二,用Node.js来处理文件上传(multipart POST请求)是比较复杂的,它不在本文的范畴,但是,如何使用外部模块却是在本书涉猎内容之内。

    15.处理POST请求

    考虑这样一个简单的例子:我们显示一个文本区(textarea)供用户输入内容,然后通过POST请求提交给服务器。最后,服务器接受到请求,通过处理程序将输入的内容展示到浏览器中。

    /start请求处理程序用于生成带文本区的表单,因此,我们将requestHandlers.js修改为如下形式:

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    
    //我们引入了一个新的Node.js模块,child_process。之所以用它,是为了实现一个既简单又实用的非阻塞操作:exec()。
    var exec = require("child_process").exec;
    
    function start(response) {
      console.log("Request handler 'start' was called.");
    
      let 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="5" cols="60"></textarea>'+
        '<input type="submit" value="Submit text" />'+
        '</form>'+
        '</body>'+
        '</html>';
    
        response.writeHead(200, {"Content-Type": "text/html;charset=utf-8"});
        response.write(body);
        response.end();
    }
    
    function upload(response) {
      console.log("Request handler 'upload' was called.");
      response.writeHead(200, {"Content-Type": "text/plain;charset=utf-8"});
      response.write("Hello Upload");
      response.end();
    }
    
    exports.start = start;
    exports.upload = upload;

    浏览器请求http://127.0.0.1:8888/start,效果图如下:

    余下的篇幅,我们来探讨一个更有趣的问题: 当用户提交表单时,触发/upload请求处理程序处理POST请求的问题。

    现在,我们已经是新手中的专家了,很自然会想到采用异步回调来实现非阻塞地处理POST请求的数据。

    这里采用非阻塞方式处理是明智的,因为POST请求一般都比较“重” —— 用户可能会输入大量的内容。用阻塞的方式处理大数据量的请求必然会导致用户操作的阻塞。

    为了使整个过程非阻塞,Node.js会将POST数据拆分成很多小的数据块,然后通过触发特定的事件,将这些小数据块传递给回调函数。这里的特定的事件有data事件(表示新的小数据块到达了)以及end事件(表示所有的数据都已经接收完毕)。

    我们需要告诉Node.js当这些事件触发的时候,回调哪些函数。怎么告诉呢? 我们通过在request对象上注册监听器(listener) 来实现。这里的request对象是每次接收到HTTP请求时候,都会把该对象传递给onRequest回调函数。

    如下所示:

    request.addListener("data", function(chunk) {
      // called when a new chunk of data was received
    });
    
    request.addListener("end", function() {
      // called when all chunks of data have been received
    });

    问题来了,这部分逻辑写在哪里呢? 我们现在只是在服务器中获取到了request对象 —— 我们并没有像之前response对象那样,把 request 对象传递给请求路由和请求处理程序。

    在我看来,获取所有来自请求的数据,然后将这些数据给应用层处理,应该是HTTP服务器要做的事情。因此,我建议,我们直接在服务器中处理POST数据,然后将最终的数据传递给请求路由和请求处理器,让他们来进行进一步的处理。

    因此,实现思路就是: 将data和end事件的回调函数直接放在服务器中,在data事件回调中收集所有的POST数据,当接收到所有数据,触发end事件后,其回调函数调用请求路由,并将数据传递给它,然后,请求路由再将该数据传递给请求处理程序。

    还等什么,马上来实现。先从server.js开始:

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    //请求(require)Node.js自带的 http 模块,并且把它赋值给 http 变量。
    let http = require("http");
    
    let url = require("url");
    
    //用一个函数将之前的内容包裹起来
    let start = (route,handle) => {
            //箭头函数
        let onRequest = (request, response) => {
    
            let postData = "";
            let 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”事件的监听器,用于收集每次接收到的新数据块,并将其赋值给postData 变量,最后,我们将请求路由的调用移到end事件处理程序中,以确保它只会当所有数据接收完毕后才触发,并且只触发一次。我们同时还把POST数据传递给请求路由,因为这些数据,请求处理程序会用到。

    上述代码在每个数据块到达的时候输出了日志,这对于最终生产环境来说,是很不好的(数据量可能会很大,还记得吧?),但是,在开发阶段是很有用的,有助于让我们看到发生了什么。

    我建议可以尝试下,尝试着去输入一小段文本,以及大段内容,当大段内容的时候,就会发现data事件会触发多次。

    再来点酷的。我们接下来在/upload页面,展示用户输入的内容。要实现该功能,我们需要将postData传递给请求处理程序,修改router.js为如下形式:

    
    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    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中,我们将数据包含在对upload请求的响应中:

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    
    //我们引入了一个新的Node.js模块,child_process。之所以用它,是为了实现一个既简单又实用的非阻塞操作:exec()。
    var exec = require("child_process").exec;
    
    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: " + postData);
      response.end();
    }
    
    exports.start = start;
    exports.upload = upload;

    好了,我们现在可以接收POST数据并在请求处理程序中处理该数据了。

    我们最后要做的是: 当前我们是把请求的整个消息体传递给了请求路由和请求处理程序。我们应该只把POST数据中,我们感兴趣的部分传递给请求路由和请求处理程序。在我们这个例子中,我们感兴趣的其实只是text字段。

    我们可以使用此前介绍过的querystring模块来实现:

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    
    //我们引入了一个新的Node.js模块,child_process。之所以用它,是为了实现一个既简单又实用的非阻塞操作:exec()。
    var exec = require("child_process").exec;
    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 the text: "+querystring.parse(postData).text);
      response.end();
    }
    
    exports.start = start;
    exports.upload = upload;

    下面我们浏览器中访问http://127.0.0.1:8888/start,如下图所示:

    点击Submit text按钮,将跳转到http://127.0.0.1:8888/upload,效果图如下:

    好了,这就是完整的POST请求。

    15.处理文件上传

    最后,我们来实现我们最终的用例:允许用户上传图片,并将该图片在浏览器中显示出来。

    我们通过它能学到这样两件事情:

    • 如何安装外部Node.js模块
    • 以及如何将它们应用到我们的应用中

    这里我们要用到的外部模块是Felix Geisendörfer开发的node-formidable模块。它对解析上传的文件数据做了很好的抽象。 其实说白了,处理文件上传“就是”处理POST数据 —— 但是,麻烦的是在具体的处理细节,所以,这里采用现成的方案更合适点。

    使用该模块,首先需要安装该模块。Node.js有它自己的包管理器,叫NPM。它可以让安装Node.js的外部模块变得非常方便。

    首先在当前项目路径下面通过npm init创建package.json文件:

    PS:在终端输入npm init后,一路回车即可。新增的package.json文件的内容如下:

    {
      "author" : "liyuechun",
      "description" : "",
      "license" : "ISC",
      "main" : "index.js",
      "name" : "fileupload",
      "scripts" : {
        "test" : "echo \"Error: no test specified\" && exit 1"
      },
      "version" : "1.0.0"
    }

    接下来,在终端输入如下命令安装formidable外部模块。
    如下所示:

    liyuechun:fileupload yuechunli$ ls
    index.js                requestHandlers.js      server.js
    package.json            router.js
    liyuechun:fileupload yuechunli$ npm install formidable
    npm notice created a lockfile as package-lock.json. You should commit this file.
    npm WARN fileupload@1.0.0 No description
    npm WARN fileupload@1.0.0 No repository field.
    
    + formidable@1.1.1
    added 1 package in 1.117s
    liyuechun:fileupload yuechunli$

    package.json文件变化如下:

    {
      "author": "liyuechun",
      "description": "",
      "license": "ISC",
      "main": "index.js",
      "name": "fileupload",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "version": "1.0.0",
      "dependencies": {
        "formidable": "^1.1.1"
      }
    }

    项目整体变化如下图所示:

    现在我们就可以用formidable模块了——使用外部模块与内部模块类似,用require语句将其引入即可:

    let formidable = require("formidable");

    这里该模块做的就是将通过HTTP POST请求提交的表单,在Node.js中可以被解析。我们要做的就是创建一个新的IncomingForm,它是对提交表单的抽象表示,之后,就可以用它解析request对象,获取表单中需要的数据字段。

    node-formidable官方的例子展示了这两部分是如何融合在一起工作的:

    let formidable = require('formidable'),
        http = require('http'),
        util = require('util');
    
    http.createServer(function(req, res) {
      if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
        // parse a file upload
        let 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;
      }
    
      // show a file upload form
      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);

    如果我们将上述代码,保存到一个文件中,并通过node来执行,就可以进行简单的表单提交了,包括文件上传。然后,可以看到通过调用form.parse传递给回调函数的files对象的内容,如下所示:

    received upload:
    
    { fields: { title: 'Hello World' },
      files:
       { upload:
          { size: 1558,
            path: './tmp/1c747974a27a6292743669e91f29350b',
            name: 'us-flag.png',
            type: 'image/png',
            lastModifiedDate: Tue, 21 Jun 2011 07:02:41 GMT,
            _writeStream: [Object],
            length: [Getter],
            filename: [Getter],
            mime: [Getter] } } }

    为了实现我们的功能,我们需要将上述代码应用到我们的应用中,另外,我们还要考虑如何将上传文件的内容(保存在./tmp目录中)显示到浏览器中。

    我们先来解决后面那个问题: 对于保存在本地硬盘中的文件,如何才能在浏览器中看到呢?

    显然,我们需要将该文件读取到我们的服务器中,使用一个叫fs的模块。

    我们来添加/showURL的请求处理程序,该处理程序直接硬编码将文件./tmp/test.png内容展示到浏览器中。当然了,首先需要将该图片保存到这个位置才行。

    requestHandlers.js修改为如下形式:

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    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 the text: "+
      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中的路由映射表中:

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    //从`server`模块中导入server对象
    
    let server = require('./server');
    let router = require("./router");
    let 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);

    重启服务器之后,通过访问http://localhost:8888/show看看效果:

    原因是当前项目路径下面没有./tmp/test.png图片,我们在当前项目路径下面添加tmp文件夹,在往里面拖拽一张图片,命名为test.png

    再重新启动服务器,访问http://localhost:8888/show查看效果如下:

    咱继续,从server.js开始 —— 移除对postData的处理以及request.setEncoding (这部分node-formidable自身会处理),转而采用将request对象传递给请求路由的方式:

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    //请求(require)Node.js自带的 http 模块,并且把它赋值给 http 变量。
    let http = require("http");
    
    let url = require("url");
    
    //用一个函数将之前的内容包裹起来
    let start = (route,handle) => {
            //箭头函数
        let onRequest = (request, response) => {
    
            let 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 —— 我们不再需要传递postData了,这次要传递request对象:

    
    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    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/html"});
        response.write("404 Not found");
        response.end();
      }
    }
    
    exports.route = route;

    现在,request对象就可以在我们的upload请求处理程序中使用了。node-formidable会处理将上传的文件保存到本地/tmp目录中,而我们需要做的是确保该文件保存成./tmp/test.png。 没错,我们保持简单,并假设只允许上传PNG图片。

    这里采用fs.renameSync(path1,path2)来实现。要注意的是,正如其名,该方法是同步执行的, 也就是说,如果该重命名的操作很耗时的话会阻塞。 这块我们先不考虑。

    接下来,我们把处理文件上传以及重命名的操作放到一起,如下requestHandlers.js所示:

    /**
     * 从零到壹全栈部落,添加小精灵微信(ershiyidianjian)
     */
    
    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-data" '+
        'method="post">'+
        '<input type="file" name="upload" multiple="multiple">'+
        '<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();
      console.log("about to parse");
      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/html"});
        response.write("received image:<br/>");
        response.write("<img src='/show' />");
        response.end();
      });
    }
    
    function show(response) {
      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;

    重启服务器,浏览器访问http://127.0.0.1:8888,效果图如下:

    选择一张图片上传,查看效果:

    16.源码下载

    所有源码:https://github.com/fullstacktribe/001nodejs

    展开全文
  • NodeJS教程完整版

    2020-07-30 23:33:34
    Node.js 就是运行在服务端的 JavaScriptNode.js 是一个基于Chrome JavaScript 运行时建立的一个平台。 Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,...
  • Node初学者入门 , 一本全面的NodeJS 教程,Manuel Kiessling著
  • node.js 安装详细步骤教程

    万次阅读 多人点赞 2019-01-08 22:43:01
    Node.js 官方网站下载:https://nodejs.org/en/ 选择操作系统对应的包: 下载完成,安装包如下:   2、安装 打开安装,傻瓜式下一步即可:       选择安装位置,我这里装在D盘下:    ...
  • node.js详细安装教程及使用

    万次阅读 2018-08-29 20:44:05
    Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。  Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。  Node.js 的包管理器 npm,是全球最大的开源库生态系统。 简单的说 Node.js ...
  • 阮一峰的nodejs讲解

    千次阅读 2017-11-14 15:54:25
    http://javascript.ruanyifeng.com/nodejs/fs.html
  • VS code中的Node.js教程

    万次阅读 2018-08-21 16:58:07
    VS code中的Node.js教程 一、简介 Node.js是一个使用JavaScript构建快速,可扩展的服务器应用程序的平台。Node.js是运行时库,NPM是Node.js模块的包管理器。 Visual Studio Code支持开箱即用的JavaScript和...
  • Node.js进阶教程第一步(基础篇)

    万人学习 2016-10-08 11:16:55
    本课程从Node.js的安装配置开始,包括函数调用,模块调用,路由,文件操作,异常处理,参数接收,正则表达式,连接数据库,事件等内容,使学员通过十六课时,学习Node.js的基础知识,掌握JS开发服务端的编写方法,...
  • 10+ 最佳的 Node.js 教程和实例

    千次阅读 2016-08-31 22:32:19
    如果你正在找Node.js的学习资料及指南,那么请继续(阅读),我们...以下是Node.js入门的简单介绍,如果你对Node.js略有了解可以直接跳过此部分。 那什么是Node.js呢? Node.js是迄今运用最多的服务端JavaScr
  • JS 入门教程-01-js入门案例

    万次阅读 2018-04-25 11:21:29
    基于 Node.js Node.js 安装 &amp; 使用 目录导航 开始之前 如果你从来没有接触过JS。去下面这个网站 5 分钟感受一下: https://www.javascript.com/ 入门案例 基于浏览器 JS 通常是和 HTML,...
  • node.jsjavascript的关系

    千次阅读 2018-06-15 11:10:19
    node.js是一个基于 Chrome V8 引擎的 JavaScript 运行时环境一、类比JavaScript和javaJavaScriptjavaV8JVMnode.jsJREJavaScript和java都是一门编程语言,至于两者的关系,是雷锋和雷峰塔的关系V8是google开源的...
  • Node.js 推荐20多个学习网站及书籍

    千次阅读 2014-06-06 08:03:07
    Node.js 推荐20多个学习网站及书籍 Web 开发人员对 Node.js 日益增多,更多的公司和开发者开始尝试使用 Node.js 来实现一些对实时性要求高,I/O密集型的业务。 很不错的书籍和案例,可以提高nodejs学习和开发,
  • 一杯茶的时间,上手 Node.js

    千次阅读 2020-03-13 12:15:26
    Node.js 太火了,火到几乎所有前端工程师都想...这篇教程将带你快速入门 Node.js,为后续的前端学习或是 Node.js 进阶打下坚实的基础。 此教程属于Node.js 后端工程师学习路线的一部分,欢迎来 Star 一波,鼓励我们...
  • Node学习笔记

    千次阅读 2019-08-31 15:37:55
    Node.js 教程目录 Node.js 基础 认识 Node.js Node.js 版本介绍 环境搭建 REPL(交互式解释器) 运行 Node.js Node.js 模块 自定义模块 npm scripts forever 部署方案 异步嵌套解决方案 ...
  • 本课程采用的技术包括小程序开发、Node.js、Express和MySQL。
  • 全面学习vue.js配置,es6命令,解构赋值,symbol类型,set,weakSet,Map,WeakMap,Iterator遍历器,Generator函数结构,Promise对象,async函数,箭头函数,class类,proxy代理,Decorator修饰器,model模块,二进制...
  • 以下教程建立在linux (centos) 操作系统基础下,也只有linux需要专门出教程,因为linux的文件位置
1 2 3 4 5 ... 20
收藏数 51,291
精华内容 20,516
关键字:

node.js 教程