С помощью API уведомлений HTML5 браузеры могут отображать для пользователей собственные всплывающие уведомления. С помощью уведомлений вы можете отображать текст и значки, а также воспроизводить с ними звук. Полный список опций находится по адресу https://developer.mozilla.org/en-US/docs/Web/API/notification. Пользователи должны предоставить разрешение на отображение уведомлений при посещении веб-приложения для просмотра уведомлений браузера.

Разработчики проделали за нас тяжелую работу, если мы будем использовать React, потому что компонент React создан для отображения уведомлений браузера. Пакет Vue-Native-Notification, расположенный по адресу https://www.npmjs.com/package/vue-native-notification, может позволить нам отображать всплывающие окна и обрабатывать события, связанные с отображением уведомлений, например, при использовании щелчков по уведомление или обрабатывать случаи, когда разрешения либо предоставлены, либо запрещены для отображения уведомлений.

В этой статье мы создадим менеджер паролей, который позволит вам вводить, редактировать и удалять пароли для веб-сайтов и показывать уведомления всякий раз, когда выполняются эти действия. Мы будем использовать Vue.js для создания приложения.

Для начала создадим проект, запустив npx @vue/cli create password-manager. В мастере выберите «Выбрать функции вручную» и выберите включение Babel, Vue Router и Vuex в наше приложение.

Далее мы устанавливаем некоторые необходимые нам библиотеки. Нам нужны Axios для выполнения HTTP-запросов, Bootstrap Vue для стилизации, V-Clipboard для копирования в буфер обмена, Vue-Native-Notification для отображения собственных уведомлений браузера и Vee-Validate для проверки формы. Устанавливаем их, запустив:

npm i axios bootstrap-vue v-clipboard vee-validate vue-native-notification

После установки библиотек мы можем приступить к созданию приложения. Сначала в папке components создайте файл с именем PasswordForm.vue для нашей формы пароля. Затем мы добавляем:

<template>
  <ValidationObserver ref="observer" v-slot="{ invalid }">
    <b-form @submit.prevent="onSubmit" novalidate>
      <b-form-group label="Name">
        <ValidationProvider name="name" rules="required" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.name"
            required
            placeholder="Name"
            name="name"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Name is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-form-group label="URL">
        <ValidationProvider name="url" rules="required|url" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.url"
            required
            placeholder="URL"
            name="url"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-form-group label="Username">
        <ValidationProvider name="username" rules="required" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.username"
            required
            placeholder="Username"
            name="username"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Username is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-form-group label="Password">
        <ValidationProvider name="password" rules="required" v-slot="{ errors }">
          <b-form-input
            type="password"
            :state="errors.length == 0"
            v-model="form.password"
            required
            placeholder="Password"
            name="password"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Password is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-button type="submit" variant="primary" style="margin-right: 10px">Submit</b-button>
      <b-button type="reset" variant="danger" @click="cancel()">Cancel</b-button>
    </b-form>
  </ValidationObserver>
</template>
<script>
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
  name: "PasswordForm",
  mixins: [requestsMixin],
  props: {
    edit: Boolean,
    password: Object
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      if (this.edit) {
        await this.editPassword(this.form);
        this.$notification.show(
          "Password edited",
          {
            body: "Password edited"
          },
          {}
        );
      } else {
        await this.addPassword(this.form);
        this.$notification.show(
          "Password added",
          {
            body: "Password added"
          },
          {}
        );
      }
      const response = await this.getPasswords();
      this.$store.commit("setPasswords", response.data);
      this.$emit("saved");
    },
    cancel() {
      this.$emit("cancelled");
    }
  },
  data() {
    return {
      form: {}
    };
  },
  watch: {
    password: {
      handler(p) {
        this.form = JSON.parse(JSON.stringify(p || {}));
      },
      deep: true,
      immediate: true
    }
  }
};
</script>

У нас есть форма пароля в этом компоненте. Форма включает поля имени, URL, имени пользователя и пароля. Все они необходимы. Мы используем Vee-Validate для проверки полей формы. Компонент ValidationObserver предназначен для проверки всей формы, а компонент ValidationProvider - для проверки полей формы, которые он обтекает. Правило проверки определяется атрибутом rule каждого поля. У нас есть специальное url правило для поля URL. Мы показываем сообщения об ошибках проверки, когда объект errors из слота области видимости имеет ненулевую длину. Параметр state предназначен для установки состояния проверки, которое показывает зеленый цвет, когда errors имеет длину 0, и красный цвет в противном случае. Сообщения об ошибках отображаются в компоненте b-form-invalid-feedback.

