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

NO IMAGE
目錄

前言

什麼是 Node.js 呢 ?

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

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

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

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

Node.js 能做什麼呢 ?

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

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

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

說 Node.js 能做什麼,不如說說我用 Node.js 做了什麼吧。

工作中:

  • 基於 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 代碼地址

實現效果如下:

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

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

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('結束監聽')
})

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

標準輸出流: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()
})

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

很多有趣的 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 有以下可用值:

rrtyperecords 包含結果的類型快捷方法
'A'IPv4 地址 (默認)stringdns.resolve4()
'AAAA'IPv6 地址stringdns.resolve6()
'ANY'任何記錄Objectdns.resolveAny()
'CNAME'規範名稱記錄stringdns.resolveCname()
'MX'郵件交換記錄Objectdns.resolveMx()
'NAPTR'名稱權限指針記錄Objectdns.resolveNaptr()
'NS'名稱服務器記錄stringdns.resolveNs()
'PTR'指針記錄stringdns.resolvePtr()
'SOA'開始授權記錄Objectdns.resolveSoa()
'SRV'服務記錄Objectdns.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 筆錄

後記

如果你和我一樣喜歡前端,也愛動手摺騰,歡迎關注我一起玩耍啊~ ❤️

博客

我的博客

公眾號

前端時刻

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

相關文章

【canvas】箭頭跟隨鼠標移動的動畫原理

ReentrantLock源碼分析從入門到入土

MySQL:如何查詢出每個分組中的topn條記錄?

十一個程序員日常表現,很真實!