《Node.js 简介》学习笔记

133 阅读18分钟

Node.js 简介

Node.js 是一个开源和跨平台的 JavaScript 运行时环境。

Node.js 在浏览器之外运行 V8 JavaScript 引擎(Google Chrome 的内核)。

Node.js 应用程序在单个进程中运行,无需为每个请求创建新的线程。

Node.js 使用单个服务器处理数千个并发连接,而不会引入管理线程并发的负担。

Node.js 进程与线程

进程是操作系统进行资源分配的最小单位,进程是线程的容器。

线程是操作系统进行运算调度的最小单位。

一个线程只能隶属于一个进程,但是一个进程是可以拥有多个线程的。

单线程就是一个进程只开一个线程。

Javascript 就是属于单线程。

Node.js 虽然是单线程模型,但是其基于事件驱动、异步非阻塞模式,可以应用于高并发场景,避免了线程创建、线程之间上下文切换所产生的资源开销。

我们启动一个服务、运行一个实例,就是开一个服务进程,Node.js 通过 node app.js 开启一个服务进程。

当你的项目中需要有大量计算,CPU 耗时的操作时候,要注意考虑开启多进程来完成了。

最简 Web 服务器

const http = require('http')

const server = http.createServer((req, res) => {
  res.end('Hello World\n')
})

server.listen(3000)

安装

nvm 是一种流行的运行 Node.js 的方式,它可以轻松地切换 Node.js 版本。

V8 JavaScript 引擎

V8 提供了 JavaScript 执行的运行时环境。 DOM 和其他 Web 平台 API 由浏览器提供。

JavaScript 由 V8 在内部使用即时 (JIT) 编译以加快执行速度。

退出 Node.js 程序

当在控制台中运行程序时,可以用 ctrl-C 关闭它。

以编程方式退出 Node.js 程序:process.exit()。进程立即被强制终止。这意味着任何待处理的回调、任何仍在发送的网络请求、任何文件系统访问、或者正在写入 stdout 或 stderr 的进程,所有这些都将立即被非正常地终止。

信号

什么是信号?信号是一个 POSIX 互通系统:发送给进程的通知,以便通知它发生的事件。

如果您调用 process.exit(),则任何当前待处理或正在运行的请求都将被中止,这并不好。在这种情况下,您需要向命令发送 SIGTERM 信号,并使用进程信号句柄处理它:

const express = require('express')

const app = express()

app.get('/', (req, res) => {
  res.send('Hi!')
})

const server = app.listen(3000, () => console.log('Server ready'))

process.on('SIGTERM', () => {
  server.close(() => {
    console.log('Process terminated')
  })
})

SIGKILL 是告诉进程立即终止的信号,理想情况下会像 process.exit() 一样。

SIGTERM 是告诉进程正常终止的信号。 这是从 upstart 或 supervisord 等进程管理器发出的信号。

你可以从程序内部,在另一个函数中发送这个信号:

process.kill(process.pid, 'SIGTERM')

或者从另一个 Node.js 运行的程序、或者从您的系统中运行的任何其他应用程序(知道您要终止的进程的 PID)。

读取环境变量

process 核心模块提供了 env 属性,该属性承载了在启动进程时设置的所有环境变量。

从命令行接收参数

process.argv 是一个包含所有命令行调用参数的数组。

第一个参数是 node 命令的完整路径。

第二个参数是正被执行的文件的完整路径。

所有其他的参数从第三个位置开始。

输出到命令行

Node.js 提供了 console 模块

打印堆栈踪迹

在某些情况下,打印函数的调用堆栈踪迹很有用,可以使用 console.trace() 实现:

const function2 = () => console.trace()
const function1 = () => function2()
function1()

image.png

计算耗时

可以使用 time() 和 timeEnd() 轻松地计算函数运行所需的时间。

为输出着色

可以使用转义序列在控制台中为文本的输出着色。转义序列是一组标识颜色的字符。

为控制台输出着色的最简单方法是使用库 Chalk

创建进度条

Progress 是一个很棒的软件包,可在控制台中创建进度条。

从命令行接收输入

从版本 7 开始,Node.js 提供了 readline 模块来执行以下操作:每次一行地从可读流(例如 process.stdin 流,在 Node.js 程序执行期间该流就是终端输入)获取输入。

const readline = require('readline').createInterface({
  input: process.stdin,
  output: process.stdout
})

