Node.js 必知必会(安装配置、应用实例及同步控制)


一、Node.js简介

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。于2009年由Google Brain团队的软件工程师Ryan Dahl发起创建,2015年后正式被NodeJS基金会接管。

NodeJS 架构

Node.js架构

Node使用事件驱动模型,当web server接收到请求,就把它关闭然后进行处理,然后去服务下一个web请求。

当这个请求完成,它被放回处理队列,当到达队列开头,这个结果被返回给用户。

这个模型非常高效可扩展性非常强,因为 webserver 一直接受请求而不等待任何读写操作。(这也称之为非阻塞式IO或者事件驱动IO)。在事件驱动模型中,会生成一个主循环来监听事件,当检测到事件时触发回调函数。

相关资源

二、安装配置

建议使用nvm来进行node版本管理,它会安装相应版本的npm。

安装nvm及Node.js

nvm全名node.js version management,顾名思义是一个nodejs的版本管理工具。通过它可以安装和切换不同版本的nodejs。

# 安装nvm( 升级nvm重新执行此命令):
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.2/install.sh | bash
# 列出所有可以安装的node版本号
nvm ls-remote
# 安装指定版本号的node
nvm install v12.4.1
# 设置 nodejs 默认版本
nvm alias default 12.4.1
# 切换node的版本
nvm use v10.15.3
# 当前node版本
nvm current
node -v
# 列出所有已经安装的node版本
nvm ls
# 卸载已安装的node版本
nvm uninstall v6.9.5

npm

npm 是世界上最大的软件注册中心,随同NodeJS一起安装,来自全球各地的开源开发人员使用 npm 来共享和复用软件包。npm 由三个独立的部分组成:

  • 网站: https://npmjs.com 是开发者查找包(package)、设置参数以及管理 npm 使用体验的主要途径。
  • 注册表(registry):是一个巨大的数据库,保存了每个包(package)的信息。
  • 命令行工具 (CLI):通过命令行或终端运行。开发者通过 CLI 与 npm 打交道。
# 查看 npm 版本
npm -v
# 更新npm版本
npm install npm@latest -g
# 搜索模块
npm search hexo
# 安装依赖包
npm install <Module Name>
npm install hexo      # 本地安装 hexo
npm install hexo -g   # 全局安装 hexo
# 查看所有全局安装的模块
npm list -g
# 查看某个模块
npm list hexo
# 卸载模块
npm uninstall hexo
# 卸载后,查看包是否还存在
npm ls
# 更新某个模块
npm update hexo
# 创建模块
npm init

每个版本的 Node 都自带一个不同版本的 npm,可以用 npm -v 来查看 npm 的版本。全局安装的 npm 包并不会在不同的 Node 环境中共享,因为这会引起兼容问题。它们被放在了不同版本的目录下,例如 ~/.nvm/versions/node/${version}/lib/node_modules 这样的目录。

运行下面这个命令,可以从特定版本导入之前安装过的 npm 包到我们将要安装的新版本 Node 中:

nvm install v12.16.1 --reinstall-packages-from=v10.15.3

三、Node特点

异步I/O

在Node中,绝大多数的操作都以异步的方式进行调用。在底层构建了很多异步I/O的API,从文件读取到网络请求等,均是如此。这样的意义在于,在Node中,我们可以从语言层面很自然地进行并行I/O操作。每个调用之间无须等待之前的I/O调用结束。在编程模型上可以极大提升效率。
下面的两个文件读取任务的耗时取决于最慢的那个文件读取的耗时:

fs.readFile('/path1', function (err, file) {
  console.log('读取文件1完成');
});
fs.readFile('/path2', function (err, file) {
  console.log('读取文件2完成');
});

而对于同步I/O而言,它们的耗时是两个任务的耗时之和。这里异步带来的优势是显而易见的。

事件与回调函数

在JavaScript中,函数被作为第一等公民来对待,可以将函数作为对象传递给方法作为实参进行调用。Node将前端浏览器中应用广泛且成熟的事件引入后端,配合异步I/O,将事件点暴露给业务逻辑。

下面的例子展示的是Ajax异步提交的服务器端处理过程。Node创建一个Web服务器,并侦听8080端口。对于服务器,我们为其绑定了request事件,对于请求对象,我们为其绑定了data事件和end事件:

var http = require('http');
var quertstring = require('querystring');

