Node.js 开发入门指南

565 阅读15分钟

1.简介

  1. 是一个可以让 JavaScript 运行在服务器端的平台。它可以让 JavaScript 脱离浏览器的束缚运行在一般的服务器环境下,它摒弃了传统平台依靠多线 程来实现高并发的设计思路,而采用了单线程、异步式I/O、事件驱动式的程序设计模型

  2. Node.js 内建了 HTTP 服务器支持,也就是说你可以轻而易举地实现一个网站和服务器的组合。这个服务器不仅可以用来调试代码,而且它本身就可以部署到产品环境,它 的性能足以满足要求

  3. Node.js 还可以部署到非网络应用的环境下,比如一个命令行工具。Node.js 还可以调用 C/C++ 的代码,这样可以充分利用已有的诸多函数库,也可以将对性能要求非常高的部分用 C/C++ 来实现。

  4. 异步I/O 事件驱动
    Node.js 进程在同一时 刻只会处理一个事件,完成后立即进入事件循环检查并处理后面的事件。这样做的好处是, CPU 和内存在同一时间集中处理一件事,同时尽可能让耗时的 I/O 操作并行执行。

  5. Node.js 的架构
    Node.js 用异步式 I/O 和事件驱动代替多线程,带来了可观的性能提升。Node.js 除了使 用 V8 作为JavaScript引擎以外,还使用了高效的 libev 和 libeio 库支持事件驱动和异步式 I/O。 图是 Node.js 架构的示意图。 Node.js 的开发者在 libev 和 libeio 的基础上还抽象出了层 libuv。对于 POSIX1操作系统, libuv 通过封装 libev 和 libeio 来利用 epoll 或 kqueue。而在 Windows 下,libuv 使用了 Windows的 IOCP(Input/Output Completion Port,输入输出完成端口)机制,以在不同平台下实现同样的高性能

  6. CommonJs规范
    CommonJS 规范包括了模块(modules)、包(packages)、系统(system)、二进制(binary)、 控制台(console)、编码(encodings)、文件系统(filesystems)、套接字(sockets)、单元测 试(unit testing)等部分,juejin.cn/post/684490…

2.创建HTTP服务

var http = require('http')
http.createServer(function( req, res ) {
    res.writeHead( 200, {'Content-Type': 'text/html'});
    res.write('<div>Node.js</div>')
    res.end('<div>Hello Word</div>')
}).listen(9988)

这时候我们发现如果修改了代码页面不会热更新,supervisor可以实现这个功能,可以解决开发过程中调试问题

npm install -g supervisor

还有另外一种方法是 nodemon

npm install -g nodemon

安装好后直接运行命令,就可以监听到文件变化自动重启了

nodemon app.js

3.异步I/O与事件式编程

I/O操作:程在执行中如果遇到磁盘读写或网络通信
I/O操作通常要耗费较长的时间,这个时候操作系统会暂停这个线程,将资源让给其他线程,这种调度方式成为阻塞,当I/O操作完成时候此线程继续执行,这种操作方式为同步I/O,也是阻塞式I/O
异步I/O则是将I/O请求发送给操作系统 ,继续执行下一条语句,当I/O执行完毕,会以事件的形式通知执行I/O操作的线程,线程会在特定时间处理这个事件,所以线程必须要有事件循环,不断检测是否有未处理的事件,依次处理

var fs = require('fs')
fs.readFile('filename', function(err, data) {
    // do something
})
console.log('end')

上面的例子,fs.readFile调用时候将异步I/O请求发送给操作系统,然后执行‘end’,执行完以后进入时间循环监听事件,当fs 收到I/O请求完成事件时候,事件循环会主动调用回调函数完成后续工作

4.事件

1.EventEmitter

Node.js 所有的异步 I/O 操作在完成时都会发送一个事件到事件队列。在开发者看来,事 件由 EventEmitter 对象提供。前面提到的 fs.readFile 和 http.createServer 的回 调函数都是通过 EventEmitter 来实现的。下面我们用一个简单的例子说明 EventEmitter 的用法 

