NodeJS. Part3. Express.js
Код https://github.com/irinainina/node.js/tree/module3
Урок 1. Настройка приложения
Быстрое создание проекта
npm init -y
Мы соглашаемся со всеми предлагаемыми полями по умолчанию
Устанавливаем фреймворк Express.js
npm install express
В файле package.json в поле dependencies появилась запись об установленной версии express
"dependencies": {
"express": "^4.17.1"
}
Устанавливаем модуль nodemon для обновления данных на серверу без перезагрузки
npm i -D nodemon
Флаг -D (-dev) означает, что модуль устанавливается только для разработки и в конечный продукт не войдёт. Флаг -S (-save), что модуль будет работать и в самом приложении тоже.
Информация о nodemon прописывается в файле package.json в поле devDependencies
"devDependencies": {
"nodemon": "^2.0.4"
}
Создаём файл index.js, подключаем фреймворк Express.js
const express = require('express')
Создаём сервер
const app = express()
Прослушиваем что происходит на сервере
app.listen(3000, () => {
console.log('Server is running')
})
Можно вынести порт в отдельную переменную
const express = require('express')
const app = express()
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`)
})
Урок 2. Работа с HTML-файлами
Создадим файл index.html и подключим его при помощи express.js
const express = require('express')
const path = require('path')
const app = express()
app.get('/', (req, res) => {
res.status(200)
res.sendFile(path.join(__dirname, 'views', 'index.html'))
})
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`)
})
Express сам добавляет заголовки странице, сам указывает статус 200 по умолчанию, т.е без строки res.status(200) тоже всё будет работать
Рассмотрим как работает роутинг. Для этого создадим страницу about.html, сделаем в ней ссылку на главную страницу, а на главной - ссылку на страницу about и в index.js напишем код
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'views', 'index.html'))
})
app.get('/about', (req, res) => {
res.sendFile(path.join(__dirname, 'views', 'about.html'))
})
Всё прекрасно переключается, при этом кода понадобилось совсем немного.
Урок 3. Подключение Handlebars
Статические html-страницы - это не то, чего мы ожидаем от js
Для динамического рендеринга html в node.js могут использоваться разные движки
pug - https://pugjs.org/api/getting-started.html
ejs - https://ejs.co/
handlebars - https://handlebarsjs.com/
Наиболее популярные среди них - pug и handlebars.
pug не предполагает создания html-кода
ejs изначально присутствует в node.js, его не нужно устанавливать, достаточно зарегистрировать
с handlebars познакомимся ближе
Устанавливаем библиотеку express-handlebars в свой проект https://www.npmjs.com/package/express-handlebars
npm i express-handlebars
Подключаем её
const exphbs = require('express-handlebars')
Создаём объект handlebars (hbs)
const hbs = exphbs.create({
defaultLayout: 'main',
extname: 'hbs'
})
Здесь указано имя главного файла handlebars - main.hbs и сокращённое имя, которое будем использовать для handlebars - hbs
Регистрируем handlebars
app.engine('hbs', hbs.engine)
И начинаем его использовать
app.set('view engine', 'hbs')
Указываем название папки, в которой будут храниться все наши шаблоны
app.set('views', 'views')
Весь код подключения handlebars
const express = require('express')
const exphbs = require('express-handlebars')
const app = express()
const hbs = exphbs.create({
defaultLayout: 'main',
extname: 'hbs'
})
app.engine('hbs', hbs.engine)
app.set('view engine', 'hbs')
app.set('views', 'views')
Переименуем файлы index.html и about.html в папке views в index.hbs и about.hbs
Теперь роутинг сильно сокращается
Вместо
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'views', 'index.html'))
})
Достаточно написать
app.get('/', (req, res) => {
res.render('index')
})
Для страницы about действуем аналогично
app.get('/about', (req, res) => {
res.render('about')
})
Внутри папки views создаём папку layouts и в ней файл main.hbs
В этот файл добавляем обычный html-код. Если в нём написать любой текст, он отобразится на странице. Но вообще main.hbs предназначен для интеграции всех остальных .hbs файлов и скриптов
Урок 4. Настройка Layout
Для того, чтобы файл main.hbs отображал размещённые в папке views .hbs файлы, изменим его код
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
{{{ body }}}
</body>
</html>
В файлах index.hbs и about.hbs оставляем только контентную часть, заголовок генерирует main.hbs
Например, index.hbs выглядит так
<h1>Home page</h1>
<a href="/about">About Page</a>
Внутри папки views создаём папку partials
В ней можно создавать фрагменты кода, которые можно использовать в приложении
В папке partials создаём файл head.hbs и переносим в него фрагмент кода из main.hbs
Чтобы этот фрагмент подключить в main.hbs используем две фигурные скобки внутри которых знак > и указываем имя файла
{{> head }}
Точно так же создаём и подключаем footer
{{> footer }}
Подключаем materialize css - https://materializecss.com/
Для этого в head.hbs подключаем стили, а в footer.hbs скрипт
Добавляем обёртку - класс container
Код main.hbs
{{> head }}
<body>
<div class="container">
{{{ body }}}
</div>
{{> footer }}
</body>
</html>
Урок 5. Добавление навигации
Копируем код navbar из materialize.css
https://materializecss.com/navbar.html
Создаём в папке partials файл navbar.hbs, добавляем в него скопированный код. Подключаем его в файл main.hbs
{{> navbar}}
Добавим собственные стили. Для этого в корне приложения создаём папку public, в ней файл index.css, в нём указываем нужные стили
В файле index.js регистрируем папку public как статическую
app.use(express.static('public'))
В файле head.hbs подключаем стили
<link rel="stylesheet" href="/index.css">
Здесь важно, что хоть путь к папке public намного длиннее, но так как папка зарегистрирована, для указания пути к ней достаточно поставить косой слэш.
Урок 6. Рендеринг данных
Как можно выводить динамические значения рассмотрим на примере динамического заголовка разного для разных страниц.
Значение заголовка указываем в файле index.js в методе render
app.get('/', (req, res) => {
res.render('index', {
title: 'Home Page'
})
})
Так для каждой страницы
Добавить нужный заголовок можно в файле head.hbs указав внутри тега title вместо текста переменную
<title>{{ title }}</title>
Рассмотрим как подсвечивать активную ссылку, указывая ей класс active
Для этого в файле index.js в методе render добавим ещё одно свойство - флаг, разный для каждой страницы
app.get('/', (req, res) => {
res.render('index', {
title: 'Home Page',
isHome: true
})
})
Теперь в файле navbar.hbs добавляем логику
{{#if isHome}}
<li class="active"><a href="/">Главная</a></li>
{{ else }}
<li><a href="/">Главная</a></li>
{{/if}}
Урок 7. Регистрация роутов
Добавлять логику в метод render файла index.js можно для очень небольших приложений. Но когда приложение увеличивается, логики становится слишком много, и её желательно разнести по разным файлам.
В корне приложения создаём папку routes и в ней три файла home.js, courses.js, add.js
Внутри каждого из этих файлов создаём роутер
const {Router} = require('express')
const router = Router()
module.exports = router
и внутрь копируем метод app.get каждой страницы, только вместо app пишем router
const {Router} = require('express')
const router = Router()
router.get('/', (req, res) => {
res.render('index', {
title: 'Home Page',
isHome: true
})
})
module.exports = router
Импортируем роуты в файл index.js
const homeRoute = require('./routes/home')
app.use(homeRoute)
Не очень удобно, что мы не видим сразу путь к файлу. Для того, чтобы его указать, можно использовать префиксы
Для этого в index.js прописываем пути
app.use('/', homeRoute)
app.use('/courses', coursesRoute)
app.use('/add', addRoute)
А в файлах папки routes их убираем, оставляя только слэш
Урок 8. Обработка форм
Для создания формы копируем код на странице https://materializecss.com/text-inputs.html и добавляем его в файл add.hbs
<form action="/add" method="POST">
<div class="input-field">
<input id="title" name="title" type="text" class="validate" required>
<label for="title">Title of course</label>
<span class="helper-text" data-error="Add title"></span>
</div>
<button class="btn waves-effect waves-light">Add course</button>
</form>
В файле add.js пишем код обработки post-запроса, который выводит запрос в консоль и перенаправляет на страницу курсов
router.post('/', (req, res) => {
console.log(req.body)
res.redirect('/courses')
})
В файле index.js регистрируем обработчик
app.use(express.urlencoded({ extended: true }));
Урок 9. Создание модели
Создадим модель для добавления, редактирования, удаления курса.Для этого в корне проекта создаём папку models, в ней файл course.js
В этом файле создаём класс Course с параметрами title, price, img
class Course {
constructor(title, price, img) {
this.title = title;
this.price = price;
this.img = img;
}
}
Добавим курсу уникальный id. Для этого устанавливаем библиотеку uuid
npm install uuidv4
Подключаем её
const { uuid } = require('uuidv4');
и добавляем в конструктор класса Course строку
this.id = uuid();
Для хранения информации используем файловую систему
В корне проекта создаём папку data в ней файл courses.json
Чтобы изменить содержание файла, его нужно прочитать. За это отвечает метод geatAll()
static getAll() {
return new Promise((resolve, reject) => {
fs.readFile(
path.join(__dirname, "..", "data", "courses.json"),
"utf-8",
(err, content) => {
if(err) {
reject(err);
} else {
resolve(JSON.parse(content))
}
}
);
});
}
Это вспомогательный метод.
Для сохранения курса отвечает метод save()
Получаем информацию из файла courses.json
async save() {
const courses = await Course.getAll();
console.log(courses);
}
Подключаем модуль к файлу add.js
const Course = require('../models/course')
router.post('/', async (req, res) => {
const course = new Course(req.body.title, req.body.price, req.body.img)
await course.save()
res.redirect('/courses')
})
Теперь при отправке формы в консоль выводится содержимое файла courses.json
Добавим данные формы в массив
Для этого создадим вспомогательную функцию toJSON()
toJSON() {
return {
title: this.title,
price: this.price,
img: this.img,
id: this.id
}
}
И запушим её в массив courses
async save() {
const courses = await Course.getAll();
courses.push(this.toJSON())
console.log(courses);
}
Донные, добавленные в форму, выводятся в консоль
Запишем их в файл courses.json
async save() {
const courses = await Course.getAll();
courses.push(this.toJSON());
return new Promise((resolve, reject) => {
fs.writeFile(
path.join(__dirname, "..", "data", "courses.json"),
JSON.stringify(courses),
(err) => {
if (err) {
reject(err);
} else {
resolve();
}
}
);
});
}
Теперь данные, введённые в форму, сохраняются в файле courses.json
Урок 10. Вывод списка курсов
Научившись записывать данные в файл, можем выводить эти данные на страницуВначале импортируем клас Courses в файл course.js
const Course = require('../models/course')
Мы уже выполняли данный импорт в файл add.js, там мы записали данные о курсе, а в файле course.js выведем записанные данные на страницу
router.get('/', async (req, res) => {
const courses = await Course.getAll();
res.render('courses', {
title: 'Courses',
isCourses: true,
courses
})
})
Перейдём в файл courses.hbs
Проверим, если ли курсы
{{#if courses.length}}
<p>{{courses.length}}</p>
{{else}}
<p>not any course</p>
{{/if}}
Создадим карточку курса
https://materializecss.com/cards.html
{{#if courses.length}}
{{#each courses}}
<div class="row">
<div class="col s6 offset-s3">
<div class="card">
<div class="card-image">
<img src={{img}} alt={{title}}>
</div>
<div class="card-content">
<span class="card-title">{{title}}</span>
<p class="price">{{price}}</p>
</div>
<div class="card-action">
<a href="/courses/{{id}}">Open course</a>
</div>
</div>
</div>
</div>
{{/each}}
{{else}}
<p>not any course</p>
{{/if}}
Урок 11. Подключение клиентских скриптов
В папке public создаём файл app.js - клиентский скриптВ нём пишем код для стилизации валюты
const prices = document.querySelectorAll('.price');
prices.forEach(el => {
el.textContent = new Intl.NumberFormat('ru-Ru', {
currency: 'rub',
style: 'currency'
}).format(el.textContent)
})
Скрипт app.js подключаем в футере footer.hbs
<script src="/app.js"></script>
Урок 12. Динамические параметры
На страницу курса ведёт ссылка вида
<a href="/courses/{{id}}">Open course</a>
У каждого курса свой уникальный id, который указан в файле courses.json
Создадим страницу курса
Создаём файл course.hbs в папке view и там добавляем разметку
<div class="course">
<h1>{{course.title}}</h1>
<img src={{course.img}} alt={{course.title}}>
<p class="price big">{{course.price}}</p>
</div>
Нам нужно получить курс по id
Для этого идём в папку models, открываем файл course.js и на основе метода getAll() создаём метод getById()
static async getById(id) {
const courses = await Course.getAll();
return courses.find(el => el.id === id)
}
В файле courses.js пишем код
router.get("/:id", async (req, res) => {
const course = await Course.getById(req.params.id);
res.render('course', {
laout: 'empty',
title: `Course ${course.title}`,
course,
});
});
При переходе по ссылке открывается страница курса
Вынесем курс в layout
за это отвечает строчка в методе render
laout: 'empty',
В папке layouts создадим файл empty.hbs
Его код
{{> head }}
<body>
{{{ body }}}
{{> footer }}
</body>
</html>
Урок 13. Редактирование курса
Создадим функционал для редактирования курса - возможности менять название, изображение, ценуВ папке routes в файле courses.js добавим ещё один роутер
router.get("/:id/edit", async (req, res) => {
});
Проверяем, можно ли редактировать курс
router.get("/:id/edit", async (req, res) => {
if(!req.query.allow) {
return res.redirect('/')
}
});
Для добавления параметра, разрешающего редактирование курса, в файле courses.hbs создаём ссылку
<a href="/courses/{{id}}/edit?allow=true">Edit course</a>
Создадим страницу редактирования. В папке views создаём файл course-edit.hbs копируем в него форму из файла add.hbs
В каждый input формы в атрибут value добавим текущие значения полей
Также добавляем скрытый input, содержащий значение id курса
<input type="hidden" name="id" value="{{course.id}}">
Дописываем метод router.get
router.get("/:id/edit", async (req, res) => {
if(!req.query.allow) {
return res.redirect('/')
}
const course = await Course.getById(req.params.id);
res.render('course-edit', {
title: `Edit ${course.title}`,
course
})
});
Страница редактирования готова.
Обработаем клик по кнопке Edit course.
router.post("/edit", async(req, res) => {
await Course.update(req,body)
res.redirect("/courses")
})
Создадим у модели курсов метод update()
static async update(course) {
const courses = await Course.getAll();
const idx = courses.findIndex(c => c.id === course.id)
courses[idx] = course
return new Promise((resolve, reject) => {
fs.writeFile(
path.join(__dirname, "..", "data", "courses.json"),
JSON.stringify(courses),
(err) => {
if (err) {
reject(err);
} else {
resolve();
}
}
);
});
}
Большая часть логики скопирована из метода save()
Всё, редактирование работает
Урок 14. Подготовка корзины
В файл courses.js добавим кнопку для покупки курса
<form action="/card/add" method="POST">
<input type="hidden" name="id" value="{{id}}">
<button type="submit" class="btn btn-primary">Sale course</button>
</form>
Создадим роут, который будет отвечать за корзину card.js
const {Router} = require('express')
const router = Router()
router.post('/add', async (req, res) => {
})
module.exports = router
Регистрируем его в index.js
const cardRoute = require("./routes/card");
app.use("/card", cardRoute);
Создадим модель корзины. Для этого в папке models создадим файл card.js
class Card {
add() {
}
fetch() {
}
}
module.exports = Card
Дополним роутер card.js
const {Router} = require('express')
const Card = require('../models/card')
const Course = require('../models/course')
const router = Router()
router.post('/add', async (req, res) => {
const course = await Course.getById(req.body.id);
await Card.add(course)
res.redirect('/card')
})
router.get('/', async (req, res) => {
const Card = await Card.feth()
res.render('card', {
title: 'Card',
card
})
})
module.exports = router
В navbar.hbs добавляем ссылку на корзину
{{#if isCard}}
<li class="active"><a href="/card">Card</a></li>
{{ else }}
<li><a href="/card">Card</a></li>
{{/if}}
Урок 15. Модель корзины
В модели корзины метод add() добавляет данные в корзину, метод fetch() считывает их оттуда.Реализуем эти методы.
В папке data создадим файл card.json
В нём создадим формат данных по умолчанию - массив с курсами и их цена
{
"courses": [],
"price": 0
}
Важно, что кавычки в данном случае могут быть только двойными, именно в таком формате хранятся данные в .json
Метод fetch() повторяет метод getAll() из модели курсов. Его задача - прочитать данные из файла
static async fetch() {
return new Promise((resolve, reject) => {
fs.readFile(
path.join(__dirname, "..", "data", "card.json"),
"utf-8",
(err, content) => {
if (err) {
reject(err);
} else {
resolve(JSON.parse(content));
}
}
);
});
}
Добавим в корзину новый курс
static async add(course) {
const card = await Card.fetch();
const idx = card.courses.findIndex(c => c.id === course.id)
const candidate = card.courses[idx]
if(candidate) {
candidate.count +=1
card.courses[idx] = candidate
} else {
course.count = 1
card.courses.push(course)
}
card.price += +course.price
return new Promise((resolve, reject) => {
fs.writeFile(
path.join(__dirname, "..", "data", "card.json"),
JSON.stringify(card),
(err) => {
if (err) {
reject(err);
} else {
resolve();
}
}
);
});
}
Переходим в папку router файл card.js
const { Router } = require("express");
const Card = require("../models/card");
const Course = require("../models/course");
const router = Router();
router.post("/add", async (req, res) => {
const course = await Course.getById(req.body.id);
await Card.add(course);
res.redirect("/card");
});
router.get("/", async (req, res) => {
const card = await Card.fetch();
res.render("card", {
title: "Card",
isCard: true,
courses: card.courses,
price: card.prise,
});
});
module.exports = router;
При клике по кнопке "Добавить в корзину" на странице курсов срабатывает метод POST и курс записывается в файл course.json. при загрузке страницы корзины срабатывает метод GET
Урок 16. Вывод данных в корзине
Файл card.hbs
<h1>Card</h1>
{{#if courses.length}}
<table>
<thead>
<tr>
<td>Title</td>
<td>Count</td>
<td>Action</td>
</tr>
</thead>
<tbody>
{{#each courses}}
<tr>
<td>{{title}}</td>
<td>{{count}}</td>
<td><button class="btn btn-small">Delete</button></td>
</tr>
{{/each}}
</tbody>
</table>
<p><strong>Price:</strong> <span class="price">{{price}}</span></p>
{{else}}
<p>Card is empty</p>
{{/if}}
Урок 17. Обработка асинхронных запросов
Обработку асинхронных запросов рассмотрим на примере удаления элемента из корзины
Добавим обработчик событий кнопке Delete в файле card.hbs
Для этого добавим кнопке класс и data-атрибут
<button class="btn btn-small js-remove" data-id="{{id}}">Delete</button>
и весь блок обернём в div с id card для того, чтобы отслеживать клики на нём и делегировать событие клика кнопкам с классом js-remove
В папке index.js исправим путь к папке public
app.use(express.static(path.join(__dirname, 'public')));
Затем переходим в папку public файл app.js
const $card = document.querySelector('#card')
if ($card) {
$card.addEventListener('click', event => {
if (event.target.classList.contains('js-remove')) {
const id = event.target.dataset.id
fetch('/card/remove/' + id, {
method: 'delete'
}).then(res => res.json())
.then(card => {
console.log(card)
})
}
})
}
В файле card.js папка models пишем метод remove() класса Card
static async remove(id) {
const card = await Card.fetch();
const idx = card.courses.findIndex(c => c.id === id)
const course = card.courses[idx]
if(course.count === 1) {
card.courses = card.courses.filter(c => c.id !== id)
} else {
card.courses[idx].count -=1
}
card.price -= course.price
return new Promise((resolve, reject) => {
fs.writeFile(
path.join(__dirname, "..", "data", "card.json"),
JSON.stringify(card),
(err) => {
if (err) {
reject(err);
} else {
resolve(card);
}
}
);
});
}
Урок 18. Динамическое изменение корзины
В предыдущем уроке мы научились выводить обновлённые данные в консоль. В этом будем выводить их на страницу без её обновления.Переходим в файл app.js
const toCurrency = price => {
return new Intl.NumberFormat('ru-RU', {
currency: 'rub',
style: 'currency'
}).format(price)
}
document.querySelectorAll('.price').forEach(node => {
node.textContent = toCurrency(node.textContent)
})
const $card = document.querySelector('#card')
if ($card) {
$card.addEventListener('click', event => {
if (event.target.classList.contains('js-remove')) {
const id = event.target.dataset.id
fetch('/card/remove/' + id, {
method: 'delete'
}).then(res => res.json())
.then(card => {
if (card.courses.length) {
const html = card.courses.map(c => {
return `
<tr>
<td>${c.title}</td>
<td>${c.count}</td>
<td>
<button class="btn btm-small js-remove" data-id="${c.id}">Удалить</button>
</td>
</tr>
`
}).join('')
$card.querySelector('tbody').innerHTML = html
$card.querySelector('.price').textContent = toCurrency(card.price)
} else {
$card.innerHTML = '<p>Корзина пуста</p>'
}
})
}
})
}