readline.question(`你叫什么名字?`, name => {
  console.log(`你好 ${name}!`)
  readline.close()
})

Inquirer.js 则提供了更完整、更抽象的解决方案。

使用 exports 公开功能

导入

const library = require('./library')

公开

第一种方式是将对象赋值给 module.exports(这是模块系统提供的对象),这会使文件只导出该对象:

module.exports = car

第二种方式是将要导出的对象添加为 exports 的属性。这种方式可以导出多个对象、函数或数据:

exports.car = car

module.exports = car 和 exports.car = car 之间有什么区别?

前者公开了它指向的对象。后者公开了它指向的对象的属性。

使用 npm 的语义版本控制

语义版本控制的概念很简单:所有的版本都有 3 个数字:x.y.z

  • 第一个数字是主版本。
  • 第二个数字是次版本。
  • 第三个数字是补丁版本。

npm 设置了一些规则,可用于在 package.json 文件中选择要将软件包更新到的版本(当运行 npm update 时)。

规则使用了这些符号:

  • ^: 只会执行不更改最左边非零数字的更新。
  • ~: 更新到补丁版本。
  • >: 接受高于指定版本的任何版本。
  • >=: 接受等于或高于指定版本的任何版本。
  • <=: 接受等于或低于指定版本的任何版本。
  • <: 接受低于指定版本的任何版本。
  • =: 接受确切的版本。
  • -: 接受一定范围的版本。例如:2.1.0 - 2.6.2
  • ||: 组合集合。例如 < 2.1 || > 2.6

可以合并其中的一些符号,例如 1.0.0 || >=1.1.0 <1.2.0,即使用 1.0.0 或从 1.1.0 开始但低于 1.2.0 的版本。

还有其他的规则:

  • 无符号: 仅接受指定的特定版本(例如 1.2.1)。
  • latest: 使用可用的最新版本。

通过 npx 使用不同的 Node.js 版本运行代码

使用 @ 指定版本,并将其与 node npm 软件包 结合使用:

npx node@10 -v #v10.18.1
npx node@12 -v #v12.14.1

npx node@18 app.js

这有助于避免使用 nvm 之类的工具或其他 Node.js 版本管理工具。

使用 npx 直接从 URL 运行任意代码片段

npx 并不限制使用 npm 仓库上发布的软件包。

可以运行位于 GitHub gist 中的代码,例如:

npx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32

Node.js 事件循环

Node.js JavaScript 代码运行在单个线程上。 每次只处理一件事。

这个限制实际上非常有用,因为它大大简化了编程方式,而不必担心并发问题。

阻塞事件循环

任何花费太长时间才能将控制权返回给事件循环的 JavaScript 代码,都会阻塞页面中任何 JavaScript 代码的执行,甚至阻塞 UI 线程,并且用户无法单击浏览、滚动页面等。

JavaScript 中几乎所有的 I/O 基元都是非阻塞的。 网络请求、文件系统操作等。

调用堆栈

调用堆栈是一个 LIFO 队列(后进先出)。

事件循环不断地检查调用堆栈,以查看是否需要运行任何函数。

当执行时,它会将找到的所有函数调用添加到调用堆栈中,并按顺序执行每个函数。

你知道在调试器或浏览器控制台中可能熟悉的错误堆栈跟踪吗? 浏览器在调用堆栈中查找函数名称,以告知你是哪个函数发起了当前的调用:

image.png

一个简单的事件循环的阐释

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

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

const foo = () => {
  console.log('foo')
  bar()
  baz()
}

foo()

当运行此代码时,会首先调用 foo()。 在 foo() 内部,会首先调用 bar(),然后调用 baz()

此时,调用堆栈如下所示:

调用堆栈的第一个示例

每次迭代中的事件循环都会查看调用堆栈中是否有东西并执行它直到调用堆栈为空:

执行顺序的第一个示例

入队函数执行

让我们看看如何将函数推迟直到堆栈被清空。

setTimeout(() => {}, 0) 的用例是调用一个函数,但是是在代码中的每个其他函数已被执行之后。

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

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

const foo = () => {
  console.log('foo')
  setTimeout(bar, 0)
  baz()
}

foo()
// foo
// baz
// bar

调用堆栈如下所示:

调用堆栈的第二个示例

这是程序中所有函数的执行顺序:

执行顺序的第二个示例

为什么会这样呢?

消息队列

