「萬字整理 」這裏有一份Node.js入門指南和實踐,請注意查收 ❤️

前言

什麼是 Node.js 呢 ?

JS 是腳本語言,腳本語言都須要一個解析器才能運行。對於寫在 HTML 頁面裏的 JS,瀏覽器充當瞭解析器的角色。而對於須要獨立運行的 JS,NodeJS 就是一個解析器。html

解析器須要運行引擎才能對 JavaScript 進行解析,Node.js 採用了 V8 引擎,Google 開源的 JavaScript 引擎。前端

因此,Node.js 就是一個基於 Chrome V8 引擎的 JavaScript 運行環境。vue

Node.js 事件驅動機制 + 異步 IO + 高性能 V8 引擎 ,也讓它成爲編寫高性能 Web 服務一個很是好的選擇。node

Node.js 能作什麼呢 ?

立刻 2020 年了,距離 2009 年 Node.js 開源以來,已經 10 個年頭了。git

這麼長時間的迭代,Node.js 生態圈已經很是成熟,有了不少優秀的實踐和輪子,好比 express,koa 等 web 開發框架。github

Node.js 無疑也帶動了前端生態的發展,好比前端工程化領域。web

說 Node.js 能作什麼,不如說說我用 Node.js 作了什麼吧。算法

工做中:docker

  • 基於 express 作了一個活動頁生成工具數據庫

  • 基於 koa + sequelize 作了一個監控系統平臺

  • 用 Node.js 擼了一些自動化腳本,優化重複性勞做

工做之餘:

null-cli 來啦 , 一行命令提升你的效率

5 個有趣的 Node.js 庫,帶你走進 彩色 Node.js 世界

nodejs + docker + github pages 定製本身的 「今日頭條」

說了這麼多廢話,我要幹嗎呢~

若是你最近恰好想要了解,學習 Node.js,那但願這篇文章能幫到你~

本文經過了解 Node.js 13 個 基礎核心模塊 和 一個基於 原生 Node.js 的 TodoList 實踐 ,帶你上手 Node.js !

13 個基礎核心模塊

1. 事件觸發器 events 模塊

2. 本地路徑 path 模塊

3. 文件操做系統 fs 模塊

4. 全局對象 process 進程

5. http 模塊

6. 統一資源定位符 url 模塊

7. 壓縮 zlib 模塊

8. 流 stream 模塊

9. 逐行讀取 readline 模塊

10. 查詢字符串 querystring 模塊

11. module 模塊

12. 緩衝器 Buffer 模塊

13. 域名服務器 dns 模塊

Node.js 內置模塊遠不止 13 個,入門階段咱們瞭解一些經常使用的基礎核心模塊,就能夠上手 實踐啦~

若是不想看通篇長文,我在github 博客 將 13 個模塊拆分紅了 13 個小節,方便閱讀,每一個模塊的 demo 代碼也能在博客中找到~

TodoList 實現了什麼?

爲了對 Node.js 核心模塊進一步加深理解,這個 demo 採用原生 api 實現,脫離 express,koa 等一些 web 框架和庫 。

  • RESTful API 實踐

  • 靜態資源映射及 gzip 壓縮

  • 後端路由 Router 簡易實現

  • Node.js 核心模塊方法實踐

實現了一個簡單的任務管理,前端採用的是 vue + element-ui ,

TodoList
└───app             // 前端代碼
│   │   ...
└───controllers     // 控制器
│   │   list.js     // api 邏輯實現
└───router
│   │   index.js    // 註冊路由
│   │   router.js   // 路由實現
└───utils           // 工具類
    │   index.js
|   data.json       // 數據存放
│   index.js        // 工程入口
複製代碼

實現沒有藉助任何庫,不用安裝任何依賴

node index.js
複製代碼

就能夠啓動服務,本身想要開發或者調試的話,這裏推薦使用nodemon,它實現了熱更新,能夠自動重啓.

npm install -g nodemon

nodemon
#or
nodemon index.js
複製代碼

TodoList 代碼地址

實現效果以下:

todolist

1. 事件觸發器 events 模塊

Node.js 使用了一個事件驅動、非阻塞式 I/O 的模型,使其輕量又高效。

大多數 Node.js 核心 API 都採用慣用的事件驅動架構,其中某些類型的對象(觸發器)會週期性地觸發命名事件來調用函數對象(監聽器),那麼 Node.js 是如何實現事件驅動的呢?

events 模塊是 Node.js 實現事件驅動的核心,在 node 中大部分的模塊的實現都繼承了 Events 類。好比 fs 的 readstream,net 的 server 模塊。

events 模塊只提供了一個對象: events.EventEmitter。EventEmitter 的核心就是事件觸發與事件監聽器功能的封裝,EventEmitter 本質上是一個觀察者模式的實現。

全部能觸發事件的對象都是 EventEmitter 類的實例。 這些對象有一個 eventEmitter.on() 函數,用於將一個或多個函數綁定到命名事件上。 事件的命名一般是駝峯式的字符串,但也可使用任何有效的 JavaScript 屬性鍵。

EventEmitter 對象使用 eventEmitter.emit()觸發事件,當 EventEmitter 對象觸發一個事件時,全部綁定在該事件上的函數都會被同步地調用。 被調用的監聽器返回的任何值都將會被忽略並丟棄。

下面咱們經過幾個簡單的例子來學習 events 模塊

1. 基礎例子

註冊 Application 實例,繼承 EventEmitter 類,經過繼承而來的 eventEmitter.on() 函數監聽事件,eventEmitter.emit()觸發事件

const EventEmitter = require('events')
/** * Expose `Application` class. * Inherits from `EventEmitter.prototype`. */
class Application extends EventEmitter {}
const app = new Application()
// 監聽hello事件
app.on('hello', data => {
  console.log(data) // hello nodeJs
})
// 觸發hello事件
app.emit('hello', 'hello nodeJs')
複製代碼

