| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Вы уже успели познакомиться с node.js в 3 лабораторной работе. но чтобы освежить в памяти вспомним снова.
JavaScript код, который мы пишем при обычной клиентской разработке, исполняется в браузере. В браузере у нас есть компилятор JavaScript кода в машинных код, в Google Chrome это движок [V8][v8]. Если мы хотим запускать код на нашем компьютере, а не в браузере, то нам нужно использовать [Node.js][node]. Node - это программная платформа, которая позволяет компилировать JavaScript код в машинный на нашем компьютере. На самом деле, за основу Node.js взять тот же самый движок [V8][v8]. Node.js добавляет возможность нам взаимодействовать с утройствами ввода-вывода, подключать внешние библиотеки.
Писать на Node.js можно практически все: веб-сервера, десктопные и мобильные приложения, и даже программировать микроконтроллеры. В данной лабораторной работе мы остановимся на первом варианте, наиболее часто используемом, написании веб-серверов. Чаще всего на Node.js пишут именно BFF (Backend-For-Frontend), но на этом мы остановимся подробнее позже.
Также очень часто на Node.js пишут различные скрипты для того, чтобы автоматизировать различные процессы в инфраструктуре клиентской разработки. Например: автоматизация деплоя, различные автогенерации и тд.
Рассмотрим несколько примеров скриптов, чтобы поподробнее посмотреть, как именно работает Node.js.
const {writeFileSync, readFileSync} = require('fs');
const path = require('path');
function doMusic() {
return 'listen to radiohead, guys';
}
writeFileSync(path.join(__dirname, 'wisdom-book.txt'), doMusic());
Как мы видим синтаксис идентичен синтаксису обычного javascript. В Node.js используется модульная система CommonJS. Подробнее про модульные системы можно почитать здесь.
Еще пример:
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
function countNumbers(str){
let count = 0;
for (let i = 0; i < str.length; ++i) {
if (!Number.isNaN(Number.parseInt(str))) {
count++;
}
}
return count;
}
rl.once('line', (line) => {
console.log(countNumbers(line));
rl.close();
});
Скрипт выше принимает строку из ввода консоли и выводит сколько в ней цифр.
У Node.js большое число встроенных библиотек, в примере выше мы видим библиотеку readline для работы с вводом/выводом. В предыдущем примеры мы использовали библиотеку fs для работы с файловой системой.
Также в Node.js есть стандратная библиотека для написания http-серверов. Давайте ее попробуем. Создаем файл server.js. Подключаем библиотеку:
const http = require("http");
Далее в переменных объявляем на каком хосте и порте будет крутиться наш сервер. В данном случае это наш локальный адрес и 8000 порт:
const host = 'localhost';
const port = 8000;
Теперь объявим функцию, которая будет принимать запросы и отправлять ответы. У этой функции есть определенный формат: принимает два аргумента req - http-запрос, res - http-ответ. В примере внизу наш обработчик на любой запрос отдает статус 200 ОК и в теле ответ Курс ПСП такой крутой!.
const handler = (req, res) => {
res.writeHead(200);
res.end('Курс ПСП такой крутой!');
};
Наконец объявим сервер и заиспользуем созданные нами функцию и переменные:
const server = http.createServer(handler);
server.listen(port, host, () => {
console.log(`Сервер запущен по адресу http://${host}:${port}`);
});
Итого получаем скрипт:
const http = require("http");
const host = 'localhost';
const port = 8000;
const handler = (req, res) => {
res.writeHead(200);
res.end('Курс ПСП такой крутой!');
};
const server = http.createServer(handler);
server.listen(port, host, () => {
console.log(`Сервер запущен по адресу http://${host}:${port}`);
});
Запускаем наш сервер командой:
node server.jsВидим, в консоли вывод:
Сервер запущен по адресу http://localhost:8000
И далее консоль зависает. В этом процессе начинает бесконечно слушать подключения наш http-сервер. Откроем новое окно консоли (не закрывая предыдущее) и пишем:
curl http://localhost:8000И видим ответ:
Курс ПСП такой крутой!Поздравляю! Вы написали свой http-сервер на node.js.
В реальности на веб-серверах у нас бывает гораздо больше обработчиков, и приходится выстраивать очень сложную структуру с большим количеством бизнес-логики. Для упрощения процесса разработки написали фреймворк express.
Т.к. express это библиотека, ее необходимо устанавливать отдельно командой:
npm i expressТеперь перепишем скрипт выше и получим:
const express = require('express');
const app = express(); // 1
const host = 'localhost';
const port = 8000;
const handler = (req, res) => {
res.writeHead(200);
res.end('ПСП такой крутой!');
};
app.all('*', handler); // 2
app.listen(port, host, () => { // 3
console.log(`Сервер запущен по адресу http://${host}:${port}`);
});
В комментарии помеченном // 1 мы создаем приложение. Далее в // 2 мы объявляем, что на все запросы по любому пути (путь указывается первым аргументом в определенном формате) обрабатывай с помощью функции handler.
Теперь давайте уберем помеченный обработчик // 2 создадим побольше разных обработчиков, и потестируем их:
app.get('/music', (req, res) => {
res.end('Я слушаю только Radiohead!');
});
app.get('/course', (req, res) => {
res.json({ course: 'ПСП' });
});
Теперь у нас есть 2 обработчика по 2-м разным адресам /music и /course.
Можем протестировать их при помощи утилиты curl и увидеть:
$ curl http://localhost:8000/music
Я слушаю только Radiohead!%
$ curl http://localhost:8000/course
{"course":"ПСП"}В 3 лабораторной работе в приложении давался пример, где данные возвращала функция getData(). Давайте вынесем отправку данных на сервер, а с клиентской части будем делать fetch на написанный нами сервер.
Создадим файл stocks.json рядом с server.js. Это будет нашей своеобразной базой-данных. Поместим туда вот эти данные:
[
{
"id": 1,
"src": "https://i.pinimg.com/originals/c9/ea/65/c9ea654eb3a7398b1f702c758c1c4206.jpg",
"title": "Акция",
"text": "Такой акции вы еще не видели 1"
},
{
"id": 2,
"src": "https://i.pinimg.com/originals/c9/ea/65/c9ea654eb3a7398b1f702c758c1c4206.jpg",
"title": "Акция",
"text": "Такой акции вы еще не видели 2"
},
{
"id": 3,
"src": "https://i.pinimg.com/originals/c9/ea/65/c9ea654eb3a7398b1f702c758c1c4206.jpg",
"title": "Акция",
"text": "Такой акции вы еще не видели 3"
}
]И теперь создадим 2 обработчика: /stocks для получения всех акций, и /stock/:id для получения одной акции по ее id.
Чтобы читать файла напишем функцию readJson, которая умеет читать файл и парсить его в объект в javascript:
const readJson = (fileName) => {
const file = fs.readFileSync(path.join(__dirname, fileName), "utf8");
const json = JSON.parse(file);
return json;
};
Теперь обработчик получения всех акций:
const storageName = 'stocks.json';
app.get('/stocks', (req, res) => {
const stocks = readJson(storageName);
res.send(stocks);
});
Сначала читаем данные из файла, и затем их отдаем. Теперь напишем обработчик для получения одной акции по ее id. Здесь будет на порядок сложнее:
app.get('/stocks/:id', (req, res) => {
const id = req.params.id; // 1
const numberId = Number.parseInt(id);
if (Number.isNaN(numberId)) { // 2
res.status(400).send({status: 'Bad Request', message: 'id must be number!'});
}
const stocks = readJson(storageName);
const stock = stocks.find((value) => { // 3
return value.id === numberId;
});
if (stock) { // 4
res.send(stock);
} else {
res.status(404).send({status: 'Not Found', message: `not found stock with id ${numberId}`});
}
});
В // 1 мы достаем из url запроса параметр id. Т.е. если, например, будет сделан запрос на адрес http://localhost:8000/stocks/2, то req.params.id вернет нам 2 и тд. Название для поля id берет из адреса который мы указали: /stocks/:id. Т.е. в теории мы могли бы его поменять на что-нибудь вроде /stocks/:stock и доставать из запроса так req.params.stock.
Далее в // 2 мы проверяем, что id, который нам передали - это число. И если нет, то отдаем ошибку со статусом 400.
В // 3 мы, уже считав данные из файла, используем метод find для поиска элемента с таким id. Если такой элемент не найдется, то в переменную stock запишется undefined.
И уже в // 4 проверяем смогли ли мы найти акцию с переданным id. Если не смогли, то отдаем ошибку с 404 статусом.
Весь код представлен ниже:
const fs = require('fs');
const path = require('path');
const express = require('express');
const app = express(); // 1
const host = 'localhost';
const port = 8000;
const readJson = (fileName) => {
const file = fs.readFileSync(path.join(__dirname, fileName), "utf8");
return JSON.parse(file);
};
const storageName = 'stocks.json';
app.get('/stocks', (req, res) => {
const stocks = readJson(storageName);
res.send(stocks);
});
app.get('/stocks/:id', (req, res) => {
const id = req.params.id;
const numberId = Number.parseInt(id);
if (Number.isNaN(numberId)) {
res.status(400).send({status: 'Bad Request', message: 'id must be number!'});
}
const stocks = readJson(storageName);
const stock = stocks.find((value) => {
return value.id === numberId;
});
if (stock) {
res.send(stock);
} else {
res.status(404).send({status: 'Not Found', message: `not found stock with id ${numberId}`});
}
});
app.listen(port, host, () => { // 3
console.log(`Сервер запущен по адресу http://${host}:${port}`);
});
У нас готов полноценный бэкенд на node.js! Но давайте теперь его немного причешем, и добавим в проект структурированности.
Основу для всех архитектур бэкенда является разделение на отображение, бизнес логику и работу с данными. В зависимости от различных подходов в архитектуры могут вноситься какие-то различные детали и дополнительные слои, но эти 3 слоя - это база.
Давайте разделим нашу сущность акций на четыре слоя: controller (отображение или сетевой слой), service (бизнес-логика), repository (низкоуровневая работа с базой), DAO (более высокоуровневая работа с данными из БД).
Создаем папки internal, db, modules. Далее внутри папки internal создаем папку stocks и внутри нее 4 файла: StocksController.js, StocksService.js, StocksRespository.js, StocksDAO.js и index.js.
Внутрь modules кладем файл DBConnector.js - это базовый класс, умеющий читать и писать в json-файл (нашу импровизированную БД).
В директорию db кладем наши json файлы, теперь все они будут лежать там.
Давайте теперь разберем каждый класс по отдельности:
DBConnector.js
const fs = require('fs');
const path = require('path');
class DBConnector {
constructor(filename) {
this.filename = filename;
}
readFile() {
return fs.readFileSync(path.join(process.cwd(), 'db', this.filename), "utf8");
}
writeFile(data) {
fs.writeFileSync(path.join(process.cwd(), 'db', this.filename), data, "utf8");
}
}
module.exports = {
DBConnector,
};
При создании инстанса класса DBConnector пробрасывает путь до файла. Также доступны 2 метода для чтение содержимого из файла, и записи в него символов.
Далее рассмотрим StocksRepository.js:
const {DBConnector} = require('../../modules/DBConnector');
class StocksRepository {
static db = new DBConnector('stocks.json');
static read() {
const file = this.db.readFile();
return JSON.parse(file);
}
static write(json) {
this.db.writeFile(JSON.stringify(json));
}
}
module.exports = {
StocksRepository,
}
StocksRepository - это можно считать статический класс (или singleton). Доступ к его методам и полям есть только у самого класс, а не у его инстансов. Здесь мы создаем 1 коннект к нашему файлу stocks.json и объявляем методы, которые умеют читать из базы и писать в нее в формате json.
Это у нас довольно низкоуровневый слой работы с базой. Он умеет только читать и писать какие-то абстрактные данные в файл stocks.json. Теперь рассмотрим, как этой сущностью управляют более высокоуровневые элементы архитектуры.
StocksDAO.js
const {StocksRepository} = require('./StocksRepository');
class StockDAO {
constructor(id, src, title, text) {
this.id = id;
this.src = src;
this.title = title;
this.text = text;
}
static _validateId(id) {
const numberId = Number.parseInt(id);
if (Number.isNaN(numberId)) {
throw new Error('invalidate id');
}
}
static _validate(stock) {
if (
stock.id === undefined ||
stock.src === undefined ||
stock.title === undefined ||
stock.text === undefined
) {
throw new Error('invalidate stock data');
}
this._validateId(stock.id);
}
static find() {
const stocks = StocksRepository.read();
return stocks.map(({id, src, title, text}) => {
return new this(id, src, title, text);
});
}
static findById(id) {
this._validateId(id);
const stocks = StocksRepository.read();
const stock = stocks.find((s) => s.id === id);
return new this(stock.id, stock.src, stock.title, stock.text);
}
static insert(stock) {
this._validate(stock);
const stocks = StocksRepository.read();
StocksRepository.write([...stocks, stock]);
return new this(stock.id, stock.src, stock.title, stock.text);
}
static delete(id) {
this._validateId(id);
const stocks = StocksRepository.read();
const filteredStocks = stocks.filter((s) => s.id !== id);
StocksRepository.write(filteredStocks);
return filteredStocks.map(({id, src, title, text}) => {
return new this(id, src, title, text);
});
}
toJSON() {
return {
id: this.id,
src: this.src,
title: this.title,
text: this.text,
}
}
}
module.exports = {
StockDAO,
}
В отличие от репозитория, тут уже у нас есть осмысленные методы, которые выполняют какие-то четкие действия связанные с сущностью акций. Например, добавление новой записи, поиск ее по id, удаление записи по id и тд.
Наш класс организован таким образом, что его методы привязаны только к самому классу, а поля же привязаны только к его инстансу. Что это значит? А то, что когда мы захотим вызвать какой-то метод, мы сможем это сделать вот так: const stock = StocksDAO.findById(id), и т.к. findById возвращает самого себя return new this(stock.id, stock.src, stock.title, stock.text), то создается инстанс класса StocksDAO, который имеет доступ ко всем полям (id, src, title, text), а также методу toJSON().
Преимущества такого подхода в том, что наш класс умеет не только запрашивать данные, но также хранить их и менеджерить.
Теперь посмотрим как DAO используется в StocksService.js:
const {StockDAO} = require('./StockDAO');
class StocksService {
static findStocks(id) {
if (id !== undefined) {
return StockDAO.findById(id).toJSON();
}
return StockDAO.find().map((stock) => stock.toJSON());
}
static addStock(stock) {
return StockDAO.insert(stock).toJSON();
}
static deleteStock(id) {
return StockDAO.delete(id).map((stock) => stock.toJSON());
}
}
module.exports = {
StocksService,
}
Класс StocksService также является статическим. Он работает напрямую с данными и абсорбирует бизнес-логику более высокого уровня. Для данной сущности нет хорошего примера, но это могли бы быть какие-то вычисления, разграничения прав и различная логика обработки данных.
Теперь наконец перейдем к сетевому слою, и рассмотрим файл StocksController.js:
const {StocksService} = require('./StocksService');
class StocksController {
static findStocks(req, res) {
try {
res.send(StocksService.findStocks());
} catch (err) {
res.status(400).send({status: 'Bad Request', message: err.message})
}
}
static findStockById(req, res) {
try {
const id = Number.parseInt(req.params.id);
res.send(StocksService.findStocks(id))
} catch (err) {
res.status(400).send({status: 'Bad Request', message: err.message})
}
}
static addStock(req, res) {
try {
res.send(StocksService.addStock(req.body));
} catch (err) {
res.status(400).send({status: 'Bad Request', message: err.message})
}
}
static deleteStock(req, res) {
try {
const id = Number.parseInt(req.params.id);
res.send(StocksService.deleteStock(id));
} catch (err) {
res.status(400).send({status: 'Bad Request', message: err.message})
}
}
}
module.exports = {
StocksController,
};
Класс StocksController является статическим. Каждый его метод принимает req и res, для того чтобы принимать запросы, и отдавать на них ответы. Данная сущность полностью изолирует на себе работу с сетью, и дальше все методы передаются внутрь express.router. Подробнее мы это можем увидеть в файле index.js внутри папки stocks.
const express = require('express');
const {StocksController} = require('./StocksController');
const router = express.Router();
router.get('/', StocksController.findStocks);
router.get('/:id', StocksController.findStockById);
router.post('/', StocksController.addStock);
router.delete('/:id', StocksController.deleteStock);
module.exports = router;
Здесь мы создаем роутер, и назначаем каждый метод нашего контроллера на определенный http-метод и url.
Далее в самом главном файле index.js в корне проекта, мы используем этот роутер под общим урлом /stocks:
const express = require('express');
const stocks = require('./internal/stocks');
const app = express();
const host = 'localhost';
const port = 8000;
app.use(express.json());
app.use('/stocks', stocks);
app.listen(port, host, () => {
console.log(`Сервер запущен по адресу http://${host}:${port}`);
});
Теперь наше API умеет читать акции, создавать, а также удалять их.
Для сильных ребят предоставляется возможность реализовать свой BFF. Т.е. перенести логику работы с VK API на бэкенд, который будет как бы прослойкой между клиентским приложением и VK API. Чтобы правильно это сделать, напишите в телеграм @nathan_kith. Я подскажу, как это лучше сделать и дам рекомендации.
Что вы за это получите? Полезный опыт Возможность попасть на стажировку в Яндекс. Пишите в телеграм (nathan_kith)!
| Back | FazBrowse Home | New Git URL |