Geekbrains Node JS. Урок 2. Консольные программы

Асинхронность

В качестве примера асинхронного кода используем следующий код

function getRequest() {
  setTimeout(() => {
    const res = 10;
    return res;
  }, 1000)
}

Как получить результат res из функции getRequest?

Вариант 1. Используем callback - функцию обратного вызова

function getRequest(callbackFn) {
  setTimeout(() => {
    const res = 10;
    callbackFn(null, res) 
  }, 1000)
}

getRequest((err, res) => {
  if(err) console.log(err)
  console.log(res)
})

Использование callback - распространённая практика. Но если таких функций много и они связаны между собой, их использование затруднено/ Может получиться большая вложенность функций. То. что называют callback hell - ад из колбеков. Такой код сложно поддерживать, в нём сложно находить ошибки, и так писать не принято.
Поэтому появились промисы.

Вариант 2. Используем промисы

const promice = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve();
  }, 1000);
});

promice.then(
  () => {}, // if resolve
  () => {} // if reject
)

Промисы удобны тем, что каждый следующий промис вызывается не внутри предыдущего, а после него через then по цепочке

const promice = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve({name: 'Oleg'});
  }, 1000);
});

promice.then(
  (user) => {return user}, // if resolve
  () => {} // if reject
).then(user => {
  console.log(user)
}) // {name: "Oleg"}

Функции обратного вызова и промисы есть как в браузере, так и в node.js
Но в node.js используется ещё один подход к обработке асинхронных запросов, который позже появился и в браузере тоже. Речь про Event Emitter

Event Emitter

Если JS - объектно-ориентированный язык, и почти всё, с чем мы сталкиваемся в JS, является объектом, то Node.js - событийно-ориентированный язык.

Две наиболее важные функции в любом объекте EventEmitter — это .on и .emit. Функция .on прослушивает события конкретного типа, а .emit — их генерирует. Подробнее про EventEmitter

Создадим класс чайник. который будет вскипать через 1 секунду и сообщать об этом

const EventEmitter = require('events')

class Kettler extends EventEmitter {
    start() {
        setTimeout(() => {
          this.emit('ready')
        }, 1000)
    }
}

const k = new Kettler()
k.start()

k.on('ready', () => {
    console.log('kettle boiled')
});


Модуль events встроенный, устанавливать его не нужно
Создаём class Kettler
В нём создаём метод start() который через 1000 мс генерирует событие 'ready'
Создаём экземпляр класса
При наступлении события 'ready' выводим в консоль уведомление.

Попробуем отследить событие создание чайника и вывести в консоль уведомление об этом.

Такой код не работает

const EventEmitter = require('events')

class Kettler extends EventEmitter {
    constructor() {
      super()
      this.emit('init')
    }
    start() {
        setTimeout(() => {
          this.emit('ready')
        }, 1000)
    }
}

const k = new Kettler()
k.start()

k.on('init', () => {
    console.log('kettle created')
});
k.on('ready', () => {
    console.log('kettle boiled')
});

Так работает

const EventEmitter = require('events')

class Kettler extends EventEmitter {
    constructor() {
      super()
      setTimeout(() => {
        this.emit('init')
      }, 0)
  }
    start() {
        setTimeout(() => {
          this.emit('ready')
        }, 1000)
    }
}

const k = new Kettler()
k.start()

k.on('init', () => {
    console.log('kettle created')
});
k.on('ready', () => {
    console.log('kettle boiled')
});

В первом примере событие 'init' вначале создаётся . потом мы его начинаем прослушивать. Но слушать нечего - событие уже создано.
В отличие от него, событие 'ready' хоть и создаётся в соседней строчке кода, но оно асинхронное, а асинхронные события выполняются после основного цикла.
Решение - сделать событие 'init' асинхронным что переместит его выполнение в конец кода. Для этого во втором примере использован setTimeout с задержкой в 0 мс
Ещё один способ сделать код асинхронным - использовать  setImmediate

      setImmediate(() => {
        this.emit('init')
      })


setTimeout и setImmediate являются общими для js и node.js

Третий способ сделать код асинхронным используется только в node.js. Речь про метод process.nextTick

process.nextTick(() => {
this.emit('init')
})

В отличие от setTimeout и setImmediate process.nextTick гарантирует порядок выполнения кода

Таймеры


Код таймера

const interval = setInterval(() => {
    console.log(typeof(interval))
}, 1000)

Остановить его может функция clearInterval(interval)

Если код запустим в консоли браузера, результатом будет number
В node.js этот же код вернёт object

В node.js у interval есть ещё два метода interval.ref() interval.unref()

Метод interval.unref() отвязывает интервал от выполнения программы. Когда программа завершается, интервал останавливается

Консольные программы