当调用 setTimeout() 时,浏览器或 Node.js 会启动定时器。 当定时器到期时(在此示例中会立即到期,因为将超时值设为 0),则回调函数会被放入“消息队列”中。

在消息队列中,用户触发的事件(如单击或键盘事件、或获取响应)也会在此排队,然后代码才有机会对其作出反应。 类似 onLoad 这样的 DOM 事件也如此。

事件循环会赋予调用堆栈优先级,它首先处理在调用堆栈中找到的所有东西,一旦其中没有任何东西,便开始处理消息队列中的东西。

我们不必等待诸如 setTimeout、fetch、或其他的函数来完成它们自身的工作,因为它们是由浏览器提供的,并且位于它们自身的线程中。

ES6 作业队列

ECMAScript 2015 引入了作业队列的概念,Promise 使用了该队列(也在 ES6/ES2015 中引入)。 这种方式会尽快地执行异步函数的结果,而不是放在调用堆栈的末尾。

在当前函数结束之前 resolve 的 Promise 会在当前函数之后被立即执行。

有个游乐园中过山车的比喻很好:消息队列将你排在队列的后面(在所有其他人的后面),你不得不等待你的回合,而工作队列则是快速通道票,这样你就可以在完成上一次乘车后立即乘坐另一趟车。

const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
  console.log('foo')
  setTimeout(bar, 0)
  new Promise((resolve, reject) =>
    resolve('pro')
  ).then(resolve => console.log(resolve))
  baz()
}
foo()
// foo
// baz
// pro
// bar

这是 Promise(以及基于 promise 构建的 async/await)与通过 setTimeout() 或其他平台 API 的普通的旧异步函数之间的巨大区别。

process.nextTick()

每当事件循环进行一次完整的行程时,我们都将其称为一个 tick。

当将一个函数传给 process.nextTick() 时,则指示 JS 引擎在当前操作结束(在下一个事件循环 tick 开始之前)时调用此函数:

process.nextTick(() => {
  // 做些事情
})

这是可以告诉 JS 引擎异步地(在当前函数之后)处理函数的方式,但是尽快执行而不是将其排入队列。

调用 setTimeout(() => {}, 0) 会在 下一个 tick 结束时 执行该函数,比使用 nextTick()(其会优先执行该调用并在 下一个 tick 开始之前 执行该函数)晚得多。

const bar = () => console.log('bar')
const baz = () => console.log('baz')
const bat = () => console.log('bat')
const foo = () => {
  console.log('foo')
  setTimeout(bar, 0)
  new Promise(r => r('pro')).then(r => console.log(r))
  process.nextTick(bat)
  baz()
}
foo()
// foo 
// baz 
// bat 
// pro 
// bar 

由此看出先后顺序:nextTick -> 作业队列 -> 消息队列

当要确保代码在下一个事件循环中执行,则使用 nextTick()

setImmediate()

当要异步地(但要尽可能快)执行某些代码时,其中一个选择是使用 Node.js 提供的 setImmediate() 函数:

setImmediate(() => {
  //运行一些东西
})

作为 setImmediate() 参数传入的任何函数都是在事件循环的 下一个迭代 中执行的回调。

setImmediate() 与 setTimeout(() => {}, 0)(传入 0 毫秒的超时)、process.nextTick() 有何不同?

传给 process.nextTick() 的函数会在事件循环的 当前迭代 中(当前操作结束之后)被执行。 这意味着它会始终在 setTimeout 和 setImmediate 之前执行。

延迟 0 毫秒的 setTimeout() 回调与 setImmediate() 非常相似。 执行顺序取决于各种因素,但是它们都会在事件循环的下一个迭代中运行。

const bar = () => console.log('bar')
const baz = () => console.log('baz')
const bai = () => console.log('bai')
const bat = () => console.log('bat')
const foo = () => {
  console.log('foo')
  setImmediate(bai)
  setTimeout(bar, 0)
  new Promise(r => r('pro')).then(r => console.log(r))
  process.nextTick(bat)
  baz()
}
foo()
// foo
// baz
// bat
// pro
// bar
// bai

setTimeout 也可以传入现有的函数名称和一组参数:

setTimeout(myFunction, 2000, firstParam, secondParam)

通过在调度程序中排队函数,可以避免在执行繁重的任务时阻塞 CPU,并在执行繁重的计算时执行其他函数。

异步编程(Promise & await/async)

