入门open in new window

事件循环

const bar = () => console.log('bar')

const baz = () => console.log('baz')

const foo = () => {
	console.log('foo')

	setTimeout(bar, 0)

	new Promise((resolve, reject) => {
		resolve('应该在baz之后,bar之前')
	}).then(d => console.log(d))

	baz()
}

foo()

// foo
// baz
// 应该在baz之后,bar之前
// bar

process.nextTick()

setTimeout(() => {
	console.log('1')
}, 1000)

process.nextTick(() => {
	console.log('2')
})

console.log('3')

// 3 2 1

setImmediate()

setTimeout(() => {
	console.log('1')
})

setImmediate(() => {
	console.log('2')
})

process.nextTick(() => {
	console.log('3')
})

定时器

setTimeout()

延迟函数的执行

setTimeout(() => {
    // 延迟2秒
}, 2000)

const fn = (a, b) => {}

// 延迟两秒
const id = setTimeout(fn, 2000, a, b)

// 清除
clearTimeout(id)

setInterval()

在指定的特定时间间隔(以毫秒为单位)一直地运行回调函数

const id = setInterval(() => {
    // 每两秒执行一次
}, 2000)

// 清除
clearInterval(id)

递归的 setTimeout

const fn = () => {
    setTimeout(fn, 1000)
}

setTimeout(fn, 1000)

事件触发器

  • once(): 添加单次监听器。
  • removeListener() / off(): 从事件中移除事件监听器。
  • removeAllListeners(): 移除事件的所有监听器。
const EventEmitter = require('events')

const eventEitter = new EventEmitter()

eventEitter.on('start', d => {
	console.log('开始')
	console.log(d)
})

eventEitter.emit('start', 'hello world')

http

const http = require('http')

const port = 3000

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

server.listen(port, () => {
	console.log(`服务器运行在 http://localhost/:${port}/`)
})

发送http post请求

const https = require('https')

const data = JSON.stringify({
	todo: '做点事情'
})

const options = {
	hostname: 'nodejs.cn',
	port: 443,
	path: '/todos',
	method: 'POST',
	headers: {
		'Content-Type': 'application/json',
		'Content-Length': data.length
	}
}

const req = https.request(options, res => {
	console.log(`状态码: ${res.statusCode}`)

	res.on('data', d => {
		process.stdout.write(d)
	})
})

req.on('error', error => {
	console.error(error)
})

req.write(data)
req.end()

请求的正文数据

const http = require('http')

const port = 3000

const server = http.createServer((req, res) => {
	req.on('data', chunk => {
		console.log(`可用的数据块: ${chunk}`)
	})

	req.on('end', () => {
		req.statusCode = 200
		res.setHeader('Content-Type', 'text/plain')
		res.end('ok')
	})

	req.on('error', error => {
		req.statusCode = 200
		res.setHeader('Content-Type', 'text/plain')
		res.end(JSON.stringify(error))
	})
})

server.listen(port, () => {
	console.log(`服务器运行在 http://localhost/:${port}/`)
})

文件

  • r+ 打开文件用于读写。
  • w+ 打开文件用于读写,将流定位到文件的开头。如果文件不存在则创建文件。
  • a 打开文件用于写入,将流定位到文件的末尾。如果文件不存在则创建文件。
  • a+ 打开文件用于读写,将流定位到文件的末尾。如果文件不存在则创建文件。
// fs.openSync 同步
const fs = require('fs')

fs.open('./share.html', 'r', (err, fd) => {
	if (err) {
		console.log(err)
		return
	}

	console.log(fd)
})

文件属性

  • 使用 stats.isFile() 和 stats.isDirectory() 判断文件是否目录或文件。
  • 使用 stats.isSymbolicLink() 判断文件是否符号链接。
  • 使用 stats.size 获取文件的大小(以字节为单位)。
// fs.statSync 同步
const fs = require('fs')

fs.stat('./share.html', (err, stats) => {
	if (err) {
		console.log(err)
		return
	}

	console.log(stats)
})

文件路径

  • dirname: 获取文件的父文件夹。
  • basename: 获取文件名部分。
  • extname: 获取文件的扩展名。
const path = require('path')

const notes = '/users/mkimq/notes.txt'

console.log(path.dirname(notes))
console.log(path.basename(notes))
console.log(path.extname(notes))

// 通过为 basename 指定第二个参数来获取不带扩展名的文件名
console.log(path.basename(notes, path.extname(notes)))

使用路径

可以使用 path.join() 连接路径的两个或多个片段:

const path = require('path')

console.log(path.join('/', 'mkimq', 'notes.txt'))

使用 path.resolve() 获得相对路径的绝对路径计算:

