JavaScript >> Javascript 文檔 >  >> Node.js

節點模式:從回調到觀察者

更新:現在也可以在 YouTube 上作為視頻(在 NodePDX 2016 上拍攝)提供。

這篇文章最初是在 ConFoo 加拿大會議上發表的。享受幻燈片?在 https://github.com/azat-co/node-patterns:

git clone https://github.com/azat-co/node-patterns

節點模式:從回調到觀察者

在開始使用 Node 模式之前,讓我們先了解一下使用 Node 的一些主要優勢和特性。他們會在以後幫助我們理解為什麼我們需要處理某些問題。

節點優勢和特點

以下是人們使用 Node 的一些主要原因:

  • JavaScript:Node 在 JavaScript 上運行,因此您可以重複使用瀏覽器代碼、庫和文件。
  • 異步 + 事件驅動:節點使用異步代碼和模式並發執行任務,這要歸功於事件循環。
  • 非阻塞 I/O:Node 非常快,因為它的非阻塞輸入/輸出架構和 Google Chrome V8 引擎。

這一切都很好,但異步代碼很難。人類的大腦並沒有進化為以異步方式處理事物,事件循環在未來調度不同的邏輯片段。它們的順序通常與實施時的順序不同。

更糟糕的是,大多數傳統語言、計算機科學課程和開發訓練營都專注於同步編程。這使得異步教學變得更加困難,因為你真的需要轉過頭來開始異步思考。

JavaScript同時是優點和缺點。很長一段時間,JavaScript 被認為是一種玩具語言。 :unamused:它阻止了一些軟件工程花時間去學習它。相反,他們會假設他們可以從 Stackoverflow 複製一些代碼,交叉手指了解它是如何工作的。 JavaScript 是開發人員認為他們不需要學習的唯一編程語言。錯了!

JavaScript 有其不好的部分,這就是為什麼了解模式更為重要。請花點時間學習基礎知識。

然後,如您所知,代碼複雜性呈指數增長。模塊 B 使用的每個模塊 A 也被使用模塊 B 的模塊 C 使用,以此類推。如果你對 A 有問題,那麼它會影響很多其他模塊。

所以良好的代碼組織很重要。這就是為什麼我們 Node 工程師需要關心它的模式。

所有你可以吃的回調

如何安排未來的事情?也就是說,如何保證在某個事件之後,我們的代碼會被執行,即保證正確的順序。一路回調!

回調只是函數,函數是一等公民,這意味著您可以將它們視為變量(字符串、數字)。您可以將它們折騰到其他功能。當我們傳遞一個函數 t 作為參數並稍後調用它,它被稱為回調:

var t = function(){...}
setTimeout(t, 1000)

t 是一個回調。並且有一定的回調約定。看看這個從文件中讀取數據的片段:

var fs = require('fs')
var callback = function(error, data){...}
fs.readFile('data.csv', 'utf-8', callback)

以下是 Node 回調約定:

[旁注]

閱讀博客文章很好,但觀看視頻課程更好,因為它們更具吸引力。

許多開發人員抱怨 Node.js 上缺乏負擔得起的高質量視頻材料。觀看 YouTube 視頻會讓人分心,花 500 美元購買 Node 視頻課程很瘋狂!

去看看 Node University,它有關於 Node 的免費視頻課程:node.university。

[旁注結束]

  • error 第一個參數,如果一切正常,則為 null
  • data 是第二個參數
  • callback 是最後一個參數

注意:命名無關緊要,但順序很重要。 Node.js 不會強制執行參數。約定不是保證——它只是一種風格。閱讀文檔或源代碼。

命名函數

現在出現了一個新問題:如何保證正確的順序?控制流?
例如,有三個HTTP請求來執行以下任務:

  1. 獲取身份驗證令牌
  2. 使用授權令牌獲取數據
  3. 使用第 2 步中獲取的數據進行更新

它們必須按照一定的順序執行,如下偽代碼所示:

... // callback is defined, callOne, callTwo, and callThree are defined
callOne({...}, function(error, data1) {
    if (error) return callback(error, null)
    // work to parse data1 to get auth token
    // fetch the data from the API
    callTwo(data1, function(error, data2) {
        if (error) return callback(error, null)
        // data2 is the response, transform it and make PUT call
        callThree(data2, function(error, data3) {
            //
            if (error) return callback(error, null)
            // parse the response
            callback(null, data3)
        })
    })
})

因此,歡迎回調地獄。這個片段取自 callbackhell.com(是的,它存在——一個糟糕的代碼死去的地方):