Когда пользователь нажимает кнопку Сохранить, вызывается onSubmit функция. Мы получаем состояние проверки формы с помощью using this.$refs.observer.validate();. Ссылка относится к исх. Из ValidationObserver. Если он принимает значение true, мы вызываем addPassword или editPassword для сохранения записи в зависимости от свойства edit. Затем мы получаем пароли, вызывая getPasswords, а затем помещаем их в наш магазин Vuex, отправляя мутацию setPasswords. Затем мы генерируем событие saved, чтобы закрыть модальное окно на домашней странице. Уведомления отображаются путем вызова this.$notification.show, предоставленного Vue-Native-Notification. Первый аргумент - это заголовок уведомления, второй содержит тело, а третий аргумент - это необязательные обработчики событий, которые вы можете добавить при необходимости. Полный список обработчиков событий находится на https://www.npmjs.com/package/vue-native-notification.

У нас есть блок watch, который в основном используется, когда редактируется существующая запись, мы получаем свойство password и устанавливаем его в this.form, создавая копию свойства, чтобы мы обновляли только объект form и ничего, когда данные связываются.

Затем мы создаем папку mixins и добавляем в нее requestsMixin.js. В файле добавить:

const APIURL = "http://localhost:3000";
const axios = require("axios");
export const requestsMixin = {
  methods: {
    getPasswords() {
      return axios.get(`${APIURL}/passwords`);
    },
addPassword(data) {
      return axios.post(`${APIURL}/passwords`, data);
    },
editPassword(data) {
      return axios.put(`${APIURL}/passwords/${data.id}`, data);
    },
deletePassword(id) {
      return axios.delete(`${APIURL}/passwords/${id}`);
    }
  }
};

Он содержит код для выполнения HTTP-запросов в серверной части. Мы включаем этот миксин в наши компоненты, чтобы мы могли делать запросы к серверной части от них.

Затем в Home.vue мы заменяем существующий код на:

<template>
  <div class="page">
    <h1 class="text-center">Password Manager</h1>
    <b-button-toolbar>
      <b-button @click="openAddModal()">Add Password</b-button>
    </b-button-toolbar>
    <br />
    <b-table-simple responsive>
      <b-thead>
        <b-tr>
          <b-th>Name</b-th>
          <b-th>URL</b-th>
          <b-th>Username</b-th>
          <b-th>Password</b-th>
          <b-th></b-th>
          <b-th></b-th>
          <b-th></b-th>
          <b-th></b-th>
        </b-tr>
      </b-thead>
      <b-tbody>
        <b-tr v-for="p in passwords" :key="p.id">
          <b-td>{{p.name}}</b-td>
          <b-td>{{p.url}}</b-td>
          <b-td>{{p.username}}</b-td>
          <b-td>******</b-td>
          <b-td>
            <b-button
              v-clipboard="() => p.username"
              @click="notify('Username copied', 'Username copied')"
            >Copy Username</b-button>
          </b-td>
          <b-td>
            <b-button
              v-clipboard="() => p.password"
              @click="notify('Password copied', 'Password copied')"
            >Copy Password</b-button>
          </b-td>
          <b-td>
            <b-button @click="openEditModal(p)">Edit</b-button>
          </b-td>
          <b-td>
            <b-button @click="deleteOnePassword(p.id)">Delete</b-button>
          </b-td>
        </b-tr>
      </b-tbody>
    </b-table-simple>
<b-modal id="add-modal" title="Add Password" hide-footer>
      <PasswordForm @saved="closeModal()" @cancelled="closeModal()" :edit="false"></PasswordForm>
    </b-modal>
<b-modal id="edit-modal" title="Edit Password" hide-footer>
      <PasswordForm
        @saved="closeModal()"
        @cancelled="closeModal()"
        :edit="true"
        :password="selectedPassword"
      ></PasswordForm>
    </b-modal>
  </div>
</template>
<script>
import { requestsMixin } from "@/mixins/requestsMixin";
import PasswordForm from "@/components/PasswordForm";
export default {
  name: "home",
  components: {
    PasswordForm
  },
  mixins: [requestsMixin],
  computed: {
    passwords() {
      return this.$store.state.passwords;
    }
  },
  beforeMount() {
    this.getAllPasswords();
  },
  data() {
    return {
      selectedPassword: {}
    };
  },
  methods: {
    notify(title, body) {
      this.$notification.show(
        title,
        {
          body
        },
        {}
      );
    },
    openAddModal() {
      this.$bvModal.show("add-modal");
    },
    openEditModal(password) {
      this.$bvModal.show("edit-modal");
      this.selectedPassword = password;
    },
    closeModal() {
      this.$bvModal.hide("add-modal");
      this.$bvModal.hide("edit-modal");
      this.selectedPassword = {};
    },
    async deleteOnePassword(id) {
      await this.deletePassword(id);
      this.$notification.show(
        "Password deleted",
        {
          body: "Password deleted"
        },
        {}
      );
      this.getAllPasswords();
    },
    async getAllPasswords() {
      const response = await this.getPasswords();
      this.$store.commit("setPasswords", response.data);
    }
  }
};
</script>

