Node.js 完全手册(下)

250 阅读52分钟

The Event Loop(事件循环)

事件循环是了解 JavaScript 的最重要的方面之一。本节解释了 JavaScript 如何在单线程中工作的内部细节,以及它如何处理异步函数。

我已经用 JavaScript 编程多年了,但我从来没有 完全 理解过事情是如何在幕后运作的。不了解这个概念的细节是完全可以的。但是像往常一样,知道它是如何工作的是很有帮助的,而且在这一点上你可能只是有点好奇。

你的 JavaScript 代码是单线程(single threaded)运行的。每次只有一件事在发生。

这是一个实际上非常有帮助的限制,因为它简化了很多你编程的方式,而不用担心并发问题。

你只需要注意如何写你的代码,避免任何可能阻塞线程的东西,比如同步网络调用或无限 循环

一般来说,在大多数浏览器中,每个浏览器标签都有一个事件循环,以使每个进程都被隔离,避免一个有无限循环的网页或繁重的处理过程阻塞你整个浏览器。

该环境管理着多个并发的事件循环,以处理例如 API 调用。Web Workers 也是在自己的事件循环中运行。

你主要需要关注的是,你的代码会在一个事件循环中运行,写代码时要考虑到这个事情,避免阻塞它。

阻塞事件循环

任何 JavaScript 代码,如果需要花费太长的时间将控制权返回到事件循环中,就会阻断页面中任何 JavaScript 代码的执行,甚至阻断 UI 线程,用户就无法点击、滚动页面,等等。

JavaScript 中几乎所有的 I/O 原生语句都是无阻塞的。如网络请求、Node.js 文件系统操作,等等。阻塞是个例外,这就是为什么 JavaScript 如此基于回调,以及最近的 promises 和 async/await 的原因。

The call stack(调用栈)

调用栈是一个 LIFO 队列(Last In, First Out)。

事件循环不断检查调用栈(call stack),看是否有任何函数需要运行。

在这样做的同时,它将发现的任何函数调用添加到调用栈中,并按顺序执行每个函数。

你知道你可能熟悉的错误堆栈跟踪,在调试器或浏览器控制台中?

浏览器查找调用堆栈中的函数名称,以告知你当前调用是由哪个函数发起的:

一个简单的事件循环解释

举个例子:

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

foo()

这段代码打印出:

foo
bar
baz

正如预期的那样。

当这段代码运行时,首先调用 foo()。在 foo() 中,我们首先调用 bar(),然后我们调用baz()

在这一点上,调用栈看起来像这样:

事件循环在每个迭代中都会查看调用栈中是否有东西,并执行它:

直到调用栈为空。

排列函数执行

上面的例子看起来很正常,没有什么特别的地方。JavaScript 找到要执行的东西,按顺序运行它们。

我们来看看如何推迟一个函数,直到堆栈清空。

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

当这段代码运行时,首先 foo() 被调用。在 foo() 中,我们首先调用 setTimeout,传递 bar 作为参数,我们指示它立即尽可能快地运行,传递 0 作为计时器。然后我们调用 baz()

在这一点上,调用栈看起来像这样:

以下是我们程序中所有函数的执行顺序:

为什么会发生这种情况?

消息队列

setTimeout() 被调用时,浏览器或 Node.js 开始计时。一旦定时器过期,在这种情况下,由于我们把 0 作为超时,回调函数就会被放到消息队列中。

消息队列也是用户启动的事件(如点击和键盘事件或获取响应)在您的代码有机会对其做出反应之前排队的地方。 或者像 onLoad 这样的 DOM 事件。

循环优先考虑调用栈。 它首先处理它在调用栈中找到的所有内容,一旦那里没有任何内容,它就会去获取消息队列中的内容。

我们不必等待像 setTimeout、fetch 或其他东西的函数来做它们自己的工作,因为它们是由浏览器提供的,而且它们生活在自己的线程上。例如,如果你把 setTimeout 的超时时间设为 2 秒,你就不必等待 2 秒,等待发生在其他地方。

ES6 Job Queue(ES6任务队列)

ECMAScript 2015 引入了 Job Queue 的概念,Promises(也在 ES6/ES2015 中引入)也使用了这个概念。这是一种尽快执行异步函数结果的方法,而不是放在调用栈的最后。

在当前函数结束之前解析的 Promise 将在当前函数之后立即执行。

我觉得用游乐园的过山车来比喻很好:消息队列让你回到队列中,排在所有其他人之后,而工作队列是一张快速通行票,让你在完成前一项工作后马上再坐一次。

Example:

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

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

const foo = () => {  
  console.log('foo')  
  setTimeout(bar, 0)  
  new Promise((resolve, reject) => resolve('should be right after baz, before bar'))
                                  .then(resolve => console.log(resolve))
                                  
  bar()
}

foo()

这打印出:

foo
baz
should be right after foo, before barbar

这是 Promises(以及建立在 Promises 之上的 async/await)和通过 setTimeout() 或其他平台 API 的普通异步函数之间的巨大区别。

理解 process.nextTick()

当你试图理解 Node.js 的事件循环时,它的一个重要部分是 process.nextTick()。它以一种特殊的方式与事件循环互动。

每当事件循环进行一次完整的旅行,我们称它为 tick。

当我们向 process.nextTick() 传递一个函数时,我们指示引擎在当前操作结束时,在下一个事件循环 tick 开始前调用这个函数:

process.nextTick(() => {
  //do something
})

事件循环正忙于处理当前的函数代码。

当这个操作结束时,JavaScript引擎会运行该操作期间传递给 nextTick 调用的所有函数。

这是我们可以告诉 JavaScript 引擎异步处理一个函数的方法(在当前函数之后),但要尽快,不要排队。

调用 setTimeout(() => {}, 0) 将在下一个 tick 中执行该函数,比使用 nextTick() 时晚得多。

当你想确保在下一个事件循环迭代中,代码已经被执行时,使用 nextTick()

了解 setImmediate()

当你想异步执行一些代码,但又想尽快执行时,一个选择是使用 Node.js 提供的 setImmediate() 函数:

setImmediate(() => {
  //run something
})

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

setImmediate()setTimeout(() => {}, 0)(传递 0ms 的超时)和 from process.nextTick() 有何不同?

传递给 process.nextTick() 的函数将在事件循环的当前迭代中执行,在当前操作结束后。这意味着它将总是在 setTimeout()setImmediate() 之前执行。

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

定时器

在编写 JavaScript 代码时,你可能想延迟一个函数的执行。学习如何使用 setTimeout()setInterval() 来安排未来的函数。

setTimeout()

在编写 JavaScript 代码时,你可能想延迟一个函数的执行。这就是 setTimeout 的工作。

你可以指定一个稍后执行的回调函数,以及一个表达你希望它多长时间运行的值,单位是毫秒:

setTimeout(() => {
  //do something
}, 2000)// runs after 2 seconds
setTimeout(() => {
  //do something
}, 50)// runs after 50 milliseconds

这种语法定义了一个新的函数。你可以在这里调用任何你想调用的其他函数,或者你可以传递一个现有的函数名和一组参数:

const myFunction = (firstParam, secondParam) => {
  // do something
}
setTimeout(myFunction, 2000, firstParam, secondParam)// runs after 2 seconds

setTimeout() 返回定时器 ID。这通常是不使用的,但你可以存储这个 ID,如果你想删除这个预定函数的执行,可以清除它:

const id = setTimeout(() => {
  // should run after 2 seconds
}, 2000) 
clearTimeout(id)   // 取消定时器

Zero delay(零延迟)

如果你指定超时延迟为 0,回调函数将尽快执行,但在当前函数执行之后:

setTimeout(() => {
  console.log('after ')
}, 0)

console.log(' before ')

将打印出 before after.

这对于避免在密集型任务上阻塞 CPU,并在执行繁重的计算时让其他函数被执行,通过在调度器中排队的函数特别有用。

一些浏览器(IE 和 Edge)实现了一个 setImmediate() 方法,可以实现这个完全相同的功能,但它不是标准的,在其他浏览器上不可用。但它是 Node.js 的一个标准函数。

setInterval()

setInterval() 是一个类似于 setTimeout() 的函数,但有一点不同。它不是运行一次回调函数,而是在你指定的特定时间间隔(以毫秒为单位)永远运行它:

setInterval(() => {
    // runs every 2 seconds
}, 2000) 

上面的函数每 2 秒运行一次,除非你用 clearInterval 告诉它停止,并把 setInterval 返回的定时器 id 传给它:

const id = setInterval(() => {
  // runs every 2 seconds
}, 2000)

通常在 setInterval 回调函数中调用 clearInterval,让它自动判断是否应该再次运行或停止。例如,这段代码在 App.somethingIWait 的值为 arrived 时才运行。

const interval = setInterval(function() {  
  if (App.somethingIWait === 'arrived') {    
    clearInterval(interval)
  }}, 100)// otherwise do things  

