JavaScript — это душа современных веб-приложений. Это — главный ингредиент фронтенд-разработки. Существуют различные JavaScript-фреймворки для создания интерфейсов веб-проектов. Vue.js — это один из таких фреймворков, который можно отнести к довольно популярным решениям.
Vue.js — это прогрессивный фреймворк, предназначенный для создания пользовательских интерфейсов. Его базовая библиотека направлена, в основном, на создание видимой части интерфейсов. В проект, основанный на Vue, при необходимости легко интегрировать и другие библиотеки. Кроме того, с помощью Vue.js и с привлечением современных инструментов и вспомогательных библиотек, можно создавать сложные одностраничные приложения.
В этом материале будет описан процесс создания простого Vue.js-приложения, предназначенного для работы с заметками о неких задачах. Вот репозиторий фронтенда проекта. Вот — репозиторий его бэкенда. Мы, по ходу дела, разберём некоторые мощные возможности Vue.js и вспомогательных инструментов.
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
vue-router
. Это — официальный маршрутизатор для Vue.js-проектов. Среди его возможностей отметим следующие: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
. Он играет роль контейнера, который содержит все компоненты, соответствующие выводимому маршруту.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>
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
, файл 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;
const data = await this.$http.get("notes/getall");
{
path: "/notes",
name: "Notes",
component: () =>
import(/* webpackChunkName: "home" */ "../views/Home.vue")
}
// Взгляните на /* webpackChunkName: "home" */
[view].[hash].js
), которые загружаются в ленивом режиме при посещении пользователем данного маршрута.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;"]
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
К сожалению, не доступен сервер mySQL