Консольные программы на node.js писать очень просто и они используются как в широких масштабах, вроде написанного на node.js webpack, так и в виде небольших утилит для решения задач самого разработчика.

У консольного приложения нет внешнего графического интерфейса для ввода данных, а управлять им как-то нужно. Для этого используются параметры командной строки и переменные среды операционной системы - environment variable

Параметры командной строки

Все параметры командной строки в node.js попадают в глобальную переменную process метод argv

Выполним команду

console.log(process.argv)

// [
  'C:\\Program Files\\nodejs\\node.exe',
  'D:\\Node.js\\node-test\\index.js',
  'index.js'
]

результат - путь к исполняемому файлу node.exe и путь к тому файлу, который мы запустили

Если запустим файл с параметрами
npm run dev -3 x c:'abc' foo bar

получим вот такой результат

[
  'C:\\Program Files\\nodejs\\node.exe',
  'D:\\Node.js\\node-test\\index.js',
  'x',
  "c:'abc'",
  'foo',
  'bar',
  'index.js'
]

Парсить параметры командной строки помогает модуль minimist
Устанавливаем: npm i minimist
Вызываем

const minimist = require('minimist')
console.log(minimist(process.argv.slice(2)))

npm run dev -3 x c:'abc' foo bar

// { _: [ 'x', "c:'abc'", 'foo', 'bar', 'index.js' ] }

Одно из преимуществ minimist - он понимает алиасы - сокращения, которые используются для запуска команд. например, когда пишем npm i вместо npm install.

const minimist = require('minimist')
const argv = minimist(process.argv.slice(2), {
  alias: {
    h: 'help',
    i: 'install'
  }
})
console.log(argv)

node index -h // { _: [], h: true, help: true }

Переменные окружения

Переменные окружения, или переменные среды, или environment variable - ещё один способ работать с консольными программами.
Используются для того, чтобы на разных компьютерах (разработчика, тестировщика, пользователя) запускать программу с разными параметрами.

Чтобы приложение понимало на какой машине его запускают и как ему работать. Например, можно указать настройки подключения к базе данных или настройки логгирования.

Переменные окружения записываются в process.env

Потоки

у консольного приложения три потока
- поток ввода - по умолчанию клавиатура
- поток вывода - консоль console.log()
- поток ошибки - тоже консоль console.error()

Но эти потоки можно перенаправить
для этого пишем команду

node index 1>log.txt 2>error.txt

и в результате получаем два текстовых файла log.txt и error.txt в одном из которых текст Hello log в другом Hello error

Поток ввода

В node.js есть встроенный модуль readline
Подключаем, создаём интерфейс

const readline = require('readline')
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
})

Мы указали, что для ввода данных используем стандартный поток ввода (клавиатура), для вывода - стандартный поток вывода (консоль)

Напишем код

rl.on('line', (cmd=> {
  console.log(`You type ${cmd}`)
  if(cmd === 'exit') {
    rl.close()
  }
})

результат его работы

D:\Node.js\node-test>node index
111
You type 111
222
You type 222
exit
You type exit
завершение работы

Получение ответа на вопрос (аналогично prompt в браузере):

rl.question('What is your favorite food?', function(answer) {
  console.log('Oh, so your favorite food is ' + answer);
});

Пауза (блокирование ввода): rl.pause() .
Разблокирование ввода: rl.resume() .
Окончание работы с интерфейсом readline: rl.close() .

Файловая система

const fs = require('fs')
fs.readFile('./file.md''utf8', (errres=> {
  if(err) {
    console.log(err)
  }
  console.log(res)
})

Указываем путь к файлу, кодировку и функцию. в которую передаём два параметра - ошибку и результат чтения файла.

Кодировку можно не указывать, тогда результатом выполнения кода буде буфер с данными. Проблему решает res.toString()

Все модули node.js работают с функциями обратного вызова. Но есть возможность писать и на промисах. для этого есть встроенный модуль.

const fs = require('fs')
const {promisify} = require('util')
const promisifiedReadFile = promisify(fs.readFile) 
promisifiedReadFile('./file.md''utf8')
.then((data=> {
  console.log(data)
})

Домашнее задание. Игра "Орёл или решка"

const readlineSync = require('readline-sync');
let endGame = false

console.log("Hello! 。◕‿◕。\nWhat a lovely day! Let's play!\n");

while(!endGame) {
  const headsOrTails = readlineSync.question("Please type 1 or 0\n");
  if(Math.random() > 0.5 && headsOrTails === '1') {
    console.log("Congratulation! You win!\n");
  } else {
    console.log("Sorry, you lost\n");
  }
  const userEndGame = readlineSync.question("Finish the game? [Y(yes)/N(no)]\n");
  if(userEndGame.toLowerCase() === 'y') {
    endGame = true
  }
}