FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [View Raw Code]   [Original HTTPS Page]

JavaScript/tutorials/lab6/README.md at main · shipengineer/JavaScript · GitHub

Latest commit

 

History

History
661 lines (487 loc) · 27.1 KB

File metadata and controls

661 lines (487 loc) · 27.1 KB

Методические указания по выполнению лабораторной работы JavaScript

План

  1. Что такое Node.js?
  2. Возможности Node.js и способы применения в production-среде
  3. Небольшие примеры скриптов
  4. Пишем "голую" простую API
  5. Пишем API на express
  6. Добавляем в проект структуру (рефакторим код)
  7. Добавляем внешний клиент (то самое BFF)
  8. Что еще умеет Node.js

1. Что такое Node.js

Вы уже успели познакомиться с node.js в 3 лабораторной работе. но чтобы освежить в памяти вспомним снова.

JavaScript код, который мы пишем при обычной клиентской разработке, исполняется в браузере. В браузере у нас есть компилятор JavaScript кода в машинных код, в Google Chrome это движок [V8][v8]. Если мы хотим запускать код на нашем компьютере, а не в браузере, то нам нужно использовать [Node.js][node]. Node - это программная платформа, которая позволяет компилировать JavaScript код в машинный на нашем компьютере. На самом деле, за основу Node.js взять тот же самый движок [V8][v8]. Node.js добавляет возможность нам взаимодействовать с утройствами ввода-вывода, подключать внешние библиотеки.

2. Возможности Node.js и способы применения в production-среде

Писать на Node.js можно практически все: веб-сервера, десктопные и мобильные приложения, и даже программировать микроконтроллеры. В данной лабораторной работе мы остановимся на первом варианте, наиболее часто используемом, написании веб-серверов. Чаще всего на Node.js пишут именно BFF (Backend-For-Frontend), но на этом мы остановимся подробнее позже.

Также очень часто на Node.js пишут различные скрипты для того, чтобы автоматизировать различные процессы в инфраструктуре клиентской разработки. Например: автоматизация деплоя, различные автогенерации и тд.

3. Небольшие примеры скриптов

Рассмотрим несколько примеров скриптов, чтобы поподробнее посмотреть, как именно работает 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 для работы с файловой системой.

4. Пишем "голую" простую API

Также в 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.

5. Пишем API на express

В реальности на веб-серверах у нас бывает гораздо больше обработчиков, и приходится выстраивать очень сложную структуру с большим количеством бизнес-логики. Для упрощения процесса разработки написали фреймворк 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":"ПСП"}

API с акциями для приложения из 3-4 лабораторной работы

В 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! Но давайте теперь его немного причешем, и добавим в проект структурированности.

6. Добавляем в проект структуру (рефакторим код)

Основу для всех архитектур бэкенда является разделение на отображение, бизнес логику и работу с данными. В зависимости от различных подходов в архитектуры могут вноситься какие-то различные детали и дополнительные слои, но эти 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 умеет читать акции, создавать, а также удалять их.

7. ЗАДАНИЕ

  1. Необходимо в ваши проекты с 3-5 лабораторную работу добавить возможность добавления и удаления карточек.
  2. Удалить все getData() и заменить их на походы в написанное вами API.
  3. Написать бэкенд на Node.js на примере данного, который будет возвращать данные (которые раньше лежали в getData) для вашего приложения.

8. ДОП. ЗАДАНИЕ

Для сильных ребят предоставляется возможность реализовать свой BFF. Т.е. перенести логику работы с VK API на бэкенд, который будет как бы прослойкой между клиентским приложением и VK API. Чтобы правильно это сделать, напишите в телеграм @nathan_kith. Я подскажу, как это лучше сделать и дам рекомендации.

Что вы за это получите? Полезный опыт Возможность попасть на стажировку в Яндекс. Пишите в телеграм (nathan_kith)!


Back | FazBrowse Home | New Git URL