Всем еще раз привет. Мы возвращаемся к созданию приложения на Node.js и MySQL для небольшого todo — приложения на Node.js для React. С прошлого раза я немного пересмотрел структуру нашего приложения, и теперь решил добавить дополнительную колонку в базу данных под названием inner_key, которая будет необходима нам для отрисовки уникальных ключей для каждого отдельного дела (в списке повторяющихся элементов React нужен уникальный ключ на каждый элемент для его обработки в Virtual DOM. Если это по прежнему вызывает у вас вопросы, вам стоит изучить эту статью).

Мы можем добавить колонку с помощью с помощью следующей команды в MySQL:
ALTER TABlE TODO ADD inner_key varchar(100);
Да, возможно не стоит timestamp (а именно с помощью Date.now() я создаю ключи для своего дела в React) складывать в колонку varchar, но я надеюсь, что это несложно будет поправить, если кто-то будет заниматься конечно оптимизацией нашего приложения. С другой стороны, я не собираюсь использовать timestamp для работы со временем в моем приложении, мне нужно только уникальное значение. Так что пока это не несет никакой проблемы.
Небольшие обновления в работе нашей модели
Дальше, в связи с этим изменениями, у нас поменялась и сама модель нашего приложения.
Теперь у нас по-другому выглядит конструктор экземпляра в начале нашей модели, у него добавилось новое свойство:
const Deal = function(deal) { this.text = deal.text; this.inner_key = deal.inner_key; };
Операцию по созданию дела в Модели я изменил чисто косметически. А вот операции по получению дела по id пришлось довольно сильно поменять, потому что мы теперь получаем дело по inner_key. Единственное, я не стал менять параметры res dealId, но в принципе, это не сильно режет читабельность кода:
Deal.findById = (dealId, result) => { sql.query(`SELECT * FROM TODO WHERE inner_key = '${dealId}'`, (err, res) => { // здесь обработка ошибок, не вижу смысла ее дублировать if (res.length) { console.log("найдено дело: ", res[0]); result(null, res[0]); return; } // если вдруг не удалось найти result({ kind: "not_found" }, null); }); };
Кроме него, небольшой мутации подвергся и запрос всех данных с таблицы. Для моего React — приложения не нужны id из базы данных, мне нужен inner_key. Поэтому немного поменялся и сам запрос:
Deal.getAll = result => { const queryAll = "SELECT text, inner_key FROM TODO"; sql.query(queryAll, (err, res) => { // обработка ошибок
В методе update мы тоже теперь обновляем дело по innerkey, и то же самое происходит и в методе удаления:
Deal.updateById = (inner_key, deal, result) => { const queryUpdate = "UPDATE TODO SET text = ? WHERE inner_key = ?"; sql.query( queryUpdate, [deal.text, inner_key], (err, res) => { //мощная обработка ошибок } //отправка данных //Дальше идет удаление const queryDelete = "DELETE FROM TODO WHERE inner_key = ?"; sql.query(queryDelete, inner_key, (err, res) => { // обработка ошибок }
Как вы заметили, у меня здесь неполный код, потому что даже фигурные скобки не закрываются, но полный код я не вижу смысл приводить полностью. Если все-таки что-то непонятно, то можно посмотреть его здесь .
Создание своего Контроллера
В контроллере мы экспортируем созданные нами в модели функции. Если в общем, то каждый раз мы валидизируем запрос проверяя не было ли отправлено пустое тело (если вы планируете трансформить API в открытое, то это must have, да и вообще это полезно). Дальнейшие действия отличаются, в зависимости от того, нужно ли чтение res.id для выполнения запроса, или это не уточненный запрос. В методе findAll я оставил разрешающие все заголовки ответа для того, чтобы упростить тестирование своего API.
const Deal = require("../models/deal.model.js"); //Создаем и сохраняем новое дело exports.create = (req, res) => { // Валидизируем запрос if (!req.body) { res.status(400).send({ message: "У нас не может не быть контента" }); } // создание своего дела const deal = new Deal({ text: req.body.text, inner_key: req.body.inner_key // у нашего дела будет текст и внутренний id, который будет использоваться как // ключ для элементов в React }); Deal.create(deal, (err, data) => { if (err) res.status(500).send({ message: err.message || "Произошла ошибка во время выполнения кода" }); else res.send(data); }); }; // Получение всех пользователей из базы данных exports.findAll = (req, res) => { Deal.getAll((err, data) => { if (err) res.status(500).send({ message: err.message || "Что-то случилось во время получения всех пользователей" }); else res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Headers', 'origin, content-type, accept'); // я оставлю заголовки, получаемые с сервера, в таком виде, но конечно в реальном продакшене лучше переписать под конкретный origin // ну или вы делаете open API какой-нибудь, тогда делаете что хотите res.send(data); }); };
Остальную часть кода я запихну под spoiler, потому что тестировать мы будем только вышеупомянутые функции. Однако остальная часть кода тоже рабочая, но она технически отличается только аргументами от того, что уже было озвучено:
// Найти одно дело по одному inner_id exports.findOne = (req, res) => { Deal.findById(req.params.dealId, (err, data) => { if (err) { if (err.kind === "not_found") { res.status(404).send({ message: `Нет дела с id ${req.params.dealId}.` }); } else { res.status(500).send({ message: "Проблема с получением пользователя по id" + req.params.dealId }); } } else res.send(data); }); }; // Обновление пользователя по inner_id exports.update = (req, res) => { // валидизируем запрос if (!req.body) { res.status(400).send({ message: "Контент не может быть пустой" }); } // обновление дела по "айди" - на самом деле inner_key Deal.updateById( req.params.dealId, new Deal(req.body), (err, data) => { if (err) { if (err.kind === "not_found") { res.status(404).send({ message: `Не найдено дело с id ${req.params.dealId}.` }); } else { res.status(500).send({ message: "Error updating deal with id " + req.params.dealId }); } } else res.send(data); } ); }; // удалить дело по inner_key exports.delete = (req, res) => { Deal.remove(req.params.dealId, (err, data) => { if (err) { if (err.kind === "not_found") { res.status(404).send({ message: `Не найдено дело с ${req.params.dealId}.` }); } else { res.status(500).send({ message: "Не могу удалить дело с " + req.params.dealId }); } } else res.send({ message: `дело было успешно удалено` }); }); }; // Удалить все дела из таблицы exports.deleteAll = (req, res) => { Deal.removeAll((err, data) => { if (err) res.status(500).send({ message: err.message || "Что-то пошло не так во время удаления всех дел" }); else res.send({ message: `Все дела успешно удалены` }); }); };
Роутинг
Теперь переходим к самому сладкому и простому: описание роутинга, который потом надо не забыть проимпортировать потом в сам server.js. По описанным путям и методам мы сможем получать и добавлять данные. В нашей папке app создаем подпапку routes, в которой нам нужно файл deals.routes.js. В нем нужно написать следующее:
module.exports = app => { //импортируем наш контроллер, что бы можно было передать им функции по запросу const deals = require("../controllers/deal.controller.js"); // Создание нового дела по методу post app.post("/deals", deals.create); // Получение всех дел сразу app.get("/deals", deals.findAll); //Получение отдельного дела по id (на самом деле в запросе должен inner_key), но я не стал это менять app.get("/deal/:dealId", deals.findOne); // обновить дело по id // здесь тоже самое про inner_key app.put("/deal/:dealId", deals.update); //Удалить дело по id app.delete("/deal/:dealId", deals.delete); // Удалить сразу все дела app.delete("/deals", deals.deleteAll); };
После этого открываем наш файл server.js и добавляем над прослушкой порта следующее:
require("./app/routes/deals.routes.js")(app);
Непосредственно использование нашего API
В большинстве подобных статей в конце тестируют API с помощью POSTMAN. Это и вправду отличный инструмент, который вам стоит освоить, если планируете хоть немного заниматься профессионально разработкой API (и не столь важно, для какой платформы и на каком языке). Если вы закончили написание вашего приложения, и на забыли включить вашу базу данных, то теперь можно и запустить само приложение:
<node server.js>
Если вы совсем новичок, то вам будет интересно услышать о пакете nodemon, который перезапускает ваше приложение, если произошли изменения в файлах проекта:
npm i nodemon -g nodemon server.js
Теперь будет куда проще отлаживать возникающие ошибки. Но мы собирались протестировать это в реальном React-приложении.
У меня есть под рукой простое react- todo, которое внимательный читатель мог заметить в статье деплоя react приложения на Heroku этого блога. Однако я не будут переписывать весь бэк под наше react — приложение (хотя это совсем не сложно, но я не хочу чрезмерно удлинять статью). Поэтому я запущу React приложение на встроенном сервере create-react-app под 3000 портом, а наше приложение работает под 5003 портом. Пускай теперь на нашем пути и стоят труднопроходимые CORS, мы легко сможем получить наши данные хотя бы все todo. Пример работы наших запросов я почти полностью взял с официальной документации React, которая посвящена fetch запросам. Запросы в React приложениях обычно осуществляются после рендеринга компонента в componentDidMount, и именно в нем нужно осуществлять запросы к удаленным ресурсам:
class App extends Component{ constructor(){ super() this.state ={ error: null, isLoaded: false, items:[], currentItem: {text:"первое дело", inner_key:"firstItem"} } } componentDidMount() { fetch("http://localhost:5003/deals") .then(res => res.json()) .then( (result) => { this.setState({ //если и произошла загрузка, тогда мы активируем наш компонент isLoaded: true, items: result }); }, // Примечание: важно обрабатывать ошибки именно здесь, а не в блоке catch(), // чтобы не перехватывать исключения из ошибок в самих компонентах. (error) => { this.setState({ isLoaded: true, error }); } ) }
Теперь у нас есть дела, которые подтягиваются из базы данных:

Все работает и в самом приложении:

На получение всех дел наше приложение работает. Однако в React — приложении пока не описаны методы ни удаления, ни добавления дел. Чтобы упростить cors — запросы, вам стоит добавить в node-приложении обслуживание js-билда на React, и тогда расписать методы добавления дел по inner_id, их удаления и обновления.
Всем спасибо за внимание. По традиции, несколько полезных ссылок:
Немного интересного из документации express по поводу работы middleware, в частности body-express
Писать асинхронные запросы на React без axios уже совсем не модно
И немного про настраивание cors в Express, раз о нем зашла речь
ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/491786/
Добавить комментарий