https://habr.com/ru/company/ruvds/blog/487684/- Блог компании RUVDS.com
- Разработка веб-сайтов
- JavaScript
- VueJS
JavaScript — это душа современных веб-приложений. Это — главный ингредиент фронтенд-разработки. Существуют различные JavaScript-фреймворки для создания интерфейсов веб-проектов. Vue.js — это один из таких фреймворков, который можно отнести к довольно популярным решениям.
Vue.js — это прогрессивный фреймворк, предназначенный для создания пользовательских интерфейсов. Его базовая библиотека направлена, в основном, на создание видимой части интерфейсов. В проект, основанный на Vue, при необходимости легко интегрировать и другие библиотеки. Кроме того, с помощью Vue.js и с привлечением современных инструментов и вспомогательных библиотек, можно создавать сложные одностраничные приложения.
В этом материале будет описан процесс создания простого Vue.js-приложения, предназначенного для работы с заметками о неких задачах.
Вот репозиторий фронтенда проекта.
Вот — репозиторий его бэкенда. Мы, по ходу дела, разберём некоторые мощные возможности Vue.js и вспомогательных инструментов.
Создание проекта
Прежде чем мы перейдём к разработке — давайте создадим и настроим базовый проект нашего приложения по управлению задачами.
- Создадим новый проект, воспользовавшись интерфейсом командной строки Vue.js 3:
vue create notes-app
- Добавим в проект файл
package.json следующего содержания:
{
"name": "notes-app",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.19.1",
"buefy": "^0.8.9",
"core-js": "^3.4.4",
"lodash": "^4.17.15",
"marked": "^0.8.0",
"vee-validate": "^3.2.1",
"vue": "^2.6.10",
"vue-router": "^3.1.3"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.1.0",
"@vue/cli-plugin-eslint": "^4.1.0",
"@vue/cli-service": "^4.1.0",
"@vue/eslint-config-prettier": "^5.0.0",
"babel-eslint": "^10.0.3",
"eslint": "^5.16.0",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-vue": "^5.0.0",
"prettier": "^1.19.1",
"vue-template-compiler": "^2.6.10"
}
}
- Установим зависимости, описанные в
package.json:
npm install
Теперь, после того, как база приложения готова, мы можем переходить к следующему шагу работы над ним.
Маршрутизация
Маршрутизация (routing) — это одна из замечательных возможностей современных веб-приложений. Маршрутизатор можно интегрировать в Vue.js-приложение, воспользовавшись библиотекой
vue-router. Это — официальный маршрутизатор для Vue.js-проектов. Среди его возможностей отметим следующие:
- Вложенные маршруты/представления.
- Модульная конфигурация маршрутизатора.
- Доступ к параметрам маршрута, запросам, шаблонам.
- Анимация переходов представлений на основе возможностей Vue.js.
- Удобный контроль навигации.
- Поддержка автоматической стилизации активных ссылок.
- Поддержка HTML5-API history, возможность использования URL-хэшей, автоматическое переключение в режим совместимости с IE9.
- Настраиваемое поведение прокрутки страницы.
Для реализации маршрутизации в нашем приложении создадим, в папке
router, файл
index.js. Добавим в него следующий код:
import Vue from "vue";
import VueRouter from "vue-router";
import DashboardLayout from "../layout/DashboardLayout.vue";
Vue.use(VueRouter);
const routes = [
{
path: "/home",
component: DashboardLayout,
children: [
{
path: "/notes",
name: "Notes",
component: () =>
import(/* webpackChunkName: "home" */ "../views/Home.vue")
}
]
},
{
path: "/",
redirect: { name: "Notes" }
}
];
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes
});
export default router;
Рассмотрим объект
routes, который включает в себя описание маршрутов, поддерживаемых приложением. Здесь используются вложенные маршруты.
Объект
children содержит вложенные маршруты, которые будут показаны на странице приложения, представляющей его панель управления (файл
DashboardLayout.vue). Вот шаблон этой страницы:
<template>
<span>
<nav-bar />
<div class="container is-fluid body-content">
<router-view :key="$router.path" />
</div>
</span>
</template>
В этом коде самое важное — тег
router-view. Он играет роль контейнера, который содержит все компоненты, соответствующие выводимому маршруту.
Основы работы с компонентами
Компоненты — это базовая составляющая Vue.js-приложений. Они дают нам возможность пользоваться модульным подходом к разработке, что означает разбиение DOM страниц на несколько небольших фрагментов, которые можно многократно использовать на различных страницах.
При проектировании компонентов, для того, чтобы сделать их масштабируемыми и подходящими для повторного использования, нужно учитывать некоторые важные вещи:
- Идентифицируйте отдельный фрагмент функционала, который можно выделить из проекта в виде компонента.
- Не перегружайте компонент возможностями, не соответствующими его основному функционалу.
- Включайте в состав компонента только тот код, который будет использоваться для обеспечения его собственной работы. Например — это код, обеспечивающий работу стандартных для некоего компонента привязок данных, вроде года, пола пользователя, и так далее.
- Не добавляйте в компонент код, обеспечивающий работу с внешними по отношению к компоненту механизмами, например — с некими API.
Здесь, в качестве простого примера, можно рассмотреть навигационную панель — компонент
NavBar, содержащий только описания DOM-структур, относящихся к средствам навигации по приложению. Код компонента содержится в файле
NavBar.vue:
<template>
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/home/notes">
<img align="center" src="@/assets/logo.png" width="112" height="28">
</a>
<a
role="button"
class="navbar-burger burger"
aria-label="menu"
aria-expanded="false"
data-target="navbarBasicExample"
>
<span aria-hidden="true" />
<span aria-hidden="true" />
<span aria-hidden="true" />
</a>
</div>
</nav>
</template>
Вот как этот компонент используется в
DashboardLayout.vue:
<template>
<span>
<nav-bar />
<div class="container is-fluid body-content">
<router-view :key="$router.path" />
</div>
</span>
</template>
<script>
import NavBar from "@/components/NavBar";
export default {
components: {
NavBar
}
};
</script>
<style scoped></style>
Взаимодействие компонентов
В любом веб-приложении чрезвычайно важна правильная организация потоков данных. Это позволяет эффективно манипулировать и управлять данными приложений.
При использовании компонентного подхода, при разделении разметки и кода приложения на небольшие части, перед разработчиком встаёт вопрос о том, как передавать и обрабатывать данные, используемые различными компонентами. Ответом на этот вопрос является организация взаимодействия компонентов.
Взаимодействие компонентов в Vue.js-проекте можно организовать с использованием следующих механизмов:
- Свойства (props) используются при передаче данных от родительским компонентам дочерним компонентам.
- Метод $emit() применяется при передаче данных от дочерних компонентов родительским компонентам.
- Глобальная шина событий (EventBus) используется в тех случаях, когда применяются структуры компонентов с глубокой вложенностью, или тогда, когда нужно, в глобальном масштабе приложения, организовать обмен между компонентами по модели «издатель/подписчик».
Для того чтобы разобраться с концепцией взаимодействия компонентов в Vue.js, добавим в проект два компонента:
- Компонент
Add, который будет использоваться для добавления в систему новых задач и для редактирования существующих задач.
- Компонент
NoteViewer, предназначенный для вывода сведений об одной задаче.
Вот файл компонента
Add (
Add.vue):
<template>
<div class="container">
<div class="card note-card">
<div class="card-header">
<div class="card-header-title title">
<div class="title-content">
<p v-if="addMode">
Add Note
</p>
<p v-else>
Update Note
</p>
</div>
</div>
</div>
<div class="card-content">
<div class="columns">
<div class="column is-12">
<template>
<section>
<b-field label="Note Header">
<b-input
v-model="note.content.title"
type="input"
placeholder="Note header"
/>
</b-field>
<b-field label="Description">
<b-input
v-model="note.content.description"
type="textarea"
placeholder="Note Description"
/>
</b-field>
<div class="buttons">
<b-button class="button is-default" @click="cancelNote">
Cancel
</b-button>
<b-button
v-if="addMode"
class="button is-primary"
@click="addNote"
>
Add
</b-button>
<b-button
v-else
class="button is-primary"
@click="updateNote"
>
Update
</b-button>
</div>
</section>
</template>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
addMode: {
type: Boolean,
required: false,
default() {
return true;
}
},
note: {
type: Object,
required: false,
default() {
return {
content: {
title: "",
description: "",
isComplated: false
}
};
}
}
},
methods: {
addNote() {
this.$emit("add", this.note);
},
updateNote() {
this.$emit("update", this.note);
},
cancelNote() {
this.$emit("cancel");
}
}
};
</script>
<style></style>
Вот файл компонента
NoteViewer (
NoteViewer.vue):
<template>
<div class="container">
<div class="card note-card">
<div class="card-header">
<div class="card-header-title title">
<div class="column is-6">
<p>Created at {{ note.content.createdAt }}</p>
</div>
<div class="column is-6 ">
<div class="buttons is-pulled-right">
<button
v-show="!note.content.isCompleted"
class="button is-success is-small "
title="Mark Completed"
@click="markCompleted"
>
<b-icon pack="fas" icon="check" size="is-small" />
</button>
<button
v-show="!note.content.isCompleted"
class="button is-primary is-small"
title="Edit Note"
@click="editNote"
>
<b-icon pack="fas" icon="pen" size="is-small" />
</button>
<button
class="button is-primary is-small "
title="Delete Note"
@click="deleteNote"
>
<b-icon pack="fas" icon="trash" size="is-small" />
</button>
</div>
</div>
</div>
</div>
<div
class="card-content"
:class="note.content.isCompleted ? 'note-completed' : ''"
>
<strong>{{ note.content.title }}</strong>
<p>{{ note.content.description }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: "NoteViewer",
props: {
note: {
type: Object,
required: true
}
},
methods: {
editNote() {
this.$emit("edit", this.note);
},
deleteNote() {
this.$emit("delete", this.note);
},
markCompleted() {
this.$emit("markCompleted", this.note);
}
}
};
</script>
<style></style>
Теперь, когда компоненты созданы, изучим их разделы
<script>.
В объекте
props объявлены некоторые объекты с указанием их типов. Это — те объекты, которые мы собираемся передавать компоненту тогда, когда он будет выводиться на некоей странице приложения.
Кроме того, обратите внимание на те участки кода, где используется метод
$emit(). С его помощью дочерний компонент генерирует события, посредством которых данные передаются родительскому компоненту.
Поговорим о том, как применять в приложении компоненты
Add и
NoteViewer. Опишем в файле
Home.vue, приведённом ниже, механизмы передачи данных этим компонентам и механизмы прослушивания событий, генерируемых ими:
<template>
<div class="container">
<div class="columns">
<div class="column is-12">
<button
class="button is-primary is-small is-pulled-right"
title="Add New Note"
@click="enableAdd()"
>
<b-icon pack="fas" icon="plus" size="is-small" />
</button>
</div>
</div>
<div class="columns">
<div class="column is-12">
<note-editor
v-show="enableAddNote"
:key="enableAddNote"
@add="addNote"
@cancel="disableAdd"
/>
<div v-for="(note, index) in data" :key="index">
<note-viewer
v-show="note.viewMode"
:note="note"
@edit="editNote"
@markCompleted="markCompletedConfirm"
@delete="deleteNoteConfirm"
/>
<note-editor
v-show="!note.viewMode"
:add-mode="false"
:note="note"
@update="updateNote"
@cancel="cancelUpdate(note)"
/>
</div>
</div>
</div>
</div>
</template>
<script>
// @ is an alias to /src
// import NoteEditor from "@/components/NoteEditor.vue";
import NoteEditor from "@/components/Add.vue";
import NoteViewer from "@/components/NoteViewer.vue";
export default {
name: "Home",
components: {
// NoteEditor,
NoteEditor,
NoteViewer
},
data() {
return {
enableAddNote: false,
data: []
};
},
mounted() {
this.getNotes();
},
methods: {
enableAdd() {
this.enableAddNote = true;
},
disableAdd() {
this.enableAddNote = false;
},
async getNotes() {
this.data = [];
const data = await this.$http.get("notes/getall");
data.forEach(note => {
this.data.push({
content: note,
viewMode: true
});
});
},
async addNote(note) {
await this.$http.post("notes/create", note.content);
this.disableAdd();
await this.getNotes();
},
editNote(note) {
note.viewMode = false;
},
async updateNote(note) {
await this.$http.put(`notes/${note.content.id}`, note.content);
note.viewMode = true;
await this.getNotes();
},
cancelUpdate(note) {
note.viewMode = true;
},
markCompletedConfirm(note) {
this.$buefy.dialog.confirm({
title: "Mark Completed",
message: "Would you really like to mark the note completed?",
type: "is-warning",
hasIcon: true,
onConfirm: async () => await this.markCompleted(note)
});
},
async markCompleted(note) {
note.content.isCompleted = true;
await this.$http.put(`notes/${note.content.id}`, note.content);
await this.getNotes();
},
deleteNoteConfirm(note) {
this.$buefy.dialog.confirm({
title: "Delete note",
message: "Would you really like to delete the note?",
type: "is-danger",
hasIcon: true,
onConfirm: async () => await this.deleteNote(note)
});
},
async deleteNote(note) {
await this.$http.delete(`notes/${note.content.id}`);
await this.getNotes();
}
}
};
</script>
Теперь, если присмотреться к этому коду, можно заметить, что компонент
Add, носящий здесь имя
note-editor, применяется дважды. Один раз — для добавления заметки, второй раз — для обновления её содержимого.
Кроме того, мы многократно используем компонент
NoteViewer, представленный здесь как
note-viewer, выводя с его помощью список заметок, загруженный из базы данных, который мы перебираем с помощью атрибута
v-for.
Тут ещё стоит обратить внимание на событие
@cancel, используемое в элементе
note-editor, которое для операций
Add и
Update обрабатывается по-разному, даже несмотря на то, что эти операции реализованы на базе одного и того же компонента.
<!-- Add Task -->
<note-editor v-show="enableAddNote"
:key="enableAddNote"
@add="addNote"
@cancel="disableAdd" />
<!-- Update Task -->
<note-editor v-show="!note.viewMode"
:add-mode="false"
:note="note"
@update="updateNote"
@cancel="cancelUpdate(note)" />
Именно так можно избежать проблемы с масштабированием. Речь идёт о том, что если есть вероятность изменения реализации некоего механизма, то в такой ситуации компонент просто генерирует соответствующее событие.
При работе с компонентами мы пользуемся динамическим внедрением данных. Например — атрибутом
:note в
note-viewer.
Вот и всё. Теперь наши компоненты могут обмениваться данными.
Использование библиотеки Axios
Axios — это библиотека, основанная на промисах, предназначенная для организации взаимодействия с различными внешними сервисами.
Она обладает множеством возможностей и ориентирована на безопасную работу. Речь идёт о том, что Axios поддерживает защиту от XSRF-атак, перехватчики запросов и ответов, средства преобразования данных запросов и ответов, она поддерживает отмену запросов и многое другое.
Подключим библиотеку Axios к приложению и настроим её, сделав так, чтобы нам не приходилось бы её импортировать при каждом её использовании. Создадим, в папке
axios, файл
index.js:
import axios from "axios";
const apiHost = process.env.VUE_APP_API_HOST || "/";
let baseURL = "api";
if (apiHost) {
baseURL = `${apiHost}api`;
}
export default axios.create({ baseURL: baseURL });
В файл
main.js добавим перехватчик ответов на запросы, предназначенный для взаимодействия с внешним API. Мы будем применять перехватчик для подготовки данных, передаваемых в приложение, и для обработки ошибок.
import HTTP from "./axios";
// Добавить перехватчик ответов
HTTP.interceptors.response.use(
response => {
if (response.data instanceof Blob) {
return response.data;
}
return response.data.data || {};
},
error => {
if (error.response) {
Vue.prototype.$buefy.toast.open({
message: error.response.data.message || "Something went wrong",
type: "is-danger"
});
} else {
Vue.prototype.$buefy.toast.open({
message: "Unable to connect to server",
type: "is-danger"
});
}
return Promise.reject(error);
}
);
Vue.prototype.$http = HTTP;
Теперь добавим в
main.js глобальную переменную
$http:
import HTTP from "./axios";
Vue.prototype.$http = HTTP;
Мы сможем работать с этой переменной во всём приложении через экземпляр Vue.js.
Теперь мы готовы к выполнению запросов к API, которые могут выглядеть так:
const data = await this.$http.get("notes/getall");
Оптимизация
Представим, что наше приложение доросло до размеров, когда в его состав входят сотни компонентов и представлений.
Это повлияет на время загрузки приложения, так как весь его JavaScript-код будет загружаться в браузер за один заход. Для того чтобы оптимизировать загрузку приложения, нам нужно ответить на несколько вопросов:
- Как сделать так, чтобы компоненты и представления, которые в данный момент не используются, не загружались бы?
- Как уменьшить размер загружаемых материалов?
- Как улучшить время загрузки приложения?
В качестве ответа на эти вопросы можно предложить следующее: сразу загружать базовые структуры приложения, а компоненты и представления загружать тогда, когда они нужны. Сделаем это, воспользовавшись возможностями Webpack и внеся в настройки маршрутизатора следующие изменения:
{
path: "/notes",
name: "Notes",
component: () =>
import(/* webpackChunkName: "home" */ "../views/Home.vue")
}
// Взгляните на /* webpackChunkName: "home" */
Это позволяет создавать для конкретного маршрута отдельные фрагменты с материалами приложения (вида
[view].[hash].js), которые загружаются в ленивом режиме при посещении пользователем данного маршрута.
Упаковка проекта в контейнер Docker и развёртывание
Теперь приложение работает так, как нужно, а значит пришло время его контейнеризации. Добавим в проект следующий файл
Dockerfile:
# build stage
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
ARG VUE_APP_API_HOST
ENV VUE_APP_API_HOST $VUE_APP_API_HOST
RUN npm run build
# production stage
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
При использовании приложения в продакшне мы размещаем его за мощным HTTP-сервером вроде Nginx. Это позволяет защитить приложение от взломов и от других атак.
Помните о переменной окружения, содержащей сведения о хосте, которую мы объявили, настраивая Axios? Вот она:
const apiHost = process.env.VUE_APP_API_HOST || "/";
Так как это — браузерное приложение, нам нужно установить и передать в приложение эту переменную во время его сборки. Сделать это очень просто, воспользовавшись опцией
--build-arg при сборке образа:
sudo docker build --build-arg VUE_APP_API_HOST=<Scheme>://<ServiceHost>:<ServicePort>/ -f Dockerfile -t vue-app-image .
Обратите внимание на то, что вам понадобится заменить
<Scheme>,
<ServiceHost> и
<ServicePort> на значения, имеющие смысл для вашего проекта.
После того, как контейнер приложения будет собран, его можно запустить:
sudo docker run -d -p 8080:80 — name vue-app vue-app-image
Итоги
Мы рассмотрели процесс разработки приложения, основанного на Vue.js, поговорили о некоторых вспомогательных средствах, затронули вопросы оптимизации производительности. Теперь с нашим приложением можно поэкспериментировать в браузере.
Вот видео, демонстрирующее работу с ним.
Уважаемые читатели! На что вы посоветовали бы обратить внимание новичкам, стремящимся разрабатывать высокопроизводительные Vue.js-приложения, которые хорошо масштабируются?
