七天学会nodejs整理

介绍

工作原因接触了NodeJS,开始学习它,不过以前对这门语言不了解,学习了基本语法可以工作后就没有加深学习了。时间长了后发现这门语言比我想象中强大了很多,于是开始准备全面了解,深入学习,本文是学习了七天学会NodeJS之后的一个整理总结,入门向的一篇文章,旨在让我们可以全面了解这门语言,有兴趣的可以参考。

NodeJS基础

学习NodeJS,首先了解它是什么。

JS我们都知道–JavaScript,一门前端都在使用的脚本语言,而NodeJS就是运行在后端的js语言。换句话说,前端使用的JS是由浏览器进行解析运行的,而NodeJS就是一个独立的js解析器,随时随地,有node环境就可以解析运行。

NodeJS诞生的目的就是为了实现高性能的Web服务器(作者自己说的),至于为什么使用JS这门语言,主要有如下几点:

  1. JS这门语言自带的事件机制和异步IO模型(契合作者的需求)
  2. 同时JS没有自带的IO功能,不会有历史包袱(我理解是作者可以随意发挥)
  3. 有一大批前端程序员的拥护(用户众多)
  4. chrome浏览器的v8高性能JS引擎的出现(可移植,有大哥维护)

在这么多的优势的加护下,NodeJS也如作者期望的那样发展的欣欣向荣。

入门向的文章,安装运行少不了,不过原文已经很详细了,这里就不赘述了。

nodejs基础

(为什么思维导图放在中间了,因为以下就开始将知识点了=-=)

模块是nodejs中很重要的一个概念,简单来说一个文件就是一个模块,文件名就是模块名。

模块中有三个预先定义好的变量:

  • require: 加载其他模块
  • exports: 当前模块的导出对象
  • module: 当前模块的信息

关于模块的内容后面会专门写一篇文章详细介绍,此处略过。

最后注意一下,Node有个缓存机制,会将require的模块中编译执行后的对象进行缓存,二次加载时使用缓存的对象。

代码的组织和部署

搭建房子首先要规划好房子结构,而稍微大一点的项目免不了要准备好代码的目录结构和部署方式,这一章主要是组织结构的介绍。

代码的组织和部署

模块解析规则

模块解析规则也就是模块导入的规则,主要分为三种

  • 内置模块
  • node_moudle模块
  • NODE_PATH环境变量

原文说的很清楚了,这里补充的一点是查看当前的node_module目录可以参考如下方法:

包(PACKAGE)可以理解是多个模块的组合。复杂的模块往往由多个子模块组成,包就是多个子模块的集合。包中有两个重点:

  • 入口模块
  • package.json

入口模块的导出对象是包的导出对象,因此我们可以直接导入入口对象就算是导入了整个包。

不过这样做并不够灵活,如果入口模块名字改了,所有导入它的代码都要跟着修改,因为我们引入了package.json

最简单的package.json文件我们可以这样写:

1
2
3
4
{
"name": "cat",
"main": "./lib/main.js"
}

这样,加再模块时,只需要使用require('/home/user/lib/cat')的方式就可以了,Node会自己根据package.json的配置找到入口模块。

package.json还有其他的成员,一个完整的package.json可以如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"name": "Hello World",
"version": "0.0.1",
"author": "张三",
"description": "第一个node.js程序",
"keywords":["node.js","javascript"],
"repository": {
"type": "git",
"url": "https://path/to/url"
},
"license":"MIT",
"engines": {"node": "0.10.x"},
"bugs":{"url":"http://path/to/bug","email":"bug@example.com"},
"contributors":[{"name":"李四","email":"lisi@example.com"}],
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "latest",
"mongoose": "~3.8.3",
"handlebars-runtime": "~1.0.12",
"express3-handlebars": "~0.5.0",
"MD5": "~1.2.0"
},
"devDependencies": {
"bower": "~1.2.8",
"grunt": "~0.4.1",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-jshint": "~0.7.2",
"grunt-contrib-uglify": "~0.2.7",
"grunt-contrib-clean": "~0.5.0",
"browserify": "2.36.1",
"grunt-browserify": "~1.3.0",
}
}