递归设置超时

setInterval 每隔 n 毫秒启动一个函数,完全不考虑函数何时执行完毕。

如果一个函数花费的时间总是一样的,那就没问题了:

也许该函数需要不同的执行时间,取决于网络条件,例如:

也许一个长期执行的项目会与下一个项目重叠:

为了避免这种情况,你可以安排一个递归的 setTimeout,当回调函数完成时被调用:

const myFunction = () => {  // do something
   setTimeout(myFunction, 1000)
} 
   
setTimeout(myFunction()}, 1000)

实现这一设想:

setTimeoutsetInterval 在 Node.js 中也可以使用,通过 Timer 模块

Node.js 还提供了 setImmediate(),相当于使用 setTimeout(() => {}, 0),主要用于与 Node.js 事件循环一起工作。

异步编程(Asynchronous Programming)和回调(Callbacks)

JavaScript 默认是同步的,而且是单线程的。这意味着,代码不能创建新的线程,也不能并行运行。

编程语言中的异步性

计算机在设计上是异步的。

异步的意思是,事情可以独立于主程序流程而发生。

在目前的消费类计算机中,每个程序都在一个特定的时间段内运行,然后它停止执行,让另一个程序继续执行。这件事的运行周期非常快,不可能注意到,我们认为我们的计算机同时运行许多程序,但这是一种错觉(除了在多处理器机器上)。

程序在内部使用中断,这是一种向处理器发出的信号,以获得系统的处理。

我不会去研究这个的内部情况,但只要记住,程序是异步的,在需要注意之前停止执行是正常的,计算机可以在这期间执行其他事情。当一个程序在等待来自网络的响应时,它不能停止处理器,直到请求完成。

通常情况下,编程语言是同步的,有些语言提供了一种管理异步性的方法,在语言中或通过库。C、Java、C#、PHP、Go、Ruby、Swift、Python,它们默认都是同步的。其中一些语言通过使用线程来处理异步性,生成一个新的进程。

JavaScript

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

代码行一个接一个地依次执行。

例如:

const a = 1
const b = 2
const c = a * b
console.log(c)
doSomething()

但是 JavaScript 是在浏览器内部诞生的。 一开始,它的主要工作是响应用户操作,如 onClickonMouseOveronChangeonSubmit 等。 它如何使用同步编程模型做到这一点?

答案就在它的环境中。 浏览器通过提供一组可以处理此类功能的 API 来提供一种方法。

最近,Node.js 引入了非阻塞 I/O 环境,将这一概念扩展到文件访问、网络调用等。

Callbacks(回调)

你不可能知道用户何时会点击一个按钮,所以你要做的是为点击事件定义一个事件处理程序

这个事件处理程序接受一个函数,当事件被触发时,该函数将被调用:

document.getElementById('button')
        .addEventListener('click', () => {
            console.log('button clicked')
        })//item clicked

这就是所谓的回调

回调是一个简单的函数,它作为一个值传递给另一个函数,只有当事件发生时才会被执行。我们可以这样做,因为 JavaScript 有的函数是一等公民,它可以被分配给变量并传递给其他函数(称为高阶函数)。

常见的做法是将所有的客户端代码包裹在 "window "对象的 "load "事件监听器中,只有当页面准备好时才运行回调函数:

window.addEventListener('load', () => {})//window loaded  //do what you want

回调无处不在,不仅仅是在 DOM 事件中使用。

一个常见的例子是通过使用定时器:

setTimeout(() => {  
  // runs after 2 seconds
}, 2000)

XHR 请求 也接受回调,在这个例子中,通过给一个属性分配一个函数,当一个特定的事件发生时(在这个例子中,请求的状态改变),该函数将被调用:

const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {  
   if (xhr.readyState === 4) {
      xhr.status === 200 ? console.log(xhr.responseText) : console.error('error')
}}

xhr.open('GET', 'https://yoursite.com')
xhr.send()

处理回调中的错误

你如何用回调来处理错误?一个很常见的策略是使用 Node.js 采用的方法:任何回调函数的第一个参数是错误对象,错误优先的回调。

如果没有错误,该对象是 null。如果有错误,它包含一些错误的描述和其他信息。

fs.readFile('/file.json', (err, data) => {  
      if (err !== null) {    //handle error    
         console.log(err)    
         return 
      }
      console.log(data) //no errors, process data 
})

使用回调会遇到的问题

回调非常适合简单的情况!

但是,每个回调都会增加一层嵌套。 当你有很多回调时,代码很快就会变得复杂:

window.addEventListener('load', () => {  
  document.getElementById('button')
          .addEventListener('click', () => {
            setTimeout(() => {
              items.forEach(item => {}) 
            }, 2000)
          })     //your code here
})

这只是一个简单的 4 级代码,但我见过更多级别的嵌套,这很不好玩。

我们如何解决这个问题呢?

回调的替代方案

从 ES6 开始,JavaScript 引入了几个功能,帮助我们处理不涉及使用回调的异步代码:

  • Promises (ES6)
  • Async/Await (ES8)

Promises

Promises 是处理 JavaScript 中异步代码的一种方式,而不需要在代码中写太多的回调。

Promises 简介

承诺通常被定义为一个最终会变得可用的值的代理

虽然已经存在多年,但它们在 ES2015 中被标准化并引入,现在它们在 ES2017 中被 async 函数所取代。

**异步函数(Async functions)**使用 Promise API 作为其构建模块,因此,即使在较新的代码中,你可能会使用异步函数而不是 Promise,理解它们也是基本的。

Promises 是如何工作的

一旦一个 Promises 被调用,它将开始处于待定状态。这意味着调用者函数继续执行,同时等待 Promises 做它自己的处理,并给调用者函数一些反馈。

在这一点上,调用者函数等待它在**解决的状态(resolved state)下返回promises,或者在拒绝的状态(rejected state)**下返回 Promises,但是你知道 JavaScript 是异步的,所以函数继续执行,而承诺在做它的工作。

哪些 JS API 使用 Promises

除了你自己的代码和库的代码外,Promises 还被标准的现代网络 API 所使用,如:

在现代 JavaScript 中,你发现自己不可能不使用承诺,所以让我们直接开始研究它们。

创建 Promise

Promise API 暴露了一个 Promise 构造函数,你可以使用 new Promise() 来初始化它:

let done = true
const isItDoneYet = new Promise(  (resolve, reject) => {
       if (done) { 
         const workDone = 'Here is the thing I built'
         resolve(workDone)
       } else {
         const why = 'Still working on something else'      
         reject(why)    
       }  
})

正如你所看到的,承诺会检查 done 全局常量,如果是真的,我们会返回一个已解决(resolved)的 Promise,否则就是拒绝(rejected )的 resolved 。

使用 resolvereject 我们可以返回一个值,在上面的例子中我们只是返回一个字符串,但也可以是一个对象。

消费(Consuming)Promise

在上一节中,我们介绍了如何创建一个 Promise。

现在让我们来看看 promise 如何被消费或使用。

const isItDoneYet = new Promise()//...

const checkIfItsDone = () => { 
  isItDoneYet.then((ok) => {console.log(ok)})
             .catch((err) => {console.error(err)})
}

运行 checkIfItsDone() 将执行 isItDoneYet() 承诺,并等待它的解析,使用 then 回调,如果有错误,它将在 catch 回调中处理。

链式 Promises

一个 Promise 可以返回给另一个 Promise,形成一个 Promise 链。

Fetch API 给出了一个很好的 Promise 链的例子,它是 XMLHttpRequest API 上面的一层,我们可以用它来获取一个资源,并在获取资源的时候排队执行一连串的 Promise。

Fetch API 是一个基于 promise 的机制,调用 fetch() 等同于使用 new Promise() 定义我们自己的 Promise。

Promises 链的例子

const status = (response) => {  
   if (response.status >= 200 && response.status < 300) 
      { return Promise.resolve(response)  }  
   return Promise.reject(new Error(response.statusText))}
const json = (response) => response.json()
fetch('/todos.json')
.then(status)
.then(json)
.then((data) => {console.log('Request succeeded with JSON response', data)})
.catch((error) => { console.log('Request failed', error) })

在这个例子中,我们调用 fetch() 从当前目录中找到的 todos.json 文件中获得一个 TODO 项目的列表,并且我们创建了一个 Promises 链。

运行 fetch() 返回一个 response,它有许多属性,在这些属性中我们引用了:

  • status, 一个代表 HTTP 状态代码的数字值
  • statusText, 一个状态信息,如果请求成功,就是 OK

response 也有一个 json() 方法,它返回一个 Promise,该 Promise 将对 body 的内容进行处理并转化为 JSON。

所以在这些前提下,会发生这样的事情:链中的第一个 Promise 是我们定义的一个函数,叫做status(),它检查响应状态,如果不是一个成功的响应(在 200 和 299 之间),它拒绝这个 Promise(rejects the promise)。

