Цель данной лабораторной работы - знакомство с node, npm, написание простого приложения на JavaScript. В ходе выполнения работы, вам предстоит ознакомиться с кодом реализации простого интерфейса и вывода данных, и затем выполнить задания по варианту.
- Инструменты для работы
- Что такое node, npm и package.json
- Как работать с html в JS
- Инициализация проекта
- Создание главной страницы, подключение bootstrap
- Простая кнопка на JavaScript
- Структурирование проекта
- Верстка главной страницы
- Верстка страницы продукта
Для работы будем использовать инструменты из предыдущих лабораторной работы: VS Code + Live Server.
Перед началом работы необходимо установить на свой компьютер Node.js.
Наш JavaScript код, который мы писали в предыдущих лабораторных, исполняется в браузере. В браузере у нас есть компилятор JavaScript кода в машинных код, в Google Chrome это движок V8. Если мы хотим запускать код на нашем компьютере, а не в браузере, то нам нужно использовать Node.js. Node - это программная платформа, которая позволяет компилировать JavaScript код в машинный на нашем компьютере. Node.js добавляет возможность нам взаимодействовать с утройствами ввода-вывода, подключать внешние библиотеки. На нем в основном пишут веб-сервера, но есть возможность разрабатывать и десктопные оконные приложения и даже программировать микроконтроллеры.
- для установки node.js на macOS используйте Homebrew
- для установки node.js на Windows используйте nvm
В любом языке программирования нам нужно уметь работать с внешними библиотеками. На фронтенде для этого используется пакетный менеджер Npm. С помощью npm мы можем скачивать нужные нам пакеты, которые потом будем использовать в нашем приложении. Все наши библиотеки скачиваются в специальную папку node_modules
, вы увидите ее у себя в проекте, когда скачаете первую библиотеку.
Package.json - это основной файл в нашем приложении, который хранит всю информацию о проекте. В этом файле хранится название проекта, описания, версия, скрипты и многое другое. Именно в этом файле храниться информация о всех пакетах, которые мы поставили через npm, и версия этих зависимостей.
Package-lock.json - это файл, который хранит дерево зависимостей. Библиотеки, которые мы устанавливаем, могут иметь вложенные зависимости и этот файл хранит полное дерево.
В прошлых лабораторных работах мы уже работали с HTML версткой из нашего JavaScript кода, для этого у нас есть общирное API по работе с DOM деревом. Сегодня мы будем использовать getElementById и insertAdjacentHTML, но функций намного больше.
- Создаем пустую папку и открываем ее в VS Code.
- Инициализируем проект в npm с помощью команды
npm init
.
При инициализации проекта у нас будут спрашивать много вопросов, но их все можно пропустить нажав Enter
. В конце у нас появится настроенный файл package.json
.
Во все проекты принято добавлять .gitignore
файл, который не будет добавлять лишнее в наш git репозиторий. Подробнее о .gitignore
можно почитать тут.
- Создаем файл
.gitignore
и копируем туда содержимое файла.
Мы создали проект, который состоит из файлов package.json
и .gitignore
. Можно приступать к написанию основного кода.
По итогу мы имеем следующую структуру проекта.
├── .gitignore
├── package.json
Мы создали проект, теперь давайте начнем писать код. Когда пользователь заходит на сайт, то ему сначала подгружается файл index.html
с базовой версткой, а потом уже подгружаются стили и скрипты. Если в вашем приложении не будет index.html
файла, то браузер не сможет загрузить его.
- Создаем файл
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple App</title>
</head>
<body>
<div>Hello world!</div>
</body>
</html>
Если мы откроем html файл, то увидим страницу с надписью Hello world!.
Для того, чтобы было удобнее работать мы можем воспользоваться расширением Live Server, для этого открываем файл index.html
и нажимаем Go Live
в правом нижнем углу.
Для того, чтобы было проще верстать используем библиотеку css стилей bootstrap. Это библиотека стилей, в которой можно брать верстку и применять у себя на сайте. Для подключения установим библиотеку через npm.
- Устанавливаем библиотеку с помощью команды
npm i bootstrap
После установки библиотеки можно увидеть, что у нас появилась папка node_modules
и файл package-lock.json
. О них мы говорили выше. Так же изменился файл package.json
, в нем появилась наша библиотека с зафиксированной версией.
- Проверим, что мы успешно скачали bootstrap, для этого добавим кнопку из библиотеки компонентов в
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple App</title>
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
</head>
<body>
<button type="button" class="btn btn-primary">Hello world!</button>
<script src="node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
Как мы видим наша кнопка видна, значит мы все подключили успешно и можно переходить к написанию JavaScript кода.
По итогу мы имеем следующую структуру проекта.
├── node_modules/
├── .gitignore
├── package-lock.json
├── package.json
├── index.html
У нас есть приложение, которое имеет главную страницу. Сейчас у нас кнопка находится в файле index.html
, попробуем ее из HTML файла и нарисовать с помощью JS. Для того, чтобы в JS получить доступ к нашему HTML дереву у нас должен быть корневой элемент. Он будет родителем и к нему мы будем добавлять остальные компоненты.
- Добавляем корневой элемент в
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple App</title>
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
</head>
<body>
<div id="root"></div>
<script src="node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
Теперь у нас есть корневой элемент, к котором мы можем обратиться из нашего JavaSctip. Создадим js файл, подключим его и попробуем обратиться к HTML дереву.
- Создаем файл
main.js
, для доступа к HTML будем использовать getElementById
const root = document.getElementById('root');
У нас есть простой JS файл, который получает корневой элемент. Для того, чтобы этот файл загрузился в браузер необходимо добавить его в наш index.html
- Подключаем этот файл в
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple App</title>
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
</head>
<body>
<div id="root"></div>
<script src="main.js" type="module"></script>
<script src="node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
У нас в main.js
файле есть корневой элемент. Попробуем в него добавить нашу кнопку, которую раньше мы создавали в HTML.
- Добавляем кнопку в
main.js
const root = document.getElementById('root');
root.insertAdjacentHTML('beforeend', '<button type="button" class="btn btn-primary">Hello world 2!</button>')
Как мы видим наша кнопка появилась на экране, значит мы правильно написали на JavaScript файл. Теперь попробуем написать что-то посложнее.
По итогу мы имеем следующую структуру проекта.
├── node_modules/
├── .gitignore
├── package-lock.json
├── package.json
├── index.html
├── main.js
Мы написали простую страничку на JS, но мы же не сможем вечно все писать в одном файле. Нам необходимо как-то разбивать наш проект по мелким файлам.
Сейчас мы имеем следующее разбиение по файлам:
├── node_modules/
├── .gitignore
├── package-lock.json
├── package.json
├── index.html
├── main.js
В фронтенде верстку разделяют на страницы (Pages) и компоненты (Components). Страница - это отдельная страница как наша главная. Компонент - маленькие блоки из которых состоит страница.
Добавим дополнительные папки в нашу структуру:
pages
- тут будут лежать наши страницыcomponents
- тут будут лежать наши компоненты
Теперь наша структура выглядит следующим образом:
├── node_modules/
├── .gitignore
├── package-lock.json
├── package.json
├── pages/
├── components/
├── index.html
├── main.js
Теперь попробуем переписать нашу страницу под новую архитектуру.
- Создаем нашу страницу
pages/main/index.js
export class MainPage {
}
Наша страница должна рендериться в root элемент. Добавим конструктор, где будем получать родительский элемент и сохранять его.
- Добавляем конструктор
export class MainPage {
constructor(parent) {
this.parent = parent;
}
}
У нас есть родительский элемент, но наш нужна функция при вызове которой мы будем рендерить эту страницу.
- Добавляем функцию рендера
export class MainPage {
constructor(parent) {
this.parent = parent;
}
render() {
}
}
- Добавляем логику рендера кнопки на странице
render() {
this.parent.insertAdjacentHTML('beforeend', '<button type="button" class="btn btn-primary">Hello world 3!</button>');
}
У нас есть класс страницы, теперь необходимо добавить вызов этого класса в нашем основном файле main.js
- Добавляем вызов файла в
main.js
import {MainPage} from "./pages/main/index.js";
const root = document.getElementById('root');
const mainPage = new MainPage(root);
mainPage.render();
Все работает, кнопка видна на странице. Мы сказали, что у нас страница должна состоять из мелки компонентов, а сейчас верстка кнопки происходит на странице. Вынесем в компонент и добавим ее на странице.
- Создаем наш компонент
components/button/index.js
export class ButtonComponent {
constructor(parent) {
this.parent = parent;
}
render() {
this.parent.insertAdjacentHTML('beforeend', '<button type="button" class="btn btn-primary">Hello world 4!</button>');
}
}
- Подключаем наш компонент на странице
import {ButtonComponent} from "../../components/button/index.js";
// ...
render() {
const button = new ButtonComponent(this.parent)
button.render()
}
Все работает, кнопка видна на странице. Теперь сделаем нашу страницу такой, чтобы она была готова к нашим данным.
По итогу мы имеем следующую структуру проекта.
├── node_modules/
├── .gitignore
├── package-lock.json
├── package.json
└── pages
└── main
└── index.js
└── components
└── button
└── index.js
├── index.html
├── main.js
Теперь добавим на главную страницу список карточек. Для отображения будем использовать карточки из bootstrap.
- Создаем компонент карточки
components/product-card/index.js
export class ProductCardComponent {
constructor(parent) {
this.parent = parent;
}
render() {
}
}
- Добавляем верстку карточки. Для удобства вынесем верстку в отдельную функцию
getHTML() {
return (
`
<div class="card" style="width: 300px;">
<img class="card-img-top" src="https://i.pinimg.com/originals/c9/ea/65/c9ea654eb3a7398b1f702c758c1c4206.jpg" alt="картинка">
<div class="card-body">
<h5 class="card-title">Акция</h5>
<p class="card-text">Вот тут информация об акции</p>
<button class="btn btn-primary"">Нажми на меня</button>
</div>
</div>
`
)
}
render() {
const html = this.getHTML()
this.parent.insertAdjacentHTML('beforeend', html)
}
- Теперь добавим наш компонент на страницу
import {ProductCardComponent} from "../../components/product-card/index.js";
// ...
render() {
const productCard = new ProductCardComponent(this.parent)
productCard.render()
}
Отлично, у нас отображается карточка. Сейчас у нас данные захардкожены в компонент, а нам бы хотелось прокидывать данные в компонент.
- Добавим отрисовку компонента из данных
getHTML(data) {
return (
`
<div class="card" style="width: 300px;">
<img class="card-img-top" src="${data.src}" alt="картинка">
<div class="card-body">
<h5 class="card-title">${data.title}</h5>
<p class="card-text">${data.text}</p>
<button class="btn btn-primary">Нажми на меня</button>
</div>
</div>
`
)
}
render(data) {
const html = this.getHTML(data)
this.parent.insertAdjacentHTML('beforeend', html)
}
Теперь у нас функция render
принимает данные, которые будет отрисовывать. При вызове компонента нам необходимо прокидывать тестывое данные со страницы, потом это мы заменим на получение данных с бекенда.
- Прокидываем тестовые данные в компонент со страницы
getData() {
return {
id: 1,
src: "https://i.pinimg.com/originals/c9/ea/65/c9ea654eb3a7398b1f702c758c1c4206.jpg",
title: "Акция",
text: "У меня есть крутая акция"
}
}
render() {
const data = this.getData()
const productCard = new ProductCardComponent(this.parent)
productCard.render(data)
}
Отлично, данные отображаются. Теперт нам хотелось бы отрисовать больше чем один компонент. У нас может приходить список данных, для отрисовки в карточках, а сейчам мы умеем рисовать только одну карточку. Для этого нам нужно добавить родительски элемент на главной странице. В этот элемент мы будем добавлять все наши компоненты.
- Добавляем родительский элемент
get pageRoot() {
return document.getElementById('main-page')
}
getHTML() {
return (
`
<div id="main-page" class="d-flex flex-wrap"><div/>
`
)
}
render() {
this.parent.innerHTML = ''
const html = this.getHTML()
this.parent.insertAdjacentHTML('beforeend', html)
const data = this.getData()
const productCard = new ProductCardComponent(this.pageRoot)
productCard.render(data)
}
У нас есть элемент, в который мы будем добавлять наши дочерние компоненты. Теперь надо изменить логику так, чтобы мы умели работать с массивом данных, а не с одним элементом.
- Перерабатываем логику для работы с массивом данных
getData() {
return [
{
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"
},
]
}
render() {
this.parent.innerHTML = ''
const html = this.getHTML()
this.parent.insertAdjacentHTML('beforeend', html)
const data = this.getData()
data.forEach((item) => {
const productCard = new ProductCardComponent(this.pageRoot)
productCard.render(item)
})
}
Мы смогли отрисовать сразу несколько компонентов, но если мы попробуем нажать на кнопку, то ничего не произойдет. Добавим обработчики нажатия на кнопку. Для того, чтобы нам это сделать нужно внутри компонента подписаться на собитие клик по кнопке и обработать вызов этой функции. Функция, которая будет срабатывать по клику будем прокидывать в компонент из страницы.
- Добавим нашей кнопку уникальный id, чтобы по нему мы могли найти кнопку и подписаться на событие клика
<button class="btn btn-primary" id="click-card-${data.id}" data-id="${data.id}">Нажми на меня</button>
У кнопки появился уникальный id и мы можем подписаться на клик по этой кнопки. Для подписки на событие используем функцию addEventListener
- Добавляем подписку на нажатие кнопки
addListeners(data, listener) {
document
.getElementById(`click-card-${data.id}`)
.addEventListener("click", listener)
}
render(data, listener) {
const html = this.getHTML(data)
this.parent.insertAdjacentHTML('beforeend', html)
this.addListeners(data, listener)
}
У кнопки в нашей появился обработчик, который будет срабатывать при нажатии на нее. Добавим на главной странице функцию, которая будет срабатывать по нажатию и прокинем ее в компонент. При создании кнопки мы добавли ей data атрибут, чтобы при обработке мы могли его достать и узнать по какому элементу мы нажали.
- Добавляем обработчик на главной странице
clickCard(e) {
const cardId = e.target.dataset.id
}
const productCard = new ProductCardComponent(this.pageRoot)
productCard.render(item, this.clickCard.bind(this))
Теперь у нас есть все что нам нужно, осталось создать вторую страницу и нарисовать ее.
По итогу мы имеем следующую структуру проекта.
├── node_modules/
├── .gitignore
├── package-lock.json
├── package.json
└── pages
└── main
└── index.js
└── components
└── product-card
└── index.js
├── index.html
├── main.js
У нас есть главная страница, добавим страницу продукта.
- Создаем страницу продукта
pages/product/index.js
. Наша страница будет принимать дополнительный аргумент id, номер выбранной страницы
export class ProductPage {
constructor(parent, id) {
this.parent = parent
this.id = id
}
getData() {
return {
id: 1,
src: "https://i.pinimg.com/originals/c9/ea/65/c9ea654eb3a7398b1f702c758c1c4206.jpg",
title: `Акция ${this.id}`,
text: "Такой акции вы еще не видели"
}
}
get pageRoot() {
return document.getElementById('product-page')
}
getHTML() {
return (
`
<div id="product-page"></div>
`
)
}
render() {
this.parent.innerHTML = ''
const html = this.getHTML()
this.parent.insertAdjacentHTML('beforeend', html)
}
}
У нас есть страница продукта, нужна создать компонент, который мы будем отрисовывать на этой странице.
- Создаем компонент продукта
components/product/index.js
export class ProductComponent {
constructor(parent) {
this.parent = parent
}
getHTML(data) {
return (
`
<div class="card mb-3" style="width: 540px;">
<div class="row g-0">
<div class="col-md-4">
<img src="${data.src}" class="img-fluid" alt="картинка">
</div>
<div class="col-md-8">
<div class="card-body">
<h5 class="card-title">${data.title}</h5>
<p class="card-text">${data.text}</p>
</div>
</div>
</div>
</div>
`
)
}
render(data) {
const html = this.getHTML(data)
this.parent.insertAdjacentHTML('beforeend', html)
}
}
- Добавим отрисовку компонента на странице продукта
import {ProductComponent} from "../../components/product/index.js";
// ...
render() {
this.parent.innerHTML = ''
const html = this.getHTML()
this.parent.insertAdjacentHTML('beforeend', html)
const data = this.getData()
const product = new ProductComponent(this.pageRoot)
product.render(data)
}
У нас есть страница продукта, сделаем так, чтобы при нажатии на карточку на главной странице у нас открывалась страница продукта.
- Добавляем открытие страницы продукта при нажатии на карточку
import {ProductPage} from "../product/index.js";
// ...
clickCard(e) {
const cardId = e.target.dataset.id
const productPage = new ProductPage(this.parent, cardId)
productPage.render()
}
Все работает. При нажатии на кнопку в карточке на главной странице у нас открывается страница продукта. Для удобства добавим кнопку, которая будет возвращать на главную страницу.
- Создаем компонент
components/back-button/index.js
export class BackButtonComponent {
constructor(parent) {
this.parent = parent;
}
addListeners(listener) {
document
.getElementById("back-button")
.addEventListener("click", listener)
}
getHTML() {
return (
`
<button id="back-button" class="btn btn-primary" type="button">Назад</button>
`
)
}
render(listener) {
const html = this.getHTML()
this.parent.insertAdjacentHTML('beforeend', html)
this.addListeners(listener)
}
}
- Добавляем кнопку на страницу продукта и ее обработчик
import {BackButtonComponent} from "../../components/back-button/index.js";
import {MainPage} from "../main/index.js";
// ...
clickBack() {
const mainPage = new MainPage(this.parent)
mainPage.render()
}
render() {
this.parent.innerHTML = ''
const html = this.getHTML()
this.parent.insertAdjacentHTML('beforeend', html)
const backButton = new BackButtonComponent(this.pageRoot)
backButton.render(this.clickBack.bind(this))
const data = this.getData()
const stock = new ProductComponent(this.pageRoot)
stock.render(data)
}
Все работает, если нажать на кнопку, то мы вернемся обратно на главную страницу. На этом лабораторная работа закончилась.
По итогу мы имеем следующую структуру проекта.
├── node_modules/
├── .gitignore
├── package-lock.json
├── package.json
└── pages
└── main
└── index.js
└── product
└── index.js
└── components
└── product-card
└── index.js
└── product
└── index.js
└── back-button
└── index.js
├── index.html
├── main.js
Создать двухстраничное приложение из примера по вариантам. Вариант состоит из темы и компонента, который необходимо использовать. Все данные должны соответствовать вашей теме. Компонент можно применить по своему усмотрению.
Варианты:
- Тема - собаки, Компонент - аккордеон.
- Тема - кошки, Компонент - уведомления.
- Тема - продукты, Компонент - значки.
- Тема - учебные предметы, Компонент - карусель.
- Тема - дизайн, Компонент - информер.
- Тема - финансы, Компонент - всплывающие сообщения.
- Тема - фотографии, Компонент - группа кнопок.