Skip to content

18. Клієнт Vue.js сервера розрахунку податків

18.1. Architecture

Ми реалізуємо клієнтсько-серверний додаток із такою архітектурою:

Image

Сервером розрахунку податків буде версія 14, розроблена в документі |https://tahe.developpez.com/tutoriels-cours/php7|

18.2. Види додатка

Види додатка [vuejs-10] відповідають версії 13 документа |https://tahe.developpez.com/tutoriels-cours/php7| сервера розрахунку податку, коли він використовується в режимі HTML. Однак у даному додатку ці екрани будуть генеруватися клієнтським JavaScript, а не сервером PHP.

Перший вигляд — це вигляд автентифікації:

Image

Другий вигляд — це вигляд розрахунку податку:

Image

Третій екран — це екран, на якому відображається список моделювань, виконаних користувачем:

Image

На екрані вище показано, що можна видалити моделювання № 1. Після цього з’являється таке вікно:

Image

Якщо тепер видалити останню симуляцію, з’явиться таке нове вікно:

Image

18.3. Елементи проєкту [vuejs-20]

Структура проекту [vuejs-20] виглядає наступним чином:

Image

Елементи проекту такі:

  • [assets/logo.jpg]: логотип проєкту;
  • [couches]: шари [métier] та [dao] додатка;
  • [plugins]: плагіни додатка;
  • [views]: подання додатка;
  • [config.js]: налаштовує додаток;
  • [router.js]: визначає маршрутизацію додатка;
  • [store.js]: сховище для [Vuex];
  • [main.js]: головний скрипт додатка;

18.3.1. Рівні [métier] та [dao]

18.3.1.1. Рівень [dao]

Рівень [dao] реалізовано класом [Dao] з розділу |vuejs-10|

18.3.1.2. Рівень [métier]

Рівень [métier] реалізовано класом [Métier] з документа |https://tahe.developpez.com/tutoriels-cours/php7|. До нього було додано такий метод [setTaxAdminData]:


// конструктор
  constructor(taxAdmindata) {
    // this.taxAdminData: дані податкової служби
    this.taxAdminData = taxAdmindata;
  }

  // налаштування
  setTaxAdminData(taxAdmindata) {
    // this.taxAdminData: дані податкової служби
    this.taxAdminData = taxAdmindata;
}

Метод [setTaxAdminData] виконує те саме, що й конструктор. Його наявність дозволяє виконати таку послідовність дій:

  1. створити екземпляр класу [Métier] за допомогою оператора [métier=new Métier()], коли потрібно створити екземпляр класу, але ще немає даних [taxAdminData];
  2. а потім заповнити його властивість [taxAdminData] за допомогою операції [métier.setTaxAdminData(taxAdmindata)];

18.3.2. Файл конфігурації [config]

Файл [config.js] має такий вигляд:


// використання бібліотеки [axios]
const axios = require('axios');
// тайм-аут запитів HTTP
axios.defaults.timeout = 2000;
// база даних сервера розрахунку податків URL
// схема [https] викликає проблеми у Firefox, оскільки сервер
// податків надсилає самопідписаний сертифікат. У Chrome та Edge все працює нормально. Safari не тестували.
axios.defaults.baseURL = 'https://localhost/php7/scripts-web/impots/version-14';
// ми будемо використовувати файли cookie
axios.defaults.withCredentials = true;

// експорт конфігурації
export default {
  axios: axios
}

Ця конфігурація належить бібліотеці [axios], яку рівень [dao] використовує для виконання своїх запитів HTTP. Зверніть увагу на рядок 8: сервер працює на захищеному порту [https].

18.3.3. Плагіни

Плагіни [pluginDao, pluginMétier, pluginConfig] призначені для створення трьох нових властивостей для функції/класу [Vue]:

  • [$dao]: матиме значення екземпляра класу [Dao];
  • [$métier]: матиме значення екземпляра класу [Métier];
  • [$config]: матиме значення об’єкта, експортованого конфігураційним файлом [config];

[pluginDao]


export default {
  install(Vue, dao) {
    // додаємо властивість [$dao] до класу Vue
    Object.defineProperty(Vue.prototype, '$dao', {
      // коли відбувається звернення до Vue.$dao, повертається другий параметр [dao]
      get: () => dao,
    })
  }
}

[pluginMétier]

export default {
  install(Vue, métier) {
    // додає властивість [$métier] до класу Vue
    Object.defineProperty(Vue.prototype, '$métier', {
      // коли згадується Vue.$métier, то другим параметром стає [métier]
      get: () => métier,
    })
  }
}

[pluginConfig]


export default {
  install(Vue, config) {
    // додає властивість [$config] до класу Vue
    Object.defineProperty(Vue.prototype, '$config', {
      // коли використовується Vue.$config, то другим параметром стає [config]
      get: () => config,
    })
  }
}

18.3.4. Штора [Vuex]

Стор [Vuex] реалізовано за допомогою наступного файлу [store]:


// плагін Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);

// сховище Vuex
const store = new Vuex.Store({
  state: {
    // таблиця симуляцій
    simulations: [],
    // номер останнього моделювання
    idSimulation: 0
  },
  mutations: {
    // видалення рядка № індекс
    deleteSimulation(state, index) {
      // eslint-disable-next-line no-console
      console.log("mutation deleteSimulation");
      // видаляємо рядок № [index]
      state.simulations.splice(index, 1);
      // eslint-disable-next-line no-console
      console.log("store simulations", state.simulations);
    },
    // додано симуляцію
    addSimulation(state, simulation) {
      // eslint-disable-next-line no-console
      console.log("mutation addSimulation");
      // номер симуляції
      state.idSimulation++;
      simulation.id = state.idSimulation;
      // додаємо симуляцію до таблиці симуляцій
      state.simulations.push(simulation);
    },
    // очищення стану
    clear(state) {
      state.simulations = [];
      state.idSimulation = 1;
    }
  }
});
// експорт об’єкта [store]
export default store;

Коментарі

  • рядки 2–4: плагін [Vuex] інтегровано у фреймворк [Vue];
  • рядки 8–13: ми додаємо до сховища [Vuex] такі елементи:
    • [simulations]: список симуляцій, виконаних користувачем;
    • [idSimulation]: номер останнього моделювання, виконаного користувачем;