这个操作将导致承诺链跳过所有列出的链式 Promise,直接跳到底部的 catch() 语句,记录 Request failed 文本和错误信息。

如果成功了,它会调用我们定义的 json() 函数。由于前一个 Promise 在成功时返回了 response 对象,我们得到它作为第二个 Promise 的输入。

在这种情况下,我们返回经过处理的 JSON 数据,所以第三个 Promise 直接接收 JSON:

.then((data) => { 
  console.log('Request succeeded with JSON response', data)
})

我们只需将其记录到控制台。

处理错误

在上一节的例子中,我们有一个附加到 Promise 链上的 catch

当 Promise 链中的任何东西失败并引发错误或拒绝 Promise 时,控制就会转到链下最近的 catch() 语句。

new Promise((resolve, reject) => {
        throw new Error('Error')}) 
       .catch((err) => { console.error(err) })

或者这样写

new Promise((resolve, reject) => { 
    reject('Error')})
    .catch((err) => { console.error(err) })

Cascading(层叠)errors

如果在 catch() 里面你引发了一个错误,你可以附加第二个 catch() 来处理它,以此类推。

new Promise((resolve, reject) => {throw new Error('Error')})
      .catch((err) => { throw new Error('Error') })  
      .catch((err) => { console.error(err) })

Orchestrating(协调) Promises

Promise.all()

如果你需要同步不同的 Promise,Promise.all() 可以帮助你定义一个 Promise 列表,并在它们都被解决(resolved)后执行一些操作。

例如:

const f1 = fetch('/something.json')
const f2 = fetch('/something2.json')

Promise.all([f1, f2])
       .then((res) => {console.log('Array of results', res)})
       .catch((err) => {console.error(err)})

ES2015 的析构赋值 语法允许你也这样做:

Promise.all([f1, f2])
       .then(([res1, res2]) => {
          console.log('Results', res1, res2)
       })

当然,你并不局限于使用 fetch任何 Promise 都可以使用

Promise.race()

Promise.race() 在你传递给它的第一个 Promise 得到解决时运行,它只运行一次所附的回调,并使用第一个 Promise 的解决结果。

例如:

const first = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'first')
})
 
const second = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'second')
})

Promise.race([first, second]).then((result) => {  
  console.log(result) 
})// second

Common error, Uncaught TypeError: undefined is not a promise

如果你在控制台得到 Uncaught TypeError: undefined is not a promise,请确保你使用 new Promise(),而不是仅仅使用 Promise()

Async and Await

探索 JavaScript 中异步函数的现代方法。

JavaScript 在很短的时间内从回调(callback)发展到了 Promise(ES2015),而从 ES2017 开始,异步 JavaScript 通过 async/await 语法变得更加简单。

异步函数是 Promises 和生成器(generators)的结合,基本上,它们是 Promises 的更高层次的抽象。让我重复一遍:async/await 是建立在 Promises 之上的。

为什么会引入 async/await

它们减少了围绕 Promises 的模板, 以及链式 Promises 的 don’t break the chain 的限制。

当 Promises 在 ES2015 中被引入时,它们旨在解决异步代码的问题,而且它们确实做到了,但在 ES2015 和 ES2017 之间的两年时间里,显然 Promises 不可能成为最终的解决方案。

Promises 的引入是为了解决著名的回调地狱(callback hell)问题,但它们本身就引入了复杂性,而且是语法上的复杂性。

它们是很好的基元(primitives),围绕它们可以向开发者展示更好的语法,所以当时机成熟的时候,我们得到了同步函数(async functions)

它们使代码看起来是同步的,但在幕后却是异步的和非阻塞的。

它是如何工作的

一个 "async" 函数会返回一个 Promise,比如在这个例子中:

const doSomethingAsync = () => {
  return new Promise((resolve) => {
    setTimeout(() => resolve('I did something'), 3000) 
  })
}

当你想调用这个函数时,你要在前面加上 await,调用代码将停止直到承诺被解决或拒绝(promise is resolved or rejected)。有一点需要注意:客户端函数必须定义为 async

这是一个例子:

const doSomething = async () => {    
  console.log(await doSomethingAsync())
}

一个简短的例子

这是一个简单的 async/await 的例子,用于异步运行一个函数:

const doSomethingAsync = () => { 
  return new Promise((resolve) => { 
    setTimeout(() => resolve('I did something'), 3000) 
  })
}

const doSomething = async () => { 
  console.log(await doSomethingAsync())
}

console.log('Before')
doSomething()
console.log('After')

上述代码将向浏览器控制台打印以下内容:

Before
After
I did something //after 3s

Promise all the things

在任何函数前加上 async 关键字意味着该函数将返回一个 promise。

即使它没有明确地这样做,也会在内部使其返回一个promise。

这就是为什么这段代码是有效的:

const aFunction = async () => { return 'test'}

aFunction().then(alert) // This will alert 'test'

跟下面的是相同的:

const aFunction = async () => { 
  return Promise.resolve('test')
}

aFunction().then(alert) // This will alert 'test'

代码阅读起来更简单

正如你在上面的例子中看到的,我们的代码看起来非常简单。与使用普通承诺的代码相比,它有链式和回调函数。

而这只是一个非常简单的例子,主要的好处将出现在代码更复杂的时候。

例如,这里是你如何获得一个 JSON 资源并解析它,使用 Promises:

const getFirstUserData = () => {  
       return fetch('/users.json') // get users list    
      .then(response => response.json()) // parse JSON    
      .then(users => users[0]) // pick first user    
      .then(user => fetch(`/users/${user.name}`)) // get user data    
      .then(userResponse => response.json()) // parse JSON
}

getFirstUserData()

这里是用以下方式提供的相同功能,通过使用 await/async:

const getFirstUserData = async () => { 
        const response = await fetch('/users.json')  // get users list  
        const users = await response.json() // parse JSON  const user = users[0] // pick first user  
        const userResponse = await fetch(`/users/${user.name}`) // get user data 
        const userData = await user.json() // parse JSON  return userData
}

getFirstUserData()

串联多个的异步函数

async 函数可以很容易地串联起来,而且语法比普通的 Promises 更易读:

const promiseToDoSomething = () => { return 
  new Promise(resolve => { 
    setTimeout(() => resolve('I did something'), 10000) 
  }
)}

const watchOverSomeoneDoingSomething = async () => { 
  const something = await promiseToDoSomething()    
  return something + ' and I watched'
}

const watchOverSomeoneWatchingSomeoneDoingSomething = async () => { 
  const something = await watchOverSomeoneDoingSomething()  
  return something + ' and I watched as well'
}

watchOverSomeoneWatchingSomeoneDoingSomething().then((res) => { 
     console.log(res)
})

将会打印:

I did something and I watched and I watched as well

更容易调试

调试 Promises 是很难的,因为调试器不会在异步代码上断点。

async/await 使之变得非常容易,因为对编译器来说,它就像同步代码。

Node.js 事件发送器(Event Emitter)

你可以在 Node.js 中使用自定义事件。

如果你在浏览器中使用 JavaScript,你就知道用户的大部分交互是通过事件处理的:鼠标点击、键盘按键、对鼠标移动的反应等等。

在后端,Node.js 为我们提供了使用 events 模块 建立类似系统的选择。

这个模块特别提供了 EventEmitter 类,我们将用它来处理我们的事件。

你可以用以下方式初始化它:

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

这个对象暴露了 onemit 方法,以及其他许多方法。

  • emit 用于触发一个事件。
  • on 用于添加一个回调函数,当事件被触发时将被执行。

例如,让我们创建一个start事件,作为提供一个样本的问题,我们对该事件的反应只是记录到控制台:

eventEmitter.on('start', () => {  
  console.log('started')
})

当我们运行:

eventEmitter.emit('start')

事件处理函数被触发,我们得到控制台日志。

你可以通过将参数作为附加参数传递给 emit() 来给事件处理程序传递参数:

eventEmitter.on('start', (number) => {  
  console.log(`started ${number}`)
})
eventEmitter.emit('start', 23)

多个参数:

eventEmitter.on('start', (start, end) => {  
  console.log(`started from ${start} to ${end}`)
})
eventEmitter.emit('start', 1, 100)

EventEmitter 对象还公开了其他几个与事件互动的方法,比如:

  • once(): 添加一个一次性的监听器
  • removeListener() / off(): 从一个事件中删除一个事件监听器
  • removeAllListeners(): 删除一个事件的所有监听器。

HTTP请求如何工作

当你在浏览器中输入一个 URL 时,从开始到结束会发生什么?

本节介绍浏览器如何使用 HTTP/1.1 协议执行页面请求。

如果你曾经做过一次面试,你可能会被问到。"当你在谷歌搜索框中输入东西并按下回车键时会发生什么?"。

