Vzory uzlů:Od zpětných volání k pozorovateli

AKTUALIZACE:Nyní k dispozici také jako video (pořízené na NodePDX 2016) na YouTube.

Tato esej začala jako prezentace na konferenci ConFoo Canada. Baví vás diapozitivy? na https://github.com/azat-co/node-patterns:

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

Vzory uzlů:Od zpětných volání k pozorovateli

Než začneme se vzory Node, dotkneme se některých hlavních výhod a funkcí používání Node. Později nám pomohou pochopit, proč musíme řešit určité problémy.

Výhody a funkce uzlu

Zde jsou některé z hlavních důvodů, proč lidé používají Node:

  • JavaScript:Node běží na JavaScriptu, takže můžete znovu použít kód prohlížeče, knihovny a soubory.
  • Asynchronní + řízený událostí:Uzel provádí úlohy souběžně s použitím asynchronního kódu a vzorů díky smyčce událostí.
  • Neblokující I/O:Uzel je extrémně rychlý díky své neblokující architektuře vstupu/výstupu a enginu Google Chrome V8.

To je vše v pořádku, ale asynchronní kód je těžký. Lidský mozek se prostě nevyvinul tak, aby zpracovával věci asynchronním způsobem, kdy smyčka událostí naplánuje v budoucnu různé části logiky. Jejich pořadí často není stejné, v jakém byly implementovány.

Aby to bylo ještě horší, většina tradičních jazyků, počítačových programů a dev bootcampů se zaměřuje na synchronní programování. To ztěžuje asynchronní výuku, protože opravdu musíte zabalit hlavu a začít myslet async.

JavaScript je výhodou i nevýhodou zároveň. Po dlouhou dobu byl JavaScript považován za jazyk hraček. :unamused:Některým softwarovým inženýrům to bránilo v tom, aby se to naučili. Místo toho by předpokládali, že mohou jen zkopírovat nějaký kód ze Stackoverflow, držet palce a jak to funguje. JavaScript je jediný programovací jazyk, o kterém si vývojáři myslí, že se nemusí učit. Špatně!

JavaScript má své špatné stránky, proto je ještě důležitější znát vzory. A prosím, věnujte čas tomu, abyste se naučili základy.

Pak, jak víte, složitost kódu exponenciálně roste. Každý modul A používaný modulem B je také používán modulem C, který používá modul B a tak dále. Pokud máte problém s A, pak to ovlivňuje mnoho dalších modulů.

Dobrá organizace kódu je tedy důležitá. To je důvod, proč se my, inženýři Node, musíme starat o jeho vzory.

All You Can Eat Zpětná volání

Jak něco naplánovat do budoucna? Jinými slovy, jak zajistit, aby se po určité události provedl náš kód, tedy zajistit správnou sekvenci. Zpětná volání po celou dobu!

Zpětná volání jsou jen funkce a funkce jsou prvotřídní občané, což znamená, že s nimi můžete zacházet jako s proměnnými (řetězci, čísla). Můžete je přehodit na další funkce. Když předáme funkci t jako argument a zavolejte jej později, nazývá se zpětné volání:

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

t je zpětné volání. A existuje určitá konvence zpětného volání. Podívejte se na tento úryvek, který čte data ze souboru:

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

Následují konvence zpětného volání uzlů:

[Sidenote]

Čtení blogových příspěvků je dobré, ale sledování videokurzů je ještě lepší, protože jsou poutavější.

Mnoho vývojářů si stěžovalo, že na Node je nedostatek dostupného kvalitního videomateriálu. Sledování videí na YouTube je rušivé a platit 500 $ za videokurz Node je šílené!

Jděte se podívat na Node University, která má na Node ZDARMA videokurzy:node.university.

[Konec vedlejší poznámky]

  • error 1. argument, null, pokud je vše v pořádku
  • data je druhý argument
  • callback je poslední argument

Poznámka:Na pojmenování nezáleží, ale na pořadí. Node.js nebude vynucovat argumenty. Úmluva není zárukou – je to jen styl. Přečtěte si dokumentaci nebo zdrojový kód.