const path = require('path')

console.log(path.resolve('mkimq', 'notes.txt'))

path.normalize() 是另一个有用的函数,当包含诸如 .、.. 或双斜杠之类的相对说明符时,其会尝试计算实际的路径:

const path = require('path')

console.log(path.normalize('/users/mkimq/..//notes.txt'))

读取文件

const fs = require('fs')

fs.readFile('./test_js.js', 'utf8', (err, data) => {
	if (err) {
		console.log(err)
		return
	}

	console.log(data)
})

写入文件

  • r+ 打开文件用于读写。
  • w+ 打开文件用于读写,将流定位到文件的开头。如果文件不存在则创建文件。
  • a 打开文件用于写入,将流定位到文件的末尾。如果文件不存在则创建文件。
  • a+ 打开文件用于读写,将流定位到文件的末尾。如果文件不存在则创建文件。
const fs = require('fs')

const content = 'hello world'

fs.writeFile('./test.txt', content, err => {
	if (err) {
		console.log(err)
		return
	}

	console.log('写入成功')
})

const fs = require('fs')

const content = 'hello world'

fs.writeFile('./test.txt', content, { flag: 'a+'}, err => {
	if (err) {
		console.log(err)
		return
	}

	console.log('写入成功')
})

追加到文件

const fs = require('fs')

const content = '追加内容'

fs.appendFile('./test.txt', content, err => {
	if (err) {
		console.log(err)
		return
	}

	console.log('追加成功')
})

使用文件夹

检查文件夹是否存在

使用 fs.access() 检查文件夹是否存在以及 Node.js 是否具有访问权限。

const fs = require('fs')

fs.access('./test.txt', fs.constants.F_OK, err => {
	if (err) {
		console.log(err)
		return
	}
	console.log('ok')
})

创建新的文件夹

const fs = require('fs')

const folderName = './test'

try {
	if (!fs.existsSync(folderName)) {
		fs.mkdir(folderName, err => {
			if (err) {
				console.log(err)
				return
			}
			console.log('文件夹创建成功')
		})
	}
} catch (error) {
	console.log(error)
}

读取目录的内容

const fs = require('fs')

const folderName = './test'

fs.readdir(folderName, { encoding: 'utf8' }, (err, files) => {
	if (err) {
		console.log(err)
		return
	}

	console.log(files)
})

重命名文件夹

const fs = require('fs')

const folderName = './test'

fs.rename(folderName, './new_test', err => {
	if (err) {
		console.log(err)
		return
	}

	console.log('重命名完成')
})

删除文件夹

const fs = require('fs')

fs.rmdir('./new_test', err => {
	if (err) {
		console.log(err)
		return
	}

	console.log('删除成功')
})

fs-extra模块

文件系统模块

  • fs.access(): 检查文件是否存在,以及 Node.js 是否有权限访问。
  • fs.appendFile(): 追加数据到文件。如果文件不存在,则创建文件。
  • fs.chmod(): 更改文件(通过传入的文件名指定)的权限。相关方法:fs.lchmod()、fs.fchmod()。
  • fs.chown(): 更改文件(通过传入的文件名指定)的所有者和群组。相关方法:fs.fchown()、fs.lchown()。
  • fs.close(): 关闭文件描述符。
  • fs.copyFile(): 拷贝文件。
  • fs.createReadStream(): 创建可读的文件流。
  • fs.createWriteStream(): 创建可写的文件流。
  • fs.link(): 新建指向文件的硬链接。
  • fs.mkdir(): 新建文件夹。
  • fs.mkdtemp(): 创建临时目录。
  • fs.open(): 设置文件模式。
  • fs.readdir(): 读取目录的内容。
  • fs.readFile(): 读取文件的内容。相关方法:fs.read()。
  • fs.readlink(): 读取符号链接的值。
  • fs.realpath(): 将相对的文件路径指针(.、..)解析为完整的路径。
  • fs.rename(): 重命名文件或文件夹。
  • fs.rmdir(): 删除文件夹。
  • fs.stat(): 返回文件(通过传入的文件名指定)的状态。相关方法:fs.fstat()、fs.lstat()。
  • fs.symlink(): 新建文件的符号链接。
  • fs.truncate(): 将传递的文件名标识的文件截断为指定的长度。相关方法:fs.ftruncate()。
  • fs.unlink(): 删除文件或符号链接。
  • fs.unwatchFile(): 停止监视文件上的更改。
  • fs.utimes(): 更改文件(通过传入的文件名指定)的时间戳。相关方法:fs.futimes()。
  • fs.watchFile(): 开始监视文件上的更改。相关方法:fs.watch()。
  • fs.writeFile(): 将数据写入文件。相关方法:fs.write()。