2. 多個事件監聽器及 this 指向

綁定多個事件監聽器時,事件監聽器按照註冊的順序執行。

當監聽器函數被調用時, this 關鍵詞會被指向監聽器所綁定的 EventEmitter 實例。也可使用 ES6 的箭頭函數做爲監聽器,但 this 關鍵詞不會指向 EventEmitter 實例。

const EventEmitter = require('events')

class Person extends EventEmitter {
  constructor() {
    super()
  }
}
const mrNull = new Person()
// 監聽play事件
mrNull.on('play', function(data) {
  console.log(this)
  // Person {
  // _events:
  // [Object: null prototype] { play: [[Function], [Function]] },
  // _eventsCount: 1,
  // _maxListeners: undefined
  // }
  console.log(`play`)
})
// 監聽play事件
mrNull.on('play', data => {
  console.log(this) // {}
  console.log(`play again`)
})
// 觸發play事件
mrNull.emit('play', 'hello nodeJs')
複製代碼

3. 同步 VS 異步

EventEmitter 以註冊的順序同步地調用全部監聽器。

const EventEmitter = require('events')

class Person extends EventEmitter {
  constructor() {
    super()
  }
}
const mrNull = new Person()
mrNull.on('play', function(data) {
  console.log(data)
})

mrNull.emit('play', 'hello nodeJs')

console.log(`hello MrNull`)

// hello nodeJs
// hello MrNull
複製代碼

監聽器函數可使用 setImmediate() 和 process.nextTick() 方法切換到異步的操做模式

const developer = new Person()
developer.on('dev', function(data) {
  setImmediate(() => {
    console.log(data)
  })
})
developer.on('dev', function(data) {
  process.nextTick(() => {
    console.log(data)
  })
})
developer.emit('dev', 'hello nodeJs')

console.log(`hello developer`)

// hello developer
// hello nodeJs
// hello nodeJs
複製代碼

4. 只調用一次的事件監聽器

使用 eventEmitter.once() 能夠註冊最多可調用一次的監聽器。 當事件被觸發時,監聽器會被註銷,而後再調用。

const EventEmitter = require('events')

class Person extends EventEmitter {
  constructor() {
    super()
  }
}
const mrNull = new Person()
mrNull.once('play', () => {
  console.log('play !')
})

mrNull.emit('play')
mrNull.emit('play')

// play ! 只輸出一次
複製代碼

5. 事件觸發順序

在註冊事件前,觸發該事件,不會被觸發 !!

const EventEmitter = require('events')

class Person extends EventEmitter {
  constructor() {
    super()
  }
}
const mrNull = new Person()

mrNull.emit('play')

mrNull.on('play', () => {
  console.log('play !')
})

// 無任何輸出
複製代碼

6. 移除事件監聽器

const EventEmitter = require('events')

class Person extends EventEmitter {
  constructor() {
    super()
  }
}
const mrNull = new Person()

function play() {
  console.log('play !')
}
mrNull.on('play', play)

mrNull.emit('play')

// mrNull.off("play", play); v10.0.0版本新增,emitter.removeListener() 的別名。
// or
mrNull.removeListener('play', play)

mrNull.emit('play')

// play ! 移除後再也不觸發
複製代碼

2. 本地路徑 path 模塊

Node.js 提供了 path 模塊,用於處理文件路徑和目錄路徑 . 不一樣操做系統 表現有所差別 !

1. 獲取路徑的目錄名

const path = require('path')

path.dirname('/path/example/index.js') // /path/example
複製代碼

2. 獲取路徑的擴展名

const path = require('path')

path.extname('/path/example/index.js') // .js
複製代碼

3. 是不是絕對路徑

const path = require('path')

path.isAbsolute('/path/example/index.js') // true

path.isAbsolute('.') // false
複製代碼

4. 拼接路徑片斷

path.join('/path', 'example', './index.js') // /path/example/index.js
複製代碼

5. 將路徑或路徑片斷的序列解析爲絕對路徑。

path.resolve('/foo/bar', './baz')
// 返回: '/foo/bar/baz'

path.resolve('/foo/bar', '/tmp/file/')
// 返回: '/tmp/file'

path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif')
// 若是當前工做目錄是 /home/myself/node,
// 則返回 '/home/myself/node/wwwroot/static_files/gif/image.gif'
複製代碼

6. 規範化路徑

path.normalize('/path///example/index.js') // /path/example/index.js
複製代碼

7. 解析路徑

path.parse('/path/example/index.js')

/* { root: '/', dir: '/path/example', base: 'index.js', ext: '.js', name: 'index' } */
複製代碼

8. 序列化路徑

path.format({
  root: '/',
  dir: '/path/example',
  base: 'index.js',
  ext: '.js',
  name: 'index'
}) // /path/example/index.js
複製代碼

9. 獲取 from 到 to 的相對路徑

path.relative('/path/example/index.js', '/path') // ../..
複製代碼

3 .文件操做系統 fs 模塊

在一些場景下,咱們須要對文件進行 增刪改查等操做, Nodejs 提供了 fs 模塊,讓咱們對文件進行操做.

下面咱們來介紹幾個常常用的 API

1. 讀取文件

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

// 異步讀取
fs.readFile('./index.txt', 'utf8', (err, data) => {
  console.log(data) // Hello Nodejs
})

// 同步讀取
const data = fs.readFileSync('./index.txt', 'utf8')

console.log(data) // Hello Nodejs

// 建立讀取流
const stream = fs.createReadStream('./index.txt', 'utf8')

// 這裏能夠看到fs.createReadStream用到了咱們前面介紹的events eventEmitter.on() 方法來監聽事件
stream.on('data', data => {
  console.log(data) // Hello Nodejs
})
複製代碼

2. 寫入/修改文件

寫入文件時,若是文件不存在,則會建立並寫入,若是文件存在,會覆蓋文件內容.

