Хабрахабр

[Перевод] Разработка более быстрых приложений на Vue.js

JavaScript — это душа современных веб-приложений. Это — главный ингредиент фронтенд-разработки. Существуют различные JavaScript-фреймворки для создания интерфейсов веб-проектов. Vue.js — это один из таких фреймворков, который можно отнести к довольно популярным решениям.

Vue.js — это прогрессивный фреймворк, предназначенный для создания пользовательских интерфейсов. Его базовая библиотека направлена, в основном, на создание видимой части интерфейсов. В проект, основанный на Vue, при необходимости легко интегрировать и другие библиотеки. Кроме того, с помощью Vue.js и с привлечением современных инструментов и вспомогательных библиотек, можно создавать сложные одностраничные приложения.

В этом материале будет описан процесс создания простого Vue.js-приложения, предназначенного для работы с заметками о неких задачах. Вот репозиторий фронтенда проекта. Вот — репозиторий его бэкенда. Мы, по ходу дела, разберём некоторые мощные возможности Vue.js и вспомогательных инструментов.

Создание проекта

Прежде чем мы перейдём к разработке — давайте создадим и настроим базовый проект нашего приложения по управлению задачами.

  1. Создадим новый проект, воспользовавшись интерфейсом командной строки Vue.js 3:
    vue create notes-app
  2. Добавим в проект файл 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" }}
  3. Установим зависимости, описанные в 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 страниц на несколько небольших фрагментов, которые можно многократно использовать на различных страницах.

При проектировании компонентов, для того, чтобы сделать их масштабируемыми и подходящими для повторного использования, нужно учитывать некоторые важные вещи:

  1. Идентифицируйте отдельный фрагмент функционала, который можно выделить из проекта в виде компонента.
  2. Не перегружайте компонент возможностями, не соответствующими его основному функционалу.
  3. Включайте в состав компонента только тот код, который будет использоваться для обеспечения его собственной работы. Например — это код, обеспечивающий работу стандартных для некоего компонента привязок данных, вроде года, пола пользователя, и так далее.
  4. Не добавляйте в компонент код, обеспечивающий работу с внешними по отношению к компоненту механизмами, например — с некими 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-проекте можно организовать с использованием следующих механизмов:

  1. Свойства (props) используются при передаче данных от родительским компонентам дочерним компонентам.
  2. Метод $emit() применяется при передаче данных от дочерних компонентов родительским компонентам.
  3. Глобальная шина событий (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-код будет загружаться в браузер за один заход. Для того чтобы оптимизировать загрузку приложения, нам нужно ответить на несколько вопросов:

  1. Как сделать так, чтобы компоненты и представления, которые в данный момент не используются, не загружались бы?
  2. Как уменьшить размер загружаемых материалов?
  3. Как улучшить время загрузки приложения?

В качестве ответа на эти вопросы можно предложить следующее: сразу загружать базовые структуры приложения, а компоненты и представления загружать тогда, когда они нужны. Сделаем это, воспользовавшись возможностями Webpack и внеся в настройки маршрутизатора следующие изменения:

{path: "/notes",name: "Notes",component: () =>import(/* webpackChunkName: "home" */ "../views/Home.vue")}// Взгляните на /* webpackChunkName: "home" */

Это позволяет создавать для конкретного маршрута отдельные фрагменты с материалами приложения (вида [view].[hash].js), которые загружаются в ленивом режиме при посещении пользователем данного маршрута.

Упаковка проекта в контейнер Docker и развёртывание

Теперь приложение работает так, как нужно, а значит пришло время его контейнеризации. Добавим в проект следующий файл Dockerfile:

# build stageFROM node:lts-alpine as build-stageWORKDIR /appCOPY package*.json ./RUN npm installCOPY . .ARG VUE_APP_API_HOSTENV VUE_APP_API_HOST $VUE_APP_API_HOSTRUN npm run build # production stageFROM nginx:stable-alpine as production-stageCOPY --from=build-stage /app/dist /usr/share/nginx/htmlCOPY nginx.conf /etc/nginx/conf.d/default.confEXPOSE 80CMD ["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-приложения, которые хорошо масштабируются?

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»