http.createServer(function(req,res){
  var postData = '';
  req.setEncoding('utf8');

  // 监听请求的data事件
  req.on('data',function(chunk){
    postData += chunk;
  });

  // 监听请求的end事件
  req.on('end', function(){
    res.end(postData);
  });
}).listen(8080);
console.log('服务器启动完成,监听端口:8080')

相应地,我们在前端为Ajax请求绑定了success事件,在发出请求后,只需关心请求成功时执行相应的业务逻辑即可,相关代码如下:

$.ajax({
  'url': '/url',
  'method': 'POST',
  'data': {},
  'success': function (data) {
    // success事件
  }
});

与其他的Web后端编程语言相比,Node除了异步和事件外,回调函数是一大特色。纵观下来,回调函数也是最好的接受异步调用返回数据的方式。但是这种编程方式对于很多习惯同步思路编程的人来说,也许是十分不习惯的。代码的编写顺序与执行顺序并无关系,这对他们可能造成阅读上的障碍。

单线程

Node保持了JavaScript在浏览器中单线程的特点。而且在Node中,JavaScript与其余线程是无法共享任何状态的。单线程的最大好处是不用像多线程编程那样处处在意状态的同步问题,这里没有死锁的存在,也没有线程上下文交换所带来的性能上的开销。
同样,单线程也有它自身的弱点。Node采用了与Web Workers相同的思路来解决单线程中大计算量的问题:child_process。子进程的出现,意味着Node可以从容地应对单线程在健壮性和无法利用多核CPU方面的问题。

擅长I/O密集型的应用

通常,说Node擅长I/O密集型的应用场景基本上是没人反对的。Node面向网络且擅长并行I/O,能够有效地组织起更多的硬件资源,从而提供更多好的服务。I/O密集的优势主要在于Node利用事件循环的处理能力,而不是启动每一个线程为每一个请求服务,资源占用极少。

性能不俗

CPU密集型应用给Node带来的挑战主要是:由于JavaScript单线程的原因,如果有长时间运行的计算(比如大循环),将会导致CPU时间片不能释放,使得后续I/O无法发起。但是适当调整和分解大型运算任务为多个小任务,使得运算能够适时释放,不阻塞I/O调用的发起,这样既可同时享受到并行异步I/O的好处,又能充分利用CPU,I/O阻塞造成的性能浪费远比CPU的影响小。

计算斐波那契数列的耗时排行

四、Node.js常用模块

更多模块详细介绍,可查阅官方文档: https://nodejs.org/api/

Global模块

浏览器JavaScript当中window是全局对象,NodeJS中全局对象是global,global最根本的作用是作为全局变量的宿主(即所有的全局变量都是global对象的属性),因此在所有模块中都可以直接使用而无需包含。

Process模块

process是全局变量(即global对象的属性),用于描述当前NodeJS进程状态。

Console模块

console用于提供控制台标准输出。

console.log():向标准输出流打印字符并以换行符结束(如果只有1个参数,则输出该参数的字符串形式;如果有2个参数,则以类似于C语言printf()的格式化输出)。

console.error():与console.log()的用法相同,只是向标准错误流进行输出。

console.trace():向标准错误流输出当前的调用栈:

$ node app.js
Trace
    at Object.<anonymous> (/workspace/app.js:1:71)
    at Module._compile (module.js:643:30)
    at Object.Module._extensions..js (module.js:654:10)
    at Module.load (module.js:556:32)
    at tryModuleLoad (module.js:499:12)
    at Function.Module._load (module.js:491:3)
    at Function.Module.runMain (module.js:684:10)
    at startup (bootstrap_node.js:187:16)
    at bootstrap_node.js:608:3

Util模块

util提供常用函数集合,用于弥补核心JavaScript功能方面的不足。

Events模块

events是NodeJS最重要的模块,因为NodeJS本身就是基于事件式的架构,该模块提供了唯一接口,所以堪称NodeJS事件编程的基石。events模块不仅用于与下层的事件循环交互,还几乎被所有的模块所依赖。

events模块只提供1个events.EventEmitter对象,EventEmitter对象封装了事件发射和事件监听器。每个EventEmitter事件由1个事件名和若干参数组成,事件名是1个字符串。EventEmitter对每个事件支持若干监听器,事件发射时,注册至该事件的监听器依次被调用,事件参数将作为回调函数参数传递。