const fs = require('fs')
// 異步寫入
fs.writeFile('./write.txt', 'Hello Nodejs', 'utf8', err => {
  if (err) throw err
})
// 同步寫入
fs.writeFileSync('./writeSync.txt', 'Hello Nodejs')
// 文件流寫入
const ws = fs.createWriteStream('./writeStream.txt', 'utf8')
ws.write('Hello Nodejs')
ws.end()
複製代碼

3. 刪除文件/文件夾

  • 刪除文件
// 異步刪除文件
fs.unlink('./delete.txt', err => {
  if (err) throw err
})

// 同步刪除文件
fs.unlinkSync('./deleteSync.txt')
複製代碼
  • 刪除文件夾
// 異步刪除文件夾
fs.rmdir('./rmdir', err => {
  if (err) throw err
})

// 同步刪除文件夾
fs.rmdirSync('./rmdirSync')
複製代碼

4. 建立文件夾

// 異步建立文件夾
fs.mkdir('./mkdir', err => {
  if (err) throw err
})

// 同步建立文件夾
fs.mkdirSync('./mkdirSync')
複製代碼

5. 重命名文件/文件夾

const fs = require('fs')

// 異步重命名文件
fs.rename('./rename.txt', './rename-r.txt', err => {
  if (err) throw err
})

// 同步重命名文件夾
fs.renameSync('./renameSync', './renameSync-r')
複製代碼

6. 複製文件/文件夾

const fs = require('fs')

// 異步複製文件
fs.copyFile('./copy.txt', './copy-c.txt', (err, copyFiles) => {
  if (err) throw err
})

// 同步複製文件夾
fs.copyFileSync('./null', 'null-c')
複製代碼

7. 文件夾狀態- 文件/文件夾

const fs = require('fs')

// 異步獲取文件狀態
fs.stat('./dir', (err, stats) => {
  if (err) throw err
  // 是不是文件類型
  console.log(stats.isFile()) // false
  // 是不是文件夾類型
  console.log(stats.isDirectory()) // true
})

// 同步獲取文件狀態
const stats = fs.statSync('./stats.txt')

// 是不是文件類型
console.log(stats.isFile()) // true
// 是不是文件夾類型
console.log(stats.isDirectory()) // false
複製代碼

在一些複雜的操做場景下,fs 模塊要作不少判斷與處理 ,這裏我推薦你們使用 fs-extra,它在 fs 的基礎上擴展了一些方法,讓一些複雜操做更簡便!

4. 全局對象 process 進程

process 對象是一個 Global 全局對象,你能夠在任何地方使用它,而無需 require。process 是 EventEmitter 的一個實例,因此 process 中也有相關事件的監聽。使用 process 對象,能夠方便處理進程相關操做。

process 經常使用屬性

進程命令行參數: process.argv

process.argv 是一個當前執行進程折參數組,第一個參數是 node,第二個參數是當前執行的.js 文件名,以後是執行時設置的參數列表。

node index.js --tips="hello nodejs"

/* [ '/usr/local/bin/node', 'xxx/process/index.js', '--tips=hello nodejs' ] */
複製代碼

Node 的命令行參數數組:process.execArgv

process.execArgv 屬性會返回 Node 的命令行參數數組。

node --harmony index.js --version

console.log(process.execArgv);  // [ '--harmony' ]

console.log(process.argv);

/* [ '/usr/local/bin/node', 'xxx/process/index.js', '--version' ] */

複製代碼

Node 編譯時的版本: process.version

process.version 屬性會返回 Node 編譯時的版本號,版本號保存於 Node 的內置變量 NODE_VERSION 中。

console.log(process.version) // v10.15.3
複製代碼

當前進程的 PID process.pid

process.pid 屬性會返回當前進程的 PID。

console.log('process PID: %d', process.pid)

//process PID: 10086
複製代碼

process 經常使用方法

當前工做目錄 process.cwd()

process.cwd()方法返回進程當前的工做目錄

console.log(process.cwd()) // /Users/null/nodejs/process
複製代碼

終止當前進程:process.exit([code])

process.exit()方法終止當前進程,此方法可接收一個退出狀態的可選參數 code,不傳入時,會返回表示成功的狀態碼 0。

process.on('exit', function(code) {
  console.log('進程退出碼是:%d', code) // 進程退出碼是:886
})

process.exit(886)
複製代碼

nodejs 微任務: process.nextTick()

process.nextTick()方法用於延遲迴調函數的執行, nextTick 方法會將 callback 中的回調函數延遲到事件循環的下一次循環中,與 setTimeout(fn, 0)相比 nextTick 方法效率高不少,該方法能在任何 I/O 以前調用咱們的回調函數。

console.log('start')
process.nextTick(() => {
  console.log('nextTick cb')
})
console.log('end')

// start
// end
// nextTick cb
複製代碼

process 標準流對象

process 中有三個標準備流的操做,與 其餘 streams 流操做不一樣的是,process 中流操做是同步寫,阻塞的。

標準錯誤流: process.stderr

process.stderr 是一個指向標準錯誤流的可寫流 Writable Stream。console.error 就是經過 process.stderr 實現的。

標準輸入流:process.stdin

process.stdin 是一個指向標準輸入流的可讀流 Readable Stream。

process.stdin.setEncoding('utf8')

process.stdin.on('readable', () => {
  let chunk
  // 使用循環確保咱們讀取全部的可用數據。
  while ((chunk = process.stdin.read()) !== null) {
    if (chunk === '\n') {
      process.stdin.emit('end')
      return
    }
    process.stdout.write(`收到數據: ${chunk}`)
  }
})

process.stdin.on('end', () => {
  process.stdout.write('結束監聽')
})
複製代碼

process-stdin

標準輸出流:process.stdout

process.stdout 是一個指向標準輸出流的可寫流 Writable Stream。console.log 就是經過 process.stdout 實現的

console.log = function(d) {
  process.stdout.write(d + '\n')
}