var event = require('events')
var EventEmitter = event.EventEmitter
var eventBus = new EventEmitter()
eventBus.on('eventName', function() {
    console.log('finish event')
})
setTimeout( function() {
    eventBus.emit('eventName')
}, 2000)

上面代码两秒后输出 finish event, event对象注册了事件 eventName的一个监听器,然后2秒后向event对象发送事件 eventName 此时会调用 监听器,

2.Node.js 的事件循环机制

Node.js 在什么时候会进入事件循环呢?

答案是 Node.js 程序由事件循环开始,到事件循环结束,所有的逻辑都是事件的回调函数,所以 Node.js 始终在事件循环中,程序入口就是 事件循环第一个事件的回调函数。事件的回调函数在执行的过程中,可能会发出 I/O 请求或 直接发射(emit)事件,执行完毕后再返回事件循环,事件循环会检查事件队列中有没有未 处理的事件,直到程序结束。Node.js的事件循环对开发者不可见 ,由 l i b e v 库 实 现 。l i b e v 支持多种类型的事件,如 ev_io、ev_timer、ev_signal、ev_idle 等,在 Node.js 中均被 EventEmitter 封装。libev 事件循环的每一次迭代,在 Node.js 中就是一次 Tick,libev 不 断检查是否有活动的、可供检测的事件监听器,直到检测不到时才退出事件循环,进程结束。 

5. 模块和包

模块(Module)和包(Package)是 Node.js 最重要的支柱, Node.js提供了 require 函数来调用其他模块,而且模块都是基于文件的,机制十分简单,参考了CommonJs标准 

1.什么是模块

模块是Node.js 应用程序的基本组成部分,文件和模块一一对应,一个node文件就是一个模块,这个文件可能是js 代码,JSON或者编译过的C/C++扩展。上面用到过的 http 就是一个模块,内部是用C++实现的,外部用js包装通过require获取到这个模块

2.创建和加载模块

在 Node.js 中,创建一个模块非常简单,因为一个文件就是一个模块,我们要关注的问 题仅仅在于如何在其他文件中获取这个模块。Node.js 提供了 exports 和 require 两个对 象,其中 exports 是模块公开的接口,require 用于从外部获取一个模块的接口,即所获 取模块的 exports 对象

//创建模块 moduleA.js 模块
var name
exports.setName = function( newName ) {
    name = newName
}
exports.getName = function() {
    console.log('name=' + name)
}


//引用模块
var moduleA = require('./moduleA.js')
moduleA.setName('outName')
moduleA.getName()

上面例子中 moduleA 中通过exports 把setName和getName作为模块的访问接口,

引入文件通过 require 引入模块

// 还可以用module.exports
var name
function setName (newName) {
   name = newName
}
function getName() {
    console.log( name )
}
module.exports = {
    setName,
    getName
}

不可以通过对 exports 直接赋值代替对 module.exports 赋值。 exports 实际上只是一个和 module.exports 指向同一个对象的变量, 它本身会在模块执行结束后释放,但 module 不会,因此只能通过指定 module.exports 来改变访问接口。 

3.创建包

包是在模块基础上更深一步的抽象,Node.js 的包类似于 C/C++ 的函数库或者 Java/.Net 的类库。它将某个独立的功能封装起来,用于发布、更新、依赖管理和版本控制。Node.js 根 据 CommonJS 规范实现了包机制,开发了 npm来解决包的发布和获取需求

Node.js 的包是一个目录,其中包含一个 JSON 格式的包说明文件 package.json。严格符 合 CommonJS 规范的包应该具备以下特征: 

  1. package.json 必须在包的顶层目录下; 

  2. 二进制文件应该在 bin 目录下;

  3. JavaScript 代码应该在 lib 目录下;

  4. 文档应该在 doc 目录下;

  5. 单元测试应该在 test 目录下

    // 创建一个package 文件夹 并创建一个lib文件夹并新建interface.js 文件

    // interface.js exports.hello = function() { console.log('hello') }

    package根目录下 创建 package.json

    // package.json { "main": "./lib/interface.js" }