路径模块

path.basename()

返回路径的最后一部分。 第二个参数可以过滤掉文件的扩展名:

require('path').basename('/test/something') //something
require('path').basename('/test/something.txt') //something.txt
require('path').basename('/test/something.txt', '.txt') //something

path.dirname()

返回路径的目录部分:

require('path').dirname('/test/something') // /test
require('path').dirname('/test/something/file.txt') // /test/something

path.extname()

返回路径的扩展名部分。

require('path').extname('/test/something') // ''
require('path').extname('/test/something/file.txt') // '.txt'

path.isAbsolute()

如果是绝对路径,则返回 true。

require('path').isAbsolute('/test/something') // true
require('path').isAbsolute('./test/something') // false

path.join()

连接路径的两个或多个部分:

const name = 'joe'
require('path').join('/', 'users', name, 'notes.txt') //'/users/joe/notes.txt'

path.normalize()

当包含类似 .、.. 或双斜杠等相对的说明符时,则尝试计算实际的路径:

require('path').normalize('/users/joe/..//test.txt') //'/users/test.txt'

path.parse()

解析对象的路径为组成其的片段:

  • root: 根路径。
  • dir: 从根路径开始的文件夹路径。
  • base: 文件名 + 扩展名
  • name: 文件名
  • ext: 文件扩展名
require('path').parse('/users/test.txt')

// {
//     root: '/',
//     dir: '/users',
//     base: 'test.txt',
//     ext: '.txt',
//     name: 'test'
// }

path.relative()

接受 2 个路径作为参数。 基于当前工作目录,返回从第一个路径到第二个路径的相对路径。

require('path').relative('/Users/joe', '/Users/joe/test.txt') //'test.txt'
require('path').relative('/Users/joe', '/Users/joe/something/test.txt') //'something/test.txt'

path.resolve()

可以使用 path.resolve() 获得相对路径的绝对路径计算:

path.resolve('joe.txt') //'/Users/joe/joe.txt' 如果从主文件夹运行

通过指定第二个参数,resolve 会使用第一个参数作为第二个参数的基准:

path.resolve('tmp', 'joe.txt') //'/Users/joe/tmp/joe.txt' 如果从主文件夹运行

如果第一个参数以斜杠开头,则表示它是绝对路径:

path.resolve('/etc', 'joe.txt') //'/etc/joe.txt'

操作系统模块

os.arch()

返回标识底层架构的字符串,例如 arm、x64、arm64。

const os = require('os')

console.log(os.arch())

os.cpus()

返回关于系统上可用的 CPU 的信息。

const os = require('os')

console.log(os.cpus())

os.endianness()

根据是使用大端序或小端序编译 Node.js,返回 BE 或 LE。

const os = require('os')

console.log(os.endianness())

os.freemem()

返回代表系统中可用内存的字节数。

const os = require('os')

console.log((os.freemem() / (1024 * 1024 * 1024)).toFixed(2) + 'GB')

os.homedir()

返回到当前用户的主目录的路径。

const os = require('os')

console.log(os.homedir())

os.hostname()

返回主机名。

const os = require('os')

console.log(os.hostname())

os.loadavg()

返回操作系统对平均负载的计算。

const os = require('os')

console.log(os.loadavg())

这仅在 Linux 和 macOS 上返回有意义的值。

os.networkInterfaces()

返回系统上可用的网络接口的详细信息。

const os = require('os')

console.log(os.networkInterfaces())

os.platform()

  • darwin
  • freebsd
  • linux
  • openbsd
  • win32
  • ...等

返回为 Node.js 编译的平台:

const os = require('os')

console.log(os.platform())

os.release()

返回标识操作系统版本号的字符串。

const os = require('os')

console.log(os.release())

os.tmpdir()

返回指定的临时文件夹的路径。

const os = require('os')

console.log(os.tmpdir())

os.totalmem()

返回表示系统中可用的总内存的字节数。

const os = require('os')

console.log((os.totalmem() / (1024 * 1024 * 1024)).toFixed(2) + 'GB')

os.type()

标识操作系统:

const os = require('os')

console.log(os.type())

os.uptime()

返回自上次重新启动以来计算机持续运行的秒数。

const os = require('os')

console.log(os.uptime())

os.userInfo()

返回包含当前 username、uid、gid、shell 和 homedir 的对象。

const os = require('os')

console.log(os.userInfo())

事件模块

const EventEmitter = require('events')
const door = new EventEmitter()

emitter.addListener()

emitter.on() 的别名。

emitter.emit()

