入门
事件循环
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