Developing faster applications on Vue.js

JavaScript is the soul of modern web applications. This is the main ingredient in front-end development. There are various JavaScript frameworks for creating web project interfaces. Vue.js is one of these frameworks, which can be attributed to fairly popular solutions.

Vue.js is a progressive framework for creating user interfaces. Its core library is aimed mainly at creating the visible part of interfaces. Other libraries can be easily integrated into a Vue-based project if necessary. In addition, with the help of Vue.js and with the involvement of modern tools and auxiliary libraries, it is possible to create complex single-page applications.



This article will describe the process of creating a simple Vue.js application designed to work with notes about certain tasks. Here is the project frontend repository. Here is the repository of his backend. We, along the way, will analyze some powerful features of Vue.js and auxiliary tools.

Project creation


Before we move on to development, let's create and configure the basic project of our task management application.

  1. Create a new project using the Vue.js 3 command line interface:

    vue create notes-app
  2. Add the package.jsonfollowing file to the project :

    {
      "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. Set the dependencies described in package.json:

    npm install

Now, after the application database is ready, we can move on to the next step of working on it.

Routing


Routing is one of the great features of modern web applications. The router can be integrated into the Vue.js application using the library vue-router. This is the official router for Vue.js projects. Among its features, we note the following:

  • Nested routes / views.
  • Modular configuration of the router.
  • Access to route parameters, requests, templates.
  • Animation of transitions of representations based on the capabilities of Vue.js.
  • Convenient navigation control.
  • Support for automatic styling of active links.
  • Support for HTML5-API history, the ability to use URL hashes, automatically switch to IE9 compatibility mode.
  • Customizable page scroll behavior.

To implement routing in our application, create, in a folder router, a file index.js. Add the following code to it:

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;

Consider an object routesthat includes a description of the routes supported by the application. Nested routes are used here.

The object childrencontains nested routes that will be shown on the application page that represents its control panel (file DashboardLayout.vue). Here is the template for this page:

<template>
  <span>
    <nav-bar />
    <div class="container is-fluid body-content">
      <router-view :key="$router.path" />
    </div>
  </span>
</template>

In this code, the most important is the tag router-view. It plays the role of a container that contains all the components corresponding to the displayed route.

Component Basics


Components are the core component of Vue.js applications. They give us the opportunity to use a modular approach to development, which means breaking DOM pages into several small fragments that can be reused on different pages.

When designing components, in order to make them scalable and suitable for reuse, there are some important things to consider:

  1. Identify a separate piece of functionality that can be selected from the project as a component.
  2. Do not overload the component with capabilities that do not correspond to its main functionality.
  3. Include only the code that will be used to ensure its own operation. For example, this is code that ensures the operation of standard data bindings for a certain component, such as year, user gender, and so on.
  4. Do not add code to the component that provides work with mechanisms external to the component, for example, with certain APIs.

Here, as a simple example, you can consider the navigation bar - a component NavBarthat contains only descriptions of DOM structures related to navigation tools for the application. The component code is contained in the file 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>

Here's how this component is used in 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>

Component Interaction


In any web application, the proper organization of data flows is extremely important. This allows you to effectively manipulate and manage application data.

When using the component approach, when separating the markup and application code into small parts, the developer asks how to transfer and process the data used by various components. The answer to this question is the organization of the interaction of components.

The interaction of components in a Vue.js project can be organized using the following mechanisms:

  1. Properties (props) are used when transferring data from parent components to child components.
  2. The $ emit () method is used when transferring data from child components to parent components.
  3. The global event bus (EventBus) is used when component structures with deep nesting are used, or when it is necessary, on a global scale, to organize an exchange between components according to the publisher / subscriber model.

In order to understand the concept of component interaction in Vue.js, we add two components to the project:

  • The component Addthat will be used to add new tasks to the system and to edit existing tasks.
  • A component NoteViewerdesigned to display information about one task.

Here is the component file 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>

Here is the component file 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>

Now that the components are created, we’ll examine their sections <script>.

The site propsannounced some objects with their types. These are the objects that we are going to pass to the component when it will be displayed on some page of the application.

In addition, pay attention to those parts of the code where the method is used $emit(). With it, a child component generates events by which data is passed to the parent component.

Let's talk about how to use the components in the application Addand NoteViewer. Let us describe in the file Home.vuebelow the mechanisms for transmitting data to these components and the mechanisms for listening to the events generated by them:

<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>

Now, if you look closely at this code, you will notice that the component Addbearing the name note-editoris applied twice. Once - to add a note, a second time - to update its contents.

In addition, we reuse the component NoteViewerpresented here as note-viewer, listing with its help a list of notes loaded from the database, which we iterate through the attribute v-for.

There is still worth paying attention to an event @cancelthat is used in the element note-editor, which is for operations Addand Updatehandled differently, despite the fact that these operations are implemented on the basis of the same component.

<!-- 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)" />