具体每个字段介绍可以参考JavaScript标准参考教程

命令行程序

NodeJS可以编写命令行程序,只需要在程序前面加下如下内容

1
#! /usr/bin/env node

然后在/usr/local/bin/下添加一个软链接就可以实现了,下面是一个很简单的实例:

node-hello

Windows会有些不一样,我没用windows测试,这里也不介绍了,感兴趣自己看原文。

工程目录

代码的组织,可以参考如下布局:

1
2
3
4
5
6
7
8
9
10
11
- /home/user/workspace/node-echo/   # 工程目录
- bin/ # 存放命令行相关代码
node-echo
+ doc/ # 存放文档
- lib/ # 存放API相关代码
echo.js
- node_modules/ # 存放三方包
+ argv/
+ tests/ # 存放测试用例
package.json # 元数据文件
README.md # 说明文件

没什么好聊的,只是个参考。

NPM

NPM是NodeJS的包管理工具,常见的使用场景如下:

  • 允许用户从NPM服务器下载别人编写的三方包到本地使用。
  • 允许用户从NPM服务器下载并安装别人编写的命令行程序到本地使用。
  • 允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用。

NPM搭建了NodeJS的生态圈。

这里有一点注意的,我们可以在之前的package.json文件中描述依赖的第三方包,直接通过npm install进行安装,避免了环境搭建的烦恼(很棒的功能)。

npm -l可以看到都有哪些命令,想看命令的详细介绍也可以使用npm help <command>,文档非常全,这里就不赘述了。

文件操作

一个后端程序怎么能少得了文件操作。

文件操作

文章有个copy文件的小程序很不错,可以引出一个知识点,程序如下:

1
2
3
4
5
6
7
8
9
var fs = require('fs');

function copy(src, dst) {
fs.createReadStream(src).pipe(fs.createWriteStream(dst));
}
function main(argv) {
copy(argv[0], argv[1]);
}
main(process.argv.slice(2));

程序很简单,这里说他主要是下面这个小知识点:

process是一个全局变量,可通过process.argv获得命令行参数。由于argv[0]固定等于NodeJS执行程序的绝对路径,argv[1]固定等于主模块的绝对路径,因此第一个命令行参数从argv[2]这个位置开始。

API

和文件相关的API有很多,这里拿出几个来对比

  • Buffer
  • Stream
  • File System

Buffer

Buffer类的官方解释:

Buffer 类的实例类似于从 0255 之间的整数数组(其他整数会通过 & 255 操作强制转换到此范围),但对应于 V8 堆外部的固定大小的原始内存分配。 Buffer 的大小在创建时确定,且无法更改。

在我看来,Buffer底层就是一个unsigned char类型的数组,申请所需的堆空间并存储二进制数据。

使用场景;

多用于在 TCP 流、文件系统操作、以及其他上下文中与八位字节流进行交互。

相关api使用去官网查看文档。

Stream

照例来段官方解释:

流(stream)是 Node.js 中处理流式数据的抽象接口。

当内存中无法一次装下需要处理的数据时,或者一边读取一边处理更加高效时,我们就需要用到数据流。

流主要分为以下几种

  • 可读流
  • 可写流
  • 读写流
  • 转换流 Transform

在node中,这四种流都是EventEmitter的实例,它们都有close、error事件,可读流具有监听数据到来的data事件等,可写流则具有监听数据已传给低层系统的finish事件等,DuplexTransform 都同时实现了 ReadableWritable 的事件和接口 。

当然,流也不是完美的,虽然消耗很少的内存,但那是比较理想的情况,如果读方没有尽快处理,会导致大量的数据被积压,而如何处理积压问题可以观看此文章数据流中的积压问题,官方文档,童叟无欺。

File System

正经的文件操作模块。

fs模块提供的API基本操作分为三类:

  • 文件属性读写
  • 文件内容读写
  • 底层文件操作

如下为一个简单而又典型的异步IO模型读取文件的示例代码:

1
2
3
4
5
6
7
fs.readFile(pathname, function (err, data) {
if (err) {
// Deal with error.
} else {
// Deal with data.
}
});

网络操作

千呼万唤始出来,终于到了网络模块。

先来个官方文档的例子:

1
2
3
4
5
6
var http = require('http');

http.createServer(function (request, response) {
response.writeHead(200, { 'Content-Type': 'text-plain' });
response.end('Hello World\n');
}).listen(8124);

这个是一个简单的HTTP服务器,打开浏览器访问该端口http://127.0.0.1:8124/就能够看到效果。

网络操作

API

与网络操作相关的API主要有如下几个:

  • HTTP:网络服务核心模块
  • HTTP/2:h2版本
  • HTTPS:加密版本
  • URL:url解析
  • Zlib:提供了数据压缩和解压的功能
  • net:用于创建Socket服务器或Socket客户端

下面介绍重点api:

HTTP

http模块是NodeJS中的核心模块,主要提供两种使用:

  • 作为服务端使用时,创建一个HTTP服务器,监听HTTP客户端请求并返回响应。
  • 作为客户端使用时,发起一个HTTP客户端请求,获取服务端响应。

服务端

本章刚开始的小例子就是一个服务端的实现,使用.createServer方法创建一个服务器,然后调用.listen方法监听端口,当有客户端请求过来时,就会有request事件被触发,调用回调函数进行返回。

HTTP请求本质是一个数据流,由请求头和请求体组成,默认大家都了解,这里不再赘述。

服务端也是继承自EventEmitter的,而它所提供的事件如下:

  • request:当客户端请求到来时,该事件被触发,提供两个参数req和res,表示请求和响应信息。

  • connection:当TCP连接建立时,该事件被触发,提供一个参数socket,是net.Socket的实例。

  • close:当服务器关闭时,触发事件(注意不是在用户断开连接时)。

注意:request事件的参数req和res分别是http.IncomingMessagehttp.ServerResponse的实例。

http.IncomingMessage是HTTP请求的信,其提供了三个事件

  • data:当请求体数据到来时,该事件被触发,该事件提供一个参数chunk,表示接受的数据,如果该事件没有被监听,则请求体会被抛弃,该事件可能会被调用多次(这与nodejs是异步的有关系)
  • end:当请求体数据传输完毕时,该事件会被触发,此后不会再有数据
  • close:用户当前请求结束时,该事件被触发,不同于end,如果用户强制终止了传输,也是用close

http.ServerResponse是返回给客户端的信息,决定了用户最终看到的内容,类似于上面,他有三个重要成员函数,用于返回响应头、内容以及结束请求:

  • res.writeHead():向请求的客户端发送响应头,该函数在一个请求中最多调用一次,如果不调用,则会自动生成一个响应头

  • res.write():向请求的客户端发送相应内容,data是一个buffer或者字符串,如果data是字符串,则需要制定编码方式,默认为utf-8,在res.end调用之前可以多次调用

  • res.end()):结束响应,告知客户端所有发送已经结束,当所有要返回的内容发送完毕时,该函数必需被调用一次,两个可选参数与res.write()相同。如果不调用这个函数,客户端将用于处于等待状态。

客户端

作为客户端就要简单很多了,先来个小栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var options = {
hostname: 'www.example.com',
port: 80,
path: '/upload',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};

var request = http.request(options, function (response) {});

request.write('Hello World');
request.end();

这里用到了http.request,另一个函数为http.get,功能是作为客户端向http服务器发起请求。

因为GET请求不需要请求体,所以请求不太一样,再来个小栗子:

1
http.get('http://www.example.com/', function (response) {});

也没什么好介绍的了,详细介绍看官方文档吧。

HTTPS

https模块与http模块极为类似,区别在于https模块需要额外处理SSL证书。

在服务端模式下,创建一个HTTPS服务器的示例如下:

1
2
3
4
5
6
7
8
var options = {
key: fs.readFileSync('./ssl/default.key'),
cert: fs.readFileSync('./ssl/default.cer')
};

