18. Клієнт Vue.js сервера розрахунку податків
18.1. Architecture
Ми реалізуємо клієнтсько-серверний додаток із такою архітектурою:

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

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

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

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

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

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

Елементи проекту такі:
- [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] виконує те саме, що й конструктор. Його наявність дозволяє виконати таку послідовність дій:
- створити екземпляр класу [Métier] за допомогою оператора [métier=new Métier()], коли потрібно створити екземпляр класу, але ще немає даних [taxAdminData];
- а потім заповнити його властивість [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:

- рядки 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]
Вигляд автентифікації має такий вигляд:

Цей вигляд отримано на основі [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: сповіщення, яке з’являється, коли сервер відхилив введені користувачем облікові дані:

- рядок 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] має такий вигляд:

- [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: результат розрахунку податку відображається під формою:

- рядок 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]
Меню навігації відображається у лівій колонці маршрутизованих переглядів:

Код подання [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]
Цей вигляд надає форму для розрахунку податку:

Його код такий:
<!-- визначення 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] відображає список симуляцій, виконаних користувачем:

Код цього подання такий:
<!-- визначення 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. Виконання проєкту

Також необхідно запустити сервер [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]:

Файл [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], щоб перевірити зміну шляхів до додатка:

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

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


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

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

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

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

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

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

Тоді виявляється, що кнопка [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]. Отримаємо такий вигляд:

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

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

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

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

Ми зробили те, чого не рекомендується робити: ввели URL вручну (введення F5 прирівнюється до цього). У результаті ми втратили наші симуляції.
Наступний проект [vuejs-21] має на меті внести два вдосконалення:
- перевіряти URL, введені користувачем;
- збереження пам’яті додатка, навіть якщо користувач вводить URL. Вище ми бачимо, що втратили список симуляцій;