Разработка более быстрых приложений на 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 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-приложения, которые хорошо масштабируются?

ссылка на оригинал статьи https://habr.com/ru/company/ruvds/blog/487684/


Комментарии

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

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