这是你被问到的最多的问题之一。人们只是想看看你是否能解释一些相当基本的概念,以及你是否了解互联网的实际运作。

在本节中,我将分析当你在浏览器的地址栏中输入一个 URL 并按下回车键时会发生什么。

在本节中,我将分析当你在浏览器的地址栏中输入一个 URL 并按下回车键时会发生什么。

这是很少变化的技术,它为人类有史以来最复杂、最广泛的生态系统之一提供动力。

HTTP 协议

我只分析 URL 请求。

现代浏览器有能力知道你在地址栏中写的东西是一个实际的 URL 还是一个搜索词,如果它不是一个有效的 URL,它们将使用默认的搜索引擎。

我假设你输入了一个实际的 URL。

当你输入 URL 并按下回车键时,浏览器首先建立完整的 URL。

与 MacOS/Linux 有关的事情

仅供参考。Windows 可能会对一些事情的处理方式略有不同。

DNS 查询阶段

浏览器开始进行 DNS 查询以获得服务器的 IP 地址。

域名对我们人类来说是方便记忆,但互联网的组织方式是,计算机可以通过其 IP 地址查询服务器的确切位置,这是一组数字,如 222.324.3.1(IPv4)。

首先,它检查 DNS 的本地缓存,看这个域名最近是否已经被解析。

Chrome 有一个方便的 DNS 缓存可视化工具,你可以在这个网址上看到:chrome://net-internals/#dns(复制并粘贴到 Chrome 浏览器地址栏)

如果没有找到,浏览器就使用 DNS 解析器,使用 gethostbyname POSIX 系统调用来检索主机信息。

gethostbyname

gethostbyname:浏览器首先查找本地主机文件,在 macOS 或 Linux 上,该文件位于 /etc/hosts,以查看系统是否在本地提供了该信息。

如果这没有提供任何关于域名的信息,系统会向 DNS 服务器发出请求。

DNS 服务器的地址存储在系统偏好中。

这些是 2 个流行的 DNS 服务器:

  • 8.8.8.8: 谷歌公共 DNS 服务器
  • 1.1.1.1: CloudFlare DNS 服务器

大多数人使用他们的互联网供应商提供的 DNS 服务器。

浏览器使用 UDP 协议执行 DNS 请求。

TCP 和 UDP 是计算机网络的两个基础性协议。它们处于相同的概念层面,但 TCP 是面向连接的,而 UDP 是一个无连接的协议,更轻巧,用于发送消息,开销很小。

如何进行 UDP 请求不在本手册的范围内。

DNS 服务器的缓存中可能有该域名的 IP。如果没有,它将询问根域名服务器。那是一个驱动整个互联网的系统(由 13 个实际的服务器组成,分布在地球上)。

DNS 服务器并知道地球上每一个域名的地址。

它所知道的是顶级 DNS 解析器的位置。

顶级域名是域名的扩展名:.com.it.pizza 等等。

一旦根 DNS 服务器收到请求,它就将请求转发到该顶级域名(TLD)DNS 服务器。

假设你正在寻找 flaviocopes.com。根域名的 DNS 服务器返回 .com TLD 服务器的 IP。

现在,我们的 DNS 解析器将缓存该 TLD 服务器的 IP,所以它不必再向根 DNS 服务器询问它。

TLD DNS 服务器将拥有我们正在寻找的域名的权威性域名服务器的 IP 地址。

怎么会这样?当你购买一个域名时,域名注册商会向域名服务器发送适当的 TDL。当你更新名称服务器时(例如,当你改变主机提供商时),这些信息将由你的域名注册商自动更新。

这些是主机提供商的 DNS 服务器。它们通常不止一个,以作为备份。

例如:

  • ns1.dreamhost.com
  • ns2.dreamhost.com
  • ns3.dreamhost.com

DNS 解析器从第一个开始,试图询问你要找的域名(也包括子域名)的 IP。

这就是 IP 地址的最终真实来源。

现在我们有了 IP 地址,我们可以继续我们的旅程了。

TCP 请求握手

有了服务器的 IP 地址,现在浏览器可以启动一个 TCP 连接。

TCP 连接在完全初始化之前需要进行一些握手,然后就可以开始发送数据。

一旦连接建立,我们可以发送请求

发送请求

请求是一个纯文本文件,以通信协议确定的精确方式结构化。

它由 3 个部分组成:

  • 请求行
  • 请求头
  • 请求体

请求行

请求行设置了,在一个单行上:

  • HTTP 方法
  • 资源位置
  • 协议版本

例如:

GET / HTTP/1.1

请求头

请求头是一组 "字段:值 "对,用于设置某些值。

有两个强制性的字段,一个是 host,另一个是 Connection,而所有其他字段是可选:

Host: flaviocopes.comConnection: close

Host 表示我们想要的目标域名,而 Connection 总是被设置为 close,除非连接必须保持开放。

一些最常用的请求头(header)字段是:

  • Origin
  • Accept
  • Accept-Encoding
  • Cookie
  • Cache-Control
  • Dnt

但还有更多。

请求头(head)部分由一个空行结束。

请求体

请求体是可选的,在 GET 请求中不使用,但在 POST 请求中非常使用,有时也用于其他动词,它可以包含 JSON 格式的数据。

由于我们现在分析的是一个 GET 请求,所以请求体是空白的,我们不做更多研究。

The response(响应)

一旦发送请求,服务器就会对其进行处理并发回一个响应。

响应以状态代码和状态信息开始。如果请求成功并返回 200:

200 OK

该请求可能会返回一个不同的状态代码和信息,比如这些信息之一:

404 Not Found
403 Forbidden
301 Moved Permanently
500 Internal Server Error
304 Not Modified
401 Unauthorized

然后,响应包含一个 HTTP 头的列表和响应体(因为我们是在浏览器中发出请求,所以它将是 HTML)。

解析(Parse)HTML

浏览器现在已经收到了 HTML,并开始解析它,它将重复我们对页面所需的所有资源所做的完全相同的过程:

  • CSS 文件
  • 图像
  • 图标
  • JavaScript 文件
  • ……

浏览器是如何渲染页面的,这不在我们的讨论范围之内,但重要的是要明白,我所描述的过程不仅仅是针对 HTML 页面,而是针对任何通过 HTTP 提供的项目。

用 Node.js 建立一个 HTTP 服务器

这里是我们在介绍使用 Node.js HTTP 网络服务器实现 Hello World 应用程序:

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\n')
})

server.listen(port, () => {  
  console.log(`Server running at http://${hostname}:${port}/`)
})s

让我们简单地分析一下。我们包括 http 模块

我们使用该模块来创建一个 HTTP 服务器。

该服务器被设置为监听指定的端口,3000。当服务器准备好时,listen 回调函数被调用。

我们传递的回调函数是在每个请求到来时都要执行的函数。每当收到一个新的请求,request event 被调用,提供两个对象:一个请求(一个 http.IncomingMessage 对象)和一个响应(一个 http.ServerResponse 对象)。

request 提供了请求的细节。通过它,我们可以访问请求头和请求数据。

response 用于填充我们要返回给客户端的数据。

response 用于填充我们要返回给客户端的数据:

res.statusCode = 200

我们将 statusCode 属性设置为 200,以表示响应成功。

我们还设置了 Content-Type 头:

res.setHeader('Content-Type', 'text/plain')

然后我们结束关闭响应,将内容作为参数添加到 end():

res.end('Hello World\n')

用 Node.js 做 HTTP 请求

如何用 Node.js 执行 HTTP 请求,使用 GET、POST、PUT 和 DELETE。

我使用 HTTP 一词,但 HTTPS 才是应该到处使用的(译者注:HTTPS 更安全),因此这些例子使用 HTTPS 而不是 HTTP。

Perform a GET Request

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

const req = https.request(options, (res) => {
    console.log(`statusCode: ${res.statusCode}`)
    res.on('data', (d) => { process.stdout.write(d) })
})

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

req.end()

Perform a POST Request

const https = require('https')

const data = JSON.stringify({  todo: 'Buy the milk'})

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