Pojmenované funkce

Nyní vyvstává nový problém:Jak zajistit správnou sekvenci? Řízení toku ?
Existují například tři požadavky HTTP k provedení následujících úkolů:

  1. Získejte ověřovací token
  2. Načítání dat pomocí ověřovacího tokenu
  3. ZADEJTE aktualizaci pomocí dat načtených v kroku 2

Musí být provedeny v určitém pořadí, jak je znázorněno v následujícím pseudokódu:

... // 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)
        })
    })
})

Proto vítejte v pekle zpětného volání. Tento úryvek byl převzat z callbackhell.com (ano, existuje – místo, kde špatný kód zemře):

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

Callback hell je také známý jako vnořený přístup a pyramida zkázy. Je jen dobré zajistit vysokou bezpečnost práce pro vývojáře, protože nikdo jiný nebude rozumět jeho kódu (vtip, nedělejte to). Charakteristické rysy pekla zpětného volání jsou:

  • Těžko čitelné
  • Je obtížné upravit/udržovat/vylepšovat
  • Pro vývojáře je snadné vytvářet chyby
  • Závorka – ?

Některá z řešení zahrnují:

  • Abstrakce do pojmenovaných funkcí (vyzvednutých nebo proměnných)
  • Používejte obverery
  • Používejte pokročilé knihovny a techniky

Začneme přístupem pojmenovaných funkcí. Kód tří vnořených požadavků lze refaktorovat do tří funkcí:

callOne({...}, processResponse1)

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

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

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

Modularizace v uzlu

Navíc můžete modularizovat funkce do samostatných souborů, aby byly vaše soubory štíhlé a čisté. Modularizace vám také umožní znovu použít kód v jiných projektech. Hlavní vstupní bod bude obsahovat pouze dva příkazy:

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

Toto je response.js modul s prvním zpětným voláním:

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

Podobně v response2.js , importujeme response3.js a exportovat pomocí druhého zpětného volání:

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

Poslední zpětné volání:

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

Vzor Middleware Node.js

Vezměme zpětná volání do extrému. Můžeme implementovat vzor předávání kontinuity známý jednoduše jako vzor middlewaru.

Middleware vzor je řada vzájemně propojených procesních jednotek, kde výstup jedné jednotky je vstupem pro další. V Node.js to často znamená řadu funkcí ve tvaru:

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

Middleware se často používá v Express, kde požadavek přichází od klienta a odpověď je zaslána zpět klientovi. Požadavek prochází řadou middlewaru:

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

next() argument je jednoduše zpětné volání, které říká Node a Express.js, aby postoupily k dalšímu kroku:

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

Vzory modulů uzlů

Když jsme začali mluvit o modularizaci, existuje mnoho způsobů, jak stáhnout sumce z kůže. Nový problém je, jak správně modularizovat kód?

Hlavní vzory modulů jsou:

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

Poznámka:exports = {...} je anti-pattern, protože nebude nic exportovat. Právě vytváříte proměnnou, nepřiřazujete module.exports .

Druhý a třetí přístup jsou totožné s tím rozdílem, že při použití exports.obj = {...} musíte zadat méně znaků .

Rozdíl mezi prvním a druhým/třetím je váš záměr. Při exportu jednoho monolitického objektu/třídy s komponentami, které se vzájemně ovlivňují (např. metody, vlastnosti), použijte module.exports = {...} .

Na druhou stranu, když se zabýváte věcmi, které spolu neinteragují, ale mohou být kategoricky stejné, můžete je vložit do stejného souboru, ale použít exports.obj = {...} nebo module.exports = {...} .

Export objektů a statických věcí je nyní jasný. Ale jak modularizovat dynamický kód nebo kde inicializovat?

Řešením je exportovat funkci, která bude fungovat jako inicializátor/konstruktor:

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

Stejná vedlejší poznámka o module.exports.name a exports.name totožné platí i pro funkce. Funkční přístup je flexibilnější, protože můžete vrátit objekt, ale můžete také provést nějaký kód, než jej vrátíte.

