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', (err, res) => {
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
}
}