В этом файле у нас есть таблица для отображения списка паролей, позволяющая пользователям открывать и закрывать модальные окна добавления и редактирования. У нас есть кнопки в каждой строке, чтобы скопировать имя пользователя и пароли, а также позволить пользователям редактировать или удалять каждую запись.

В разделе scripts у нас есть ловушка beforeMount для получения всех паролей во время загрузки страницы с помощью функции getPasswords, которую мы написали в нашем миксине. При нажатии кнопки «Изменить» устанавливается переменная selectedPassword, и мы передаем ее в PasswordForm для редактирования.

Чтобы удалить пароль, мы вызываем deletePassword в нашем миксине, чтобы запросить серверную часть.

Сюда добавляется функция копирования в буфер обмена. Для кнопок копирования имени пользователя и пароля мы используем директиву v-clipboard, которая позволяет нам копировать имя пользователя и пароль соответственно в буфер обмена при нажатии кнопки.

У нас есть уведомления об удалении записи здесь. Называются они так же, как в PasswordForm. Кроме того, мы добавили функцию notify, чтобы мы могли показывать уведомления, когда имя пользователя или пароль копируются, щелкая соответствующие кнопки.

Затем в App.vue мы заменяем существующий код на:

<template>
  <div id="app">
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand href="#">Password Manager</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav>
          <b-nav-item to="/" :active="path  == '/'">Home</b-nav-item>
        </b-navbar-nav>
      </b-collapse>
    </b-navbar>
    <router-view />
  </div>
</template>
<script>
export default {
  data() {
    return {
      path: this.$route && this.$route.path
    };
  },
  watch: {
    $route(route) {
      this.path = route.path;
    }
  }
};
</script>
<style lang="scss">
.page {
  padding: 20px;
}
button {
  margin-right: 10px;
}
</style>

чтобы добавить панель навигации Bootstrap в верхнюю часть наших страниц и router-view для отображения определяемых нами маршрутов.

Затем в main.js замените код на:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import Clipboard from "v-clipboard";
import { required } from "vee-validate/dist/rules";
import VueNativeNotification from "vue-native-notification";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
extend("required", required);
extend("url", {
  validate: value => {
    return /^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/.test(
      value
    );
  },
  message: "URL is invalid."
});
Vue.use(BootstrapVue);
Vue.use(Clipboard);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.use(VueNativeNotification, {
  requestOnNotify: true
});
Vue.config.productionTip = false;
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");

так что мы добавляем библиотеки, которые мы установили в наше приложение, чтобы мы могли использовать их в наших компонентах. Мы вызываем extend из Vee-Validate, чтобы добавить правила проверки формы, которые мы хотим использовать. Кроме того, мы добавляем сюда библиотеку V-Clipboard, чтобы мы могли использовать ее на нашей домашней странице.

Мы включаем сюда библиотеку Vue-Native-Notification, добавляя:

Vue.use(VueNativeNotification, {
  requestOnNotify: true
});

Параметры requestOnNotify предназначены для отображения запроса о разрешении при первом уведомлении, если установлено значение true.

В router.js мы заменяем существующий код на:

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
Vue.use(Router)
export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    }
  ]
})

чтобы включить только нашу домашнюю страницу.

Затем в store.js мы заменяем существующий код на:

import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
  state: {
    passwords: []
  },
  mutations: {
    setPasswords(state, payload) {
      state.passwords = payload;
    }
  },
  actions: {}
});

чтобы добавить наше passwords состояние в хранилище, чтобы мы могли наблюдать за ним в блоке computed компонентов PasswordForm и HomePage. У нас есть функция setPasswords для обновления состояния passwords, и мы используем ее в компонентах с помощью вызова this.$store.commit(“setPasswords”, response.data);, как мы это делали в PasswordForm. Кроме того, мы импортировали CSS Bootstrap в этот файл, чтобы получить стили.

Наконец, в index.html замените существующий код на:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
    <title>Password Manager</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but vue-clipboard-tutorial-app doesn't work properly
        without JavaScript enabled. Please enable it to continue.</strong
      >
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

изменить название.

После всей тяжелой работы мы можем запустить наше приложение, запустив npm start.

Чтобы запустить серверную часть, мы сначала устанавливаем пакет json-server, запустив npm i json-server. Затем перейдите в папку нашего проекта и запустите:

json-server --watch db.json

В db.json измените текст на:

{
  "passwords": [
  ]
}

Итак, у нас есть passwords endpoints, определенные в requests.js.

После всей кропотливой работы получаем: