В данной лабораторной работе необходимо объединить знания полученные на предыдущих лабах, для выполнения интеграции между клиентом, сервером и БД.
Далее пошагово описан процесс настройки и взаимодействия отдельных модулей, а т.ж приведен список заданий в конце документа.
В качестве отправной точки мы будем использовать итоговый проект из Лабы №1, с немного изменненной структурой. Для начала установим npm-зависимости. Для этого в командной строке выполним команду
npm install
В данной лабе мы подключаем клиентскую часть, как зависимость проекта (library-ui в package.json), и нам не нужно запускать ее отдельной командой. После установки зависимостей и автоматической сборки клиентского приложения нам остается только запустить наш сервер командой
npm start
после чего перейдем по адресу http://localhost:8080, где должна появиться наша коллекция книг.
P.S. В package.json в поле "scripts" определена команда start:dev. Эта команда использует библиотеку nodemon, которая позволяет существенно ускорить процесс разработки node-приложений (вам не нужно будет перезапускать сервер каждый раз после внесения изменений в код!). Запустите ее вместо команды start следующим образом: npm run start:dev.
Для того, чтобы обеспечить взаимную работу нашей БД с сервером необходимо установить специальный npm-пакет mongodb, который является официальным драйвером MongoDB для Node.js и предоставляет высокоуровневое API поверх mongodb-ядра. Скорее всего он у вас уже установлен. Для этого загляните в файл package.json (объект "dependencies"), если же нет, введите в консоли следующую команду
npm install --save mongodb
Теперь необходимо подключить установленный пакет к проекту. Для того, чтобы использовать объект нашей БД в любом месте проекта вынесем логику взаимодействия с ним в отдельный модуль: создадим папку ./db
в корне проекта, а т.ж файл index.js
в ней.
Импортируем нужную нам библиотеку с помощью подхода CommonJS (функции required). В данном случае нас интересует объект MongoClient, который позволит установить соединение с БД, а затем при ее успешном установлении получить объект БД.
Ниже приведен код модуля, который экспортирует метод connect, который принимает на вход два парамера -- переменную, в которой хранится путь к БД и функцию-callback (обратного вызова), затем пробует установить соединение, и кладет в переменную _db объект БД, если соединение установлено, далее вызывается callback. Перенесите этот код в свой модуль.
const MongoClient = require('mongodb').MongoClient;
let _db = null;
module.exports = {
connect(url, callback) {
return MongoClient.connect(url, (err, database) => {
if (!err) {
_db = database;
}
callback(err, database);
});
},
};
Важно! В данном модуле мы используем подход callback-функции, т.е. передаем функцию в качестве одного из параметров функции, и вызываем ее, когда завершился процесс установки соедиения с БД. Данный подход один из возможных вариантов работы с асинхронностью в JavaScript. Этот подход мы будем использовать далее в лаботаторной работе.
Отлично, теперь переменная _db хранит объект базы данных. Но сейчас он не очень полезен, т.к используется только в текущем модуле. Целесообразно будет написать метод, который бы отдавал нам этот объект.
Напишите метод
getInstance
(по примеру#connect
), который будет отдавать объект БД, если тот имеется, в противном случае, выбрасываем ошибку, что соединение прервано.
Для удобства добавим метод getCollection
, который будет принимать название коллекции БД, и возвращать объект этой коллекции соответственно.
module.exports = {
...
getCollection(name) {
return this.getInstance().collection(name);
}
}
P.S. Если коллекция не найдена, MongoDB неявно создает новую коллекцию самостоятельно.
Далее Вам необходимо запустить сервер mongod и оболочку mongo как в прошлой лабе.
Шаг 1 почти завершен, осталось выполнить само подключение, используя наш модуль db. Перейдем в файл "server.js", подключим наш модуль, а т.ж зададим URL нашей БД в константе.
const MongoDB = require('./db');
const DB_URL = 'mongodb://localhost:27017/library';
Далее используя написанный ранее метод connect установим соединение с БД. В функции-callback'е проверим, что процесс установки соединения завершился без ошибок, и запустим сервер, в противном случае выводим ошибку соединения и завершаем программу.
// импорт зависимостей
...
MongoDB.connect(DB_URL, (err, database) => {
if (err) {
return console.log(`An error with connection to db ${err}!`);
}
console.log(`Connected to DB succsessfully!`);
// код имеющейся серверной части
...
});
Если все сделано правильно, то в консоли вы увидете следующую информацию:
[nodemon] starting `node server.js`
Connected succsessfully!
Server is running: localhost:8080
Важно! Существует ряд соглашений о том, как вызывать callback-функции. В среде Node.js самый распространенный шаблон -- "error-first" ("node-style callback"), т.е. первым параметром функции всегда является объект ошибки. Подробнее об этом можно прочитать здесь.
В предыдущих лабораторных работах вами были созданы несколько путей для доступа к API (./api/index.js
). Сейчас они предоставляют фиктивные данные, взятые из JSON. Наша задача -- создать модуль, который будет иметь доступ к объекту БД и вытаскивать из нее необходимые данные, согласно запросам с клиента.
Рассмотрим пример с созданием сервиса для коллекции книг.
Создадим папку ./services
, и файл book.service.js
.
Получим объект коллекции "books" из нашей БД ./db/index.js
с помощью #getCollection
и для удобства запишем его в отдельную переменную.
const bookCol = require('../db').getCollection('books');
Далее создадим объект сервиса, который в дальнейшем будет пополняться методами для работы с БД, а т.ж выполним его export
. На примере запроса получения всех книг - GET /api/books напишем метод, который будем использовать в API:
const service = {
getAllBooks(callback) {
return bookCol.find({}).toArray(callback);
},
};
module.exports = service;
Синтаксис запросов с помощью mongodb очень похож на mongo-shell, используемый вами на предыдущей лабе. Однако, вы могли заметить метод "toArray". Дело в том, что метод find возвращает не данные, а объект Cursor - указатель на результирующий набор данных, полученных с помощью запроса. Но нам нужны документы, а не указатель. Для этого и используется метод "toArray".
Подробнее про cursor можно прочитать здесь, а про метод toArray здесь.
Важно! Драйвер mongodb позволяет использовать 2 вида управления асинхронными операциями:
callback
, передаваемый в используемую функцию- возвращая
Promise
из используемой функцииВыполняя запрос к БД мы также выполняем асинхронную операцию. Ранее в этой лекции приводилось описание применения callback-функции. В данном случае callback, переданный в метод toArray будет вызван со следующими аргументами: 1 - ошибкой (да, mongodb т.ж использует "error-first" подход), 2 - массив документов.
Далее импортируем наш сервис в файл с API и используем его по-назначению:
...
const bookService = require('../services/book.service');
router
.get('/books', (req, res) => {
bookService.getAllBooks((err, books) => {
if (err) {
console.error(err);
return res.send(err);
}
return res.send(books);
});
})
...
Теперь, если мы обновим страницу, то увидим список книг, хранящихся в БД.
Отлично! У нас написан сервис для коллекции книжек. Но это еще не все 😬
На нашем клиенте реализована сортировка и категоризация имеющихся книжек. Список категорий и фильтров также, как и книг, мы будем передавать с сервера при начальной загрузке нашего клиентского приложения из коллекций Фильтров и Категорий соответственно.
По примеру создания bookService, написать сервис для коллекции:
- "categories" -- четный вариант
- "filters" -- нечетный вариант
который будут иметь единственный метод
getAll(Categories|Filters)
и подключить его к соответствующим ручкам роутера.
P.S. т.к эти сервисы понадобятся каждому, код сервиса другого варианта можно стащить у соседа 💩, или написать самому 👍.
P.P.S. теперь можно удалить более не используемые импорты моделей JSON в api/index.js.
Как вы могли заметить, на нашем клиенте имеется возможность ввести что-либо в стоку поиска, выбрать категорию или фильтр. Чтобы сообщить серверу о таких дополнительных параметрах, клиент в процессе GET-запроса может передавать параметры выполнения в URI ресурса после символа ?
. В нашем случае дополнительными параметрами строки запроса являются:
search
-- символы строки поискаactiveFilter
-- тип выбранного фильтраactiveCategory
-- тип выбранной категории
Каждый из параметров не обязательно может быть выбран/введен, в этом случае он не отправляется.
Для получения этих параметров запроса в Express.js используется объект request функции обработчика .get('/', (reqest, response) => ...)
, а именно его свойство .query.
Важно! Не путать request.query и request.params:
params
GET /api/books/1 app.get('/books/:id', (reqest, response)=>...) request.params.id // 1
query
GET /api/books?search='Figth Club' app.get('/books', (reqest, response)=>...) request.query.search // Figth Club
Подробнее о query здесь, о params тут.
На примере, описанного ранее запроса для получения всех книг GET /api/books
, добавим поддержку выборки с фильтрацией. В нашем случае запрос GET может содержать 3 параметра, т.е для того, чтобы на сервере проверить одно из возможных состояний фильтрации, нам необходимо написать 2^3 проверок (каждый фильтр может быть как в активном состоянии, так и не активен -- не отправляться).
Для исключения написания большого boilerplate на проверку всех условий, в проекте уже имеется функция-утилита, которая будет выполнять для каждой книги проверку на соответствие каждому из фильтров. Если фильтр не указан -- проверка опускается.
Эта утилита содержится в папке ./utils
в корне проекта, а именно в файле meet-query.utils.js
.
Выполним импорт нашей функции в файле ./api/index.js
, и в обработчике нашего GET запроса, предварительно получив список всех книг, проверим каждую из них с помощью новой супер-утилиты 🚀:
// GET /api/books handler
...
// получим параметры фильтрации из query
const searchString = req.query.search;
const activeFilter = req.query.activeFilter;
const activeCategory = req.query.activeCategory;
bookService.getAllBooks((err, books) => {
if (err) {
console.error(err);
return res.send(err);
}
// отфильтруем книги
const requiredBooks = books.filter(book =>
// с помощью утилиты meetQuery
meetQuery(book, search, activeFilter, activeCategory));
return res.send(requiredBooks);
});
...
Отлично, теперь мы можем фильтровать нашу коллекцию! Проверьте это в приложении. Ознакомительная практическая часть закончена. Далее приведены самостоятельные задания, которые основаны на пройденном материале.
Необходимо написать функцию-обработчик POST-запроса /api/book
в зависимости от параметра action
(добавление, изменение или удаление книги).
Важно! Дополнительные параметры в POST-запросе передаются в теле запроса (объект request.body). Подробнее про POST можно почитать здесь.
Для каждого задания указана схема тела запроса (объекта body
), т.е то, что нам отправляет в запросе клиент.
Под функцией-обработчиком в данном контексте понимается функция, которая выполняет следующие действия:
- с помощью типа action и параметров указанных в теле запроса, а т.ж. функции одного из сервисов, изменяет данные, хранящиеся в БД
- отправляет на клиент список всех книг (с изменениями, указанными в п.1), удовлетворяющий фильтрам, указанным в теле запроса
P.S. Параметр
_id
, который пердается с клиента, представляет собой шестнадцатеричный код, который MongoDB генерирует при создании объета автоматически. Для выбора необходимой книги по такому id необходимо будет импортировать функцию .ObjectID из пакетаmongodb
, и использовать ее как обертку над принятым id, прим.{"_id": ObjectID(_idFromBody)}
. Подробнее об ObjectId.
Обработчик запроса на добавление книги (кнопка ADD A BOOK).
// request.body schema
{
action: 'create',
book: {
title: string,
author: {
firstName: string,
lastName: string,
},
categories: string[],
keywords: string[],
img: string,
}
search: string | undefined,
activeFilter: string | undefined,
activeCategory: string | undefined,
}
Обработчик запроса на изменение книги (нажать на книгу, затем изменить данные и нажать submit, клик на звезды под книгой).
// request.body schema
{
action: 'update',
book: {
_id: string,
title: string,
author: {
firstName: string,
lastName: string,
},
categories: string[],
keywords: string[],
img: string,
},
search: string | undefined,
activeFilter: string | undefined,
activeCategory: string | undefined,
}
Обработчик запроса на удаление книги (клик на книгу, затем кнопка DELETE в модальном окне).
// request.body schema
{
action: 'delete',
_id: string,
search: string | undefined,
activeFilter: string | undefined,
activeCategory: string | undefined,
}
The End.
🎉