This is how scaling problems can be avoided. The point is that if there is a probability of a change in the implementation of a certain mechanism, then in such a situation the component simply generates the corresponding event.

When working with components, we use dynamic data injection. For example, attribute :notec note-viewer.

That's all. Now our components can exchange data.

Using the Axios Library


Axios is a Promise-based library for organizing interactions with various external services.

It has many features and is focused on safe work. We are talking about the fact that Axios supports protection against XSRF attacks, request and response interceptors, means for converting request and response data, it supports request cancellation, and much more.

We’ll connect the Axios library to the application and configure it so that we don’t have to import it every time we use it. Create a axiosfile in the folder 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 });

Add a main.jsresponse interceptor to the file , designed to interact with an external API. We will use the interceptor to prepare the data transferred to the application and to handle errors.

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;

Now add to the main.jsglobal variable $http:

import HTTP from "./axios";
Vue.prototype.$http = HTTP;

We will be able to work with this variable throughout the application through an instance of Vue.js.

Now we are ready to make API requests, which may look like this:

const data = await this.$http.get("notes/getall");

Optimization


Imagine that our application has grown to sizes when it includes hundreds of components and views.

This will affect the loading time of the application, since all of its JavaScript code will be downloaded to the browser in one go. In order to optimize the loading of the application, we need to answer several questions:

  1. How to make sure that components and views that are not currently in use are not loaded?
  2. How to reduce the size of the downloaded materials?
  3. How to improve application loading time?

As an answer to these questions, the following can be suggested: immediately load the basic structure of the application, and load components and views when they are needed. We will do this by using the capabilities of Webpack and making the following changes to the router settings:

{
path: "/notes",
name: "Notes",
component: () =>
import(/* webpackChunkName: "home" */ "../views/Home.vue")
}
//   /* webpackChunkName: "home" */

This allows you to create separate fragments for a particular route with application (view [view].[hash].js) materials that are loaded in lazy mode when the user visits this route.

Packing a project in a Docker container and deploying it


Now the application works as it should, which means it's time to containerize it. Add the following file to the project 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;"]

When using the application in production, we place it behind a powerful HTTP server like Nginx. This protects the application from hacking and other attacks.

Remember the environment variable containing the host information that we declared when setting up Axios? Here she is:

const apiHost = process.env.VUE_APP_API_HOST || "/";

Since this is a browser application, we need to set and pass this variable to the application during its assembly. It is very simple to do this by using the option --build-argwhen assembling the image:

sudo docker build --build-arg VUE_APP_API_HOST=<Scheme>://<ServiceHost>:<ServicePort>/ -f Dockerfile -t vue-app-image .

Pay attention to what you need to replace <Scheme>, <ServiceHost>and <ServicePort>to values ​​that make sense for your project.

After the application container is assembled, it can be launched:

sudo docker run -d -p 8080:80 β€” name vue-app vue-app-image

Summary


We examined the development process of an application based on Vue.js, talked about some auxiliary tools, and touched upon performance optimization issues. Now with our application you can experiment in the browser. Here is a video demonstrating how to work with it.

Dear readers! What would you advise to pay attention to beginners seeking to develop high-performance Vue.js applications that scale well?

Source: https://habr.com/ru/post/undefined/


All Articles