Нагадаємо, що стор буде спільним для всіх подань, а його вміст є реактивним: коли його змінюють, подання, які його використовують, автоматично оновлюються. У нашому додатку реактивним має бути лише елемент [simulations], а не елемент [idSimulation]. Цей елемент було залишено у сховищі для зручності;

  • рядки 14–40: дозволені зміни об’єкта [state] із рядків 8–13. Нагадуємо, що ці зміни завжди отримують об’єкт [state] із рядків 8–13 як перший параметр;
    • рядок 16: модифікація [deleteSimulation] дозволяє видалити симуляцію з номером [index];
    • рядок 25: операція [addSimulation] дозволяє додати нове моделювання до таблиці моделювань;
    • рядок 35: зміна [clear] дозволяє скинути об’єкт [state] із рядків 8–13;

18.3.5. Файл маршрутизації [router]

Файл маршрутизації має такий вигляд:


// імпорт
import Vue from 'vue'
import VueRouter from 'vue-router'
// види
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'

// плагін маршрутизації
Vue.use(VueRouter)

// маршрути додатка
const routes = [
  // аутентифікація
  {
    path: '/', name: 'authentification', component: Authentification
  },
  // розрахунок податку
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot
  },
  // список симуляцій
  {
    path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations
  },
  // завершення сеансу
  {
    path: '/fin-session', name: 'finSession', component: Authentification
  }
]

// маршрутизатор
const router = new VueRouter({
  // маршрути
  routes,
  // режим відображення маршрутів у браузері
  mode: 'history',
})

// експорт маршрутизатора
export default router

Коментарі

  • рядок 16: під час запуску програми відображається вікно [Authentification], оскільки його URL є кореневим [/];
  • рядок 20: погляд [CalculImpot] відображається, коли запитується URL [/calcul-impot];
  • рядок 24: вигляд [ListeSimulations] відображається при запиті на URL або [/liste-des-simualtions];
  • рядок 28: вигляд [Authentification] відображається, коли запитується URL [/fin-session];
  • рядки 33–38: створюється об’єкт [router] із цими маршрутами (рядок 35) та режимом [history] (рядок 37) управління URL;
  • рядок 41: цей маршрутизатор експортується;

18.3.6. Головний скрипт [main.js]

Скрипт [main.js] має такий вигляд:


// імпорт
import Vue from 'vue'

// головний екран
import Main from './views/Main.vue'

// плагін [bootstrap-vue]
import BootstrapVue from 'bootstrap-vue'
Vue.use(BootstrapVue);

// CSS бутстрап
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

// маршрутизатор
import router from './router'

// плагін [config]
import config from './config';
import pluginConfig from './plugins/pluginConfig'
Vue.use(pluginConfig, config)

// інстанціювання шару [dao]
import Dao from './couches/Dao';
const dao = new Dao(config.axios);

// плагін [dao]
import pluginDao from './plugins/pluginDao'
Vue.use(pluginDao, dao)

// інстанціювання шару [métier]
import Métier from './couches/Métier';
const métier = new Métier();

// плагін [métier]
import pluginMétier from './plugins/pluginMétier'
Vue.use(pluginMétier, métier)

// Vuex-сторінка
import store from './store'

// запуск програми UI
new Vue({
  el: '#додаток»,
  // маршрутизатор
  router: router,
  // жалюзі Vuex
  store: store,
  // головний екран
  render: h => h(Main),
})

Зверніть увагу на наступні моменти:

  • у рядках 18–21 об’єкт, експортований скриптом [./config], буде доступний в атрибуті [Vue.$config], а отже, доступний для всіх поданнях додатка. У даному випадку це було зайвим, оскільки об’єкт [config] використовується лише скриптом [main] (рядок 25). Проте часто буває так, що конфігурація потрібна для кількох подань. Тому ми вирішили дотримуватися принципу, згідно з яким її слід зробити доступною в атрибуті подання;
  • рядки 24–25: створення екземпляра шару [dao]. Клас [Dao] імпортується у рядку 24, а потім інстанціюється у рядку 25. Його конструктор приймає як єдиний параметр об’єкт [axios], що є властивістю конфігурації;
  • рядки 27–29: шар [dao] стає доступним в атрибуті [$dao] усіх представлень;
  • рядки 31–37: повторюється та сама послідовність для шару [métier]. Конструктор класу [Métier] має параметр [taxAdminData], який представляє дані податкової адміністрації. Цих даних у нас ще немає. Тому об’єкт [métier] у рядку 33 доведеться заповнити пізніше;
  • рядок 40: імпортується об’єкт [Vuex];
  • рядки 43–51: створюється екземпляр головного виду [Main] (рядки 5 та 50) з передачею двох параметрів:
    • рядок 46: маршрутизатор [router], визначений у рядку 16;
    • рядок 48: стор [Vuex] [store], визначений у рядку 40;
    • в обох випадках ім’я властивості вказано ліворуч, а її значення — праворуч. Імена властивостей [router, store] визначаються фреймворками [vue-router] та [vuex]. Відповідні значення можуть бути будь-якими;

18.4. Види додатка

18.4.1. Головний вигляд [Main]

Код головного вікна [Main] має такий вигляд:


<!-- визначення HTML цього екрану -->
<template>
  <div class="container">
    <b-card>
      <!-- джамботрон -->
      <b-jumbotron>
        <b-row>
          <b-col cols="4">
            <img src="../assets/logo.jpg" alt="Cerisier en fleurs" />
          </b-col>
          <b-col cols="8">
            <h1>Calculez votre impôt</h1>
          </b-col>
        </b-row>
      </b-jumbotron>
      <!-- помилка запиту HTTP -->
      <b-alert
        show
        variant="danger"
        v-if="showError"
      >L'erreur suivante s'est produite : {{error.message}}</b-alert>
      <!-- поточний вигляд -->
      <router-view v-if="showView" @loading="mShowLoading" @error="mShowError" />
      <!-- завантаження -->
      <b-alert show v-if="showLoading" variant="light">
        <strong>Requête au serveur de calcul d'impôt en cours...</strong>
        <div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
      </b-alert>
    </b-card>
  </div>
</template>