触发事件。 按照事件被注册的顺序同步地调用每个事件监听器。

door.emit('slam') // 触发slam事件

emitter.eventNames()

返回字符串(表示在当前 EventEmitter 对象上注册的事件)数组:

door.eventNames()

emitter.getMaxListeners()

获取可以添加到 EventEmitter 对象的监听器的最大数量(默认为 10,但可以使用 setMaxListeners() 进行增加或减少)。

door.getMaxListeners()

emitter.listenerCount()

获取作为参数传入的事件监听器的计数:

door.listenerCount('open')

emitter.listeners()

获取作为参数传入的事件监听器的数组:

door.listeners('open')

emitter.off()

emitter.removeListener() 的别名,新增于 Node.js 10。

emitter.on()

添加当事件被触发时调用的回调函数。

door.on('open', () => {
    console.log('打开')
})

emitter.once()

添加当事件在注册之后首次被触发时调用的回调函数。 该回调只会被调用一次,不会再被调用。

emitter.prependListener()

当使用 on 或 addListener 添加监听器时,监听器会被添加到监听器队列中的最后一个,并且最后一个被调用。 使用 prependListener 则可以在其他监听器之前添加并调用。

emitter.prependOnceListener()

当使用 once 添加监听器时,监听器会被添加到监听器队列中的最后一个,并且最后一个被调用。 使用 prependOnceListener 则可以在其他监听器之前添加并调用。

emitter.removeAllListeners()

移除 EventEmitter 对象的所有监听特定事件的监听器:

door.removeAllListeners('open')

emitter.removeListener()

移除特定的监听器。 可以通过将回调函数保存到变量中(当添加时),以便以后可以引用它:

const doSomething = () => {}
door.on('open', doSomething)
door.removeListener('open', doSomething)

emitter.setMaxListeners()

设置可以添加到 EventEmitter 对象的监听器的最大数量(默认为 10,但可以增加或减少)。

door.setMaxListeners(50)

http模块

属性

http.METHODS

此属性列出了所有支持的 HTTP 方法:

console.log(require('http').METHODS)

http.STATUS_CODES

此属性列出了所有的 HTTP 状态代码及其描述:

console.log(require('http').STATUS_CODES)

http.globalAgent

指向 Agent 对象的全局实例,该实例是 http.Agent 类的实例。

方法

http.createServer()

返回 http.Server 类的新实例。

const server = http.createServer((req, res) => {
    //使用此回调处理每个单独的请求。
})

http.request()

发送 HTTP 请求到服务器,并创建 http.ClientRequest 类的实例。

http.get()

类似于 http.request(),但会自动地设置 HTTP 方法为 GET,并自动地调用 req.end()。

HTTP 模块提供了 5 个类

  • http.Agent
  • http.ClientRequest
  • http.Server
  • http.ServerResponse
  • http.IncomingMessage

http.Agent

Node.js 会创建 http.Agent 类的全局实例,以管理 HTTP 客户端连接的持久性和复用,这是 Node.js HTTP 网络的关键组成部分。

该对象会确保对服务器的每个请求进行排队并且单个 socket 被复用。

它还维护一个 socket 池。 出于性能原因,这是关键。

http.ClientRequest

当 http.request() 或 http.get() 被调用时,会创建 http.ClientRequest 对象。

当响应被接收时,则会使用响应(http.IncomingMessage 实例作为参数)来调用 response 事件。

返回的响应数据可以通过以下两种方式读取:

  • 可以调用 response.read() 方法。
  • 在 response 事件处理函数中,可以为 data 事件设置事件监听器,以便可以监听流入的数据。

http.Server

当使用 http.createServer() 创建新的服务器时,通常会实例化并返回此类。

拥有服务器对象后,就可以访问其方法:

  • close() 停止服务器不再接受新的连接。
  • listen() 启动 HTTP 服务器并监听连接。

http.ServerResponse

由 http.Server 创建,并作为第二个参数传给它触发的 request 事件。

通常在代码中用作 res:

const server = http.createServer((req, res) => {
    //res 是一个 http.ServerResponse 对象。
})

在事件处理函数中总是会调用的方法是 end(),它会关闭响应,当消息完成时则服务器可以将其发送给客户端。 必须在每个响应上调用它。

以下这些方法用于与 HTTP 消息头进行交互:

  • getHeaderNames() 获取已设置的 HTTP 消息头名称的列表。
  • getHeaders() 获取已设置的 HTTP 消息头的副本。
  • setHeader('headername', value) 设置 HTTP 消息头的值。
  • getHeader('headername') 获取已设置的 HTTP 消息头。
  • removeHeader('headername') 删除已设置的 HTTP 消息头。
  • hasHeader('headername') 如果响应已设置该消息头,则返回 true。
  • headersSent() 如果消息头已被发送给客户端,则返回 true。