console.log('Hello Nodejs') // Hello Nodejs
複製代碼

5. http 模塊

http 模塊是 Node.js 中很是重要的一個核心模塊。經過 http 模塊,你可使用其 http.createServer 方法建立一個 http 服務器,也可使用其 http.request 方法建立一個 http 客戶端。(本文先不說),Node 對 HTTP 協議及相關 API 的封裝比較底層,其僅能處理流和消息,對於消息的處理,也僅解析成報文頭和報文體,可是不解析實際的報文頭和報文體內容。這樣不只解決了 HTTP 本來比較難用的特性,也能夠支持更多的 HTTP 應用.

http.IncomingMessage 對象

IncomingMessage 對象是由 http.Server 或 http.ClientRequest 建立的,並做爲第一參數分別傳遞給 http.Server 的'request'事件和 http.ClientRequest 的'response'事件。

它也能夠用來訪問應答的狀態、頭文件和數據等。 IncomingMessage 對象實現了 Readable Stream 接口,對象中還有一些事件,方法和屬性。

在 http.Server 或 http.ClientRequest 中略有不一樣。

http.createServer([requestListener])建立 HTTP 服務器

實現 HTTP 服務端功能,要經過 http.createServer 方法建立一個服務端對象 http.Server。

這個方法接收一個可選傳入參數 requestListener,該參數是一個函數,傳入後將作爲 http.Server 的 request 事件監聽。不傳入時,則須要經過在 http.Server 對象的 request 事件中單獨添加。

var http = require('http')

// 建立server對象,並添加request事件監聽器
var server = http.createServer(function(req, res) {
  res.writeHeader(200, { 'Content-Type': 'text/plain' })
  res.end('Hello Nodejs')
})

// 建立server對象,經過server對象的request事件添加事件事件監聽器
var server = new http.Server()
server.on('request', function(req, res) {
  res.writeHeader(200, { 'Content-Type': 'text/plain' })
  res.end('Hello Nodejs')
})
複製代碼

http.Server 服務器對象

http.Server 對象是一個事件發射器 EventEmitter,會發射:request、connection、close、checkContinue、connect、upgrade、clientError 事件。

其中 request 事件監聽函數爲 function (request, response) { },該方法有兩個參數:request 是一個 http.IncomingMessage 實例,response 是一個 http.ServerResponse 實例。

http.Server 對象中還有一些方法,調用 server.listen 後 http.Server 就能夠接收客戶端傳入鏈接。

http.ServerResponse

http.ServerResponse 對象用於響應處理客戶端請求。

http.ServerResponse 是 HTTP 服務器(http.Server)內部建立的對象,做爲第二個參數傳遞給 'request'事件的監聽函數。

http.ServerResponse 實現了 Writable Stream 接口,其對於客戶端的響應,本質上是對這個可寫流的操做。它仍是一個 EventEmitter,包含:close、finish 事件。

建立一個 http.Server

建立 http.Server 使用 http.createServer()方法,爲了處理客戶端請求,須要在服務端監聽來自客戶的'request'事件。

'request'事件的回調函數中,會返回一個 http.IncomingMessage 實例和一個 http.ServerResponse。

const http = require('http')
/** * @param {Object} req 是一個http.IncomingMessag實例 * @param {Object} res 是一個http.ServerResponse實例 */
const server = http.createServer((req, res) => {
  console.log(req.headers)
  res.end(`Hello Nodejs`)
})

server.listen(3000)
複製代碼

http.ServerResponse 實例是一個可寫流,因此能夠將一個文件流轉接到 res 響應流中。下面示例就是將一張圖片流傳送到 HTTP 響應中:

const http = require('http')
/** * @param {Object} req 是一個http.IncomingMessag實例 * @param {Object} res 是一個http.ServerResponse實例 */
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'image/jpg' })
  const r = require('fs').createReadStream('./kobe.jpg')
  r.pipe(res)
})

server.listen(3000)
複製代碼

6. 統一資源定位符 url 模塊

Node.js 提供了 url 模塊,用於處理與解析 URL。

1. URL 對象都有哪些屬性 ?

const { URL } = require("url");

const myURL = new URL("https://github.com/webfansplz#hello");
console.log(myURL);
{
  href: 'https://github.com/webfansplz#hello',  // 序列化的 URL
  origin: 'https://github.com', // 序列化的 URL 的 origin
  protocol: 'https:', // URL 的協議
  username: '', // URL 的用戶名
  password: '', // URL 的密碼
  host: 'github.com', // URL 的主機
  hostname: 'github.com',   // URL 的主機名
  port: '',  // URL 的端口
  pathname: '/webfansplz',  // URL 的路徑
  search: '', // URL 的序列化查詢參數
  searchParams: URLSearchParams {}, // URL 查詢參數的 URLSearchParams 對象
  hash: '#hello'  // URL 的片斷
}
複製代碼

URL 對象屬性 除了 origin 和 searchParams 是隻讀的,其餘都是可寫的.

2. 序列化 URL

const { URL } = require('url')

const myURL = new URL('https://github.com/webfansplz#hello')

console.log(myURL.href) // https://github.com/webfansplz#hello

console.log(myURL.toString()) // https://github.com/webfansplz#hello

console.log(myURL.toJSON()) // https://github.com/webfansplz#hello
複製代碼

7. 壓縮 zlib 模塊

在流傳輸過程當中,爲減小傳輸數據加快傳輸速度,每每會對流進行壓縮。

HTTP 流就是如此,爲提升網站響應速度,會在服務端進行壓縮,客戶端收到數據後再進行相應的解壓。

Node.js 中的 Zlib 模塊提供了流壓縮與解壓縮功能,Zlib 模塊提供了對 Gzip/Gunzip、Deflate/Inflate、DeflateRaw/InflateRaw 類的綁定,這些類能夠實現對可讀流/可寫流的壓縮與解壓。

關於 gzip 與 deflate