<script>
export default {
  // ім'я
  name: "app",
  // внутрішній стан
  data() {
    return {
      // перевіряє сповіщення про очікування
      showLoading: false,
      // керує попередженням про помилку
      showError: false,
      // керує відображенням поточного виду маршрутизації
      showView: true,
      // повідомлення про помилку
      error: ""
    };
  },
  // обробники подій
  methods: {
    // помилка асинхронного запиту
    mShowError(error) {
      // eslint-disable-next-line
      console.log("Main evt error");
      // виводиться повідомлення про помилку
      this.error = error;
      this.showError = true;
      // приховується маршрутизований вигляд
      this.showView = false;
      // приховування повідомлення про очікування
      this.showLoading = false;
    },
    // відображення або приховування іконки очікування
    mShowLoading(value) {
      // eslint-disable-next-line
      console.log("Main evt showLoading");
      // відображення або приховування попередження про очікування
      this.showLoading = value;
    }
  }
};
</script>

Коментарі

  • вигляд [Main] забезпечує верстку вигляду, що маршрутизується та відображається у рядку 23:

Image

  • рядки 5–15 відображають зону 1;
  • у рядку 23 відображається маршрутизований вигляд [2];
  • рядки 16–19: сповіщення, що відображається лише у разі помилки зв’язку з сервером розрахунку податку;
  • рядки 25–28: повідомлення про очікування, що відображається при кожному запиті HTTP, надісланому на сервер;
  • усі подання будуть відображатися з таким макетом, оскільки кожне маршрутизоване подання відображається у рядках 20–24. Подання [Main] слугує для виокремлення елементів, які можуть бути спільними для різних подань;
  • рядок 23: кожен маршрутизований вигляд може генерувати три події:
    • [loading]: було запущено запит HTTP. Необхідно відобразити повідомлення про очікування відповіді;
    • [error]: запит HTTP завершився з помилкою. Необхідно відобразити повідомлення про помилку та приховати маршрутизований вигляд;
  • рядки 38–49: стан подання:
    • рядок 41: [showLoading] контролює відображення повідомлення про очікування завершення запиту HTTP (рядок 25);
    • рядок 43: [showError] керує відображенням повідомлення про помилку запиту HTTP (рядки 17–21);
    • рядок 45: [showView] контролює відображення маршрутизованого виду (рядок 23);
  • рядки 53–63: метод [mShowError] обробляє подію [error], що генерується маршрутизованим видом (рядок 23);
  • рядки 65–70: метод [mShowLoading] обробляє подію [loading], що генерується маршрутизованим видом (рядок 23);
  • рядок 23: слід звернути увагу на події [error] та [loading]. Вони перехоплюються лише в тому випадку, якщо маршрутизований вигляд відображається ([showView=true]). Саме тому маршрутизований вигляд спочатку відображається (рядок 45). Вона приховується лише у разі помилки (рядок 60). Щоб уникнути цієї проблеми, можна було б використати директиву [v-show] замість [v-if]. Різниця між цими двома директивами полягає в наступному:
    • [v-if=’false’] приховує контрольований блок, видаляючи його з глобального коду HTML. У такому разі події маршрутизованого виду більше не можуть бути перехоплені;
    • [v-show=’false’] приховує контрольований блок, впливаючи на його CSS, але код блоку залишається у глобальному HTML і, таким чином, може перехоплювати події маршрутизованого виду;

18.4.2. Вигляд макета [Layout]

Код подання [Layout] має такий вигляд:


<!-- визначення HTML макета маршрутизованого виду -->
<template>
  <!-- рядок -->
  <div>
    <b-row>
      <!-- область із трьома стовпцями ліворуч -->
      <b-col cols="3" v-if="left">
        <slot name="left" />
      </b-col>
      <!-- зона з дев'ятьма стовпцями праворуч -->
      <b-col cols="9" v-if="right">
        <slot name="right" />
      </b-col>
    </b-row>
  </div>
</template>

<script>
  export default {
    // параметри подання
    props: {
      // керує лівим стовпцем
      left: {
        type: Boolean
      },
      // керує правою колонкою
      right: {
        type: Boolean
      }
    }
  };
</script>

Коментарі

  • Вигляд [Layout] дозволяє розділити маршрутизований вигляд на дві зони:
    • зону з 3 колонками Bootstrap ліворуч (рядки 7–9). У цій зоні розміщуватиметься меню навігації, якщо воно є;
    • зона з 9 колонками праворуч (рядки 11–13). У цій зоні розміщуватиметься інформація, що надходить із маршрутизованого виду;

18.4.3. Вигляд [Authentification]

Вигляд автентифікації має такий вигляд:

Image

Цей вигляд отримано на основі [Layout] шляхом видалення лівого стовпця, щоб відобразити лише правий стовпець.

Його код такий:


<!-- визначення HTML для подання -->
<template>
  <Layout :left="false" :right="true">
    <template slot="right">
      <!-- форма HTML — значення передаються за допомогою дії [authentifier-utilisateur] -->
      <b-form @submit.prevent="login">
        <!-- заголовок -->
        <b-alert show variant="primary">
          <h4>Bienvenue. Veuillez vous authentifier pour vous connecter</h4>
        </b-alert>
        <!-- 1-й рядок -->
        <b-form-group label="Nom d'utilisateur" label-for="user" label-cols="3">
          <!-- поле введення користувача -->
          <b-col cols="6">
            <b-form-input type="text" id="user" placeholder="Nom d'utilisateur" v-model="user" />
          </b-col>
        </b-form-group>
        <!-- 2-й рядок -->
        <b-form-group label="Mot de passe" label-for="password" label-cols="3">
          <!-- поле введення пароля -->
          <b-col cols="6">
            <b-input type="password" id="password" placeholder="Mot de passe" v-model="password" />
          </b-col>
        </b-form-group>
        <!-- третій рядок -->
        <b-alert
          show
          variant="danger"
          v-if="showError"
          class="mt-3"
        >L'erreur suivante s'est produite : {{message}}</b-alert>
        <!-- кнопка типу [submit] у третьому рядку -->
        <b-row>
          <b-col cols="2">
            <b-button variant="primary" type="submit" :disabled="!valid">Valider</b-button>
          </b-col>
        </b-row>
      </b-form>
    </template>
  </Layout>
</template>