Node.js 在调用某个包时,会首先检查包中 package.json 文件的 main 字段,将其作为包的接口模块,如果 package.json 或 main 字段不存在,会尝试寻找 index.js 或 index.node 作 为包的接口。 

package.json 是 CommonJS 规定的用来描述包的文件,完全符合规范的 package.json 文 件应该含有以下字段。

  1. name: 包的名称,必须是唯一的,由小写英文字母、数字和下划线组成,不能包含 空格。description: 包的简要说明。

  2. version: 符合语义化版本识别规范的版本字符串

  3. keywords: 关键字数组,通常用于搜索

  4. maintainers: 维护者数组,每个元素要包含 name、email (可选)、web (可选)字段。contributors: 贡献者数组,格式与maintainers相同。包的作者应该是贡献者数组的第一个元素。 
    bugs: 提交bug的地址,可以是网址或者电子邮件地址
    licenses: 许可证数组,每个元素要包含 type(许可证名称) 和 url (链接到许可证文本的地址)
    repositories: 仓库托管地址数组,每个元素包含 type(仓库类型如git),url(仓库地址)和path(相对于仓库的路径)

  5. bugs: 提交bug的地址,可以是网址或者电子邮件地址

  6. licenses: 许可证数组,每个元素要包含 type(许可证名称) 和 url (链接到许可证文本的地址)

  7. repositories: 仓库托管地址数组,每个元素包含 type(仓库类型如git),url(仓库地址)和path(相对于仓库的路径)

  8. dependencies:包的依赖,一个关联数组,由包名称和版本号组成。 

4.创建全局链接

npm 提供了一个有趣的命令 npm link,它的功能是在本地包和全局包之间创建符号链 接。我们说过使用全局模式安装的包不能直接通过 require 使用,但通过 npm link命令 可以打破这一限制

npm link express ./node_modules/express -> /usr/local/lib/node_modules/express

 我们可以在 node_modules 子目录中发现一个指向安装到全局的包的符号链接。通过这 种方法,我们就可以把全局包当本地包来使用了

除了将全局的包链接到本地以外,使用 npm link命令还可以将本地的包链接到全局。 使用方法是在包目录( package.json 所在目录)中运行 npm link 命令。如果我们要开发 一个包,利用这种方法可以非常方便地在不同的工程间进行测试 

5.包的发布

npm 可以非常方便的发布一个包,首先 需要让我们的包符合npm的规范,npm有一套以CommonJS为基础包规范,但与CommonJS 并不完全一致,其主要差别在于必填字段的不同

  1. 通过使用 npm init 可以根据交互式问答 产生一个符合标准的 package.json 

  2. 在发布前,我们还需要获得一个账号用于今后维护自己的包,使用 npm adduser 根据 ,提示输入用户名、密码、邮箱,等待账号创建完成

  3. 完成后可以使用 npm whoami 测验是 否已经取得了账号。

  4. 接下来,在 package.json 所在目录下运行 npm publish, 

  5. 如果你的包将来有更新,只需要在 package.json 文件中修改 version 字段,然后重新 使用 npm publish 命令就行了。

  6. 如果你对已发布的包不满意(比如我们发布的这个毫无意 义的包),可以使用 npm unpublish 命令来取消发布。 

6.调试

1.命令行调试

在命令行下执行 node debug debug.js,将会启动调试工具: 

2.使用 node-inspector 调试 Node.js 

大部分基于 Node.js 的应用都是运行在浏览器中的,例如强大的调试工具 node-inspector。 node-inspector 是一个完全基于 Node.js 的开源在线调试工具,提供了强大的调试功能和友好 的用户界面,它的使用方法十分简便 