在处理消息头之后,可以通过调用 response.writeHead()(该方法接受 statusCode 作为第一个参数,可选的状态消息和消息头对象)将它们发送给客户端。

若要在响应正文中发送数据给客户端,则使用 write()。 它会发送缓冲的数据到 HTTP 响应流。

如果消息头还未被发送,则使用 response.writeHead() 会先发送消息头,其中包含在请求中已被设置的状态码和消息,可以通过设置 statusCode 和 statusMessage 属性的值进行编辑:

response.statusCode = 500
response.statusMessage = '内部服务器错误'

http.IncomingMessage

http.IncomingMessage 对象可通过以下方式创建:

  • http.Server,当监听 request 事件时。
  • http.ClientRequest,当监听 response 事件时。

它可以用来访问响应:

  • 使用 statusCode 和 statusMessage 方法来访问状态。
  • 使用 headers 方法或 rawHeaders 来访问消息头。
  • 使用 method 方法来访问 HTTP 方法。
  • 使用 httpVersion 方法来访问 HTTP 版本。
  • 使用 url 方法来访问 URL。
  • 使用 socket 方法来访问底层的 socket。

因为 http.IncomingMessage 实现了可读流接口,因此数据可以使用流访问。

Buffer

什么是 buffer?

Buffer 是内存区域。 JavaScript 开发者可能对这个概念并不熟悉,比每天与内存交互的 C、C++ 或 Go 开发者(或使用系统编程语言的任何程序员)要少得多。

它表示在 V8 JavaScript 引擎外部分配的固定大小的内存块(无法调整大小)。

可以将 buffer 视为整数数组,每个整数代表一个数据字节。

它由 Node.js Buffer 类实现。

为什么需要 buffer?

Buffer 被引入用以帮助开发者处理二进制数据,在此生态系统中传统上只处理字符串而不是二进制数据。

Buffer 与流紧密相连。 当流处理器接收数据的速度快于其消化的速度时,则会将数据放入 buffer 中。

一个简单的场景是:当观看 YouTube 视频时,红线超过了观看点:即下载数据的速度比查看数据的速度快,且浏览器会对数据进行缓冲。

如何创建 buffer

使用 Buffer.from()、Buffer.alloc() 和 Buffer.allocUnsafe() 方法可以创建 buffer。

const buf = Buffer.from('Hello world')

console.log(buf)
  • Buffer.from(array)
  • Buffer.from(arrayBuffer[, byteOffset[, length]])
  • Buffer.from(buffer)
  • Buffer.from(string[, encoding])

也可以只初始化 buffer(传入大小)。 以下会创建一个 1KB 的 buffer:

const buf = Buffer.alloc(1024)
// 或
const buf = Buffer.allocUnsafe(1024)

虽然 alloc 和 allocUnsafe 均分配指定大小的 Buffer(以字节为单位),但是 alloc 创建的 Buffer 会被使用零进行初始化,而 allocUnsafe 创建的 Buffer 不会被初始化。 这意味着,尽管 allocUnsafe 比 alloc 要快得多,但是分配的内存片段可能包含可能敏感的旧数据。

当 Buffer 内存被读取时,如果内存中存在较旧的数据,则可以被访问或泄漏。 这就是真正使 allocUnsafe 不安全的原因,在使用它时必须格外小心。

使用 buffer

访问 buffer 的内容

Buffer(字节数组)可以像数组一样被访问:

const buf = Buffer.from('Hey!')
console.log(buf[0]) //72
console.log(buf[1]) //101
console.log(buf[2]) //121

这些数字是 Unicode 码,用于标识 buffer 位置中的字符(H => 72、e => 101、y => 121)。

可以使用 toString() 方法打印 buffer 的全部内容:

console.log(buf.toString())

注意,如果使用数字(设置其大小)初始化 buffer,则可以访问到包含随机数据的已预初始化的内存(而不是空的 buffer)!

获取 buffer 的长度

使用 length 属性:

const buf = Buffer.from('Hey!')
console.log(buf.length)

迭代 buffer 的内容

const buf = Buffer.from('Hey!')
for (const item of buf) {
    console.log(item) //72 101 121 33
}

更改 buffer 的内容

可以使用 write() 方法将整个数据字符串写入 buffer:

const buf = Buffer.alloc(4)
buf.write('Hey!')

就像可以使用数组语法访问 buffer 一样,你也可以使用相同的方式设置 buffer 的内容:

const buf = Buffer.from('Hey!')
buf[1] = 111 //o
console.log(buf.toString()) //Hoy!

复制 buffer

使用 copy() 方法可以复制 buffer:

const buf = Buffer.from('Hey!')
let bufcopy = Buffer.alloc(4) //分配 4 个字节。
buf.copy(bufcopy)

默认情况下,会复制整个 buffer。 另外的 3 个参数可以定义开始位置、结束位置、以及新的 buffer 长度:

const buf = Buffer.from('Hey!')
let bufcopy = Buffer.alloc(2) //分配 2 个字节。
buf.copy(bufcopy, 0, 0, 2)
bufcopy.toString() //'He

切片 buffer

如果要创建 buffer 的局部视图,则可以创建切片。 切片不是副本:原始 buffer 仍然是真正的来源。 如果那改变了,则切片也会改变。

使用 slice() 方法创建它。 第一个参数是起始位置,可以指定第二个参数作为结束位置:

const buf = Buffer.from('Hey!')
buf.slice(0).toString() //Hey!
const slice = buf.slice(0, 2)
console.log(slice.toString()) //He
buf[1] = 111 //o
console.log(slice.toString()) //Ho

什么是流

流是为 Node.js 应用程序提供动力的基本概念之一。

它们是一种以高效的方式处理读/写文件、网络通信、或任何类型的端到端的信息交换。

流不是 Node.js 特有的概念。 它们是几十年前在 Unix 操作系统中引入的,程序可以通过管道运算符(|)对流进行相互交互。

例如,在传统的方式中,当告诉程序读取文件时,这会将文件从头到尾读入内存,然后进行处理。

使用流,则可以逐个片段地读取并处理(而无需全部保存在内存中)。

Node.js 的 stream 模块 提供了构建所有流 API 的基础。 所有的流都是 EventEmitter 的实例。

为什么是流

相对于使用其他的数据处理方法,流基本上提供了两个主要优点:

  • 内存效率: 无需加载大量的数据到内存中即可进行处理。
  • 时间效率: 当获得数据之后即可立即开始处理数据,这样所需的时间更少,而不必等到整个数据有效负载可用才开始。

流的示例

个典型的例子是从磁盘读取文件。

使用 Node.js 的 fs 模块,可以读取文件,并在与 HTTP 服务器建立新连接时通过 HTTP 提供文件:

const http = require('http')
const fs = require('fs')

const server = http.createServer((req, res) => {
	fs.readFile('./test.txt', (err, data) => {
		if (err) {
			console.log(err)
			return
		}
		res.statusCode = 200
		res.setHeader('Content-Type', 'text/plain')
		res.end(data)
	})
})

server.listen(3000)

readFile() 读取文件的全部内容,并在完成时调用回调函数。

回调中的 res.end(data) 会返回文件的内容给 HTTP 客户端。

如果文件很大,则该操作会花费较多的时间。 以下是使用流编写的相同内容:

const http = require('http')
const fs = require('fs')

const server = http.createServer((req, res) => {
	const stream = fs.createReadStream('./test.txt')

	stream.pipe(res)
})

server.listen(3000)

pipe()

上面的示例使用了 stream.pipe(res) 这行代码:在文件流上调用 pipe() 方法。

该代码的作用是什么? 它获取来源流,并将其通过管道传输到目标流。

在来源流上调用它,在该示例中,文件流通过管道传输到 HTTP 响应。

pipe() 方法的返回值是目标流,这是非常方便的事情,它使得可以链接多个 pipe() 调用,如下所示:

src.pipe(dest1).pipe(dest2)

此构造相对于:

src.pipe(dest1)
dest1.pipe(dest2)

驱动的 Node.js API

由于它们的优点,许多 Node.js 核心模块提供了原生的流处理功能,最值得注意的有:

  • process.stdin 返回连接到 stdin 的流。
  • process.stdout 返回连接到 stdout 的流。
  • process.stderr 返回连接到 stderr 的流。
  • fs.createReadStream() 创建文件的可读流。
  • fs.createWriteStream() 创建到文件的可写流。
  • net.connect() 启动基于流的连接。
  • http.request() 返回 http.ClientRequest 类的实例,该实例是可写流。
  • zlib.createGzip() 使用 gzip(压缩算法)将数据压缩到流中。
  • zlib.createGunzip() 解压缩 gzip 流。
  • zlib.createDeflate() 使用 deflate(压缩算法)将数据压缩到流中。
  • zlib.createInflate() 解压缩 deflate 流。

不同类型的流