deflate(RFC1951)是一種壓縮算法,使用 LZ77 和哈弗曼進行編碼。gzip(RFC1952)一種壓縮格式,是對 deflate 的簡單封裝,gzip = gzip 頭(10 字節) + deflate 編碼的實際內容 + gzip 尾(8 字節)。在 HTTP 傳輸中,gzip 是一種經常使用的壓縮算法,使用 gzip 壓縮的 HTTP 數據流,會在 HTTP 頭中使用 Content-Encoding:gzip 進行標識。

HTTP Request Header 中 Accept-Encoding 是瀏覽器發給服務器,聲明瀏覽器支持的解壓類型

Accept-Encoding: gzip, deflate, br
複製代碼

HTTP Response Header 中 Content-Encoding 是服務器告訴瀏覽器 使用了哪一種壓縮類型

Content-Encoding: gzip
複製代碼

對 web 性能優化有所瞭解的同窗,相信對 gzip 都不陌生,咱們就經過 gzip 來了解 zlib 模塊.

1. 文件壓縮/解壓

文件壓縮

const zlib = require('zlib')
const fs = require('fs')
const gzip = zlib.createGzip()
const inp = fs.createReadStream('zlib.txt')
const out = fs.createWriteStream('zlib.txt.gz')
inp.pipe(gzip).pipe(out)
複製代碼

文件解壓

const zlib = require('zlib')
const fs = require('fs')
const gunzip = zlib.createGunzip()
const inp = fs.createReadStream('./un-zlib.txt.gz')
const out = fs.createWriteStream('un-zlib.txt')
inp.pipe(gunzip).pipe(out)
複製代碼

2. 服務端 gzip 壓縮

const fs = require('fs')
const http = require('http')
const zlib = require('zlib')
const filepath = './index.html'

const server = http.createServer((req, res) => {
  const acceptEncoding = req.headers['accept-encoding']
  if (acceptEncoding.includes('gzip')) {
    const gzip = zlib.createGzip()
    res.writeHead(200, {
      'Content-Encoding': 'gzip'
    })
    fs.createReadStream(filepath)
      .pipe(gzip)
      .pipe(res)
  } else {
    fs.createReadStream(filepath).pipe(res)
  }
})

server.listen(4396)
複製代碼

8. 流 stream 模塊

流(stream)是 Node.js 中處理流式數據的抽象接口。 stream 模塊用於構建實現了流接口的對象。

Node.js 提供了多種流對象。 例如,HTTP 服務器的請求和 process.stdout 都是流的實例。

流能夠是可讀的、可寫的、或者可讀可寫的。 全部的流都是 EventEmitter 的實例。

儘管理解流的工做方式很重要,可是 stream 模塊主要用於開發者建立新類型的流實例。 對於以消費流對象爲主的開發者,極少須要直接使用 stream 模塊。

stream 類型

Node.js 中有四種基本的流類型:

  • Writable - 可寫入數據的流(例如 fs.createWriteStream())。

  • Readable - 可讀取數據的流(例如 fs.createReadStream())。

  • Duplex - 可讀又可寫的流(例如 net.Socket)。

  • Transform - 在讀寫過程當中能夠修改或轉換數據的 Duplex 流(例如 zlib.createDeflate())。

用於消費流的 API

const http = require('http')

const server = http.createServer((req, res) => {
  // req 是一個 http.IncomingMessage 實例,它是可讀流。
  // res 是一個 http.ServerResponse 實例,它是可寫流。

  let body = ''
  // 接收數據爲 utf8 字符串,
  // 若是沒有設置字符編碼,則會接收到 Buffer 對象。
  req.setEncoding('utf8')

  // 若是添加了監聽器,則可讀流會觸發 'data' 事件。
  req.on('data', chunk => {
    body += chunk
  })

  // 'end' 事件代表整個請求體已被接收。
  req.on('end', () => {
    try {
      const data = JSON.parse(body)
      // 響應信息給用戶。
      res.write(typeof data)
      res.end()
    } catch (er) {
      // json 解析失敗。
      res.statusCode = 400
      return res.end(`錯誤: ${er.message}`)
    }
  })
})

server.listen(1337)

// curl localhost:1337 -d "{}"
// object
// curl localhost:1337 -d "\"foo\""
// string
// curl localhost:1337 -d "not json"
// 錯誤: Unexpected token o in JSON at position 1
複製代碼

當數據能夠從流讀取時,可讀流會使用 EventEmitter API 來通知應用程序 (好比例子中的 req data 事件)。 從流讀取數據的方式有不少種。

可寫流(好比例子中的 res)會暴露了一些方法,好比 write() 和 end() 用於寫入數據到流。

可寫流和可讀流都經過多種方式使用 EventEmitter API 來通信流的當前狀態。Duplex 流和 Transform 流都是可寫又可讀的。

對於只需寫入數據到流或從流消費數據的應用程序,並不須要直接實現流的接口,一般也不須要調用 require('stream')。

對於大部分的 nodejs 開發者來講,日常並不會直接用到 stream 模塊,可是理解 stream 流的運行機制倒是尤爲重要的.

9. 逐行讀取 readline 模塊

readline 模塊是一個流內容的逐行讀取模塊,經過 require('readline')引用模塊。你能夠用 readline 模塊來讀取 stdin,能夠用來逐行讀取文件流,也可用它來在控制檯和用戶進行一些交互。

const readline = require('readline')

const rl = readline.createInterface({
  // 監聽的可讀流
  input: process.stdin,
  // 逐行讀取(Readline)數據要寫入的可寫流
  output: process.stdout
})

rl.question('你如何看待 null-cli ?', answer => {
  console.log(`感謝您的寶貴意見:${answer}`)
  rl.close()
})
複製代碼

readline

不少有趣的 CLI 工具是基於 readline 造的哦,有興趣的同窗也能夠嘗試~

10. 查詢字符串 querystring 模塊