const req = https.request(options, (res) => { 
  console.log(`statusCode: ${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 值。

在 Node.js 中使用 Axios 的 HTTP 请求

Axios 是一个非常流行的 JavaScript 库,你可以用来执行 HTTP 请求,它可以在浏览器和 Node.js 平台上工作。

它支持所有的现代浏览器,包括对 IE8 和更高版本的支持。

它是 promise-based, 这让我们可以非常容易编写异步/等待代码来执行 XHR 请求。

与原生的 Fetch API 相比,使用 Axios 有很多优势:

  • 支持旧的浏览器(Fetch 需要一个 polyfill,即降级方案)
  • 可以中止请求
  • 可以设置响应超时
  • 有内置的 CSRF 保护
  • 支持上传进度
  • 执行自动 JSON 数据转换
  • 可以在 Node.js 中使用

安装

Axios 可以用 npm 安装:

npm install axios

或者 yarn:

yarn add axios

或简单地使用 unpkg.com,在你的页面引用:

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

Axios API

你可以从 axios 对象中开始一个 HTTP 请求:

axios({  
  url: 'https://dog.ceo/api/breeds/list/all',  
  method: 'get',  
  data: {foo:'bar'}
})

但为了方便起见,你一般会使用:

  • axios.get()
  • axios.post()

(就像在 jQuery 中你会使用 $.get()$.post() 而不是 $.ajax())

Axios 为所有的 HTTP 动词提供了方法,这些动词不太流行,但仍在使用:

  • axios.delete()
  • axios.put()
  • axios.patch()
  • axios.options()

和一个方法来获取一个请求的 HTTP 头信息,并丢弃正文(discarding the body):

  • axios.head()

GET 请求

使用 Axios 的一个方便方法是使用现代(ES2017)的 async/await 语法。

这个 Node.js 例子查询了 Dog API,使用 axios.get() 检索了所有狗的品种列表,并对它们进行了统计:

const axios = require('axios')

const getBreeds = async () => { 
  try {  
       return await axios.get('https://dog.ceo/api/breeds/list/all')  
  } 
  catch (error) {    
    console.error(error)  
  }
}

const countBreeds = async () => {  
  const breeds = await getBreeds()
  if (breeds.data.message) {
    console.log(`Got ${Object.entries(breeds.data.message).length} breeds`) 
  }
}

countBreeds()

如果你不想使用 async/await,你可以使用 Promises 语法:

const axios = require('axios')

const getBreeds = () => {  
  try { 
    return axios.get('https://dog.ceo/api/breeds/list/all')
  } 
  catch (error) {
    console.error(error) 
   }
}

const countBreeds = async () => {  
  const breeds = getBreeds().then(response => { 
    if (response.data.message) {       
      console.log(`Got ${Object.entries(response.data.message).length} breeds`)
    }
  }).catch(error => {
    console.log(error)})
}

countBreeds()

在 GET 请求中添加参数

一个 GET 响应可以在 URL 中包含参数,像这样 site.com/?foo=bar

使用 Axios,你可以通过简单地使用该 URL 来执行:

axios.get('https://site.com/?foo=bar')

或者你可以在选项中使用一个 params 属性:

axios.get('https://site.com/', {  params: {    foo: 'bar'  }})

POST 请求

执行 POST 请求就像执行 GET 请求一样,但你使用的不是 axios.get,而是 axios.post:

axios.post('https://site.com/')

一个包含 POST 参数的对象是第二个参数:

axios.post('https://site.com/', {  foo: 'bar'})

在 Node.js 中使用 WebSockets

WebSockets 是网络应用中 HTTP 通信的替代方案。

它们在客户端和服务器之间提供了一个长期的、双向的通信通道。

一旦建立,通道就会保持开放,提供一个非常快速的连接,延迟和开销都很低。

浏览器对 WebSockets 的支持

所有现代浏览器都支持 WebSockets。

WebSockets 与 HTTP 有什么不同

HTTP 是一个非常不同的协议,并且有不同的通信方式。

HTTP 是一个请求/响应协议:服务器在客户端请求时返回一些数据。

WebSockets:

  • 服务器可以向客户端发送一个消息,而不需要客户端明确请求什么
  • 客户端和服务器可以同时彼此对话
  • 发送消息所需的数据开销非常小。这意味着低延迟的通信

WebSockets 非常适用于实时和长期的通信。

HTTP 非常适用于偶尔的数据交换和由客户端发起的互动。

HTTP 的实现要简单得多,而 WebSockets 则需要更多的开销。

安全的 WebSockets

始终使用安全的、加密的 WebSockets 协议,wss://

ws:// 指的是不安全的 WebSockets 版本(WebSockets 的http://),由于明显的原因,应该避免使用。

创建一个新的 WebSockets 连接

const url = 'wss://myserver.com/something'
const connection = new WebSocket(url)

connection 是一个 WebSocket 对象。

当连接被成功建立时,"open" 事件被触发。

通过给 connection 对象的 onopen 属性分配一个回调函数来监听它:

connection.onopen = () => {} //...

如果有任何错误,onerror 函数回调被触发:

connection.onerror = error => { 
  console.log(`WebSocket error: ${error}`)
}

使用 WebSockets 向服务器发送数据

一旦连接被打开,你就可以向服务器发送数据。

你可以在 onopen 回调函数中方便地这样做:

connection.onopen = () => {  connection.send('hey')}

使用 WebSockets 从服务器接收数据

onmessage 上使用回调函数进行监听,当收到 message 事件时被调用:

connection.onmessage = e => {  console.log(e.data)}

在 Node.js 中实现一个 WebSockets 服务器

ws 是一个用于 Node.js 的流行的 WebSockets 库。

我们将用它来建立一个 WebSockets 服务器。它也可以用来实现一个客户端,并使用 WebSockets 在两个后端服务之间通信。

使用以下方法轻松地安装它:

yarn init
yarn add ws

你需要写的代码非常少:

const WebSocket = require('ws')
const wss = new WebSocket.Server({ port: 8080 })
wss.on('connection', ws => {  ws.on('message', message => { 
  console.log(`Received message => ${message}`)  
})  
ws.send('ho!')})

这段代码在 8080 端口(WebSockets 的默认端口)创建了一个新的服务器,并在建立连接时添加了一个回调函数,向客户端发送 ho!,并记录它收到的消息。

请看 Glitch 上的一个运行中的例子

Here 是一个 WebSockets 服务器的活例子。

Here 是一个与服务器互动的 WebSockets 客户端。

在 Node.js 中使用文件描述符

在你能够与文件系统中的文件互动之前,你必须获得一个文件描述符。

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

const fs = require('fs')
fs.open('/Users/flavio/test.txt', 'r', (err, fd) => { })//fd is our file descriptor

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

这个标志意味着我们打开文件进行阅读。

你经常使用的其他标志是

  • r+ 打开文件进行读写
  • w+ 打开文件进行读写,将流定位在文件的开头。如果文件不存在,将被创建。
  • a 打开文件进行写入,将数据流定位在文件的末端。如果不存在,文件将被创建。
  • a+ `打开文件进行读写,将数据流定位在文件的末端。如果不存在,文件被创建。

你也可以通过使用 fs.openSync 方法来打开文件,它不是在回调中提供文件描述符对象,而是返回它:

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

一旦你得到了文件描述符,无论你选择什么方式,你都可以执行所有需要它的操作,比如调用 fs.open() 和许多其他与文件系统互动的操作。

Node.js 文件统计(stats)

每个文件都带有一组细节,我们可以使用 Node.js 检查。

特别是,使用 fs 模块提供的 stat() 方法。

你通过一个文件路径来调用它,一旦 Node.js 得到了文件的细节,它将调用你传递的带有 2 个参数的回调函数:一个错误信息和文件统计:

const fs = require('fs')fs.stat('/Users/flavio/test.txt', (err, stats) => { 
  if (err) { 
    console.error(err) 
    return  
  } 
}) //we have access to the file stats in `stats`

Node.js 还提供了一个同步方法,它可以阻塞线程,直到文件统计准备就绪:

const fs = require('fs')
try { 
  const stats = fs.stat('/Users/flavio/test.txt')
} 
catch (err) { 
   console.error(err)
}

文件信息被包含在 stats 变量中。我们可以用 stats 提取什么样的信息?

很多,包括:

  • 如果文件是一个目录或一个文件,使用 stats.isFile()stats.isDirectory()
  • 如果文件是一个符号链接,使用 stats.isSymbolicLink()
  • 使用 stats.size 来计算文件的字节数

还有其他高级方法,但你在日常编程中会用到的大部分方法是这样的:

const fs = require('fs')fs.stat('/Users/flavio/test.txt', (err, stats) => {
   if (err) {
     console.error(err)   
     return  
   }
   stats.isFile() //true  
   stats.isDirectory() //false  
   stats.isSymbolicLink() //false  
   stats.size //1024000 //= 1MB})
}

Node.js 文件路径

系统中的每个文件都有一个路径。

在 Linux 和 MacOS 上,一个路径可能看起来像:

/users/flavio/file.txt

而 Windows 电脑则不同,它的结构如:

C:\users\flavio\file.txt

在你的应用程序中使用路径时,你需要注意,因为必须考虑到这种差异。

你在你的文件中包括这个模块,使用:

const path = require('path')

你可以开始使用它的方法。

从路径中获取信息

给定一个路径,你可以用这些方法提取其中的信息:

  • dirname: 获取文件的父文件夹
  • basename: 获取文件名部分
  • extname: 获得文件的扩展名

例如:

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

path.dirname(notes) // /users/flavio
path.basename(notes) // notes.txt
path.extname(notes) // .txt

你可以通过给 basename 指定第二个参数来获得不带扩展名的文件名:

path.basename(notes, path.extname(notes)) //notes

使用路径工作

你可以通过使用 path.join() 来连接一个路径的两个或多个部分:

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

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

path.resolve('flavio.txt') //'/Users/flavio/flavio.txt' if run from my home folder

在这种情况下,Node.js 将简单地把 /flavio.txt 追加到当前工作目录。如果你指定了第二个参数文件夹,resolve 将使用第一个作为第二个的基础:

path.resolve('tmp', 'flavio.txt')// '/Users/flavio/tmp/flavio.txt' if run from my home folder

如果第一个参数以斜线开头,这意味着它是一个绝对路径:

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

path.normalize() 是另一个有用的函数,它将尝试计算实际的路径,当它包含像..这样的相对指定符,或双斜线时:

path.normalize('/users/flavio/..//test.txt') // /users/test.txt

但是 resolvenormalize 不会 检查路径是否存在。它们只是根据得到的信息计算出一个路径。

用 Node.js 读取文件

在 Node.js 中读取文件的最简单方法是使用 fs.readFile() 方法,将文件路径和一个回调函数传递给它,该函数将被调用,并带有文件数据(和错误):

const fs = require('fs')
fs.readFile('/Users/flavio/test.txt', (err, data) => {  
  if (err) {    
    console.error(err)    
    return  
  }  
  console.log(data)
})

或者,你可以使用同步版本 fs.readFileSync():

const fs = require('fs')
try {  
  const data = fs.readFileSync('/Users/flavio/test.txt') 
  console.log(data)
} catch (err) {  
  console.error(err)
}

默认编码是 "utf8",但你可以使用第二个参数指定一个自定义编码。

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

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

在这种情况下,一个更好的选择是使用流(streams)读取文件内容。

用 Node.js 写文件

在 Node.js 中写入文件的最简单方法是使用 fs.writeFile() API。

例如:

const fs = require('fs')
const content = 'Some content!'
fs.writeFile('/Users/flavio/test.txt', content, (err) => {  
  if (err) {
    console.error(err)    
    return  
  }  
})//file written successfully

或者,你可以使用同步版本 fs.writeFileSync():

const fs = require('fs')
const content = 'Some content!'
try {  
  const data = fs.writeFileSync('/Users/flavio/test.txt', content) 
  //file written successfully
} 
catch (err) {  
  console.error(err)
}

默认情况下,如果文件已经存在,该 API 将 替换 该文件的内容。

你可以通过指定一个标志(flag)来修改默认值:

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

你可能会用到的标志(flags):

  • r+ 打开文件进行读写。
  • w+ 打开文件进行读写,将流定位在文件的开头。如果文件不存在,将被创建。
  • a 打开文件进行写入,将数据流定位在文件的末端。如果不存在,文件将被创建。
  • a+ 打开文件进行读写,将数据流定位在文件的末端。如果不存在,该文件将被创建

你可以找到更多关于 flags 的信息

追加到一个文件中

一个方便的方法是 fs.appendFile() (和它的 fs.appendFileSync() 对应同步的方法),将内容附加到文件的末尾:

const content = 'Some content!'
fs.appendFile('file.log', content, (err) => {  
  if (err) { 
    console.error(err)    
    return  
  }  
})//done!

使用流(streams)

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

在这种情况下,一个更好的选择是使用流(streams)来写文件内容。

在 Node.js 中使用文件夹

Node.js fs 核心模块提供了许多方便的方法,你可以用来处理文件夹。

检查一个文件夹是否存在

使用 fs.access() 来检查文件夹是否存在,并且 Node.js 可以用其权限来访问它。

创建文件夹

使用 fs.mkdir()fs.mkdirSync() 来创建一个新文件夹:

const fs = require('fs')
const folderName = '/Users/flavio/test'
try {  
   if (!fs.existsSync(dir))
     {fs.mkdirSync(dir)}
} catch (err){ 
   console.error(err)
}

读取一个目录的内容

使用 fs.readdir()fs.readdirSync 来读取一个目录的内容。

这段代码读取一个文件夹的内容,包括文件和子文件夹,并返回其相对路径:

const fs = require('fs')
const path = require('path')
const folderPath = '/Users/flavio'
fs.readdirSync(folderPath)

你可以得到完整的路径:

fs.readdirSync(folderPath)
  .map(fileName => {
     return path.join(folderPath, fileName)
})

你还可以过滤结果,只返回文件,并排除文件夹:

const isFile = fileName => {
  return fs.lstatSync(fileName).isFile()
}
fs.readdirSync(folderPath).map(fileName => { 
   return path.join(folderPath, fileName).filter(isFile)
})

重命名文件夹

使用 fs.rename()fs.renameSync() 来重命名文件夹。

第一个参数是当前路径,第二个参数是新路径:

const fs = require('fs')
fs.rename('/Users/flavio', '/Users/roger', (err) => { 
    if (err) {
      console.error(err)    
      return  
    }  
})//done

fs.renameSync() 是同步版本:

const fs = require('fs')
try { fs.renameSync('/Users/flavio', '/Users/roger')
} catch (err) {  
  console.error(err)
}

删除文件夹

使用 fs.rmdir()fs.rmdirSync() 来删除一个文件夹。

删除一个有内容的文件夹可能比你需要的更复杂。

在这种情况下,我建议安装 fs-extra 模块,它非常受欢迎,维护得很好,它可以直接替换 fs 模块,在其基础上提供更多的功能。

在这种情况下,remove() 方法是你想要的。

用以下方法安装它:

npm install fs-extra

像这样使用它:

const fs = require('fs-extra')
const folder = '/Users/flavio'
fs.remove(folder, err => {console.error(err)})

它也可以与 Promises 一起使用:

fs.remove(folder).then(() => {done}).catch(err => {
  console.error(err)
}) //done 

或者使用 async/await:

async function removeFolder(folder) {  
  try { 
    await fs.remove(folder)
  }//done 
  catch (err) {console.error(err) }
}

const folder = '/Users/flavio'
removeFolder(folder)

Node.js fs 模块

fs 模块提供了很多非常有用的功能来访问文件系统并与之互动。

不需要安装它。作为 Node.js 核心的一部分,它可以通过简单地要求它来使用:

const fs = require('fs')

一旦你这样做,你就可以使用它的所有方法,其中包括:

  • fs.access(): 检查文件是否存在,并且 Node 可以用其权限访问它。
  • 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 而同步工作。

例子:

  • fs.rename()
  • fs.renameSync()
  • fs.write()
  • fs.writeSync()

这对你的应用流程有很大的影响。

Node 10 包括对基于 Promise 的 API 的 实验性支持

例如,让我们检查 fs.rename() 方法。异步 API 是用一个回调来实现的:

const fs = require('fs')
fs.rename('before.json', 'after.json', (err) => {  
     if (err) {    
       return console.error(err)  
    }
})//done

一个同步的 API 可以这样使用,用一个 try/catch 块来处理错误:

const fs = require('fs')
try {
  fs.renameSync('before.json', 'after.json')//done
} catch(err) {  
  console.error(err)
}

这里的关键区别是,在第二个例子中,你的脚本的执行将被阻塞,直到文件操作成功。

Node.js 的路径模块

path 模块提供了很多非常有用的功能,可以访问文件系统并与之互动。

没有必要安装它。作为 Node.js 核心的一部分,它可以通过简单地要求它来使用:

const path = require('path')

这个模块提供了 path.sep,它提供了路径段的分隔符(在 Windows 下为 /,在 Linux/MacOS 下为 /),以及 path.delimiter,它提供了路径分隔符(在 Windows 下为,在 Linux/MacOS 下为)。

这些是 路径 方法。

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').dirname('/test/something') // ''
require('path').dirname('/test/something/file.txt') // '.txt'

path.isAbsolute()

如果它是一个绝对路径,则返回 true:

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

path.join()

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

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

path.normalize()

试图计算实际路径,当它包含相对指定符如 ...,或双斜线(//):

require('path').normalize('/users/flavio/..//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()

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

例如:

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

path.resolve()

你可以使用 path.resolve() 获得从相对路径得到绝对路径:

path.resolve('flavio.txt') //'/Users/flavio/flavio.txt' if run from my home folder

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

path.resolve('tmp', 'flavio.txt')//'/Users/flavio/tmp/flavio.txt' if run from my home folder

如果第一个参数以斜线开头,这意味着它是一个绝对路径:

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

Node.js 的 os 模块

这个模块提供了许多功能,你可以用来从底层的操作系统和程序运行的计算机上检索信息,并与之进行交互。

const os = require('os')

有几个有用的属性告诉我们一些与处理文件有关的关键事情:

os.EOL 给出了行的定界符序列。在 Linux 和 MacOS 上是 \n,而在 Windows 上是 \r\n

当我说 Linux 和 MacOS 时,我指的是 POSIX 平台。为了简单起见,我排除了其他不太流行的操作系统,Node 可以在上面运行。

os.constants.signals 告诉我们所有与处理进程信号有关的常量,如 SIGHUP, SIGKILL 等。

os.constants.errno 设置错误报告的常量,如 EADDRINUSE、EOVERFLOW 等。

你可以全部阅读 这里

现在让我们看看 os 提供的主要方法:

  • os.arch()
  • os.cpus()
  • os.endianness()
  • os.freemem()
  • os.homedir()
  • os.hostname()
  • os.loadavg()
  • os.networkInterfaces()
  • os.platform()
  • os.release()
  • os.tmpdir()
  • os.totalmem()
  • os.type()
  • os.uptime()
  • os.userInfo()

os.arch()

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

os.cpus()

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

例如:

[{
    model: 'Intel(R) Core(TM)2 Duo CPU     P8600  @ 2.40GHz',
    speed: 2400,
    times: {
        user: 281685380,
        nice: 0,
        sys: 187986530,
        idle: 685833750,
        irq: 0
    }
}, {
    model: 'Intel(R) Core(TM)2 Duo CPU     P8600  @ 2.40GHz',
    speed: 2400,
    times: {
        user: 282348700,
        nice: 0,
        sys: 161800480,
        idle: 703509470,
        irq: 0
    }
}]

os.endianness()

返回 BELE,取决于 Node.js 是用 Big Endian or Little Endian 编译的。

os.freemem()

返回代表系统中空闲内存的字节数。

os.homedir()

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

例如:

'/Users/flavio'

os.hostname()

返回主机名。

os.loadavg()

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

它只在 Linux 和 MacOS 上返回一个有意义的值。

例如:

[ 3.68798828125, 4.00244140625, 11.1181640625 ]

os.networkInterfaces()

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

例如:

{
    lo0: [{
        address: '127.0.0.1',
        netmask: '255.0.0.0',
        family: 'IPv4',
        mac: 'fe:82:00:00:00:00',
        internal: true
    }, {
        address: '::1',
        netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
        family: 'IPv6',
        mac: 'fe:82:00:00:00:00',
        scopeid: 0,
        internal: true
    }, {
        address: 'fe80::1',
        netmask: 'ffff:ffff:ffff:ffff::',
        family: 'IPv6',
        mac: 'fe:82:00:00:00:00',
        scopeid: 1,
        internal: true
    }],
    en1: [{
        address: 'fe82::9b:8282:d7e6:496e',
        netmask: 'ffff:ffff:ffff:ffff::',
        family: 'IPv6',
        mac: '06:00:00:02:0e:00',
        scopeid: 5,
        internal: false
    }, {
        address: '192.168.1.38',
        netmask: '255.255.255.0',
        family: 'IPv4',
        mac: '06:00:00:02:0e:00',
        internal: false
    }],
    utun0: [{
        address: 'fe80::2513:72bc:f405:61d0',
        netmask: 'ffff:ffff:ffff:ffff::',
        family: 'IPv6',
        mac: 'fe:80:00:20:00:00',
        scopeid: 8,
        internal: false
    }]
}

os.platform()

返回 Node.js 编译时使用的平台:

  • darwin
  • freebsd
  • linux
  • openbsd
  • win32
  • ……more

os.release()

返回一个字符串,用于识别操作系统的发行号。

os.tmpdir()

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

os.totalmem()

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

os.type()

识别操作系统:

  • Linux
  • Darwin on MacOS
  • Windows_NT on Windows

os.uptime()

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

Node.js 的事件模块

events 模块为我们提供了 EventEmitter 类,它是在 Node.js 中处理事件的关键。

我在这方面发表了一篇完整的 文章,所以在这里我将只描述 API,而不进一步举例说明如何使用它。

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

事件监听器吃自己的狗粮(译者注: dog food 是指自己写的代码),使用这些事件:

  • newListener 当一个监听器被添加
  • removeListener 当一个监听器被移除

当一个监听器被移除:

  • emitter.addListener()
  • emitter.emit()
  • emitter.eventNames()
  • emitter.getMaxListeners()
  • emitter.listenerCount()
  • emitter.listeners()
  • emitter.off()
  • emitter.on()
  • emitter.once()
  • emitter.prependListener()
  • emitter.prependOnceListener()
  • emitter.removeAllListeners()
  • emitter.removeListener()
  • emitter.setMaxListeners()

emitter.addListener()

别名 emitter.on().

emitter.emit()

发出一个事件。它按注册的顺序同步调用每个事件监听器。

emitter.eventNames()

返回一个字符串数组,代表在当前 EventListener 上注册的事件:

door.eventNames()

emitter.getMaxListeners()

获取可以添加到 EventListener 对象中的最大监听器数量,默认为 10,但可以通过使用 setMaxListeners() 增加或减少:

door.getMaxListeners()

emitter.listenerCount()

获取作为参数传递的事件的监听数量:

door.listenerCount('open')

emitter.listeners()

获取作为参数传递的事件的监听者数组:

door.listeners('open')

emitter.off()

别名 emitter.removeListener(),在 Node 10 加入的。

emitter.on()

添加一个回调函数,当一个事件被发出时被调用。

使用案例:

door.on('open', () => {console.log('Door was opened')})

emitter.once()

添加一个回调函数,在注册后第一次发出事件时被调用。这个回调函数只被调用一次,不会再被调用。

const EventEmitter = require('events')
const ee = new EventEmitter()
ee.once('my-event', () => {  
  //call callback function once
})

emitter.prependListener()

当你使用 onaddListener 添加监听器时,它在监听器队列中被最后添加,并被最后调用。使用 prependListener,它将在其他监听器之前被添加和调用。

emitter.prependOnceListener()

当你使用 once 添加一个监听器时,它在监听器队列中最后被添加,并最后被调用。使用 prependOnceListener,它将在其他监听器之前被添加和调用。

emitter.removeAllListeners()

移除监听某一特定事件的事件发射器对象的所有监听者:

door.removeAllListeners('open')

emitter.removeListener()

删除一个特定的监听器。你可以这样做,在添加回调函数时,将其保存到一个变量中,这样你以后就可以引用它了:

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

emitter.setMaxListeners()

设置一个人可以添加到 EventListener 对象中的最大监听器数量,默认为 10,但可以增加或减少:

door.setMaxListeners(50)

Node.js http 模块

Node.js 的 http 模块提供了有用的函数和类来建立一个 HTTP 服务器。它是 Node.js 网络的一个关键模块。

它可以用以下方式引入:

const http = require('http')

该模块提供了一些属性和方法,以及一些类。

Properties

http.METHODS

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

> require('http').METHODS
[ 'ACL',  'BIND',  'CHECKOUT',  'CONNECT',  'COPY',  'DELETE',  'GET',  'HEAD',  'LINK',  'LOCK',  'M-SEARCH',  'MERGE',  'MKACTIVITY',  'MKCALENDAR',  'MKCOL',  'MOVE',  'NOTIFY',  'OPTIONS',  'PATCH',  'POST',  'PROPFIND',  'PROPPATCH',  'PURGE',  'PUT',  'REBIND',  'REPORT',  'SEARCH',  'SUBSCRIBE',  'TRACE',  'UNBIND',  'UNLINK',  'UNLOCK',  'UNSUBSCRIBE' ]

http.STATUS_CODES

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

> require('http').STATUS_CODES
{ '100': 'Continue',  '101': 'Switching Protocols',  '102': 'Processing',  '200': 'OK',  '201': 'Created',  '202': 'Accepted',  '203': 'Non-Authoritative Information',  '204': 'No Content',  '205': 'Reset Content',  '206': 'Partial Content',  '207': 'Multi-Status',  '208': 'Already Reported',  '226': 'IM Used',  '300': 'Multiple Choices',  '301': 'Moved Permanently',  '302': 'Found',  '303': 'See Other',  '304': 'Not Modified',  '305': 'Use Proxy',  '307': 'Temporary Redirect',  '308': 'Permanent Redirect',  '400': 'Bad Request',  '401': 'Unauthorized',  '402': 'Payment Required',  '403': 'Forbidden',  '404': 'Not Found',  '405': 'Method Not Allowed',  '406': 'Not Acceptable',  '407': 'Proxy Authentication Required',  '408': 'Request Timeout',  '409': 'Conflict',  '410': 'Gone',  '411': 'Length Required',  '412': 'Precondition Failed',  '413': 'Payload Too Large',  '414': 'URI Too Long',  '415': 'Unsupported Media Type',  '416': 'Range Not Satisfiable',  '417': 'Expectation Failed',  '418': 'I\'m a teapot',  '421': 'Misdirected Request',  '422': 'Unprocessable Entity',  '423': 'Locked',  '424': 'Failed Dependency',  '425': 'Unordered Collection',  '426': 'Upgrade Required',  '428': 'Precondition Required',  '429': 'Too Many Requests',  '431': 'Request Header Fields Too Large',  '451': 'Unavailable For Legal Reasons',  '500': 'Internal Server Error',  '501': 'Not Implemented',  '502': 'Bad Gateway',  '503': 'Service Unavailable',  '504': 'Gateway Timeout',  '505': 'HTTP Version Not Supported',  '506': 'Variant Also Negotiates',  '507': 'Insufficient Storage',  '508': 'Loop Detected',  '509': 'Bandwidth Limit Exceeded',  '510': 'Not Extended',  '511': 'Network Authentication Required' }

http.globalAgent

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

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

在后面的 http.Agent 类描述中会有更多内容。

Methods

http.createServer()

返回一个 http.Server 类的新实例。

用法:

const server = http.createServer((req, res) => {})//handle every single request with this callback

http.request()

http.get()

向服务器发出一个 HTTP 请求,创建一个 http.ClientRequest 类的实例。

Classes

HTTP 模块提供了 5 个类(classes):

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

http.Agent

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

这个对象确保每一个向服务器发出的请求都是排队的,并且一个套接字被重复使用。

它还维护一个套接字池。这是性能方面的关键。

http.ClientRequest

http.request()http.get() 被调用时,一个 http.ClientRequest 对象被创建。

当收到一个响应时,response 事件会被调用,并以一个 http.IncomingMessage 实例作为参数。

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

  • 你可以调用 response.read() 方法
  • response 事件处理程序中,你可以为 data 事件设置一个事件监听器,所以你可以监听流进的数据。

http.Server

这个类通常在使用 http.createServer() 创建一个新的服务器时被实例化并返回。

一旦你有了一个服务器对象,你就可以访问它的方法:

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

http.ServerResponse

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

通常在代码中被称为 res:

const server = http.createServer((req, res) => { })//res is an http.ServerResponse object

你总是在处理程序中调用的方法是 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() 还没有发送头信息,它将首先发送头信息,并在请求中设置状态码和信息,你可以通过设置 statusCodestatusMessage 属性值来编辑:

response.statusCode = 500
response.statusMessage = 'Internal Server Error'

http.IncomingMessage

一个 "http.IncomingMessage" 对象是通过以下方式创建的:

  • http.Server 监听 request 事件
  • http.ClientRequest 监听 "response" 事件

它可以用来访问响应(response):

  • status,使用 statusCodestatusMessage 方法
  • headers,使用 headers 方法或 rawHeaders
  • HTTP method 使用它的 method method
  • HTTP version 使用 httpVersion method
  • URL 使用 url method
  • 使用 "socket "方法的底层套接字

由于http.IncomingMessage 实现了可读流接口,所以数据可以使用流访问。

Node.js流(Streams)

流是支持 Node.js 应用程序的基本概念之一。

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

流不是 Node.js 特有的概念。几十年前,它们就被引入到 Unix 操作系统中,程序之间可以通过管道操作符(|)传递流进行交互。

例如,在传统的方式中,当你告诉程序读取一个文件时,文件被读入内存,从头到尾,然后你处理它。

使用流,你会一块一块地读取它,处理它的内容,而不把它全部保留在内存中。

Node.js 的 stream 模块 提供了所有流媒体 API 的基础。

为什么是流

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

  • 内存效率: 你不需要在处理数据之前在内存中加载大量的数据
  • 时间效率: 一旦你有了数据,就开始处理,而不是等到整个数据负载可用时才开始,这需要的时间要少得多。

一个流的例子

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

使用 Node.js fs 模块,你可以读取一个文件,并在与你的 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)

这个结构做相同的事:

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

Node.js 的流(stream) 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: 类似于 Duplex,但输出是其输入的变换

如何创建一个可读流

我们从 stream 模块获得 可读(Readable) 流,并初始化它:

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

现在,流已经被初始化,我们可以向它发送数据了:

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

使用 MySQL 和 Node.js 的基础知识

MySQL 是世界上最流行的关系型数据库之一。

Node.js 生态系统有几个不同的包,允许你与 MySQL 接口,存储数据,检索数据,等等。

我们将使用 mysqljs/mysql,这个包在 GitHub 上有超过 12,000 颗星,已经存在多年。

安装 Node.js MySql 包

安装命令:

npm install mysql

初始化与数据库的连接

你首先要引入包:

const mysql = require('mysql')

并创建一个连接:

const options = {  
  user: 'the_mysql_user_name', 
  password: 'the_mysql_user_password',  
  database: 'the_mysql_database_name'
}

const connection = mysql.createConnection(options)

你通过调用以下命令启动一个新的连接:

connection.connect(err => {  
  if (err) {  
      console.error('An error occurred while connecting to the DB')    
      throw err  
  }
})

连接选项

在上面的例子中,options 对象包含 3 个选项:

const options = {  
  user: 'the_mysql_user_name',  
  password: 'the_mysql_user_password',
  database: 'the_mysql_database_name'
}

你还可以使用很多,包括:

  • host, 数据库主机名,默认为 localhost
  • port, MySQL服务器端口号,默认为 3306
  • socketPath, 用于指定 Unix 套接字,而不是主机和端口
  • debug, 默认为禁用,可用于调试
  • trace, 默认为启用,当发生错误时打印堆栈跟踪
  • ssl, 用于设置与服务器的 SSL 连接(不在本教程范围内)

执行一个 SELECT 查询

现在你已经准备好在数据库上执行一个 SQL 查询。查询一旦执行,将调用一个回调函数,其中包含一个最终的错误、结果和字段(fields):

connection.query('SELECT * FROM todos', (error, todos, fields) => {  
  if (error) { 
    console.error('An error occurred while executing the query')    
    throw error  
  }  
  console.log(todos)
})

你可以传入将被自动转义的值:

const id = 223
connection.query('SELECT * FROM todos WHERE id = ?', [id], (error, todos, fields) => {  
  if (error) { 
    console.error('An error occurred while executing the query')    
    throw error  
  }  
  console.log(todos)
})

要传递多个值,只需在你作为第二个参数传递的数组中放入更多元素即可:

const id = 223const author = 'Flavio'
connection.query('SELECT * FROM todos WHERE id = ? AND author = ?', [id, author], (error, todos, fields) => {  
  if (error) {
    console.error('An error occurred while executing the query')    
    throw error  
  }  
  console.log(todos)
})

执行一个 INSERT 语句

你可以传递一个对象:

const todo = {  thing: 'Buy the milk'  author: 'Flavio'}
connection.query('INSERT INTO todos SET ?', todo, (error, results, fields) => {  
  if (error) {    
    console.error('An error occurred while executing the query')    
    throw error  
  }
})

如果表有一个 "自动增量" 的主键,其值将在 "results.insertId" 值中返回:

const todo = {  thing: 'Buy the milk'  author: 'Flavio'}
connection.query('INSERT INTO todos SET ?', todo, (error, results, fields) => {  
  if (error) {    
    console.error('An error occurred while executing the query')    
    throw error  
  }
}  
const id = results.resultId 
console.log(id))

关闭连接

当你需要终止与数据库的连接时,你可以调用 end() 方法:

connection.end()

这可以确保任何未决的查询被发送,并且连接被优雅地终止。

开发环境和生产环境之间的区别

你可以为生产和开发环境进行不同的配置。

Node.js 假定它总是在开发环境中运行。你可以通过设置 NODE_ENV=production 环境变量向 Node.js 发出信号,表明你正在生产环境中运行。

这通常是通过执行以下命令来完成的:

export NODE_ENV=production

在 Shell 中,但最好把它放在你的 Shell 配置文件中(比如 Bash shell 的 .bash_profile ),因为否则在系统重启的情况下,这个设置会失效。

你也可以通过在你的应用程序初始化命令前加上环境变量来应用它:

NODE_ENV=production node app.js

这个环境变量是一个惯例,在外部库中也被广泛使用。

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

  • 日志记录保持在最小的、必要的水平上
  • 更多的缓存级别,以优化性能

例如 Pug,Express 使用的模板库,如果 NODE_ENV 没有设置为 production,则在开发(development)模式下进行编译。在开发模式下,Express 视图在每个请求中都被编译,而在生产(production)模式下,它们被缓存起来。还有很多例子。

Express 提供了特定于环境的配置钩子,这些钩子根据NODE_ENV变量值自动调用:

app.configure('development', () => {})//...
app.configure('production', () => {})//...
app.configure('production', 'staging', () => {})//...

例如,你可以用它来为不同的模式设置不同的错误处理程序:

app.configure('development', () => {
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
})

app.configure('production', () => {
  app.use(express.errorHandler())
})

结语

我希望对 Node.js 的介绍,能帮助你开始使用它,或者帮助你掌握它的一些概念。希望你现在知道的足够多,可以开始创造一些好东西!