流分为四类:

  • Readable: 可以通过管道读取、但不能通过管道写入的流(可以接收数据,但不能向其发送数据)。 当推送数据到可读流中时,会对其进行缓冲,直到使用者开始读取数据为止。
  • Writable: 可以通过管道写入、但不能通过管道读取的流(可以发送数据,但不能从中接收数据)。
  • Duplex: 可以通过管道写入和读取的流,基本上相对于是可读流和可写流的组合。
  • Transform: 类似于双工流、但其输出是其输入的转换的转换流。

如何创建可读流

从 stream 模块获取可读流,对其进行初始化并实现 readable._read() 方法。

首先创建流对象:

const Stream = require('stream')
const readableStream = new Stream.Readable()

然后实现 _read:

readableStream._read = () => {}

也可以使用 read 选项实现 _read:

const readableStream = new Stream.Readable({
  read() {}
})

现在,流已初始化,可以向其发送数据了:

readableStream.push('hi!')
readableStream.push('ho!')

如何创建可写流

若要创建可写流,需要继承基本的 Writable 对象,并实现其 _write() 方法。

首先创建流对象:

const Stream = require('stream')
const writableStream = new Stream.Writable()

然后实现 _write:

writableStream._write = (chunk, encoding, next) => {
  console.log(chunk.toString())
  next()
}

现在,可以通过以下方式传输可读流:

process.stdin.pipe(writableStream)

如何从可读流中获取数据

如何从可读流中读取数据? 使用可写流:

const Stream = require('stream')

const readableStream = new Stream.Readable({
  read() {}
})
const writableStream = new Stream.Writable()

writableStream._write = (chunk, encoding, next) => {
  console.log(chunk.toString())
  next()
}

readableStream.pipe(writableStream)

readableStream.push('hi!')
readableStream.push('ho!')

也可以使用 readable 事件直接地消费可读流:

readableStream.on('readable', () => {
    console.log(readableStream.read())
})

如何发送数据到可写流

使用流的 write() 方法:

writableStream.write('hey!\n')

使用信号通知已结束写入的可写流

使用 end() 方法:

const Stream = require('stream')

const readableStream = new Stream.Readable({
    read() {}
})
const writableStream = new Stream.Writable()

writableStream._write = (chunk, encoding, next) => {
    console.log(chunk.toString())
    next()
}

readableStream.pipe(writableStream)

readableStream.push('hi!')
readableStream.push('ho!')

writableStream.end()

错误处理

创建异常

使用 throw 关键字创建异常:

throw value

错误对象

错误对象是 Error 对象的实例、或者继承自 Error 类(由 Error 核心模块提供):

throw new Error('错误信息')

class NotEnoughCoffeeError extends Error {
    //...
}
throw new NotEnoughCoffeeError()

处理异常

异常处理程序是 try/catch 语句。

try 块中包含的代码行中引发的任何异常都会在相应的 catch 块中处理:

try {
    //代码行
} catch (e) {}

捕获未捕获的异常

如果在程序执行过程中引发了未捕获的异常,则程序会崩溃。

若要解决此问题,则监听 process 对象上的 uncaughtException 事件:

process.on('uncaughtException', err => {
    console.error('有一个未捕获的错误', err)
    process.exit(1) //强制性的(根据 Node.js 文档)
})

Promise 的异常

使用 promise 可以链接不同的操作,并在最后处理错误:

doSomething1()
    .then(doSomething2)
    .then(doSomething3)
    .catch(err => console.error(err))

async/await 的错误处理

使用 async/await 时,仍然需要捕获错误,可以通过以下方式进行操作:

async function someFunction() {
    try {
        await someOtherFunction()
    } catch (err) {
        console.error(err.message)
    }
}

多进程

Node.js 是以单线程的模式运行的,但它使用的是事件驱动来处理并发,这样有助于我们在多核 cpu 的系统上创建多个子进程,从而提高性能。

每个子进程总是带有三个流对象:child.stdin, child.stdout 和child.stderr。他们可能会共享父进程的 stdio 流,或者也可以是独立的被导流的流对象。

Node 提供了 child_process 模块来创建子进程,方法有:

  • exec - child_process.exec 使用子进程执行命令,缓存子进程的输出,并将子进程的输出以回调函数参数的形式返回。
  • spawn - child_process.spawn 使用指定的命令行参数创建新进程。
  • fork - child_process.fork 是 spawn()的特殊形式,用于在子进程中运行的模块,如 fork('./son.js') 相当于 spawn('node', ['./son.js']) 。与spawn方法不同的是,fork会在父进程与子进程之间,建立一个通信管道,用于进程之间的通信。

exec() 方法

child_process.exec 使用子进程执行命令,缓存子进程的输出,并将子进程的输出以回调函数参数的形式返回。

语法如下所示:

child_process.exec(command[, options], callback)