Tento přístup se někdy nazývá přístup substack, protože je oblíbený u plodných substacků přispěvatelů uzlů.

Pokud si pamatujete, že funkce jsou objekty v JavaScriptu (možná ze čtení základů JavaScriptu), pak víte, že můžeme vytvářet vlastnosti funkcí. Proto je možné kombinovat dva vzory:

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

To se však používá zřídka, protože je považováno za Node Kung Fu. Nejlepší přístup je mít jeden export na soubor. Soubory tak zůstanou štíhlé a malé.

Kód v modulech uzlů

A co kód mimo exporty? Můžete to mít také, ale funguje to jinak než kód uvnitř exportů. Má to něco společného se způsobem, jakým Node importuje moduly a ukládá je do mezipaměti. Například máme kód A mimo export a kód B uvnitř:

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

Když require , kód A je spuštěn a kód B nikoli. Kód A se spustí pouze jednou, bez ohledu na to, kolikrát require , protože moduly jsou ukládány do mezipaměti podle svého vyřešeného názvu souboru (Node můžete oklamat změnou velikosti písmen a cest!).

Nakonec musíte vyvolat objekt ke spuštění kódu B, protože jsme exportovali definici funkce. Je potřeba to vyvolat. S tímto vědomím níže uvedený skript vytiskne pouze „Kód A“. Udělá to jen jednou.

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

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

Ukládání modulů do mezipaměti funguje napříč různými soubory, takže vyžadování stejného modulu mnohokrát v různých souborech spustí „Kód A“ pouze jednou.

Singleton Pattern in Node

Softwaroví inženýři obeznámeni se vzorem singleton vědí, že jejich účelem je poskytnout jedinou obvykle globální instanci. Nechte stranou konverzace o tom, že singletony jsou špatné, jak je implementujete v Node?

Můžeme využít funkci mezipaměti modulů, tj. require ukládá moduly do mezipaměti. Máme například proměnnou b, kterou exportujeme s hodnotou 2:

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

Poté v souboru skriptu (který importuje modul) zvyšte hodnotu b a importujte modul main :

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

Modul main importuje module znovu, ale tentokrát hodnota b není 2, ale 3!

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

Nový problém:moduly jsou ukládány do mezipaměti na základě jejich vyřešeného názvu souboru. Z tohoto důvodu název souboru přeruší ukládání do mezipaměti:

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

Nebo různé cesty poruší ukládání do mezipaměti. Řešením je použít global

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

Zvažte tento příklad, který mění naše milované console.log z výchozí bílé na alarmující červenou:

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

Tento modul musíte vyžadovat jednou a všechny vaše záznamy budou červené. Nemusíte ani nic vyvolávat, protože nic neexportujeme.

Globální použití je mocné... ale anti-pattern, protože je velmi snadné pokazit a přepsat něco, co používají jiné moduly. Proto byste o tom měli vědět, protože můžete použít knihovnu, která se opírá o tento vzorec (např. vývoj řízený chováním), ale používejte ji střídmě, pouze v případě potřeby.

Je velmi podobný prohlížeči window.jQuery = jQuery vzor. Nicméně v prohlížečích, které moduly nemáme, je lepší používat explicitní exporty v Node než používat globals.

Import složek

Pokračujeme v importu, v Node je zajímavá funkce, která umožňuje importovat nejen soubory JavaScript/Node nebo soubory JSON, ale celé složky.

Import složky je abstraktní vzor, ​​který se často používá k uspořádání kódu do balíčků nebo zásuvných modulů (nebo modulů – zde synonymum). Chcete-li importovat složku, vytvořte index.js v této složce s module.exports úkol:

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

Poté v hlavním souboru můžete importovat složku pod názvem:

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

Všechny vlastnosti v index.js jako uživatelé, účty atd. budou vlastnosti routes v main.js . Vzor importu složky používají téměř všechny moduly npm. Existují knihovny pro automatický export VŠECH souborů v dané složce:

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

Funkční tovární vzor