var server = https.createServer(options, function (request, response) {
// ...
});

可以看到,与创建HTTP服务器相比,多了一个options对象,通过keycert字段指定了HTTPS服务器使用的私钥和公钥。

HTTPS与HTTP的区别这里就不介绍了,后面会专门出一篇文章,感兴趣的小伙伴也可以自己去查,网上有很多相关文章。

进程管理

NodeJS可以感知和控制自身进程的运行环境和状态,也可以创建子进程并与其协同工作,这使得NodeJS可以把多个程序组合在一起共同完成某项工作,并在其中充当胶水和调度器的作用。

进程管理

API

进程管理有关的API如下:

  • process: 当前进程管理
  • child_process: 创建和控制子进程
  • cluster: 对child_process模块的封装,充分利用多核CPU

process

process是一个全局对象,提供了当前NodeJS进程的信息并可以对其进行控制,以下为process所提供的一系列属性:

  • process.argv:返回一个数组,成员是当前进程的所有命令行参数。
  • process.env:返回一个对象,成员为当前Shell的环境变量,比如process.env.HOME
  • process.installPrefix:返回一个字符串,表示 Node 安装路径的前缀,比如/usr/local。相应地,Node 的执行文件目录为/usr/local/bin/node
  • process.pid:返回一个数字,表示当前进程的进程号。
  • process.platform:返回一个字符串,表示当前的操作系统,比如Linux
  • process.title:返回一个字符串,默认值为node,可以自定义该值。
  • process.version:返回一个字符串,表示当前使用的 Node 版本,比如v7.10.0

当然,还有各种事件以及成员方法,方法简要说下吧,事件自己去看。

  • process.chdir():切换工作目录到指定目录。
  • process.cwd():返回运行当前脚本的工作目录的路径。
  • process.exit():退出当前进程。
  • process.getgid():返回当前进程的组ID(数值)。
  • process.getuid():返回当前进程的用户ID(数值)。
  • process.nextTick():指定回调函数在当前执行栈的尾部、下一次Event Loop之前执行。
  • process.on():监听事件。
  • process.setgid():指定当前进程的组,可以使用数字ID,也可以使用字符串ID。
  • process.setuid():指定当前进程的用户,可以使用数字ID,也可以使用字符串ID。

child_process

child_process模块用来创建和控制子进程,其核心方法为child_process.spawn(),常用的四个方法如下:

  • exec:用于执行bash命令,它的参数是一个命令字符串。
  • execFile:直接执行特定的程序,参数作为数组传入,不会被bash解释
  • fork:创建一个子进程,执行Node脚本
  • spawn:创建一个子进程来执行特定命令,没有回调函数

每个都有自己的用途,一个简单的实例如下:

1
2
3
4
5
6
7
8
9
var exec = require('child_process').exec;

var ls = exec('ls -l', function (error, stdout, stderr) {
if (error) {
console.log(error.stack);
console.log('Error code: ' + error.code);
}
console.log('Child Process STDOUT: ' + stdout);
});

具体介绍和使用官方写的很详细了,不过有一段话这里可以介绍下

child_process.execFile(): 类似于 child_process.exec(),但是默认情况下它会直接衍生命令而不先衍生 shell。

这是一段对execFile的介绍,看到这里后对这段话很不理解,于是去查看了源码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function normalizeExecArgs(command, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}

// Make a shallow copy so we don't clobber the user's options object.
options = { ...options };
options.shell = typeof options.shell === 'string' ? options.shell : true;

return {
file: command,
options: options,
callback: callback
};
}
function exec(command, options, callback) {
const opts = normalizeExecArgs(command, options, callback);
return module.exports.execFile(opts.file,
opts.options,
opts.callback);
}

上面这段是exec()方法的实现,可以看到是调用了execFile();

execFile

官网中对shell参数有如上图描述,在上述代码中我们可以看到exec()传进来的默认为true,所以会启动shell,也就是可以执行一些shell命令和脚本,而execFile()默认为false,则不会启动shell,可以执行一些可运行的程序,效率会高过exec()。

cluster

