009 Сравнение сред выполнения JS
NodeJS имеет своё собственное API, которое несовместимо с браузером, поэтому могут быть некоторые сложности из-за несовместимости со стандартным JS (в браузере).
Так же нода имеет свои глобальные переменные, так как она работает непосредственно с ПК
Так же существует множество других сред исполнения, которые позволяют запустить JS на сервере и один из них - Deno.
Эта технология пока молода и коммьюнити не такое и большое.
010 Запускаем код и REPL
REPL-режим - это режим, при котором мы можем запустить определённую среду в консоли
Запускается REPL в ноде командой:
node
И работать в этом режиме так же как и в браузере
Для примера создадим файл app.js
, в котором опишем небольшую логику чтения данных из файла:
fs = require("fs");
const data = fs.readFileSync("./data.txt");
console.log(data.toString());
011 Модули в JavaScript
Плюсы модулей:
История выраженности нужности импортов достаточно небольшая и она состояла из трёх этапов:
- IIFE функции
- Второй стандарт появился в NodeJS и работал через
require
. Для работы в браузере он требовал сборку в один файл (через сборщики модулей) - Последняя версия стандарта появилась сначала для работы на фронте, но в последние несколько лет её можно использовать и на бэке
Самые первые импорты - IIFE. Это функции, которые вызвались самостоятельно. Внутри них происходил не просто самовызов на странице пользователя, а взаимное связывание и присвоение отдельных компонентов функции к главному скриптовому файлу. Порядок подключения был важен.
Второй способ - CommonJS . В первую очередь, они работают именно на бэке. Для работы на фронте им был нужен сборщик модулей, который собирал бы на ноде файл в один модуль.
И последний способ импорта - ES Modules. Особенность ноды заключается в том, чтобы использовать современные импорты, нужно использовать расширение .mjs
Главным преймуществом CommonJS является то, что его можно использовать в любом месте кода, а так же в условиях (что позволяет нам не подключать какой-либо модуль, если нам то не нужно) Главным преймуществом ES Modules является то, что он подключает только указанные части модуля, а не его полностью (если то не требуется) + он асинхронен (не останавливает выполнение приложения, если импорт не подгрузился)
Подключить модули в NodeJS можно тремя способами:
012 CommonJS Modules
Напишем небольшой пример, где мы будем определять владельца кольца.
С помощью module.exports = { имена экспортов };
мы определяем, что будем экспортировать из модуля
characters.js
let characters = [
{ name: 'Фродо', hasRing: false },
{ name: 'Бильбо', hasRing: false },
];
function steelRing(characters, owner) {
// тут функция конкретно изменяла вложенное в неё значение
characters.map(c => {
if (c.name == owner) {
c.hasRing = true;
} else {
c.hasRing = false;
}
});
}
module.exports = { characters, steelRing };
Уже тут через деструктуризацию и функцию require()
, мы получаем доступ к экспортированным объектам модуля
app.js
const { characters, steelRing } = require('./characters.js');
steelRing(characters, 'Фродо');
for (const c of characters) {
console.log(c);
}
И дальше нужно переписать код так, чтобы функция работала правильно и не мутировала внутри себя значение, а возвращала изменённое значение вовне.
characters.js
let characters = [
{ name: 'Фродо', hasRing: false },
{ name: 'Бильбо', hasRing: false },
];
function steelRing(characters, owner) {
return characters.map(c => {
if (c.name == owner) {
c.hasRing = true;
} else {
c.hasRing = false;
}
});
}
module.exports = { characters, steelRing };
app.js
const { characters, steelRing } = require('./characters.js');
// Нужно создать новую переменную, которая будет хранить старую версию массива и принимать изменённый
let myChars = characters;
myChars = steelRing(myChars, 'Фродо');
for (const c of characters) {
console.log(c);
}
так же экспортировать можно непосредственно и функцию прямо на месте её создания
// characters.js
module.exports = function log() {
console.log('log');
}
// app.js
const log = require('./characters.js');
log();
Круговые зависимости, когда у нас есть импорты и экспорты между двумя файлами - могут привести к ошибке (потому как интерпретатор будет пытаться достучаться до элемента, которого пока не существует)
И тут далее можно увидеть пример, когда мы подгружаем модуль в проект по условию (при выполнении условия - модуль подгружается)
// characters.js
console.log('characters.js загружен');
module.exports = function log() {
console.log('log');
}
// app.js
const a = 1;
if (a > 0) {
const log = require('./characters.js');
log();
} else {
console.log('Меньше 0');
}
013 ES Modules
В обычной модульной системе, мы можем с помощью ключевого слова export
экспортировать объект из модуля
characters.mjs
export const characters = ["Фродо", "Бильбо"];
export function greet(character) {
console.log(`Поздравляю, ${character}!`);
}
Далее через конструкцию import
импортировать его в другом файле
app.mjs
import { characters, greet } from "./characters.mjs";
for (const c of characters) {
greet(c);
}
Так же мы можем импортировать сразу все объекты через конструкцию * as имя
app.mjs
import * as char from "./characters.mjs";
for (const c of char.characters) {
char.greet(c);
}
Так же мы имеем дефолтный экспорт export default
- это принимаемое значение из модуля по умолчанию
characters.mjs
export const characters = ["Фродо", "Бильбо"];
export function greet(character) {
console.log(`Поздравляю, ${character}!`);
}
export default function log() {
console.log("log");
}
app.mjs
import log, { characters, greet } from "./characters.mjs";
for (const c of characters) {
greet(c);
}
log();
Так же нам ничто не мешает объединять конструкции и импортировать дефолтную функцию + все объекты модуля
app.mjs
import log, * as char from "./characters.mjs";
for (const c of char.characters) {
char.greet(c);
}
log();
Дальше мы можем столкнуться с такой проблемой при большом количестве импортов, что имена совпадают у объектов. Чтобы решить проблему, можно переопределить импортируемый объект
app.mjs
import log, {characters, greet as hello} from "./characters.mjs";
for (const c of characters) {
hello(c);
}
log();
Так же присутствует в ноде асинхронный импорт модуля, который позволяет произвести es module испорт из любого места. Сразу нужно сказать, что он возвращает промис, поэтому его нужно дожидаться
app.mjs
async function main() {
const { characters, greet } = await import("./characters.mjs");
for (const c of characters) {
greet(c);
}
}
main();
Так же никто нам не мешает ловить и обрабатывать ошибки при данных импортах
app.mjs
async function main() {
try {
// ошибка при импорте
const { characters, greet } = await import("./charactersыфвв.mjs");
for (const c of characters) {
greet(c);
}
} catch (error) {
console.log("Ошибка");
}
}
main();
По возможности стоит пользоваться конкретно таким импортом, потому что он позволяет импортировать только то, что нужно, он асинхронен и удобен
014 Глобальные переменные
Глобальные переменные - это переменные, доступ к которым мы можем получить из любого места приложения без дополнительных импортов и вызовов.
Модули делятся на глобальные и глобальные модульные. Конкретно вторые зависят от того модуля, из которого мы их вызваем
Модуль global
- это корневой объект всех переменных. Мы можем записать global.console
, а так же и просто console
- Группа глобальных объектов, которые не категоризируются
preformance
- помогает определить производительностьBuffer
- читает набор байтAbortController
- позволяет прерывать API, основанные на промисахqueueMicrotask
- это планировщик задач дляV8
WebAssembly
- хранит модулиWebAssembly
- Группа таймеров
- Группа работы с URL
- Группа работы с сообщениями (используются в вебсокетах)
- Группа работы с событиями
- Группа работы с текстом (например, перевод текста в другую кодировки и обратно)
- Группа модулей
__dirname
- получает путь до модуля__filename
- получает имя файлаexports
module
require()
__dirname
возвращает нам просто путь до нашего модуля
А уже __filename
возвращает нам путь с именем файла
console.log(__dirname);
console.log(__filename);
015 Events
Модуль Event Emitter - это модуль, который позволяет обмениваться сообщениями между модулями внутри приложения на ноде. Мы как и на фронте можем подписаться на определённый ивент и при срабатывании ивента передавать определённое сообщение в другой модуль
Есть два способа генерировать события для наших модулей системы, но стоит пользоваться только одним - Event Emitter, который полностью покрывает все потребности в повседневной разработке
Через функции addListener
и on
мы подписываем слушателя на определённые события и можем выполнять их через emit
.
Чтобы удалить события, нам нужно воспользоваться removeListener
, removeAllListeners
или off
. Удалять события важно, так как они отвечают за огромные утечки памяти.
app.js
// добавляем модуль ивентов
const EventEmitter = require("events");
// создаём отдельный инстанс события (их может быть много)
const myEmitter = new EventEmitter();
// функция подключения к базе данных
const lodDBConnection = () => {
console.log("DB Connected");
};
// тут мы создаём новое событие, при триггере которого будет срабатывать подключение к базе данных
myEmitter.addListener("connected", lodDBConnection);
// это сам триггер этого события
myEmitter.emit("connected"); // "DB Connected"
// так же нам стоит удалять подписки на ивент листенеры, так как это является одной из основных проблем утечек памяти программы
myEmitter.removeListener("connected", lodDBConnection);
// myEmitter.off("connected", lodDBConnection); // отклюает листенер с события
// myEmitter.removeAllListeners("connected"); // удалит все листенеры с события
// уже этот вызов листенера не сработает, так как мы убрали листенер с события
myEmitter.emit("connected"); // ничего не произойдет
Таким образом мы можем передавать данные в наши листенеры
app.js
// Создание листенера с приёмом данных
myEmitter.on("message", (data) => {
console.log(`Получено: ${data}`);
});
// триггерим листенер с передачей в него данных
myEmitter.emit("message", {
name: "Valery",
surname: "Lvov",
});
Так же функция once()
позволяет создать событие, которое сработает только однажды, а потом удалится
app.js
// Это событие сработает только один раз и потом удалится
myEmitter.once("off", () => {
console.log("Я отключился");
});
myEmitter.emit("off"); // "Я отключился"
myEmitter.emit("off"); // ничего не произойдёт
Мы можем определить своё собственное максимальное количество слушателей на наше событие и получить это количество
app.js
console.log(myEmitter.getMaxListeners()); // 10
myEmitter.setMaxListeners(1); // устанавливаем максимальное число листенеров на наше событие
console.log(myEmitter.getMaxListeners()); // 1
console.log(myEmitter.listenerCount("message")); // 1 - так как этот листенер до сих пор существует
console.log(myEmitter.listenerCount("off")); // 0 - так как уже отработал и удалился
Так же у листенеров есть порядок выполнения. Самыми первыми выполняются те, что находятся в начале массива. Мы можем влиять на выполнение этих листенеров определёнными методами.
// Создание листенера с приёмом данных
myEmitter.on("message", (data) => {
console.log(`Получено: ${data}`); // вызовется вторым
});
myEmitter.prependListener("message", () => {
console.log("Prepand"); // вызовется первым
});
// триггерим листенер с передачей в него данных
myEmitter.emit("message", {
name: "Valery",
surname: "Lvov",
});
console.log(myEmitter.listeners("message")); // выведет массив листенеров данного события
Тут мы можем вывести все имена наших имеющихся событий
console.log(myEmitter.eventNames()); // выведет массив имён событий
Так же ивенты можно использовать для обработки ошибок в приложении
// Обработка ошибок
myEmitter.on("error", (err) => {
console.log(`Error ${err.message}`);
});
myEmitter.emit("error", new Error("BOOOOOOOOOOOOOOOOOOM!"));
Использование Event Target (не рекомендуется):
// инициализация ивента
const target = new EventTarget();
// какая-то функция
const logTarget = () => {
console.log("That's a target");
};
// добавление слушателя
target.addEventListener("connected", logTarget);
// вызов ивента
target.dispatchEvent(new Event("connected"));