下面例子中,emitter为事件targetEvent注册2个事件监听器,然后发射targetEvent事件,结果2个事件监听器的回调函数被依次先后调用。

var events = require("events");

var emitter = new events.EventEmitter();

emitter.on("targetEvent", function(arg1, arg2) {
  console.log("listener1", arg1, arg2);
});

emitter.on("targetEvent", function(arg1, arg2) {
  console.log("listener2", arg1, arg2);
});

emitter.emit("targetEvent", "Hank", 2018);
$ node app.js
listener1 Hank 2018
listener2 Hank 2018

EventEmitter常用API

  • EventEmitter.on(event, listener):为指定事件注册监听器,接受1个字符串事件名event和1个回调函数listener。
  • EventEmitter.emit(event,[arg1],[arg2],[...]):发射event事件,传递若干可选参数到事件监听器的参数列表。
  • EventEmitter.once(event, listener):为指定事件注册1个单次监听器,即该监听器最多只会触发一次,触发后立刻解除。
  • EventEmitter.removeListener(event, listener):移除指定事件的某个监听器,listener必须是该事件已经注册过的监听器。
  • EventEmitter.removeAllListeners([event]):移除所有事件的所有监听器,如果指定event,则移除指定事件的所有监听器。

File System模块

fs模块封装了文件操作,提供了文件读取、写入、更名、删除、遍历、链接等POSIX文件系统操作,该模块中所有操作都提供了异步和同步2个版本。

fs.readFile(filename,[encoding],[callback(err,data)])用于读取文件,第1个参数filename表示要读取的文件名。第2个参数encoding表示文件的字符编码,第3个参数callback是回调函数,用于接收文件内容。