JavaScript 默认情况下是同步的,并且是单线程的。 这意味着代码无法创建新的线程并且不能并行运行。

在任何函数之前加上 async 关键字意味着该函数会返回 promise。即使没有显式地这样做,它也会在内部使它返回 promise。这就是为什么此代码有效的原因:

const aFunction = async () => {
  return '测试'
}

aFunction().then(alert) // 这会 alert '测试'

这与以下代码一样:

const aFunction = () => {
  return Promise.resolve('测试')
}

aFunction().then(alert) // 这会 alert '测试'

这是使用 promise 获取并解析 JSON 资源的方法:

const getFirstUserData = () => {
  return fetch('/users.json') // 获取用户列表
    .then(response => response.json()) // 解析 JSON
    .then(users => users[0]) // 选择第一个用户
    .then(user => fetch(`/users/${user.name}`)) // 获取用户数据
    .then(userResponse => userResponse.json()) // 解析 JSON
}

getFirstUserData()

这是使用 await/async 提供的相同功能:

const getFirstUserData = async () => {
  const response = await fetch('/users.json') // 获取用户列表
  const users = await response.json() // 解析 JSON
  const user = users[0] // 选择第一个用户
  const userResponse = await fetch(`/users/${user.name}`) // 获取用户数据
  const userData = await userResponse.json() // 解析 JSON
  return userData
}

getFirstUserData()

调试 promise 很难,因为调试器不会跳过异步的代码。

Async/await 使这非常容易,因为对于编译器而言,它就像同步代码一样。

Node.js 事件触发器

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

eventEmitter.on('start', (a, b) => {
  console.log('开始')
})

eventEmitter.emit('start', 1, 2)
  • emit 用于触发事件。
  • on 用于添加回调函数(会在事件被触发时执行)。
  • once(): 添加单次监听器。
  • removeListener() / off(): 从事件中移除事件监听器。
  • removeAllListeners(): 移除事件的所有监听器。

发起 HTTP 请求

执行 GET 请求

const https = require('https')
const options = {
  hostname: 'nodejs.cn',
  port: 443,
  path: '/todos',
  method: 'GET'
}

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.end()

执行 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()

PUT 和 DELETE

PUT 和 DELETE 请求使用相同的 POST 请求格式,只需更改 options.method 的值即可。

HTTP web 服务器

const http = require('http')

const port = 3000

const server = http.createServer((req, res) => {
  res.statusCode = 200
  res.setHeader('Content-Type', 'text/plain')
  res.end('你好世界\n')
})

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

获取请求的正文数据

当使用 http.createServer() 初始化 HTTP 服务器时,服务器会在获得所有 HTTP 请求头(而不是请求正文时)时调用回调。

在连接回调中传入的 request 对象是一个流。

因此,必须监听要处理的主体内容,并且其是按数据块处理的。

首先,通过监听流的 data 事件来获取数据,然后在数据结束时调用一次流的 end 事件:

const server = http.createServer((req, res) => {
  // 可以访问 HTTP 请求头
  req.on('data', chunk => {
    console.log(`可用的数据块: ${chunk}`)
  })
  req.on('end', () => {
    //数据结束
  })
})

因此,若要访问数据(假设期望接收到字符串),则必须将其放入数组中:

const server = http.createServer((req, res) => {
  let data = '';
  req.on('data', chunk => {
    data += chunk;
  })
  req.on('end', () => {
    JSON.parse(data).todo // '做点事情'
  })
})

文件描述符

操作系统会为每个打开的文件分配一个名为文件描述符的数值标识,文件操作使用这些文件描述符来识别与追踪每个特定的文件,Window 系统使用了一个不同但概念类似的机制来追踪资源,为方便用户,NodeJS 抽象了不同操作系统间的差异,为所有打开的文件分配了数值的文件描述符。

在与位于文件系统中的文件进行交互之前,需要先获取文件的描述符。

文件描述符是使用 fs 模块提供的 open() 方法打开文件后返回的:

const fs = require('fs')

fs.open('/Users/joe/test.txt', 'r', (err, fd) => {
  // fd 是文件描述符。
})

注意,将 r 作为 fs.open() 调用的第二个参数。

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

也可以使用 fs.openSync 方法打开文件,该方法会返回文件描述符(而不是在回调中提供):

const fs = require('fs')

try {
  const fd = fs.openSync('/Users/joe/test.txt', 'r')
} catch (err) {
  console.error(err)
}