cluster意为集群,表示多个Node进程构成的服务。cluster封装了child_process.fork方法创建node子进程.。利用cluster模块,我们可以创建多进程的Web服务器,充分利用多核处理器的计算资源。下面给出一个具体示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
console.log(`主进程 ${process.pid} 正在运行`);
// 衍生工作进程。
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}

cluster.on('exit', (worker, code, signal) => {
console.log(`工作进程 ${worker.process.pid} 已退出`);
});
} else {
// 工作进程可以共享任何 TCP 连接。
// 在本例子中,共享的是一个 HTTP 服务器。
http.createServer((req, res) => {
res.writeHead(200);
res.end('你好世界\n');
}).listen(8000);

console.log(`工作进程 ${process.pid} 已启动`);
}

我们将master称为主进程,worker进程称为工作进程,利用cluster模块,使用Node的cluster模块封装好的API、IPC通道和调度机制可以非常简单的创建包括一个master进程下HTTP代理服务器 + 多个worker进程多个HTTP应用服务器的架构。

我们可以看到多个子进程共同监听了同一端口,这是违背规则的,那么Node是如何实现的呢?
这是因为,子进程并没有创建ServerSocket作监听,而是交由父进程创建ServerSocket监听指定端口,接收到连接后创建Socket,而得到的请求会根据“指定的分发规则”通过IPC发送给子进程,子进程处理后的结果再通过IPC由父进程转发给请求方。

最后,cluster的请求分发策略有两种:

  • Round-Robin法:即轮询,依次循环将请求分配给子线程。
  • 共享服务端socket方式:由操作系统进行调度。

cluster的底层对master和child有着不同的实现:

cluster

上图为cluster实现的源码,具体原理还需要对其进行源码解读。

异步编程

先来说异步是什么,我的理解是:

异步就是有事件发生找个人去处理,自己继续忙自己的任务,等那个人处理完了通知到你,你再验收。

我们都知道,Node是单线程的,可是如果是单线程,又如何完成异步机制和事件机制呢?

实际上,NodeJS的单线程是指的只有一个主线程,其他线程包括而不限于如下几种:

  • js引擎执行线程
  • 定时器线程
  • 异步IO线程

这些线程可以称之为工作线程,这种机制避免了node因为线程切换造成的损耗,使得Node非常适合IO密集型应用。而本章的异步编程主要是创建工作线程与主线程并发执行,但需要等主线程空闲时才能执行回调函数。

异步编程

回调

在代码中,异步编程的直接体现就是回调。异步编程依托于回调来实现,但不能说使用了回调后程序就异步化了,比如如下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function heavyCompute(n, callback) {
var count = 0,
var i, j;
for (i = n; i > 0; --i) {
for (j = n; j > 0; --j) {
count += 1;
}
}
callback(count);
}
heavyCompute(10000, function (count) {
console.log(count);
});
console.log('hello');

-- Console ------------------------------
100000000
hello

显然,并没有异步执行,那如果想写一个异步的程序需要怎么写呢,学到的一个很简单的实例:

1
2
3
4
5
6
7
8
setTimeout(function () {
console.log('world');
}, 1000);
console.log('hello');

-- Console ------------------------------
hello
world

通过调用setTimeout()这个原生的异步函数来实现异步。

你说这耍赖,不是你自己实现的,那再来个异步遍历数组的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function (i, len, count, callback) {
for (; i < len; ++i) {
(function (i) {
async(arr[i], function (value) {
arr[i] = value;
if (++count === len) {
callback();
}
});
}(i));
}
}(0, arr.length, 0, function () {
// All array items have processed.
}));

看到这里就结束了,有人可能好奇,不是七天么,这不是才六章吗?

第七天是个综合示例,自己照着敲学习吧🤪。

引用文献

七天学会NodeJS

官方文档(中文版)

node.js模块简明详解

JavaScript标准参考教程

认识node核心模块–从Buffer、Stream到fs

Node.js中的流(Streams):你需要知道的一切

浅析nodejs的http模块

Node的进程详解

-------------本文结束,感谢您的阅读-------------