回调函数提供errdata两个参数,err表示有无错误发生,data是文件内容。如果指定encodingdata将是1个解析后的字符串,否则data将会是以Buffer`形式表示的二进制数据。

fs.readFileSync()

NodeJS提供的fs.readFileSync()函数是readFile()的同步版本,两者接受的参数相同,读取到的文件内容会以函数返回值形式返回。如果有错误发生fs将会抛出异常,需要使用try...catch捕捉并处理异常。

与同步I/O函数不同,NodeJS中异步函数大多没有返回值。

fs.open()

fs.open(path,flags,[mode],[callback(err,fd)])封装了POSIX的open()函数,与C语言标准库中fopen()函数类似。该函数接受2个必选参数,第1个参数path为文件路径,第2个参数flags代表文件打开模式,第3个参数mode用于创建文件时给文件指定权限(默认0666),第4个参数是回调函数,函数中需要传递文件描述符fd

fs.read()

fs.read(fd,buffer,offset,length,position,[callback(err,bytesRead,buffer)])封装了POSIX的read函数,相比fs.readFile()提供了更底层的接口。

fs.read()的功能是从指定的文件描述符fd中读取数据并写入buffer指向的缓冲区对象。offsetbuffer的写入偏移量。length是要从文件中读取的字节数。position是文件读取的起始位置,如果position的值为null,则会从当前文件指针的位置读取。回调函数传递bytesReadbuffer,分别表示读取的字节数缓冲区对象

Http模块

NodeJS标准库提供的http模块封装了一个高效的HTTP服务器http.Server和一个简易的HTTP客户端http.request

http模块中的HTTP服务器对象,核心由NodeJS底层依靠C++实现,接口使用JavaScript封装,兼顾了高性能与简易性。

五、创建Node.js应用

使用Node创建http服务器

使用 require 指令来载入 http 模块,并将实例化的 HTTP 赋值给变量 http,实例如下:

var http = require("http");

使用 http.createServer() 方法创建服务器,并使用 listen 方法绑定 8888 端口。 函数通过 request, response 参数来接收和响应数据。在你项目的根目录下创建一个叫 server.js 的文件,并写入以下代码:

const http = require('http');
const hostname = '127.0.0.1';
const port = 8080;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World');
});

// 终端打印如下信息
server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

以上代码我们完成了一个可以工作的 HTTP 服务器。使用 node 命令执行以上的代码:

node server.js
Server running at http://127.0.0.1:8080/

打开浏览器访问 http://127.0.0.1:8080/,会看到一个写着 “Hello World”的网页。

web框架express简单使用

express 是 Node应用最广泛的快速、开放、极简主义 web 框架,现在是 4.x 版本。官方提供了应用程序生成器工具 express-generator 可以快速创建应用程序骨架。安装:

npm install express --save
npm install express-generator -g

创建名称为 ExpressDemo 的 Express 应用。此应用将在当前目录下的 ExpressDemo 目录中创建,并且设置为使用 Pug 模板引擎:

express --view=pug ExpressDemo

   create : ExpressDemo/
   create : ExpressDemo/public/
   create : ExpressDemo/public/javascripts/
   create : ExpressDemo/public/images/
   create : ExpressDemo/public/stylesheets/
   create : ExpressDemo/public/stylesheets/style.css
   create : ExpressDemo/routes/
   create : ExpressDemo/routes/index.js
   create : ExpressDemo/routes/users.js
   create : ExpressDemo/views/
   create : ExpressDemo/views/error.pug
   create : ExpressDemo/views/index.pug
   create : ExpressDemo/views/layout.pug
   create : ExpressDemo/app.js
   create : ExpressDemo/package.json
   create : ExpressDemo/bin/
   create : ExpressDemo/bin/www

   change directory:
     $ cd ExpressDemo

   install dependencies:
     $ npm install

   run the app:
     $ DEBUG=expressdemo:* npm start

按提示安装依赖并启动。

cd ExpressDemo
npm install
DEBUG=expressdemo:* npm start  # MacOS

在浏览器中打开 http://localhost:3000/ 就可以看到这个应用了。

通过生成器创建的应用一般都有如下目录结构:

tree -I "node_modules"
.
├── app.js
├── bin
│   └── www
├── package-lock.json
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
│       └── style.css
├── routes
│   ├── index.js
│   └── users.js
└── views
    ├── error.pug
    ├── index.pug
    └── layout.pug

7 directories, 10 files

六、异步编程方案(Promise & Async)

异步是Node得天独厚的特点和优势,但我们经常还是会需要解决同步执行的场景。如方法A执行完才可以执行方法B。如下面这个例子:

setTimeout(() => {
    console.log('A')
}, 3000)
console.log('B');

执行结果为:

[Running] node "test.js"
B
A
[Done] exited with code=0 in 3.12 seconds

如果想要输出结果为 A B,可以采取 PromiseAsync 来实现。

基于Promise实现同步控制

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。ES6 原生提供了Promise对象,提供统一的 API,各种异步操作都可以用同样的方法进行处理。示例如下:

var f = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('A');
        resolve();
    }, 3000);
});

f.then(() => {
    console.log('B');
}, (err) => {
    console.log('Err');
});

执行结果:

[Running] node "test.js"
A
B
[Done] exited with code=0 in 3.123 seconds

可以把 Promise 对象比喻为一个容器,里面有一个异步操作,Promise 容器只有在收到信号(resolve或者reject)时才会调用then方法。通过 Promise.All方法将多个Promise对象实例包装,生成并返回一个新的Promise实例,等执行完所有异步操作之后执行then方法:

const a = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('A');
    resolve();
  }, 3000);
});

const b = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('B');
    resolve();
  }, 3000);
});


Promise.all([a, b]).then(() => {
  console.log('end');
});

执行结果如下:

[Running] node test.js
A
B
end

[Done] exited with code=0 in 3.122 seconds

Promise 扩展信息

Promise 构造函数接受一个函数作为参数,该函数两个参数分别是resolvereject。它们是两个函数,由 avaScript 引擎提供。resolve 函数在异步操作成功时用,其作用是将Promise对象的状态从“pending”变为resolved”,并将异步操作的结果作为参数传递出去;reject函数在异步操作失败时调用,其作用是将Promise`对象的状态从pending”变为“rejected”,并将异步操作报出的错误作为参传递出去。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。then方法以接受两个回调函数作为参数,第一个回调函数是Promise对的状态变为resolved时调用,第二个回调函数(可选提供)是Promise对象的状态变为rejected时调用。这两个函数都受Promise对象传出的值作为参数。

Promise状态转换图

Promise对象有以下两个特点。

  • 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就会一直保持不再改变,称为 resolved。

下面是一个Promise对象的简单例子。

function f() {
  return new Promise(function(resolve, reject) {
    setTimeout(resolve, 3000, 'done.');
    console.log('A');  //会立即执行
  });
}
f().then(function (result) {
  console.log(`resolve result: ${result}`);
}),function(error){
  console.log(`reject error: ${error}`);
};