参数

  • command: 字符串, 将要运行的命令,参数使用空格隔开

  • options :对象,可以是:

    • cwd ,字符串,子进程的当前工作目录
    • env,对象 环境变量键值对
    • encoding ,字符串,字符编码(默认: 'utf8')
    • shell ,字符串,将要执行命令的 Shell(默认: 在 UNIX 中为/bin/sh, 在 Windows 中 cmd.exe, Shell 应当能识别 -c开关在 UNIX 中,或 /s /c 在 Windows 中。 在Windows 中,命令解析应当能兼容cmd.exe)
    • timeout,数字,超时时间(默认: 0)
    • maxBuffer,数字, 在 stdout 或 stderr 中允许存在的最大缓冲(二进制),如果超出那么子进程将会被杀死 (默认: 200*1024)
    • killSignal ,字符串,结束信号(默认:'SIGTERM')
    • uid,数字,设置用户进程的 ID
    • gid,数字,设置进程组的 ID
  • callback :回调函数,包含三个参数error, stdout 和 stderr。

exec() 方法返回最大的缓冲区,并等待进程结束,一次性返回缓冲区的内容。

实例

const child_process = require('child_process')

for (let i = 0; i < 3; i++) {
    const workerProcess = child_process.exec('node ./child.js '+i, (err, stdout, stderr) => {
        if (err) {
            console.log(err.stack)
            console.log('Error code: '+err.code)
            console.log('Signal received: '+err.signal)
        }
        console.log('stdout: ' + stdout)
        console.log('stderr: ' + stderr)
    })

    workerProcess.on('exit', code => {
        console.log('子进程已退出,退出码 '+code)
    })
}

// 子进程已退出,退出码 0
// stdout: 进程0执行。

// stderr:
// 子进程已退出,退出码 0
// stdout: 进程2执行。

// stderr:
// 子进程已退出,退出码 0
// stdout: 进程1执行。

// stderr:
console.log('进程' + process.argv[2] + '执行。')

spawn() 方法

child_process.spawn 使用指定的命令行参数创建新进程,语法格式如下:

child_process.spawn(command[, args][, options])

参数

参数说明如下:

  • command: 将要运行的命令

  • args: Array 字符串参数数组

  • options Object

    • cwd String 子进程的当前工作目录
    • env Object 环境变量键值对
    • stdio Array|String 子进程的 stdio 配置
    • detached Boolean 这个子进程将会变成进程组的领导
    • uid Number 设置用户进程的 ID
    • gid Number 设置进程组的 ID

spawn() 方法返回流 (stdout & stderr),在进程返回大量数据时使用。进程一旦开始执行spawn() 就开始接收响应。

实例

const child_process = require('child_process')

for (let i = 0; i < 3; i++) {
    const workerProcess = child_process.spawn('node', ['child.js', i])

    workerProcess.stdout.on('data', function (data) {
        console.log('stdout: ' + data)
    })

    workerProcess.stderr.on('data', function (data) {
        console.log('stderr: ' + data);
    })

    workerProcess.on('close', code => {
        console.log('子进程已退出,退出码 '+code)
    })
}

// stdout: 进程0执行。

// 子进程已退出,退出码 0
// stdout: 进程1执行。

// 子进程已退出,退出码 0
// stdout: 进程2执行。

// 子进程已退出,退出码 0

fork 方法

child_process.fork 是 spawn() 方法的特殊形式,用于创建进程,语法格式如下:

child_process.fork(modulePath[, args][, options])

参数

参数说明如下:

  • modulePath: String,将要在子进程中运行的模块

  • args: Array 字符串参数数组

  • options:Object

    • cwd String 子进程的当前工作目录
    • env Object 环境变量键值对
    • execPath String 创建子进程的可执行文件
    • execArgv Array 子进程的可执行文件的字符串参数数组(默认: process.execArgv)
    • silent Boolean 如果为true,子进程的stdin,stdout和stderr将会被关联至父进程,否则,它们将会从父进程中继承。(默认为:false)
    • uid Number 设置用户进程的 ID
    • gid Number 设置进程组的 ID

返回的对象除了拥有ChildProcess实例的所有方法,还有一个内建的通信信道。

实例

const child_process = require('child_process')

for (let i = 0; i < 3; i++) {
    const workerProcess = child_process.fork('child.js', [i])

    workerProcess.on('close', code => {
        console.log('子进程已退出,退出码 '+code)
    })
}

// 进程0执行。
// 子进程已退出,退出码 0
// 进程1执行。
// 子进程已退出,退出码 0
// 进程2执行。
// 子进程已退出,退出码 0
贡献者: mankueng