首先,使用 npm install -g node-inspector 命令安装 node-inspector,然后在终 端中通过 node --debug-brk=5858 debug.js 命令连接你要除错的脚本的调试服务器, 启动 node-inspector:

node-inspector

在浏览器中打开 http://127.0.0.1:8080/debug?port=5858,即可显示出优雅的 Web 调试工 具 

6.Node.js 核心模块

1.全局对象

  1. global

  2. process 是一个全局变量,即 global对象的属性,它是用于描述当前Node.js进程状态的对象,提供了一个与操作系统的简单接口,通常在写本地命令行程序的时候用到的比较多
    process.argv是命令行的参数数组,第一个元素是node执行路径,第二个元素是脚本文件名,之后是传入的参数

    //如命令行输入
    node app.js name=node age=20 sex=male
    //打印出来 process.argv 为
    ['node执行文件路径', '执行文件的路径', 'name=node', 'age=20', 'sex=male']
    

2.常用工具 Util

  1. util.inherits, 是一个实现对象原型继承的函数

    //我们先创建一个Person类var util = require('util')
    
    function Person( name ) {
      this.name = name || 'person'
      this.age = '1991'
      this.sayHello = function() {
        console.log( 'Hello', this.name )
      }
    }
    Person.prototype.showName = function() {
      console.log(this.name);
    }
    
    //然后创建一个sub类
    
    function Sub() {
      this.name = 'sub'
    }
    util.inherits(Sub, Person)
    
    var person = new Sub()
    这个时候打印person,发现Sub仅仅继承了定义在原型链上的函数,构造函数内部的方法和属性不会被继承
    
  2. util.inspect ,可以将任意对象转化为字符串的方法,通常用于调试和错误分析
    第一个参数是object
    第二个参数是showHidden, true 或 false 是否输出更多隐藏信息
    第三个参数是depth 表示最大递归层数,默认是2,null时候会完整递归遍历对象
    第四个参数是color true 输出颜色会更漂亮

  3. util.isArray()

  4. util.isRegExp()

  5. util.isDate()

3.事件驱动 events

7.HTTP服务端与客户端

1.HTTP服务端

http.Server是http模块中的HTTP服务器对象

  1. request事件:当用户客户端请求来到,该事件被触发,提供两个参数 req 和 res,分别是http.ServerRequest 和 http.ServerResponse的实例,表示请求和响应信息

  2. connection: 当TCP连接建立时候,改事件被触发,提供一个参数socket,为net.Socket实例

  3. close: 当服务器关闭时候,该事件被触发

    // 这是显式的写法,createServer是 封装之后的 request
    var http = require('http')
    var server = new http.Server()
    server.on('request', function( req, res ) {
        res.write('node')
        res.end('hello word')
    })
    server.listen(3000)
    

http.ServerRequest 是HTTP请求的信息,**一般分为两部分请求头(Request Header )和请求体(Request Body),**请求头一般可以立即读取比较短,请求体相对较长,需要一定的时间传输,因此提供了3个事件用于控制请求体传输

  1. data: 当请求体数据来到时,该事件被触发,该事件提供一个参数chunk,表示接收到的数据

  2. end: 当请求体数据传输完成时,该事件被触发,之后就不会有数据再来了

  3. close: 用户当前请求结束时候,该事件触发

  4. httpVersion: HTTP协议版本

  5. method: HTTP请求方法 GET.POST.PUT等

  6. url: 原始的请求路径

  7. headers: HTTP请求头

  8. connection: 当前HTTP连接套接子

    var http = require('http')
    http.createServer( function(req,res) {
        var post = ''
        req.on('data', function(chunk) {
            post += chunk
        })
        req.on('end', function() {
        
        })
    })
    

获取GET方法请求内容

Node.js 的url模块提供了parse 函数,用来解析get请求的路径

var http = require('http')
var url = require('url')
http.createServer(function(req, res ){
    console.log( url.parse(req.url) )
    res.end('finish')
})