V Node nejsou žádné třídy. Jak tedy uspořádat svůj modulární kód do tříd? Objekty dědí z jiných objektů a funkce jsou také objekty.

Poznámka:Ano, v ES6 existují třídy, ale nepodporují vlastnosti. Čas ukáže, zda jsou dobrou náhradou pseudoklasického dědictví. Vývojáři uzlů dávají přednost vzoru továrny funkcí pro jeho jednoduchost před neohrabaným pseudoklasickým.

Řešením je vytvořit továrnu na funkce aka funkční vzor dědičnosti. V něm je funkce výrazem, který přebírá možnosti, inicializuje a vrací objekt. Každé vyvolání výrazu vytvoří novou instanci. Instance budou mít stejné vlastnosti.

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

Na rozdíl od pseudoklasiky nebudou metody z prototypu. Každý nový objekt bude mít svou vlastní kopii metod, takže se nemusíte bát, že by změna v prototypu ovlivnila všechny vaše instance.

Někdy stačí použít pseudoklasické (např. pro emitory událostí), pak je tu inherits . Použijte jej takto:

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

Injekce závislosti uzlů

Tu a tam máte nějaké dynamické objekty, které potřebujete v modulech. Jinými slovy, v modulech existují závislosti na něčem, co je v hlavním souboru.

Například při použití čísla portu ke spuštění serveru zvažte vstupní soubor Express.js server.js . Má modul boot.js který potřebuje konfigurace app objekt. Implementace boot.js je přímočará jako export funkce a předejte app :

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

Funkce, která vrací funkci

boot.js soubor ve skutečnosti používá jiný (pravděpodobně můj nejoblíbenější) vzor, ​​který jednoduše nazývám funkcí, která vrací funkci. Tento jednoduchý vzor vám umožňuje vytvářet různé režimy/verze vnitřní funkce, abych tak řekl.

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

Jednou jsem četl blogový příspěvek, kde se tento vzor jmenoval monáda, ale pak mi jeden naštvaný fanoušek funkcionálního programování řekl, že to není monáda (a také se na to zlobil). No dobře.

Vzor pozorovatele v uzlu

Přesto je obtížné spravovat zpětná volání i s moduly! Máte například toto:

  1. Modul Job provádí úkol.
  2. Do hlavního souboru importujeme úlohu.

Jak určíme zpětné volání (nějaká budoucí logika) při dokončení úlohy úlohy? Možná předáme zpětné volání modulu:

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

A co vícenásobná zpětná volání? Není příliš škálovatelný vývoj?

Řešení je poměrně elegantní a ve skutečnosti se hodně používá zejména v modulech core Node. Seznamte se se vzorem pozorovatele s emitory událostí!

Toto je náš modul, který vysílá událost done až bude vše hotové:

// 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

V hlavním skriptu můžeme přizpůsobit co dělat až bude práce hotová.

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

Je to jako zpětné volání, jen lepší, protože můžete mít více událostí a můžete je odstranit nebo spustit jednou.

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

30sekundový souhrn

  1. Zpětná volání
  2. Pozorovatel
  3. Singleton
  4. Pluginy
  5. Middleware
  6. Spousta dalších věcí?

Další studie

Je zřejmé, že existuje více vzorů, jako jsou proudy. Správa asynchronního kódu je zcela nový soubor problémů, řešení a vzorů. Tato esej je však již dostatečně dlouhá. Děkujeme za přečtení!

Začněte s těmito základními vzory uzlů a použijte je tam, kde je to potřeba. Chcete-li zvládnout Node, podívejte se na své oblíbené moduly; jak implementují určité věci?

To jsou věci, které stojí za to si prohlédnout pro další studium:

  • async a neo-async :Skvělé knihovny pro správu asynchronního kódu
  • Sliby:Přijďte s ES6
  • Generátory:Slibné
  • Asynchronní čekání:Pěkný obal pro sliby, které brzy přijdou
  • hooks :Modul vzoru háčků
  • Kniha Node Design Patterns není moje, právě ji čtu.