<!-- динаміка відображення -->
<script>
import Layout from "./Layout";
export default {
  // стан компонента
  data() {
    return {
      // користувач
      user: "",
      // його пароль
      password: "",
      // контролює відображення повідомлення про помилку
      showError: false,
      // повідомлення про помилку
      message: "",
      // сесія розпочата
      sessionStarted: false
    };
  },

  // використані компоненти
  components: {
    Layout
  },

  // розраховані властивості
  computed: {
    // дійсні введені дані
    valid() {
      return this.user && this.password && this.sessionStarted;
    }
  },

  // обробники подій
  methods: {
    // ----------- автентифікація
    async login() {
      try {
        // початок очікування
        this.$emit("loading", true);
        // блокуюча автентифікація на сервері
        const response = await this.$dao.authentifierUtilisateur(
          this.user,
          this.password
        );
        // завершення завантаження
        this.$emit("loading", false);
        // аналіз відповіді
        if (response.état != 200) {
          // відображення помилки
          this.message = response.réponse;
          this.showError = true;
          return;
        }
        // помилки немає
        this.showError = false;
        // --------- зараз запитуються дані податкової служби
        // початок очікування
        this.$emit("loading", true);
        // запит, що блокує роботу сервера
        const response2 = await this.$dao.getAdminData();
        // завантаження завершено
        this.$emit("loading", false);
        // аналіз відповіді
        if (response2.état != 1000) {
          // відображення помилки
          this.message = response2.réponse;
          this.showError = true;
          return;
        }
        // помилки немає
        this.showError = false;
        // збереження отриманих даних у шарі [métier]
        this.$métier.setTaxAdminData(response2.réponse);
        // перехід до екрану розрахунку податку
        this.$router.push({ name: "calculImpot" });
      } catch (error) {
        // помилка передається до головного компонента
        this.$emit("error", error);
      }
    }
  },
  // життєвий цикл: компонент щойно створено
  created() {
    // eslint-disable-next-line
    console.log("authentification", "created");
    // запускається сесія jSON із сервером
    // початок очікування
    this.$emit("loading", true);
    // ініціалізується сесія з сервером — асинхронний запит
    // використовується обіцянка, повернута методами шару [dao]
    this.$dao
      // ініціалізується сесія jSON
      .initSession()
      // отримано відповідь
      .then(response => {
        // завершено очікування
        this.$emit("loading", false);
        // аналіз відповіді
        if (response.état != 700) {
          // виводиться повідомлення про помилку
          this.message = response.réponse;
          this.showError = true;
          return;
        }
        // сесія розпочалася
        this.sessionStarted = true;
      })
      // у разі помилки
      .catch(error => {
        // помилка передається у вікно [Main]
        this.$emit("error", error);
      });
  }
};
</script>

Коментарі

  • рядок 3: вигляд [Authentification] використовує лише правий стовпець з [Layout] (рядки 3 і 4);
  • рядки 6–38: форма Bootstrap, яка генерує область 1 на знімку екрана вище;
  • рядок 6: подія [@submit] відбувається, коли користувач натискає кнопку типу [submit] у рядку 35. Модифікатор [prevent] вимагає, щоб сторінка не перезавантажувалася під час події [submit]. Також можна було б написати:
    • тег <b-form> без обробки події [submit];
    • тег <b-button> з подією [@click=’login’] і без атрибута [type=’submit’];

Це також працює. Перевага обраного рішення полягає в тому, що відправка відбувається не тільки при натисканні кнопки [Valider], але й при підтвердженні (клавіша [Entrée]) у полях введення. Отже, саме з міркувань зручності для користувача тут було обрано рішення [<b-form @submit.prevent="login">];

  • рядки 33–37: сповіщення, яке з’являється, коли сервер відхилив введені користувачем облікові дані:

Image

  • рядок 35: кнопка [Valider] не завжди активна. Її стан залежить від обчислюваного атрибута [valid] у рядках 71–73. Атрибут [valid] має значення «true», якщо:
    • у полях [user, password] форми є дані;
    • сесія jSON розпочалася. Спочатку ця сесія не розпочалася (рядок 59), тому кнопка [Valider] неактивна.
  • рядки 49–60: стан подання;
    • [user] відображає введені користувачем дані у полі [user] (рядки 12–17) форми. Директива [v-model] у рядку 15 встановлює двосторонній зв’язок між введеними користувачем даними та атрибутом [user] подання;
    • [password] представляє введені користувачем дані у полі [password] (рядки 19–24) форми. Директива [v-model] у рядку 22 встановлює двосторонній зв’язок між введеними користувачем даними та атрибутом [password] подання;
    • [showError] контролює (рядок 29) відображення попередження в рядках 26–31;
    • [message] — це повідомлення про помилку (рядок 31), яке має відображатися у попередженні рядків 26–31;
    • [sessionStarted] вказує, чи розпочалася сесія jSON із сервером. Спочатку цей атрибут має значення [false] (рядок 59). Сесія jSON із сервером ініціалізується в події [created] життєвого циклу подання, рядки 126–156. Якщо сервер відповідає позитивно, то атрибут [sessionStarted] змінюється на [true] (рядок 149);
  • рядки 126–156: функція [created] виконується після створення подання [Authentification] (яке ще не обов’язково відображається). У фоновому режимі ініціалізується сесія jSON із сервером. Відомо, що це перша дія, яку потрібно виконати з сервером розрахунку податку. Для цього використовується рівень [dao] додатка (рядок 134). Усі методи цього шару є асинхронними. Тут використовується обіцянка (Promise), повернена методом [$dao.initSession], який ініціалізує сесію jSON із сервером.
  • рядки 138–150: код, що виконується, коли сервер надав відповідь без помилок;
  • рядок 142: перевіряється властивість [état] у відповіді. Для успішної операції вона повинна мати значення [700]. В іншому випадку сталася помилка, причина якої вказана у властивості [response.réponse] (рядок 144). Тоді відображається повідомлення про помилку у вікні (рядок 145);
  • рядок 149: відзначається, що сесія jSON розпочалася;
  • рядки 152–155: код, що виконується у разі помилки. Помилка передається до батьківського подання [Main], яке
    • відобразить помилку;
    • приховає повідомлення про очікування;
    • приховає перенаправлений вигляд, вигляд [Autentification];
  • рядки 79–124: метод [login] обробляє натискання кнопки [Valider];
  • рядок 79: метод отримав префікс у вигляді ключового слова [async], щоб дозволити використання ключового слова [await], рядки 84 та 103;
  • рядки 84–87: блокуючий виклик методу [$dao.authentifierUtilisateur(user, password)]. Можна було б використати обіцянку [Promise], як це зроблено у функції [created]. Ми хотіли урізноманітнити стилі. Немає ризику заблокувати користувача, оскільки ми встановили [timeout] тривалістю 2 секунди для всіх запитів HTTP. Він не чекатиме довго. Крім того, він нічого не може зробити, доки сервер не надасть відповідь, оскільки в цей час кнопка [Valider] залишається неактивною;
  • рядок 91: сервер розрахунку податку надсилає відповіді jSON, які всі мають структуру [{‘action’:action, ‘état’:val, ‘réponse’:réponse}]. Аутентифікація пройшла успішно, якщо [état==200]. Якщо це не так, відображається повідомлення про помилку, рядки 93–94;
  • рядок 98: приховується можливе повідомлення про помилку попередньої операції;
  • рядки 99–116: тепер надсилається запит до сервера на отримання даних податкової адміністрації, які дозволяють розрахувати податок. У [this.$métier] ми маємо екземпляр класу [Métier], який наразі не може нічого зробити, оскільки не має цих даних;
  • рядок 103: дані податкової служби запитуються у сервера за допомогою блокуючої операції;
  • рядки 107–112: аналізується відповідь сервера. Вона повинна мати значення стану, рівне 1000; інакше це означає, що сталася помилка. В останньому випадку виводиться повідомлення про помилку (рядки 109–110);
  • рядки 113–118: у разі успішного виконання операції:
    • приховується повідомлення про помилку (рядок 114);
    • передаються дані податкової адміністрації до шару [métier] (рядок 116);
    • відображається вікно [CalculImpot], рядок 118. Нагадаємо, що [this.$router] позначає маршрутизатор додатка. Метод [push] дозволяє визначити наступне вікно, до якого буде спрямовано маршрут. Тут його позначають за допомогою атрибута [name]. Його також можна було б позначити за допомогою атрибута [path]. Ця інформація міститься у файлі маршрутизації:

// розрахунок податку
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot
  },
  • рядки 119–122: [catch] запускається, коли один із двох запитів HTTP завершився невдало (сервер відсутній, перевищено час очікування тощо). Помилка передається до батьківського виду [Main], який відобразить її, приховає повідомлення про очікування та вид [Authentification];

18.4.4. Вигляд [CalculImpot]

Вигляд [CalculImpot] має такий вигляд:

Image

  • [1]: меню навігації займає ліву колонку перенаправленого виду;
  • [2]: форма розрахунку податку займає правий стовпець маршрутизованого виду;

Код подання [CalculImpot] такий:


<!-- визначення HTML для подання -->
<template>
  <div>
    <Layout :left="true" :right="true">
      <!-- форма розрахунку податку праворуч -->
      <FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
      <!-- меню навігації зліва -->
      <Menu slot="left" :options="options" />
    </Layout>
    <!-- область відображення результатів розрахунку податку під формою -->
    <b-row v-if="résultatObtenu" class="mt-3">
      <!-- порожня область із трьома стовпцями -->
      <b-col cols="3" />
      <!-- область з дев'ятьма стовпцями -->
      <b-col cols="9">
        <b-alert show variant="success">
          <span v-html="résultat"></span>
        </b-alert>
      </b-col>
    </b-row>
  </div>
</template>

<script>
// імпортні дані
import FormCalculImpot from "./FormCalculImpot";
import Menu from "./Menu";
import Layout from "./Layout";

export default {
  // внутрішній звіт
  data() {
    return {
      // параметри меню
      options: [
        {
          text: "Liste des simulations",
          path: "/liste-des-simulations"
        },
        {
          text: "Fin de session",
          path: "/fin-session"
        }
      ],
      // результат розрахунку податку
      résultat: "",
      résultatObtenu: false
    };
  },
  // використані компоненти
  components: {
    Layout,
    FormCalculImpot,
    Menu
  },
  // методи управління подіями
  methods: {
    // результат розрахунку податку
    handleResultatObtenu(résultat) {
      // результат формується у вигляді ланцюжка HTML
      const impôt = "Montant de l'impôt : " + résultat.impôt + " euro(s)";
      const décôte = "Décôte : " + résultat.décôte + " euro(s)";
      const réduction = "Réduction : " + résultat.réduction + " euro(s)";
      const surcôte = "Surcôte : " + résultat.surcôte + " euro(s)";
      const taux = "Taux d'imposition : " + résultat.taux;
      this.résultat =
        impôt +
        "<br/>" +
        décôte +
        "<br/>" +
        réduction +
        "<br/>" +
        surcôte +
        "<br/>" +
        taux;
      // виведення результату
      this.résultatObtenu = true;
      // ---- оновлення магазину [Vuex]
      // симуляція додавання
      this.$store.commit("addSimulation", résultat);
    }
  }
};
</script>

Коментарі

  • рядок 4: тут присутні обидва стовпці [Layout];
  • рядок 6: форма розрахунку податку займає правий стовпець. Вона генерує подію [resultatObtenu] після отримання результату розрахунку податку. Слід зауважити, що назви подій та назви методів, які їх обробляють, не можуть містити символів з діакритичними знаками;
  • рядок 8: меню навігації займає лівий стовпець;
  • рядки 11–20: результат розрахунку податку відображається під формою:

Image

  • рядок 11: результат відображається лише в тому випадку, якщо значення атрибута [résultatObtenu] (рядок 47) дорівнює [true];
  • рядки 34–48: стан подання:
    • [options]: список опцій меню навігації. Ця таблиця передається як параметр компоненту [Menu], рядок 8;
    • [résultat]: результат розрахунку податку. Цей результат є рядком HTML. Тому для його відображення у рядку 17 використано директиву [v-html];
    • [résultatObtenu]: логічне значення, яке керує відображенням результату, рядок 11;
  • рядки 59–81: метод [handleResultatObtenu] відображає результат обчислення податку, який йому надіслала дочірня подача [FormCalculImpot], рядок 6. Цей результат є об’єктом із властивостями [impot, décôte, réduction, surcôte, taux, marié, enfants, salaire];
  • рядки 61–75: об’єкт [impot, décôte, réduction, surcôte, taux] записується в текст HTML, який відображається у рядку 17 шаблону;
  • рядок 77: відображається цей результат;
  • рядок 80: викликається мутація [addSimulation] зі сховища Vuex, яка додасть [résultat] до симуляцій, що вже є у сховищі;