上面代码中,f1方法返回一个Promise实例,表示一段时以后才会发生的结果。过了指定的时间(3000毫秒)以后,Promise实例的状态为resolved,就会触发then`方法定的回调函数。执行结果如下:

[Running] node "test.js"
A
resolve result: done.
[Done] exited with code=0 in 3.175 seconds

另外,resolve函数的参数除了正常的值以外,还可能是另一个 Promise 实例。then方法是定义在原型对象Promise.prototype上的。返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。

Promise也有一些缺点。首先是无法取消,一旦新建它就会立即执行,无法中途取消;其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段。

基于Async实现同步控制

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。随着Node.js 8的发布,期待已久的async函数也在其中默认实现了。async 函数的实现原理,是将 Generator 函数和自动执行器,包装在一个函数里。

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变(除非遇到return语句或者抛出错误),再接着执行函数体内后面的语句。看下面这个例子:

function f() {
  return new Promise((resolve) => {
    setTimeout(resolve, 3000, 'Hello');
    console.log('A');
  });
}

async function asyncF(value) {  //前面的 `async` 关键字,表明该函数内部有异步操作。
  console.log('B');
  await f().then(value => console.log(value));
  console.log('C');
  return value; // return语句的返回值,会成为`then`方法回调函数的参数。
}

asyncF('world').then(result => console.log(result));

执行结果如下:

[Running] node "test.js"
B      #立即输出
A      #立即输出
Hello  #3秒后输出
C
world
[Done] exited with code=0 in 3.165 seconds

async 函数的await命令后面,可以是 Promise 对象或原始类型的值(数值、字符串和布尔值,但会自动转成立即 resolved 的 Promise 对象)。

sync函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。如下面这个例子:

 async function f() {
 throw new Error('发生异常');
 }

 f().then(
 result => console.log(`resolve result: ${result}`),
 error => console.log(error)
 );

执行结果:

 [Running] node "test.js"
 Error: 发生异常
  at f (/Users/lixl.cn/nodework/blog/test.js:4:9)
  at Object.<anonymous> (/Users/lixl.cn/nodework/blog/test.js:7:1)
  at Module._compile (internal/modules/cjs/loader.js:701:30)
  at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)
  at Module.load (internal/modules/cjs/loader.js:600:32)
  at tryModuleLoad (internal/modules/cjs/loader.js:539:12)
  at Function.Module._load (internal/modules/cjs/loader.js:531:3)
  at Function.Module.runMain (internal/modules/cjs/loader.js:754:12)
  at startup (internal/bootstrap/node.js:283:19)
  at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)

 [Done] exited with code=0 in 0.153 seconds

七、基于ESLint保障质量

JavaScript 是一个动态的弱类型语言,在开发中比较容易出错,一般会借助 Lint 工具来保障质量。

ESLint 是新一代开源 JavaScript 代码检查工具,使用 Node.js 编写,常用于寻找有问题的模式或者代码,并且不依赖于具体的编码风格。

# 全局安装 ESLint
npm install -g eslint

# 进入项目
cd ~/NodeWork/NodeDemo

# 初始化 package.json
npm init -f

# 初始化 ESLint 配置
eslint --init

通过 Lint 工具可以让我们:

  • 避免低级bug,找出可能发生的语法错误
  • 提示删除多余的代码
  • 确保代码遵循最佳实践 (可参考 airbnb stylejavascript standard)
  • 统一团队的代码风格

项目初始化完毕,可以开始在 ESLint 的提示下,高质量编写代码了。

八、参考


文章作者: 悟尘
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-ND 4.0 许可协议,转载请注明来源 悟尘纪 !
评论
 上一篇
VSCode常用快捷键Mac版 VSCode常用快捷键Mac版
全局 Command + K + Command + S 打开快捷键查找/编辑页 Command + Shift + P / F1 显示命令面板 Command + P 快速打开文件 Command + Shift + N 打开新窗口 Comm...
2020-02-02
下一篇 
利用AutoSSH建立SSH隧道,实现内网穿透 利用AutoSSH建立SSH隧道,实现内网穿透
当我们使用公司或家中电脑搭建了 Web 服务时,一般不能直接从外网访问,为了实现从外网直接访问到内网的服务,一般会需要用到 内网穿透 技术。常用的内网穿透工...
2020-01-06
  目录