一旦获得文件描述符,就可以以任何方式执行所有需要它的操作,例如调用 fs.open() 以及许多与文件系统交互的其他操作。

使用 fs 模块提供的 stat() 方法获得文件的详细信息。

读取文件

fs.readFile() 和 fs.readFileSync() 都会在返回数据之前将文件的全部内容读取到内存中。

这意味着大文件会对内存的消耗和程序执行的速度产生重大的影响。

在这种情况下,更好的选择是使用流来读取文件的内容。

写入文件

默认情况下,fs.writeFile()fs.writeFileSync() 会替换文件的内容(如果文件已经存在)。

可以通过指定标志来修改默认的行为:

fs.writeFile('/Users/joe/test.txt', content, { flag: 'a+' }, err => {})

可能会使用的标志有:

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

将内容追加到文件末尾的便捷方法是 fs.appendFile()(及其对应的 fs.appendFileSync())。

所有这些方法都是在将全部内容写入文件之后才会将控制权返回给程序(在异步的版本中,这意味着执行回调)。

在这种情况下,更好的选择是使用流写入文件的内容。

文件夹

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

使用 fs.mkdir() 或 fs.mkdirSync() 可以创建新的文件夹。

使用 fs.readdir() 或 fs.readdirSync() 可以读取目录的内容。

使用 fs.rename() 或 fs.renameSync() 可以重命名文件夹。

使用 fs.rmdir() 或 fs.rmdirSync() 可以删除文件夹。

删除包含内容的文件夹可能会更复杂。

在这种情况下,最好安装 fs-extra 模块,删除文件夹需要的是 remove() 方法。

fs 模块

  • 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()

关于 fs 模块的特殊之处是,所有的方法默认情况下都是异步的,但是通过在前面加上 Sync 也可以同步地工作。

path 模块

该模块提供了 path.sep(作为路径段分隔符,在 Windows 上是 \,在 Linux/macOS 上是 /)和 path.delimiter(作为路径定界符,在 Windows 上是 ;,在 Linux/macOS 上是 :)。

path.parse() 解析对象的路径为组成其的片段:

  • root: 根路径
  • dir: 从根路径开始的文件夹路径
  • base: 文件名 + 扩展名
  • name: 文件名
  • ext: 文件扩展名

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

os 模块

os.arch() 返回标识底层架构的字符串,例如 armx64arm64

os.cpus() 返回关于系统上可用的 CPU 的信息。

os.platform() 返回为 Node.js 编译的平台。

os.type() 标识操作系统:

  • Linux
  • macOS 上为Darwin
  • Windows 上为 Windows_NT

os.userInfo() 返回包含当前 usernameuidgidshell 和 homedir 的对象。

events 模块

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

emitter.addListener()emitter.on() 的别名。

emitter.off()emitter.removeListener() 的别名。

emitter.emit():触发事件。按照事件被注册的顺序同步地调用每个事件监听器。

http 模块

http.globalAgent

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

用于管理 HTTP 客户端连接的持久性和复用,它是 Node.js HTTP 网络的关键组件。

http.Agent

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

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

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

http.request()

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

http.get()

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

http.ClientRequest

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

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

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

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

http.createServer()

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

返回 http.Server 类的新实例。

http.Server

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

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

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

http.ServerResponse

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

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

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

http.IncomingMessage

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

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

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

Buffer

Buffer 是内存区域。它表示在 V8 JavaScript 引擎外部分配的固定大小的内存块(无法调整大小)。可以将 buffer 视为整数数组,每个整数代表一个数据字节。

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

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

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

const buf = Buffer.from('Hey!')

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

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

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

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

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

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

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

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

使用 slice() 方法创建切片。切片不是副本:原始 buffer 仍然是真正的来源。 如果那改变了,则切片也会改变。

Stream

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

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

所有的流都是 EventEmitter 的实例。

流的两个主要优点:

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

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

读取文件,并在与 HTTP 服务器建立新连接时通过 HTTP 提供文件:

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