18.4.5. Меню навігації [Menu]

Меню навігації відображається у лівій колонці маршрутизованих переглядів:

Image

Код подання [Menu] такий:


<!-- визначення HTML для подання -->
<template>
  <!-- вертикальне меню Bootstrap -->
  <b-nav vertical>
    <!-- параметри меню -->
    <b-nav-item
      v-for="(option,index) of options"
      :key="index"
      :to="option.path"
      exact
      exact-active-class="active"
    >{{option.text}}</b-nav-item>
  </b-nav>
</template>

<script>
export default {
  // параметри подання
  props: {
    options: {
      type: Array
    }
  }
};
</script>

Коментарі

  • опції меню задаються параметром [options] (рядки 7, 20–22);
  • кожен елемент масиву [options] має властивість [text] (рядок 12), яка є текстом посилання, та властивість [path] (рядок 9), яка буде шляхом до цільового виду посилання;

18.4.6. Погляд [FormCalculImpot]

Цей вигляд надає форму для розрахунку податку:

Image

Його код такий:


  <!-- визначення HTML для подання -->
  <template>
  <!-- форма HTML -->
  <b-form @submit.prevent="calculerImpot" class="mb-3">
    <!-- повідомлення у 12 стовпцях на синьому тлі -->
    <b-alert show variant="primary">
      <h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
    </b-alert>
    <!-- елементи форми -->
    <!-- перший рядок -->
    <b-form-group label="Etes-vous marié(e) ou pacsé(e) ?" label-cols="4">
      <!-- перемикачі у 5 стовпців-->
      <b-col cols="5">
        <b-form-radio v-model="marié" value="oui">Oui</b-form-radio>
        <b-form-radio v-model="marié" value="non">Non</b-form-radio>
      </b-col>
    </b-form-group>
    <!-- другий рядок -->
    <b-form-group label="Nombre d'enfants à charge" label-cols="4" label-for="enfants">
      <b-input
        type="text"
        id="enfants"
        placeholder="Indiquez votre nombre d'enfants"
        v-model="enfants"
        :state="enfantsValide"
      />
      <!-- можливе повідомлення про помилку -->
      <b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- третій рядок -->
    <b-form-group
      label="Salaire annuel"
      label-cols="4"
      label-for="salaire"
      description="Arrondissez à l'euro inférieur"
    >
      <b-input
        type="text"
        id="salaire"
        placeholder="Salaire annuel"
        v-model="salaire"
        :state="salaireValide"
      />
      <!-- можливе повідомлення про помилку -->
      <b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- четвертий рядок, кнопка [submit] у 5 стовпців -->
    <b-col cols="5">
      <b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
    </b-col>
  </b-form>
</template>

<!-- скрипт -->
<script>
export default {
  // внутрішній стан
  data() {
    return {
      // одружений чи ні
      marié: "non",
      // кількість дітей
      enfants: "",
      // річний оклад
      salaire: ""
    };
  },
  // розрахований внутрішній статус
  computed: {
    // підтвердження форми
    formInvalide() {
      return (
        // заробітна плата недійсна
        !this.salaireValide ||
        // або недійсні дані про дітей
        !this.enfantsValide ||
        // або податкові дані не отримано
        !this.$métier.taxAdminData
      );
    },
    // перевірка заробітної плати
    salaireValide() {
      // повинно бути числом >=0
      return Boolean(this.salaire.match(/^\s*\d+\s*$/));
    },
    // перевірка даних про дітей
    enfantsValide() {
      // повинно бути числом >=0
      return Boolean(this.enfants.match(/^\s*\d+\s*$/));
    }
  },
  // менеджер подій
  methods: {
    calculerImpot() {
      // податок обчислюється за допомогою шару [métier]
      const résultat = this.$métier.calculerImpot(
        this.marié,
        this.enfants,
        this.salaire
      );
      // eslint-disable-next-line
      console.log("résultat=", résultat);
      // доповнюється результат
      résultat.marié = this.marié;
      résultat.enfants = this.enfants;
      résultat.salaire = this.salaire;
      // генерується подія [resultatObtenu]
      this.$emit("resultatObtenu", résultat);
    }
  }
};
</script>

Коментарі

  • рядки 4–51: форма Bootstrap;
  • рядки 11–17: група перемикачів із їхніми підписами;
  • рядки 14–15: тег <b-form-radio> забезпечує відображення перемикача:
    • рядок 14: директива [v-model] гарантує, що при натисканні на кнопку атрибут [marié] у рядку 61 отримає значення [oui] (атрибут [value="oui"]);
    • рядок 15: директива [v-model] гарантує, що при натисканні на кнопку атрибут [marié] у рядку 61 отримає значення [non] (атрибут [value="non"]);
  • рядки 19–29: частина, де вводиться кількість дітей:
    • рядок 24: введене значення кількості дітей пов’язане з атрибутом [enfants] у рядку 63;
    • рядок 25: правильність введених даних перевіряється за допомогою обчислюваного атрибута [enfantsValide] з рядків 87–89;
    • рядок 28: забезпечує відображення повідомлення про помилку, якщо введені дані є недійсними;
  • рядки 31–45: поле для введення річної заробітної плати:
    • рядок 35: відображає підказку безпосередньо під полем введення;
    • рядок 41: введення заробітної плати пов’язане з атрибутом [salaire] у рядку 65;
    • рядок 42: правильність введених даних перевіряється за допомогою обчислюваного атрибута [salaireValide] з рядків 82–85;
    • рядок 45: забезпечує відображення повідомлення про помилку, якщо введені дані є недійсними;
  • рядки 48–50: кнопка типу [submit]. При натисканні на цю кнопку або при підтвердженні введених даних клавішею [Entrée] виконується метод [calculerImpot] (рядок 94);
    • рядок 49: стан кнопки (активна/неактивна) контролюється обчислюваним атрибутом [formInvalide] із рядків 71–80;
  • рядки 71–80: форма є дійсною, якщо:
    • кількість дітей є допустимою;
    • зарплата є дійсною;
    • додаток отримав від сервера дані податкової адміністрації, що дозволяють розрахувати податок. Нагадуємо, що ці дані зберігаються у властивості [$métier.taxAdminData]. Вигляд [FormCalculImpot] може відображатися до того, як ці дані будуть отримані, оскільки вони запитуються асинхронно одночасно з відображенням вигляду. Тут забезпечується, щоб користувач не міг натиснути кнопку [Valider], доки дані не будуть отримані;
  • рядки 94–109: метод розрахунку податку:
    • рядки 96–100: цей розрахунок виконує шар [métier]. Це синхронний розрахунок. Після отримання даних [taxAdminData] клієнт [Vue] більше не повинен зв’язуватися з сервером. Усе відбувається локально. Отримуємо об’єкт [résultat] із властивостями [impôt, décôte, surcôte, réduction, taux];
    • рядки 104–106: до результату додаються властивості [marié, enfants, salaire];
    • рядок 108: результат передається до батьківського виду [CalculImpot] через подію [resultatObtenu]. Цей вид відповідає за відображення результату;