获取POST请求内容

post请求内容全部在请求体中, node 提供了 querystring.parse(),可以格式化post过来的参数

var http = require('http')
var querystring = require('querystring')
http.createServer( function(req,res) {
    var post = ''
    req.on('data', function(chunk) {
        post += chunk
    })
    req.on('end', function() {
        post = querystring.parse(post)
        res.end('finish')
    })
}).listen(3000)

http.serverResponse是用来返回给客户端信息,也是由http.Server的request 事件发送的

一般有三个函数,用于返回响应头,响应内容,结束请求

  1. response.writeHead(statusCode, [headers]),向请求的客户端发送响应头,statusCode是HTTP状态码,200, 404等;headers表示响应头的每个属性
  2. response.write(data, [encoding]), 向请求的客户发送响应内容, data 是一个字符串或者Buffer,如果data是字符串要指定encoding来说明编码方式,默认 utf-8,可以被多次调用
  3. response.end(data, [encoding]):  结束响应,告知客户端所有发送已经完成,当所有要返回的内容发送完毕,必须被调用一次,如果不调用该函数,客户端将永远出于pedding状态

2.HTTP客户端

1.http模块提供了两个函数http.request 和 http.get, 作为客户端向http服务发起请求

  1. http.request( options, callback ) 发起HTTP请求,options 请求参数,callback是请求的回调函数,options常用参数如下

    1.host:请求网站的域名或者ip
    2.port: 请求网站的端口,默认80
    3.method: 请求方式,默认 GET
    4.path: 请求相对于跟的路径,默认是 /
    5.headers: 一个对象,为请求头内容
    

       callback: 传递一个参数,为http.ClientResponse的实例,http.request返回一个http.ClientRequest的实例

//下面是POST请求 聚合数据的ip查询接口
var http = require('http')
var querystring = require('querystring')

var params = querystring.stringify({
    IP: '你想查的IP'
})
var options = {
    host: 'aps.juhe.cn',
    path: '/ip/Example/query.php',
    method: 'POST',
    headers: {
        'Content-Type': 'application/x-www.form-urlencoded;charset=UTF-8'
    }
}
var req = http.request( options, functioin( res ) {
    res.setEncoding('utf8')
    res.on('data', function(data) {
        
    })
})
req.write( params )
req.end()

2. http.get( options, callback ) http模块还提供了快捷GET请求

var http = require('http')
let options = {
    host:'',
    path: '',
    headers: {},
    qs: {} //get请求参数
}
http.get( options, function( res ) {
    res.setEncoding('utf8')
    res.on('data', function( data ) {

    })
})

8.模块加载机制

Node.js模块可以分为两大类,核心模块,文件模块

  1. 核心模块就是自带的API模块,如fs, http等,能通过require直接获取,如果文件模块与核心模块命名冲突,总会加载核心模块

  2. 文件模块是存储的单独的文件,可能是js代码,JSON或者C/C++,
    加载方式有两种,一个是按路径加载,一种是查找node_modules 文件夹

    如果 require 以 / 开头就会以绝对路径的方式查找模块
    如require('/home/module'),
    这个时候按照优先级依次尝试加载
    /home/module.js
    /home/module.json
    /homt/module.node

    如果require 以 ./ 或者 ../ 开头
    那么就要以相对路径方式来查找模块,通常是自己扩展的模块

    **通过查找node_modules目录加载模块
    **如果不以相对路径和绝对路径开头,而模块又不是核心模块,那么就要通过node_modules加载模块了,我们使用npm获取的包通常都是以这种方式加载的

模块加载顺序总结如下

  • 如果module是一个核心模块,直接加载,结束
  • 如果是以 / ./ ../ 开头,按路径加载模块,结束
  • 假设当前目录为dir,按路径加载 dir/node_modules/module
    如果加载成功,结束
    如果加载失败,令dir为其父目录
    重复这个过程,直到根目录