querystring 模塊是 Node.js 中的工具模塊之一,用於處理 URL 中的查詢字符串,即:querystring 部分。查詢字符串指:URL 字符串中,從問號"?"(不包括?)開始到錨點"#"或者到 URL 字符串的結束(存在#,則到#結束,不存在則到 URL 字符串結束)的部分叫作查詢字符串。querystring 模塊可將 URL 查詢字符串解析爲對象,或將對象序列化爲查詢字符串。

1. 對象序列化爲查詢字符串

querystring.stringify(obj[, sep][, eq][, options])

const querystring = require('querystring')

const obj = {
  url: 'github.com/webfansplz',
  name: 'null'
}

console.log(querystring.stringify(obj)) // url=github.com%2Fwebfansplz&name=null
複製代碼

2. 查詢字符串解析爲對象

const querystring = require('querystring')

const o = querystring.parse(`url=github.com%2Fwebfansplz&name=null`)

console.log(o.url) // github.com/webfansplz
複製代碼

3. 編碼查詢字符串中的參數

querystring.escape 方法會對查詢字符串進行編碼,在使用 querystring.stringify 方法時可能會用到.

const str = querystring.escape(`url=github.com%2Fwebfansplz&name=null`)

console.log(str) // url%3Dgithub.com%252Fwebfansplz%26name%3Dnull
複製代碼

4. 解碼查詢字符串中的參數

querystring.unescape 方法是和 querystring.escape 相逆的方法,在使用 querystring.parse 方法時可能會用到。

const str = querystring.escape(`url=github.com%2Fwebfansplz&name=null`)

console.log(querystring.parse(str)) // { 'url=github.com%2Fwebfansplz&name=null': '' } ✖️

console.log(querystring.parse(querystring.unescape(str))) // { url: 'github.com/webfansplz', name: 'null' }
複製代碼

11. module 模塊

Node.js 實現了一個簡單的模塊加載系統。在 Node.js 中,文件和模塊是一一對應的關係,能夠理解爲一個文件就是一個模塊。其模塊系統的實現主要依賴於全局對象 module,其中實現了 exports(導出)、require()(加載)等機制。

1. 模塊加載

Node.js 中一個文件就是一個模塊。如,在 index.js 中加載同目錄下的 circle.js:

// circle.js
const PI = Math.PI

exports.area = r => PI * r * r

exports.circumference = r => 2 * PI * r
複製代碼
// index.js
const circle = require('./circle.js')

console.log(`半徑爲 4 的圓面積爲 ${circle.area(4)}`) // 半徑爲 4 的圓面積爲 50.26548245743669
複製代碼

circle.js 中經過 exports 導出了 area()和 circumference 兩個方法,這兩個方法能夠其它模塊中調用。

exports 與 module.exports

exports 是對 module.exports 的一個簡單引用。若是你須要將模塊導出爲一個函數(如:構造函數),或者想導出一個完整的出口對象而不是作爲屬性導出,這時應該使用 module.exports。

// square.js

module.exports = width => {
  return {
    area: () => width * width
  }
}
複製代碼
// index.js

const square = require('./square.js')
const mySquare = square(2)
console.log(`The area of my square is ${mySquare.area()}`) // The area of my square is 4
複製代碼

2. 訪問主模塊

當 Node.js 直接運行一個文件時,require.main 屬性會被設置爲 module 自己。這樣,就可經過這個屬性判斷模塊是否被直接運行:

require.main === module
複製代碼

好比,對於上面例子的 index.js 來講, node index.js 上面值就是 true, 而經過 require('./index')時, 值倒是 false.

module 提供了一個 filename 屬性,其值一般等於__filename。 因此,當前程序的入口點能夠經過 require.main.filename 來獲取。

console.log(require.main.filename === __filename) // true
複製代碼

3. 解析模塊路徑

使用 require.resolve()函數,能夠獲取 require 加載的模塊的確切文件名,此操做只返回解析後的文件名,不會加載該模塊。

console.log(require.resolve('./square.js')) // /Users/null/meet-nodejs/module/square.js
複製代碼

require.resolve 的工做過程:

require(X) from module at path Y
1. If X is a core module,
   a. return the core module
   b. STOP
2. If X begins with './' or '/' or '../'
   a. LOAD_AS_FILE(Y + X)
   b. LOAD_AS_DIRECTORY(Y + X)
3. LOAD_NODE_MODULES(X, dirname(Y))
4. THROW "not found"

LOAD_AS_FILE(X)
1. If X is a file, load X as JavaScript text.  STOP
2. If X.js is a file, load X.js as JavaScript text.  STOP
3. If X.json is a file, parse X.json to a JavaScript Object.  STOP
4. If X.node is a file, load X.node as binary addon.  STOP

LOAD_AS_DIRECTORY(X)
1. If X/package.json is a file,
   a. Parse X/package.json, and look for "main" field.
   b. let M = X + (json main field)
   c. LOAD_AS_FILE(M)
2. If X/index.js is a file, load X/index.js as JavaScript text.  STOP
3. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
4. If X/index.node is a file, load X/index.node as binary addon.  STOP

LOAD_NODE_MODULES(X, START)
1. let DIRS=NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
   a. LOAD_AS_FILE(DIR/X)
   b. LOAD_AS_DIRECTORY(DIR/X)

NODE_MODULES_PATHS(START)
1. let PARTS = path split(START)
2. let I = count of PARTS - 1
3. let DIRS = []
4. while I >= 0,
   a. if PARTS[I] = "node_modules" CONTINUE
   c. DIR = path join(PARTS[0 .. I] + "node_modules")
   b. DIRS = DIRS + DIR
   c. let I = I - 1
5. return DIRS
複製代碼

4. 模塊緩存

模塊在第一次加載後會被緩存到 require.cache 對象中, 今後對象中刪除鍵值對將會致使下一次 require 從新加載被刪除的模塊。

屢次調用 require('index'),未必會致使模塊中代碼的屢次執行。這是一個重要的功能,藉助這一功能,能夠返回部分完成的對象;這樣,傳遞依賴也能被加載,即便它們可能致使循環依賴。

若是你但願一個模塊屢次執行,那麼就應該輸出一個函數,而後調用這個函數。

模塊緩存的注意事項

模塊的基於其解析後的文件名進行緩存。因爲調用的位置不一樣,可能會解析到不一樣的文件(如,須要從 node_modules 文件夾加載的狀況)。因此,當解析到其它文件時,就不能保證 require('index')老是會返回確切的同一對象。

另外,在不區分大小寫的文件系統或系統中,不一樣的文件名可能解析到相同的文件,但緩存仍會將它們視爲不一樣的模塊,會屢次加載文件。如:require('./index')和 require('./INDEX')會返回兩個不一樣的對象,不管'./index'和'./INDEX'是不是同一個文件。

5. 循環依賴

當 require()存在循環調用時,模塊在返回時可能並不會被執行。

// a.js
console.log('a starting')
exports.done = false
const b = require('./b.js')
console.log('in a, b.done = %j', b.done)
exports.done = true
console.log('a done')
複製代碼
// b.js
console.log('b starting')
exports.done = false
const a = require('./a.js')
console.log('in b, a.done = %j', a.done)
exports.done = true
console.log('b done')
複製代碼
// main.js
console.log('main starting')
const a = require('./a.js')
const b = require('./b.js')
console.log('in main, a.done=%j, b.done=%j', a.done, b.done)
複製代碼

首先 main.js 會加載 a.js,接着 a.js 又會加載 b.js。這時,b.js 又會嘗試去加載 a.js。

爲了防止無限的循環,a.js 會返回一個 unfinished copy 給 b.js。而後 b.js 就會中止加載,並將其 exports 對象返回給 a.js 模塊。

這樣 main.js 就完成了 a.js、b.js 兩個文件的加載。輸出以下:

$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done=true, b.done=true
複製代碼

6. 文件模塊

當加載文件模塊時,若是按文件名查找未找到。那麼 Node.js 會嘗試添加.js 和.json 的擴展名,並再次嘗試查找。若是仍未找到,那麼會添加.node 擴展名再次嘗試查找。

對於.js 文件,會將其解析爲 JavaScript 文本文件;而.json 會解析爲 JOSN 文件文件;.node 會嘗試解析爲編譯後的插件文件,並由 dlopen 進行加載。

路徑解析

當加載的文件模塊使用'/'前綴時,則表示絕對路徑。如,require('/home/null/index.js')會加載/home/null/index.js 文件。

而使用'./'前綴時,表示相對路徑。如,在 index.js 中 require('./circle')引用時,circle.js 必須在相同的目錄下才能加載成功。

當沒有'/'或'./'前綴時,所引用的模塊必須是「核心模塊」或是 node_modules 中的模塊。

若是所加載的模塊不存在,require()會拋出一個 code 屬性爲'MODULE_NOT_FOUND'的錯誤。

7. __dirname

當前模塊的目錄名。 與 __filename 的 path.dirname() 相同。

console.log(__dirname) // /Users/null/meet-nodejs/module

console.log(require('path').dirname(__filename)) // /Users/null/meet-nodejs/module

console.log(__dirname === require('path').dirname(__filename)) // true
複製代碼

8. module 對象

module 在每一個模塊中表示對當前模塊的引用。 而 module.exports 又能夠經過全局對象 exports 來引用。module 並非一個全局對象,而更像一個模塊內部對象。

module.children

這個模塊引入的全部模塊對象

module.exports

module.exports 經過模塊系統建立。有時它的工做方式與咱們所想的並不一致,有時咱們但願模塊是一些類的實例。所以,要將導出對象賦值給 module.exports,可是導出所需的對象將分配綁定本地導出變量,這可能不是咱們想要的結果。

// a.js

const EventEmitter = require('events')

module.exports = new EventEmitter()

// Do some work, and after some time emit
// the 'ready' event from the module itself.
setTimeout(() => {
  module.exports.emit('ready')
}, 1000)
複製代碼
const a = require('./a')
a.on('ready', () => {
  console.log('module a is ready')
})
複製代碼

須要注意,分配給 module.exports 的導出值必須能馬上獲取到,當使用回調時其不能正常執行。

exports 別名

exports 能夠作爲 module.exports 的一個引用。和任何變量同樣,若是爲它分配新值,其舊值將會失效:

function require(...) {
  // ...
  ((module, exports) => {
    // Your module code here
    exports = some_func;        // re-assigns exports, exports is no longer
                                // a shortcut, and nothing is exported.
    module.exports = some_func; // makes your module export 0
  })(module, module.exports);
  return module;
}

複製代碼
  • module.filename - 模塊解析後的完整文件名

  • module.id - 用於區別模塊的標識符,一般是徹底解析後的文件名。

  • module.loaded - 模塊是否加載完畢

  • module.parent - 父模塊,即:引入這個模塊的模塊

  • module.require(id)

  • module.require 提供了相似 require()的功能,能夠從最初的模塊加載一個模塊

12. 緩衝器 Buffer 模塊

在引入 TypedArray 以前,JavaScript 語言沒有用於讀取或操做二進制數據流的機制。 Buffer 類是做爲 Node.js API 的一部分引入的,用於在 TCP 流、文件系統操做、以及其餘上下文中與八位字節流進行交互。

建立緩衝區

console.log(Buffer.from([1, 2, 3, 4, 5])) // <Buffer 01 02 03 04 05>

console.log(Buffer.from(new ArrayBuffer(8))) // <Buffer 00 00 00 00 00 00 00 00>

console.log(Buffer.from('Hello world')) // <Buffer 48 65 6c 6c 6f 20 77 6f 72 6c 64>
複製代碼

Buffer 與字符編碼

當字符串數據被存儲入 Buffer 實例或從 Buffer 實例中被提取時,能夠指定一個字符編碼。

// 緩衝區轉換爲 UTF-8 格式的字符串

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

console.log(buffer.toString()) // Hello world
複製代碼
// 緩衝區數據轉換爲base64格式字符串

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

console.log(buffer.toString('base64')) // SGVsbG8gd29ybGQ=
複製代碼
// 將base64編碼的字符串,轉換爲UTF-8編碼

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

const base64Str = buffer.toString('base64')

const buf = Buffer.from(base64Str, 'base64')

console.log(buf.toString('utf8')) // Hello world
複製代碼

13. 域名服務器 dns 模塊

DNS(Domain Name System,域名系統),DNS 協議運行在 UDP 協議之上,使用端口號 53。DNS 是因特網上做爲域名和 IP 地址相互映射的一個分佈式數據庫,可以使用戶更方便的訪問互聯網,而不用去記住可以被機器直接讀取的 IP 數串。簡單的說,就是把域名(網址)解析成對應的 IP 地址。Node.js 的 dns 模塊,提供了 DNS 解析功能。當使用 dns 模塊中的 net.connect(80, 'github.com/webfansplz')方法 或 http 模塊的 http.get({ host: 'github.com/webfansplz' })方法時,在其底層會使用 dns 模塊中的 dns.lookup 方法進行域名解析。

dns 模塊的兩種域名解析方式

1.使用操做系統底層的 DNS 服務解析

使用操做系統底層的 DNS 服務進行域名解析時,不須要鏈接到網絡僅使用系統自帶 DNS 解析功能。這個功能由 dns.lookup()方法實現。

dns.lookup(hostname[, options], callback):將一個域名(如:'www.baidu.com')解析爲第一個找到的 A 記錄(IPv4)或 AAAA 記錄(IPv6)

hostname 表示要解析的域名。

options 能夠是一個對象或整數。若是沒有提供 options 參數,則 IP v4 和 v6 地址均可以。若是 options 是整數,則必須是 4 或 6。若是 options 是對象時,會包含如下兩個可選參數:

  • family:可選,IP 版本。若是提供,必須是 4 或 6。不提供則,IP v4 和 v6 地址均可以

  • hints:可選。若是提供,能夠是一個或者多個 getaddrinfo 標誌。若不提供,則沒有標誌會傳給 getaddrinfo。

callback 回調函數,參數包含(err, address, family)。出錯時,參數 err 是 Error 對象。address 參數表示 IP v4 或 v6 地址。family 參數是 4 或 6,表示 address 協議版本。

const dns = require('dns')

dns.lookup(`www.github.com`, (err, address, family) => {
  if (err) throw err
  console.log('地址: %j 地址族: IPv%s', address, family) // 地址: "13.229.188.59" 地址族: IPv4
})
複製代碼

2.鏈接到 DNS 服務器解析域名

在 dns 模塊中,除 dns.lookup()方法外都是使用 DNS 服務器進行域名解析,解析時須要鏈接到網絡。

dns.resolve(hostname[, rrtype], callback):將一個域名(如 'www.baidu.com')解析爲一個 rrtype 指定類型的數組

hostname 表示要解析的域名。

rrtype 有如下可用值:

rrtype records 包含 結果的類型 快捷方法
'A' IPv4 地址 (默認) string dns.resolve4()
'AAAA' IPv6 地址 string dns.resolve6()
'ANY' 任何記錄 Object dns.resolveAny()
'CNAME' 規範名稱記錄 string dns.resolveCname()
'MX' 郵件交換記錄 Object dns.resolveMx()
'NAPTR' 名稱權限指針記錄 Object dns.resolveNaptr()
'NS' 名稱服務器記錄 string dns.resolveNs()
'PTR' 指針記錄 string dns.resolvePtr()
'SOA' 開始受權記錄 Object dns.resolveSoa()
'SRV' 服務記錄 Object dns.resolveSrv()
'TXT' 文本記錄 string[] dns.resolveTxt()

callback 回調函數,參數包含(err, addresses)。出錯時,參數 err 是 Error 對象。addresses 根據記錄類型的不一樣返回值也不一樣。

const dns = require('dns')

dns.resolve('www.baidu.com', 'A', (err, addresses) => {
  if (err) throw err
  console.log(`IP地址 : ${JSON.stringify(addresses)}`) // IP地址 : ["163.177.151.110","163.177.151.109"]
})

// or

dns.resolve4('www.baidu.com', (err, addresses) => {
  if (err) throw err
  console.log(`IP地址 : ${JSON.stringify(addresses)}`) // IP地址 : ["163.177.151.110","163.177.151.109"]
})
複製代碼

反向 DNS 查詢

將 IPv4 或 IPv6 地址解析爲主機名數組。

使用 getnameinfo 方法將傳入的地址和端口解析爲域名和服務

dns.reverse(ip, callback)

ip 表示要反向解析的 IP 地址。

callback 回調函數,參數包含(err, domains)。出錯時,參數 err 是 Error 對象。domains 解析後的域名數組。

dns.reverse('8.8.8.8', (err, domains) => {
  if (err) throw err
  console.log(domains) // [ 'dns.google' ]
})
複製代碼

dns.lookupService(address, port, callback)

address 表示要解析的 IP 地址字符串。

port 表示要解析的端口號。

callback 回調函數,參數包含(err, hostname, service)。出錯時,參數 err 是 Error 對象。

dns.lookupService('127.0.0.1', 80, function(err, hostname, service) {
  if (err) throw err
  console.log('主機名:%s,服務類型:%s', hostname, service) // 主機名:localhost,服務類型:http
})
複製代碼

參考

Node.js 中文網

IT 筆錄

後記

若是你和我同樣喜歡前端,也愛動手摺騰,歡迎關注我一塊兒玩耍啊~ ❤️

博客

個人博客

公衆號

前端時刻

公衆號
相關文章
相關標籤/搜索