18.4.7. Вигляд [ListeSimulations]

Вигляд [ListeSimulations] відображає список симуляцій, виконаних користувачем:

Image

Код цього подання такий:


<!-- визначення HTML подання -->
<template>
  <div>
    <!-- форматування -->
    <Layout :left="true" :right="true">
      <!-- моделювання у правій колонці -->
      <template slot="right">
        <template v-if="simulations.length==0">
          <!-- без моделювань -->
          <b-alert show variant="primary">
            <h4>Votre liste de simulations est vide</h4>
          </b-alert>
        </template>
        <template v-if="simulations.length!=0">
          <!-- є симуляції -->
          <b-alert show variant="primary">
            <h4>Liste de vos simulations</h4>
          </b-alert>
          <!-- таблиця симуляцій -->
          <b-table striped hover responsive :items="simulations" :fields="fields">
            <template v-slot:cell(action)="data">
              <b-button variant="link" @click="supprimerSimulation(data.index)">Supprimer</b-button>
            </template>
          </b-table>
        </template>
      </template>
      <!-- меню навігації у лівій колонці -->
      <Menu slot="left" :options="options" />
    </Layout>
  </div>
</template>

<script>
  // імпорт
  import Layout from "./Layout";
  import Menu from "./Menu";
  export default {
    // компоненти
    components: {
      Layout,
      Menu
    },
    // внутрішній стан
    data() {
      return {
        // параметри меню навігації
        options: [
          {
            text: "Calcul de l'impôt",
            path: "/calcul-impot"
          },
          {
            text: "Fin de session",
            path: "/fin-session"
          }
        ],
        // параметри таблиці HTML
        fields: [
          { label: "#", key: "id" },
          { label: "Marié", key: "marié" },
          { label: "Nombre d'enfants", key: "enfants" },
          { label: "Salaire", key: "salaire" },
          { label: "Impôt", key: "impôt" },
          { label: "Décôte", key: "décôte" },
          { label: "Réduction", key: "réduction" },
          { label: "Surcôte", key: "surcôte" },
          { label: "", key: "action" }
        ]
      };
    },
    // розрахований внутрішній стан
    computed: {
      // список симуляцій, взятих із сховища Vuex
      simulations() {
        return this.$store.state.simulations;
      }
    },
    // методи
    methods: {
      supprimerSimulation(index) {
        // eslint-disable-next-line
        console.log("supprimerSimulation", index);
        // видалення симуляції № [index]
        this.$store.commit("deleteSimulation", index);
      }
    }
  };
</script>

Коментарі

  • рядок 5: погляд займає обидва стовпці макета [Layout] маршрутизованих поглядів;
  • рядки 7–26: симуляції розміщуються у правій колонці;
  • рядок 28: меню навігації розміщується у лівій колонці;
  • рядки 8, 14, 20, 75: симуляції надходять із сховища [Vuex] [$this.store];
  • рядки 8–13: сповіщення, що відображається, коли список симуляцій порожній;
  • рядки 14–25: таблиця HTML відображається, коли список симуляцій не порожній;
  • рядки 20–24: таблиця HTML генерується тегом <b-table>;
    • рядок 20: таблиця симуляцій надається обчисленим атрибутом [simulations] із рядків 74–76;
    • рядок 20: конфігурація таблиці HTML визначається обчислюваним атрибутом [fields] із рядків 58–69. Рядок 67: стовпець ключа [action] є останнім стовпцем таблиці HTML;
    • рядки 21–23: шаблон останнього стовпця таблиці HTML;
    • рядок 22: тут розміщується кнопка типу «посилання». При натисканні на неї викликається метод [supprimerSimulation(data.index)], де [data] позначає поточний рядок (рядок 21). [data.index] позначає номер цього рядка у списку відображених рядків;
  • рядок 28: формування меню навігації. Його опції задаються атрибутом [options] рядків 47–56;
  • рядки 80–85: метод, який реагує на клік по посиланню [Supprimer] на сторінці HTML;
    • рядок 84: викликається метод [deleteSimulation] з модуля [Vuex] (див. параграф |vuejs-15|);

18.5. Виконання проєкту

Image