const server = http.createServer(function(req, res) {
  fs.readFile(__dirname + '/data.txt', (err, data) => {
    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(__dirname + '/data.txt')
  stream.pipe(res)
})
server.listen(3000)

当要发送的数据块已获得时就立即开始将其流式传输到 HTTP 客户端,而不是等待直到文件被完全读取。

pipe()

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

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

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

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

src.pipe(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 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()

关于流的一篇很不错的文章:[译] Node.js 流: 你需要知道的一切

The applications of combining streams are endless.

transform stream(转换流)

const fs = require('fs');
const zlib = require('zlib');
const crypto = require('crypto');
const { Transform } = require('stream');

const file = process.argv[2];

// 进度
const reportProgress = new Transform({
  transform(chunk, encoding, callback) {
    process.stdout.write('.');
    callback(null, chunk);
  }
});

// 压缩、加密
fs.createReadStream(file)
  .pipe(zlib.createGzip())
  .pipe(crypto.createCipher('aes192', 'a_secret'))
  .pipe(reportProgress)
  .pipe(fs.createWriteStream(file + '.zz'))
  .on('finish', () => console.log('Done'));

// 解密、解压
fs.createReadStream(file)
  .pipe(crypto.createDecipher('aes192', 'a_secret'))
  .pipe(zlib.createGunzip())
  .pipe(reportProgress)
  .pipe(fs.createWriteStream(file.slice(0, -3)))
  .on('finish', () => console.log('Done'));

开发环境与生产环境

NODE_ENV=production node app.js

设置环境为 production 通常可以确保:

  • 日志记录保持在最低水平。
  • 更高的缓存级别以优化性能。

错误处理

使用 throw 关键字创建异常:

throw value

通常,在客户端代码中,value 可以是任何 JavaScript 值(包括字符串、数字、或对象)。

在 Node.js 中,我们不抛出字符串,而仅抛出 Error 对象。

错误对象是 Error 对象的实例、或者继承自 Error 类。

throw new Error('错误信息')

或:

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

捕获未捕获的异常

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

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

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

漂亮地打印对象

console.log(JSON.stringify(obj, null, 2)) // 2 缩进的空格数

WebAssembly

WebAssembly 是一种高性能的类汇编语言,可以从包括 C/C++、Rust 和 AssemblyScript 在内的无数语言进行编译。 目前,Chrome、Firefox、Safari、Edge 和 Node.js 都支持它!

WebAssembly 规范详细说明了两种文件格式,一种是二进制格式,被称为 WebAssembly 模块,扩展名为 .wasm;另一种是相应的文本表示格式,被称为 WebAssembly 文本,扩展名为 .wat

理解 WebAssembly 文本格式,本质上,这种文本形式更类似于处理器的汇编指令。

关键概念

  • 模块 - 编译后的 WebAssembly 二进制文件,即 .wasm 文件。
  • 内存 - 可调整大小的 ArrayBuffer。
  • 表格 - 未存储在内存中的引用的可调整大小的类型化数组。
  • 实例 - 模块及其内存、表格、以及变量的实例化。

为了使用 WebAssembly,你需要 .wasm 二进制文件和一组 API 来与 WebAssembly 通信。 Node.js 通过全局的 WebAssembly 对象提供必要的 API。

console.log(WebAssembly);
/*
Object [WebAssembly] {
  compile: [Function: compile],
  validate: [Function: validate],
  instantiate: [Function: instantiate]
}
*/

生成 WebAssembly 模块

有多种方法可用于生成 WebAssembly 二进制文件,包括:

  • 手工编写 WebAssembly(.wat)并使用 wabt 等工具转换为二进制格式
  • 在 C/C++ 应用程序中使用 emscripten
  • 在 Rust 应用程序中使用 wasm-pack
  • 如果你喜欢类似 TypeScript 的体验,则使用 AssemblyScript

其中一些工具不仅会生成二进制文件,还会生成 JavaScript 代码和相应的 HTML 文件以在浏览器中运行。

如何使用

一旦你有了 WebAssembly 模块,则你就可以使用 Node.js WebAssembly 对象来实例化它。

// 假设 add.wasm 文件存在,其中包含添加了 2 个提供的参数的函数
const fs = require('fs');
const wasmBuffer = fs.readFileSync('/path/to/add.wasm');
WebAssembly.instantiate(wasmBuffer).then(wasmModule => {
  // 导出的函数位于 instance.exports 下
  const add = wasmModule.instance.exports.add;
  const sum = add(5, 6);
  console.log(sum); // 输出:11
});

与操作系统交互

WebAssembly 模块本身不能直接访问操作系统功能。 可以使用第三方工具 Wasmtime 来访问此功能。 Wasmtime 利用 WASI API 来访问操作系统功能。