fs.readdir(source, function (err, files) {
  if (err) {
    console.log('Error finding files: ' + err)
  } else {
    files.forEach(function (filename, fileIndex) {
      console.log(filename)
      gm(source + filename).size(function (err, values) {
        if (err) {
          console.log('Error identifying file size: ' + err)
        } else {
          console.log(filename + ' : ' + values)
          aspect = (values.width / values.height)
          widths.forEach(function (width, widthIndex) {
            height = Math.round(width / aspect)
            console.log('resizing ' + filename + 'to ' + height + 'x' + height)
            this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
              if (err) console.log('Error writing file: ' + err)
            })
          }.bind(this))
        }
      })
    })
  }
}

回調地獄也被稱為嵌套方法和末日金字塔。確保開發人員的高工作安全性是一件好事,因為沒有其他人會理解他/她的代碼(開玩笑,不要這樣做)。回調地獄的顯著特點是:

  • 難以閱讀
  • 難以修改/維護/增強
  • 易於開發人員製作錯誤
  • 右括號 - ?

一些解決方案包括:

  • 抽象為命名函數(提升或變量)
  • 使用反義詞
  • 使用高級庫和技術

我們從命名函數方法開始。三個嵌套請求的代碼可以重構為三個函數:

callOne({...}, processResponse1)

function processResponse1(error, data1) {
  callTwo(data1, processResponse2)
}

function processResponse2(error, data2) {
  callThere(data2, processResponse3)
}

function processResponse3(error, data1) {
  ...
}

Node 中的模塊化

此外,您可以將函數模塊化到單獨的文件中,以保持文件簡潔明了。此外,模塊化將允許您在其他項目中重用代碼。主入口點將只包含兩個語句:

var processResponse1 = require('./response1.js')
callOne({...}, processResponse1)

這是 response.js 帶有第一個回調的模塊:

// response1.js
var processResponse2 = require('./response2.js')
module.exports = function processResponse1(error, data1) {
  callTwo(data1, processResponse2)
}

同樣在 response2.js ,我們導入 response3.js 並使用第二個回調導出:

// response2.js
var processResponse3 = require('./response3.js')
module.exports = function processResponse2(error, data2) {
  callThere(data2, processResponse3)
}

最後的回調:

// response3.js
module.exports = function processResponse3(error, data3) {
  ...
}

Node.js 中間件模式

讓我們把回調發揮到極致。我們可以實現連續性傳遞模式,簡稱為中間件模式。

中間件模式是一系列連接在一起的處理單元,其中一個單元的輸出是下一個單元的輸入。在 Node.js 中,這通常意味著表單中的一系列函數:

function(args, next) {
  // ... Run some code
  next(output) // Error or real output
}

Express 中經常使用中間件,其中請求來自客戶端,響應被發送回客戶端。請求通過一系列中間件:

request->middleware1->middleware2->...middlewareN->route->response

next() 參數只是一個回調,它告訴 Node 和 Express.js 繼續下一步:

app.use(function(request, response, next) {
  // ...
  next()
}, function(request, response, next) {
  next()
}, function(request, response, next) {
  next()
})

節點模塊模式

當我們開始談論模塊化時,有很多方法可以給鯰魚剝皮。新的問題是如何正確模塊化代碼?

主要的模塊模式有:

  • module.exports = {...}
  • module.exports.obj = {...}
  • exports.obj = {...}

注意:exports = {...} 是反模式,因為它不會導出任何東西。您只是在創建一個變量,而不是分配 module.exports .

第二種和第三種方法是相同的,只是在使用 exports.obj = {...} 時需要輸入更少的字符 .

第一和第二/第三之間的區別是你的意圖。當導出具有相互交互的組件(例如,方法、屬性)的單個整體對象/類時,請使用 module.exports = {...} .

另一方面,當處理彼此不交互但可能完全相同的事物時,您可以將它們放在同一個文件中,但使用 exports.obj = {...}module.exports = {...} .

導出對象和靜態事物現在很清楚了。但是如何模塊化動態代碼或者在哪裡初始化呢?

解決方案是導出一個作為初始化器/構造器的函數:

  • module.exports = function(options) {...}
  • module.exports.func = function(options) {...}
  • exports.func = function(options) {...}

關於 module.exports.name 的相同旁注 和 exports.name 相同也適用於功能。函數式方法更靈活,因為您可以返回一個對象,但您也可以在返回之前執行一些代碼。

這種方法有時被稱為子棧方法,因為它受到多產的節點貢獻者子棧的喜愛。

如果您記得函數是 JavaScript 中的對象(可能來自閱讀 JavaScript 基礎知識),那麼您就知道我們可以在函數上創建屬性。因此,可以組合兩種模式:

module.exports = function(options){...}
module.exports.func = function(options){...}
module.exports.name = {...}

雖然這很少使用,因為它被認為是節點功夫。最好的方法是每個文件有一個導出。這將使文件保持精簡和小。

節點模塊中的代碼

導出之外的代碼呢?你也可以擁有它,但它的工作方式與導出中的代碼不同。它與 Node 導入模塊和緩存它們的方式有關。例如,我們在 export 之外有代碼 A,在裡面有代碼 B:

//import-module.js
console.log('Code A')
module.exports = function(options){
  console.log('Code B')
}

當你 require ,代碼 A 運行,代碼 B 不運行。代碼 A 只運行一次,無論您 require 多少次 , 因為模塊是通過它們解析的文件名緩存的(你可以通過改變大小寫和路徑來欺騙 Node!)。

最後,您需要調用對象來運行代碼 B,因為我們導出了一個函數定義。它需要被調用。知道了這一點,下面的腳本將只打印“代碼 A”。它只會執行一次。

var f = require('./import-module.js')

require('./import-module.js')

模塊的緩存在不同的文件中起作用,因此在不同的文件中多次要求同一個模塊只會觸發一次“代碼A”。

節點中的單例模式

熟悉單例模式的軟件工程師知道他們的目的是提供一個通常是全局的單一實例。拋開單例不好的說法,Node中如何實現呢?

我們可以利用模塊的緩存特性,即 require 緩存模塊。例如,我們有一個變量 b,我們用值 2 導出它:

// module.js
var a = 1 // Private
module.exports = {
  b: 2 // Public
}

然後,在腳本文件(導入模塊)中,增加 b 的值並導入模塊 main

// program.js
var m = require('./module')
console.log(m.a) // undefined
console.log(m.b) // 2
m.b ++
require('./main')

模塊 main 導入 module 再次,但是這次b的值不是2而是3!

// main.js
var m = require('./module')
console.log(m.b) // 3

手頭上的一個新問題:模塊根據其解析的文件名進行緩存。因此,文件名會破壞緩存:

var m = require('./MODULE')
var m = require('./module')

或者不同的路徑會破壞緩存。解決方案是使用 global

global.name = ...
GLOBAL.name = ...

考慮這個改變我們心愛的 console.log 的例子 從默認的白色到驚人的紅色:

_log = global.console.log
global.console.log = function(){
  var args = arguments
  args[0] = '\033[31m' +args[0] + '\x1b[0m'
  return _log.apply(null, args)
}

你需要這個模塊一次,你的所有日誌都會變成紅色。你甚至不需要調用任何東西,因為我們不導出任何東西。

使用全局是強大的……但是反模式,因為它很容易搞砸和覆蓋其他模塊使用的東西。因此,您應該了解它,因為您可能會使用依賴於這種模式的庫(例如,應該行為驅動的開發),但要謹慎使用,僅在需要時使用。

很像瀏覽器window.jQuery = jQuery 圖案。但是,在我們沒有模塊的瀏覽器中,最好在 Node 中使用顯式導出,而不是使用全局。

導入文件夾

繼續導入,Node 中有一個有趣的功能,它不僅可以讓您導入 JavaScript/Node 文件或 JSON 文件,還可以導入整個文件夾。

導入文件夾是一種抽像模式,通常用於將代碼組織到包或插件(或模塊——此處為同義詞)中。要導入文件夾,請創建 index.js 在帶有 module.exports 的文件夾中 作業:

// routes/index.js
module.exports = {
  users: require('./users.js'),
  accounts: require('./accounts.js')
  ...
}

然後,在主文件中,您可以按名稱導入文件夾:

// main.js
var routes = require('./routes')

index.js 中的所有屬性 比如用戶、賬號等將是routes的屬性 在 main.js .幾乎所有 npm 模塊都使用文件夾導入模式。有一些庫可以自動導出給定文件夾中的所有文件:

  • require-dir
  • require-directory
  • require-all

函數工廠模式

Node 中沒有類。那麼如何將你的模塊化代碼組織成類呢?對象繼承自其他對象,函數也是對象。

注意:是的,ES6 中有類,但它們不支持屬性。時間會證明它們是否是偽經典繼承的良好替代品。與笨重的偽經典模式相比,Node 開發人員更喜歡函數工廠模式的簡單性。

解決方案是創建一個函數工廠,也就是函數繼承模式。其中,函數是一個表達式,它接受選項、初始化並返回對象。表達式的每次調用都會創建一個新實例。實例將具有相同的屬性。

module.exports = function(options) {
  // initialize
  return {
    getUsers: function() {...},
    findUserById: function(){...},
    limit: options.limit || 10,
    // ...
  }
}

與偽經典不同,方法不會來自原型。每個新對像都有自己的方法副本,因此您不必擔心原型的更改會影響您的所有實例。

有時,您只需要使用偽經典(例如,對於 Event Emitters),然後就有 inherits .像這樣使用它:

require('util').inherits(child, parent)

節點依賴注入

時不時地,您在模塊中需要一些動態對象。換句話說,模塊中存在依賴於主文件中的某些內容。

例如,在使用端口號啟動服務器時,考慮 Express.js 入口文件 server.js .它有一個模塊 boot.js 需要app的配置 目的。直接實現 boot.js 作為函數導出並傳遞 app

// server.js
var app = express()
app.set(port, 3000)
...
app.use(logger('dev'))
...
var boot = require('./boot')(app)
boot({...}, function(){...})

返回函數的函數

boot.js 文件實際上使用了另一種(可能是我最喜歡的)模式,我只是調用它返回一個函數的函數。可以這麼說,這種簡單的模式允許您創建內部函數的不同模式/版本。

// boot.js
module.exports = function(app){
  return function(options, callback) {
    app.listen(app.get('port'), options, callback)
  }
}

有一次我讀到一篇博客文章,其中這種模式被稱為 monad,但後來一位憤怒的函數式編程愛好者告訴我,這不是 monad(並且也對它感到憤怒)。哦,好吧。

節點中的觀察者模式

儘管如此,即使使用模塊,回調也很難管理!例如,你有這樣的:

  1. Module Job 正在執行一項任務。
  2. 在主文件中,我們導入 Job。

我們如何在 Job 的任務完成時指定回調(一些未來的邏輯)?也許我們向模塊傳遞一個回調:

var job = require('./job.js')(callback)

多個回調呢?開發可擴展性不是很好?

該解決方案非常優雅,實際上被大量使用,尤其是在核心 Node 模塊中。用事件發射器滿足觀察者模式!

這是我們發出事件 done 的模塊 當一切都完成時:

// module.js
var util = require('util')
var Job = function Job() {
  // ...
  this.process = function() {
    // ...
    job.emit('done', { completedOn: new Date() })
  }
}

util.inherits(Job, require('events').EventEmitter)
module.exports = Job

在主腳本中,我們可以自定義做什麼 當工作完成時。

// main.js
var Job = require('./module.js')
var job = new Job()

job.on('done', function(details){
  console.log('Job was completed at', details.completedOn)
  job.removeAllListeners()
})

job.process()

這就像一個回調,只是更好,因為你可以有多個事件,你可以刪除它們,或者執行一次。

emitter.listeners(eventName)
emitter.on(eventName, listener)
emitter.once(eventName, listener)
emitter.removeListener(eventName, listener)

30 秒總結

  1. 回調
  2. 觀察者
  3. 單身
  4. 插件
  5. 中間件
  6. 一堆其他的東西?

進一步研究

顯然,還有更多的模式,比如流。管理異步代碼是一組全新的問題、解決方案和模式。不過,這篇文章已經夠長了。感謝閱讀!

從這些基石節點模式開始,在需要的地方使用它們。要掌握 Node,請查看您喜歡的模塊;他們如何實現某些東西?

這些是值得進一步研究的東西:

  • asyncneo-async :用於管理異步代碼的優秀庫
  • Promise:ES6 自帶
  • 生成器:有希望
  • 異步等待:即將推出的承諾的不錯包裝器
  • hooks :鉤子模式模塊
  • Node Design Patterns 這本書不是我的,我現在正在看。

Tutorial JavaScript 教程
  1. JavaScript 數據結構:回顧:列表、堆棧、隊列

  2. 正則表達式 - 被低估或被高估?第 1/3 部分

  3. CSS `Position:Fixed` 在滾動時不移動

  4. 如何在 JavaScript 中檢查對像是否為空

  5. 使用純 JS 發送異步 POST 請求:拋棄 jQuerys ajax

  6. 帶有 Array 擴展類構造函數調用的 JS Array.prototype.filter

  7. 為用戶創建身份驗證 - 使用 Adonis 創建博客

  1. CSS 變量不會被設置 |電子

  2. 9 個 Javascript 項目創意,適合初學者構建令人印象深刻的作品集並被錄用

  3. 如何開始編程?

  4. React Native 🚀 Github Actions

  5. 使用 Github WebHooks 自動部署您的項目

  6. 為什麼我們使用 Terraform 在 AWS 上自託管我們的無服務器 Next.js 站點

  7. Rakkit:使用 TypeScript 和裝飾器創建您的 GraphQL 和 REST API!

  1. 了解 JavaScript 原型

  2. 如何循環遍歷 HTML 元素上的類

  3. 插槽巡邏

  4. NodeJs - 靜態站點生成器版本 0.1