Також необхідно запустити сервер [Laragon] (див. документ |https://tahe.developpez.com/tutoriels-cours/php7|), щоб сервер розрахунку податків був у режимі онлайн.

18.6. Розгортання додатка на локальному сервері

Наразі наш клієнт [Vue] розгорнуто на тестовому сервері URL [http://localhost:8080]. Ми розгорнемо його на сервері [Laragon] у мережі URL [http://localhost:80]. Для цього потрібно виконати кілька кроків.

Крок 1

Спочатку ми забезпечимо розгортання клієнта [Vue] на тестовому сервері за адресами URL та [http://localhost:8080/client-vuejs-impot/].

Створюємо файл [vue.config.js] у кореневому каталозі нашого поточного проєкту [VSCode]:

Image

Файл [vue.config.js] [1] матиме такий вміст:


// vue.config.js
module.exports = {
  // URL — сервіс клієнта [vuejs] — сервер розрахунку податку
  publicPath: '/client-vuejs-impot/'
}

Також нам потрібно змінити файл маршрутизації [router.js] [2]:


// імпортні дані
import Vue from 'vue'
import VueRouter from 'vue-router'
// перегляди
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'

// плагін маршрутизації
Vue.use(VueRouter)

// маршрути додатка
const routes = [
  // аутентифікація
  {
    path: '/', name: 'authentification', component: Authentification
  },
  // розрахунок податку
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot
  },
  // список симуляцій
  {
    path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations
  },
  // завершення сеансу
  {
    path: '/fin-session', name: 'finSession', component: Authentification
  }
]

// маршрутизатор
const router = new VueRouter({
  // маршрути
  routes,
  // режим відображення маршрутів у браузері
  mode: 'history',
  // базова версія програми URL
  base: '/client-vuejs-impot/'
})

// експорт маршрутизатора
export default router
  • рядок 39: ми вказуємо маршрутизатору, що шляхи маршрутів, визначених у рядках 13–30, є відносними до шляху, визначеного в рядку 39. Наприклад, шлях із рядка 20 [/calcul-impot] стане [/client-vuejs-impot/calcul-impot];

Після цього можна знову протестувати проект [vuejs-20], щоб перевірити зміну шляхів до додатка:

Image

крок 2

Тепер ми збираємо виробничу версію проєкту [vuejs-20]:

Image

  • у [1-2] ми налаштовуємо завдання [build] [2] у файлі [package.json] [1];
  • у [3-5] ми виконуємо це завдання. Саме воно створить виробничу версію проєкту [vuejs-20];

Виконання завдання [build] відбувається в терміналі [VSCode]:

Image

Image

  • у [3-6] попередження вказують, що згенерований код занадто великий і його слід розділити на [8]. Це стосується оптимізації архітектури коду, яку ми тут не розглядатимемо;
  • у [7] нам повідомляють, що папка [dist] містить згенеровану версію для виробництва:

Image

  • у [3] файл [index.html] — це той файл, який буде використано при запиті на URL [https://localhost:80/client-vue-js-impot/];

Перед нами статичний веб-сайт, який можна розгорнути на будь-якому сервері. Ми розгорнемо його на локальному сервері Laragon (див. документ |https://tahe.developpez.com/tutoriels-cours/php7|). Папку [dist] [2] скопіюємо в папку [<laragon>/www] [4], де <laragon> — це папка, в якій встановлено сервер Laragon. Ми перейменовуємо цю папку на [client-vuejs-impot] [5], оскільки ми налаштували виробничу версію на роботу в папці URL [/client-vuejs-impot/].

крок 3

До щойно створеної папки [client-vuejs-impot] додаємо такий файл [.htaccess]:


<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /client-vuejs-impot/
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /client-vuejs-impot/index.html [L]
</IfModule>

Image

Цей файл є файлом конфігурації веб-сервера Apache. Якщо ми не додамо його і спробуємо безпосередньо завантажити URL або [https://localhost/client-vuejs-impot/calcul-impot], не пройшовши спочатку через URL або [https://localhost/client-vuejs-impot/], ми отримаємо помилку 404. За допомогою цього файлу ми успішно отримуємо сторінку [CalculImpot].

Після цього запускаємо сервер Laragon, якщо це ще не зроблено, і запитуємо URL [https://localhost/client-vuejs-impot/]:

Image

Пропонуємо читачеві протестувати виробничу версію нашого додатка.

Ми можемо змінити сервер розрахунку податку в одному аспекті: заголовки CORS, які він систематично надсилає своїм клієнтам. Це було необхідно для версії клієнта, що виконувалася з домену [localhost:8080]. Тепер, коли і клієнт, і сервер працюють у домені [localhost:80], заголовки CORS стають непотрібними.

Ми змінюємо файл [config.json] у версії 14 сервера:

Image

  • на [4], вказуємо, що відтепер запити CORS відхиляються;

Збережемо цю зміну та повторно надішлемо запит URL [https://localhost/client-vuejs-impot/]. Все має продовжувати працювати.

18.7. Управління ручними запитами URL

Замість того, щоб слухняно користуватися посиланнями в меню навігації, користувач може захотіти ввести URL додатка вручну в адресний рядок браузера. Спробуємо, наприклад, відкрити URL [https://client-vuejs-impot/calcul-impot], оминувши етап автентифікації. Хакер, безсумнівно, спробував би це зробити. Ми отримаємо такий вигляд:

Image

Ми дійсно бачимо сторінку розрахунку податку. Тепер спробуємо заповнити поля введення даних і підтвердити їх:

Image

Тоді виявляється, що кнопка [1] [Valider] залишається неактивною, навіть якщо дані введені правильно. Давайте подивимося на код екрану [FormCalculImpot]:


<b-col cols="5">
      <b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
</b-col>

У рядку 2 видно, що його стан (активний/неактивний) залежить від властивості [formInvalide]. Це така обчислювана властивість:


formInvalide() {
      return (
        // недійсна заробітна плата
        !this.salaireValide ||
        // або недійсні дані про дітей
        !this.enfantsValide ||
        // або податкові дані не отримано
        !this.$métier.taxAdminData
      );
},

У рядку 8 видно, що для того, щоб форма була дійсною, необхідно отримати податкові дані. Однак ці дані отримуються під час перевірки подання [Authentification], яке користувач «пропустив». Отже, він не зможе підтвердити форму. Якби він зміг це зробити, то отримав би повідомлення про помилку від сервера, яке б вказувало, що він не пройшов аутентифікацію. Перевірки завжди мають здійснюватися на стороні сервера. Перевірки на стороні браузера завжди можна обійти. Достатньо використати клієнт типу [Postman], який надсилатиме необроблені запити на сервер.

Тепер запитаємо URL [https://localhost/client-vuejs-impot/liste-des-simulations]. Отримаємо такий вигляд:

Image

Тепер URL та [https://localhost/client-vuejs-impot/fin-session]. Ми отримуємо такий вигляд:

Image

Тепер перегляд, який не існує — [https://localhost/client-vuejs-impot/abcd]:

Image

Наш додаток досить добре протистоїть вручну введеним URL. Коли їх викликають, маршрутизатор додатка про це знає. Тому можна втрутитися, перш ніж вигляд остаточно відобразиться. Ми розглянемо цей момент у проєкті [vuejs-21].

Ще один момент, на який варто звернути увагу, такий. Уявімо, що користувач виконав кілька симуляцій за правилами:

Image

Тепер оновимо сторінку за допомогою F5:

Image

Ми зробили те, чого не рекомендується робити: ввели URL вручну (введення F5 прирівнюється до цього). У результаті ми втратили наші симуляції.

Наступний проект [vuejs-21] має на меті внести два вдосконалення:

  • перевіряти URL, введені користувачем;
  • збереження пам’яті додатка, навіть якщо користувач вводить URL. Вище ми бачимо, що втратили список симуляцій;