14. Клієнти HTTP JavaScript служби розрахунку податку
14.1. Introduction
Тут ми пропонуємо написати клієнт [node.js] для версії 14 сервісу розрахунку податку. Архітектура «клієнт/сервер» буде такою:

Ми розглянемо дві версії клієнта:
- версія 1 клієнта матиме таку багаторівневу структуру [main, dao]:

- версія 2 клієнта матиме структуру [main, métier, dao]. Рівень [métier] сервера буде перенесено на клієнт:

14.2. Клієнт HTTP 1

Як ми вже зазначали, клієнт HTTP 1 реалізує таку архітектуру «клієнт/сервер»:

Ми реалізуємо:
- рівень [dao] у вигляді класу;
- шар [main] у вигляді скрипта, що використовує цей клас;
14.2.1. Рівень [dao]
Рівень [dao] буде реалізовано за допомогою наступного класу [Dao1.js]:
'use strict';
// імпорти
import qs from 'qs'
class Dao1 {
// конструктор
constructor(axios) {
// бібліотека axios для виконання запитів HTTP
this.axios = axios;
// сесійний файл cookie
this.sessionCookieName = "PHPSESSID";
this.sessionCookie = '';
}
// ініціалізація сеансу
async initSession() {
// параметри запиту HHTP [get /main.php?action=init-session&type=json]
const options = {
method: "GET",
// параметри запиту URL
params: {
action: 'init-session',
type: 'json'
}
};
// виконання запиту HTTP
return await this.getRemoteData(options);
}
async authentifierUtilisateur(user, password) {
// параметри запиту HHTP [post /main.php?action=authentifier-utilisateur]
const options = {
method: "POST",
headers: {
'Content-type': 'application/x-www-form-urlencoded',
},
// тіло POST
data: qs.stringify({
user: user,
password: password
}),
// параметри URL
params: {
action: 'authentifier-utilisateur'
}
};
// виконання запиту HTTP
return await this.getRemoteData(options);
}
// розрахунок податку
async calculerImpot(marié, enfants, salaire) {
// параметри запиту HHTP [post /main.php?action=calculer-impot]
const options = {
method: "POST",
headers: {
'«Content-type»: «application/x-www-form-urlencoded»,
},
// тіло запиту POST [marié, enfants, salaire]
data: qs.stringify({
marié: marié,
enfants: enfants,
salaire: salaire
}),
// параметри URL
params: {
action: 'calculer-impot'
}
};
// виконання запиту HTTP
const data = await this.getRemoteData(options);
// результат
return data;
}
// список симуляцій
async listeSimulations() {
// параметри запиту HHTP [get /main.php?action=lister-simulations]
const options = {
method: "GET",
// параметри запиту URL
params: {
action: 'lister-simulations'
},
};
// виконання запиту HTTP
const data = await this.getRemoteData(options);
// результат
return data;
}
// список симуляцій
async supprimerSimulation(index) {
// параметри запиту HHTP [get /main.php?action=supprimer-simulation&numéro=index]
const options = {
method: "GET",
// параметри запиту URL
params: {
action: 'supprimer-simulation',
numéro: index
},
};
// виконання запиту HTTP
const data = await this.getRemoteData(options);
// результат
return data;
}
async getRemoteData(options) {
// для сесійного файлу cookie
if (!options.headers) {
options.headers = {};
}
options.headers.Cookie = this.sessionCookie;
// виконання запиту HTTP
let response;
try {
// асинхронний запит
response = await this.axios.request('main.php', options);
} catch (error) {
// параметр [error] є екземпляром винятку — він може мати різні форми
if (error.response) {
// відповідь сервера міститься в [error.response]
response = error.response;
} else {
// помилка повторюється
throw error;
}
}
// response — це повна відповідь HTTP від сервера (заголовки HTTP + сама відповідь)
// отримуємо сесійний файл cookie, якщо він існує
const setCookie = response.headers['set-cookie'];
if (setCookie) {
// setCookie — це масив
// шукаємо сесійний файл cookie в цьому масиві
let trouvé = false;
let i = 0;
while (!trouvé && i < setCookie.length) {
// шукаємо сесійний файл cookie
const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
if (results) {
// зберігаємо сесійний файл cookie
// eslint-disable-next-line require-atomic-updates
this.sessionCookie = results[1];
// знайдено
trouvé = true;
} else {
// наступний елемент
i++;
}
}
}
// відповідь сервера знаходиться в [response.data]
return response.data;
}
}
// експорт класу
export default Dao1;
- тут ми використовуємо те, що вивчили в розділі «посилання», де ми представили бібліотеку [axios], що дозволяє виконувати запити HTTP як у [node.js], так і в браузері. Зокрема, розглянемо скрипт із розділу «посилання»;
- рядки 9–15: конструктор класу. Цей клас матиме три властивості:
- [axios]: об’єкт [axios], що дозволяє виконувати запити HTTP. Він передається кодом, що викликає;
- [sessionCookieName]: залежно від сервера, сесійний файл cookie може мати різні імена. У даному випадку це [PHPSESSID];
- [sessionCookie]: сесійний файл cookie, надісланий сервером і збережений клієнтом;
- рядки 53–76: асинхронна функція [calculerImpot] виконує запит [post /main.php?action=calculer-impot], відправляючи параметри [marié, enfants, salaire]. Вона повертає рядок jSON, переданий сервером, у вигляді об’єкта JavaScript;
- рядки 79–92: асинхронна функція [listeSimulations] надсилає запит [get /main.php?action=lister-simulations. Вона повертає рядок jSON, переданий сервером, у вигляді об’єкта JavaScript;
- рядки 95–109: асинхронна функція [supprimerSimulation] виконує запит [get /main.php?action=supprimer-simulation&numéro=index]. Вона повертає рядок jSON, переданий сервером, у вигляді об’єкта JavaScript;
- рядок 121: використовується запис [this.axios], оскільки тут об’єкт [axios], переданий конструктору, було збережено у властивості [this.axios];
- рядок 161: клас [Dao1] експортується для подальшого використання;
14.2.2. Скрипт [main1.js]
Скрипт [main1.js] виконує серію звернень до сервера за допомогою класу [Dao1]:
- ініціалізація сеансу jSON;
- аутентифікація за допомогою [admin, admin];
- запит на три розрахунки податків;
- запитує список моделювань;
- видаляє одну з них;
Код такий:
// імпорт axios
import axios from 'axios';
// імпорт класу Dao1
import Dao from './Dao1';
// асинхронна функція [main]
async function main() {
// конфігурація axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
// інстанціювання шару [dao]
const dao = new Dao(axios);
// використання шару [dao]
try {
// ініціалізація сеансу
log("-----------init-session");
let response = await dao.initSession();
log(response);
// аутентифікація
log("-----------authentifier-utilisateur");
response = await dao.authentifierUtilisateur("admin", "admin");
log(response);
// розрахунок податків
log("-----------calculer-impot x 3");
response = await Promise.all([
dao.calculerImpot("oui", 2, 45000),
dao.calculerImpot("non", 2, 45000),
dao.calculerImpot("non", 1, 30000)
]);
log(response);
// перелік моделювань
log("-----------liste-des-simulations");
response = await dao.listeSimulations();
log(response);
// видалення моделювання
log("-----------suppression simulation n° 1");
response = await dao.supprimerSimulation(1);
log(response);
} catch (error) {
// запис помилки в журнал
console.log("erreur=", error.message);
}
}
// журнал jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// виконання
main();
Коментарі
- рядок 2: імпортуємо бібліотеку [axios];
- рядок 4: імпортується клас [Dao];
- рядок 7: функція [main], яка взаємодіє з сервером, є асинхронною;
- рядки 9–10: стандартна конфігурація запитів HTTP, які будуть надсилатися на сервер:
- рядок 9: [timeout] — 2 секунди;
- рядок 10: усі запити URL мають префікс URL, базовий для версії 14 сервера розрахунку податку;
- рядок 12: побудовано шар [Dao]. Тепер його можна використовувати;
- рядки 46–48: функція [log] призначена для відображення рядка jSON об’єкта JavaScript у прикрашеному вигляді: у вертикальному форматі з відступом у два пробіли (3-й параметр);
- рядки 15–18: ініціалізація сесії jSON;
- рядки 19–22: автентифікація;
- рядки 23–30: паралельно запитуються три розрахунки податку. Завдяки [await Promise.all] виконання блокується, доки не будуть отримані всі три результати;
- рядки 31–34: список моделювань;
- рядки 35–38: видалення симуляції;
- рядки 39–42: обробка можливого винятку;
Результати виконання такі:
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\client impôts\client http 1\main1.js"
"-----------init-session"
{
"action": "init-session",
"état": 700,
"réponse": "session démarrée avec type [json]"
}
"-----------authentifier-utilisateur"
{
"action": "authentifier-utilisateur",
"état": 200,
"réponse": "Authentification réussie [admin, admin]"
}
"-----------calculer-impot x 3"
[
{
"action": "calculer-impot",
"état": 300,
"réponse": {
"marié": "oui",
"enfants": "2",
"salaire": "45000",
"impôt": 502,
"surcôte": 0,
"décôte": 857,
"réduction": 126,
"taux": 0.14
}
},
{
"action": "calculer-impot",
"état": 300,
"réponse": {
"marié": "non",
"enfants": "2",
"salaire": "45000",
"impôt": 3250,
"surcôte": 370,
"décôte": 0,
"réduction": 0,
"taux": 0.3
}
},
{
"action": "calculer-impot",
"état": 300,
"réponse": {
"marié": "non",
"enfants": "1",
"salaire": "30000",
"impôt": 1687,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.14
}
}
]
"-----------liste-des-simulations"
{
"action": "lister-simulations",
"état": 500,
"réponse": [
{
"marié": "oui",
"enfants": "2",
"salaire": "45000",
"impôt": 502,
"surcôte": 0,
"décôte": 857,
"réduction": 126,
"taux": 0.14,
"arrayOfAttributes": null
},
{
"marié": "non",
"enfants": "2",
"salaire": "45000",
"impôt": 3250,
"surcôte": 370,
"décôte": 0,
"réduction": 0,
"taux": 0.3,
"arrayOfAttributes": null
},
{
"marié": "non",
"enfants": "1",
"salaire": "30000",
"impôt": 1687,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.14,
"arrayOfAttributes": null
}
]
}
"-----------suppression simulation n° 1"
{
"action": "supprimer-simulation",
"état": 600,
"réponse": [
{
"marié": "oui",
"enfants": "2",
"salaire": "45000",
"impôt": 502,
"surcôte": 0,
"décôte": 857,
"réduction": 126,
"taux": 0.14,
"arrayOfAttributes": null
},
{
"marié": "non",
"enfants": "1",
"salaire": "30000",
"impôt": 1687,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.14,
"arrayOfAttributes": null
}
]
}
[Done] exited with code=0 in 0.516 seconds
14.3. Клієнт HTTP 2

Архітектура клієнта HTTP2 така:

Рівень [métier] було перенесено з сервера на клієнтський JavaScript. На відміну від того, що ми робили в курсі PHP7, рівень [main] тут не повинен проходити через рівень [métier], щоб дістатися до рівня [dao]. Ми будемо використовувати ці два рівні як центри компетенцій:
- рівень [main] звертається до рівня [dao], щойно йому потрібні дані, які знаходяться на сервері;
- рівень [main] звертається до рівня [métier] з проханням виконати розрахунки податку;
- рівень [métier] є незалежним від рівня [dao] і ніколи до нього не звертається;
14.3.1. Клас JavaScript [Métier]
Суть класу [Métier] у PHP була описана у статті за посиланням. Це досить складний код, який ми наводимо тут не для того, щоб пояснити його, а щоб мати змогу перекласти його на JavaScript:
<?php
// простір імен
namespace Application;
class Metier implements InterfaceMetier {
// шар Dao
private $dao;
// дані податкової адміністрації
private $taxAdminData;
//---------------------------------------------
// налаштування рівня [dao]
public function setDao(InterfaceDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceDao $dao) {
// зберігається посилання на рівень [dao]
$this->dao = $dao;
// отримуються дані, необхідні для розрахунку податку
// метод [getTaxAdminData] може викликати виняток ExceptionImpots
// у цьому випадку виняток передається до коду, що викликає
$this->taxAdminData = $this->dao->getTaxAdminData();
}
// розрахунок податку
// --------------------------------------------------------------------------
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// $marié: так, ні
// $enfants: кількість дітей
// $salaire: річний оклад
// $this->taxAdminData: дані податкової служби
//
// перевіряємо, чи є дані податкової служби
if ($this->taxAdminData === NULL) {
$this->taxAdminData = $this->getTaxAdminData();
}
// розрахунок податку з урахуванням дітей
$result1 = $this->calculerImpot2($marié, $enfants, $salaire);
$impot1 = $result1["impôt"];
// розрахунок податку без урахування дітей
if ($enfants != 0) {
$result2 = $this->calculerImpot2($marié, 0, $salaire);
$impot2 = $result2["impôt"];
// застосування обмеження сімейного коефіцієнта
$plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
if ($enfants < 3) {
// $PLAFOND_QF_DEMI_PART євро за перших двох дітей
$impot2 = $impot2 - $enfants * $plafonDemiPart;
} else {
// $PLAFOND_QF_DEMI_PART євро за перших двох дітей, удвічі більше за наступних
$impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
}
} else {
$impot2 = $impot1;
$result2 = $result1;
}
// береться найвищий податок
if ($impot1 > $impot2) {
$impot = $impot1;
$taux = $result1["taux"];
$surcôte = $result1["surcôte"];
} else {
$surcôte = $impot2 - $impot1 + $result2["surcôte"];
$impot = $impot2;
$taux = $result2["taux"];
}
// розрахунок можливого зниження
$décôte = $this->getDecôte($marié, $salaire, $impot);
$impot -= $décôte;
// розрахунок можливого зменшення податків
$réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
$impot -= $réduction;
// результат
return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
}
// --------------------------------------------------------------------------
private function calculerImpot2(string $marié, int $enfants, float $salaire): array {
// $marié: так, ні
// $enfants: кількість дітей
// $salaire: річний оклад
// $this->taxAdminData: дані податкової служби
//
// кількість часток
$marié = strtolower($marié);
if ($marié === "oui") {
$nbParts = $enfants / 2 + 2;
} else {
$nbParts = $enfants / 2 + 1;
}
// 1 частка на кожну дитину, починаючи з третьої
if ($enfants >= 3) {
// півчастки додатково за кожну дитину, починаючи з третьої
$nbParts += 0.5 * ($enfants - 2);
}
// оподатковуваний дохід
$revenuImposable = $this->getRevenuImposable($salaire);
// надбавка
$surcôte = floor($revenuImposable - 0.9 * $salaire);
// для виправлення округлень
if ($surcôte < 0) {
$surcôte = 0;
}
// сімейний коефіцієнт
$quotient = $revenuImposable / $nbParts;
// розрахунок податку
$limites = $this->taxAdminData->getLimites();
$coeffR = $this->taxAdminData->getCoeffR();
$coeffN = $this->taxAdminData->getCoeffN();
// встановлюється в кінці таблиці обмежень для зупинки наступного циклу
$limites[count($limites) - 1] = $quotient;
// пошук ставки оподаткування
$i = 0;
while ($quotient > $limites[$i]) {
$i++;
}
// оскільки $quotient розміщено в кінці таблиці $limites, попередній цикл
// не може виходити за межі масиву $limites
// тепер можна обчислити податок
$impôt = floor($revenuImposable * $coeffR[$i] - $nbParts * $coeffN[$i]);
// результат
return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
}
// revenuImposable = річна зарплата – пільга
// податкова пільга має мінімальний та максимальний розмір
private function getRevenuImposable(float $salaire): float {
// відрахування у розмірі 10 % від заробітної плати
$abattement = 0.1 * $salaire;
// ця знижка не може перевищувати $this->taxAdminData->getAbattementDixPourCentMax()
if ($abattement > $this->taxAdminData->getAbattementDixPourCentMax()) {
$abattement = $this->taxAdminData->getAbattementDixPourcentMax();
}
// знижка не може бути меншою за $this->taxAdminData->getAbattementDixPourcentMin()
if ($abattement < $this->taxAdminData->getAbattementDixPourcentMin()) {
$abattement = $this->taxAdminData->getAbattementDixPourcentMin();
}
// оподатковуваний дохід
$revenuImposable = $salaire - $abattement;
// результат
return floor($revenuImposable);
}
// розраховує можливу знижку
private function getDecôte(string $marié, float $salaire, float $impots): float {
// спочатку дисконт дорівнює нулю
$décôte = 0;
// максимальна сума податку для отримання знижки
$plafondImpôtPourDécôte = $marié === "oui" ?
$this->taxAdminData->getPlafondImpotCouplePourDecote() :
$this->taxAdminData->getPlafondImpotCelibatairePourDecote();
if ($impots < $plafondImpôtPourDécôte) {
// максимальна сума знижки
$plafondDécôte = $marié === "oui" ?
$this->taxAdminData->getPlafondDecoteCouple() :
$this->taxAdminData->getPlafondDecoteCelibataire();
// теоретична знижка
$décôte = $plafondDécôte - 0.75 * $impots;
// знижка не може перевищувати суму податку
if ($décôte > $impots) {
$décôte = $impots;
}
// знижка <0
if ($décôte < 0) {
$décôte = 0;
}
}
// результат
return ceil($décôte);
}
// розраховує можливе зниження
private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
// максимальний рівень доходу для отримання права на знижку в розмірі 20 %
$plafondRevenuPourRéduction = $marié === "oui" ?
$this->taxAdminData->getPlafondRevenusCouplePourReduction() :
$this->taxAdminData->getPlafondRevenusCelibatairePourReduction();
$plafondRevenuPourRéduction += $enfants * $this->taxAdminData->getValeurReducDemiPart();
if ($enfants > 2) {
$plafondRevenuPourRéduction += ($enfants - 2) * $this->taxAdminData->getValeurReducDemiPart();
}
// оподатковуваний дохід
$revenuImposable = $this->getRevenuImposable($salaire);
// знижка
$réduction = 0;
if ($revenuImposable < $plafondRevenuPourRéduction) {
// знижка у розмірі 20 %
$réduction = 0.2 * $impots;
}
// результат
return ceil($réduction);
}
// розрахунок податків у пакетному режимі
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// дозволяється передавати винятки, що походять з рівня [dao]
// отримання даних про платників податків
$taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// таблиця результатів
$results = [];
// здійснюється їх аналіз
foreach ($taxPayersData as $taxPayerData) {
// розраховується податок
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// заповнюємо [$taxPayerData]
$taxPayerData->setMontant($result["impôt"]);
$taxPayerData->setDécôte($result["décôte"]);
$taxPayerData->setSurCôte($result["surcôte"]);
$taxPayerData->setTaux($result["taux"]);
$taxPayerData->setRéduction($result["réduction"]);
// заносимо результат у таблицю результатів
$results [] = $taxPayerData;
}
// запис результатів
$this->dao->saveResults($resultsFileName, $results);
}
}
- рядки 19–26: конструктор класу PHP. Оскільки ми зазначили, що створюємо шар [métier], незалежний від шару [dao], ми внесемо в JavaScript дві зміни до цього конструктора:
- він не отримуватиме екземпляр шару [dao] (він більше не потрібен);
- він не запитуватиме податкові дані адміністрації [taxAdminData] у шару [dao]: саме код, що викликає, передаватиме ці дані конструктору;
- рядки 197–122: ми не будемо реалізовувати метод [executeBatchImpots], кінцевою метою якого було збереження результатів моделювання у текстовому файлі. Нам потрібен код, який працюватиме як у [node.js], так і в браузері. Однак зберегти дані у файловій системі комп’ютера, на якому працює клієнтський браузер, неможливо;
З огляду на ці обмеження, код класу JavaScript [Métier] виглядає наступним чином:
'use strict';
// клас «Métier»
class Métier {
// конструктор
constructor(taxAdmindata) {
// this.taxAdminData: дані податкової адміністрації
this.taxAdminData = taxAdmindata;
}
// розрахунок податку
// --------------------------------------------------------------------------
calculerImpot(marié, enfants, salaire) {
// одружений: так, ні
// діти: кількість дітей
// зарплата: річний дохід
// this.taxAdminData: дані податкової служби
//
// розрахунок податку з урахуванням дітей
const result1 = this.calculerImpot2(marié, enfants, salaire);
const impot1 = result1["impôt"];
// розрахунок податку без урахування дітей
let result2, impot2, plafondDemiPart;
if (enfants !== 0) {
result2 = this.calculerImpot2(marié, 0, salaire);
impot2 = result2["impôt"];
// застосування обмеження сімейного коефіцієнта
plafondDemiPart = this.taxAdminData.plafondQfDemiPart;
if (enfants < 3) {
// PLAFOND_QF_DEMI_PART євро за перших двох дітей
impot2 = impot2 - enfants * plafondDemiPart;
} else {
// PLAFOND_QF_DEMI_PART євро за перших двох дітей, удвічі більше за наступних
impot2 = impot2 - 2 * plafondDemiPart - (enfants - 2) * 2 * plafondDemiPart;
}
} else {
// податок не перераховується
impot2 = impot1;
result2 = result1;
}
// береться найвищий податок із [impot1, impot2]
let impot, taux, surcôte;
if (impot1 > impot2) {
impot = impot1;
taux = result1["taux"];
surcôte = result1["surcôte"];
} else {
surcôte = impot2 - impot1 + result2["surcôte"];
impot = impot2;
taux = result2["taux"];
}
// розрахунок можливої знижки
const décôte = this.getDecôte(marié, impot);
impot -= décôte;
// розрахунок можливого зменшення податку
const réduction = this.getRéduction(marié, salaire, enfants, impot);
impot -= réduction;
// результат
return {
"impôt": Math.floor(impot), "surcôte": surcôte, "décôte": décôte, "réduction": réduction,
"taux": taux
};
}
// --------------------------------------------------------------------------
calculerImpot2(marié, enfants, salaire) {
// одружений: так, ні
// діти: кількість дітей
// зарплата: річна зарплата
// this->taxAdminData: дані податкової служби
//
// кількість часток
marié = marié.toLowerCase();
let nbParts;
if (marié === "oui") {
nbParts = enfants / 2 + 2;
} else {
nbParts = enfants / 2 + 1;
}
// 1 частка на дитину, починаючи з третьої
if (enfants >= 3) {
// півчастки додатково за кожну дитину, починаючи з третьої
nbParts += 0.5 * (enfants - 2);
}
// оподатковуваний дохід
const revenuImposable = this.getRevenuImposable(salaire);
// надбавка
let surcôte = Math.floor(revenuImposable - 0.9 * salaire);
// щодо проблем з округленням
if (surcôte < 0) {
surcôte = 0;
}
// сімейний коефіцієнт
const quotient = revenuImposable / nbParts;
// розрахунок податку
const limites = this.taxAdminData.limites;
const coeffR = this.taxAdminData.coeffR;
const coeffN = this.taxAdminData.coeffN;
// встановлюється в кінці таблиці меж для зупинки наступного циклу
limites[limites.length - 1] = quotient;
// пошук ставки оподаткування
let i = 0;
while (quotient > limites[i]) {
i++;
}
// оскільки коефіцієнт розміщено в кінці таблиці меж, попередній цикл
// не може вийти за межі масиву меж
// тепер можна обчислити податок
const impôt = Math.floor(revenuImposable * coeffR[i] - nbParts * coeffN[i]);
// результат
return { "impôt": impôt, "surcôte": surcôte, "taux": coeffR[i] };
}
// revenuImposable = річна зарплата – пільга
// податкова пільга має мінімальний та максимальний розмір
getRevenuImposable(salaire) {
// відрахування у розмірі 10 % від заробітної плати
let abattement = 0.1 * salaire;
// ця знижка не може перевищувати taxAdminData.getAbattementDixPourCentMax()
if (abattement > this.taxAdminData.abattementDixPourCentMax) {
abattement = this.taxAdminData.abattementDixPourcentMax;
}
// знижка не може бути меншою за taxAdminData.getAbattementDixPourcentMin()
if (abattement < this.taxAdminData.abattementDixPourcentMin) {
abattement = this.taxAdminData.abattementDixPourcentMin;
}
// оподатковуваний дохід
const revenuImposable = salaire - abattement;
// фінансовий результат
return Math.floor(revenuImposable);
}
// розраховує можливу знижку
getDecôte(marié, impots) {
// спочатку знижка дорівнює нулю
let décôte = 0;
// максимальна сума податку для отримання знижки
let plafondImpôtPourDécôte = marié === "oui" ?
this.taxAdminData.plafondImpotCouplePourDecote :
this.taxAdminData.plafondImpotCelibatairePourDecote;
let plafondDécôte;
if (impots < plafondImpôtPourDécôte) {
// максимальна сума знижки
plafondDécôte = marié === "oui" ?
this.taxAdminData.plafondDecoteCouple :
this.taxAdminData.plafondDecoteCelibataire;
// теоретична знижка
décôte = plafondDécôte - 0.75 * impots;
// знижка не може перевищувати суму податку
if (décôte > impots) {
décôte = impots;
}
// знижка <0
if (décôte < 0) {
décôte = 0;
}
}
// результат
return Math.ceil(décôte);
}
// розраховує можливе зниження
getRéduction(marié, salaire, enfants, impots) {
// максимальний рівень доходу для отримання права на знижку в розмірі 20 %
let plafondRevenuPourRéduction = marié === "oui" ?
this.taxAdminData.plafondRevenusCouplePourReduction :
this.taxAdminData.plafondRevenusCelibatairePourReduction;
plafondRevenuPourRéduction += enfants * this.taxAdminData.valeurReducDemiPart;
if (enfants > 2) {
plafondRevenuPourRéduction += (enfants - 2) * this.taxAdminData.valeurReducDemiPart;
}
// оподатковуваний дохід
const revenuImposable = this.getRevenuImposable(salaire);
// знижка
let réduction = 0;
if (revenuImposable < plafondRevenuPourRéduction) {
// знижка у розмірі 20 %
réduction = 0.2 * impots;
}
// результат
return Math.ceil(réduction);
}
}
// експорт класу
export default Métier;
- код JavaScript суворо відповідає коду PHP;
- експортується клас [Métier], рядок 187;
14.3.2. Клас JavaScript [Dao2]

Клас [Dao2] реалізує рівень [dao] вищезазначеного клієнта Javascript наступним чином:
'use strict';
// імпорт
import qs from 'qs'
class Dao2 {
// конструктор
constructor(axios) {
this.axios = axios;
// сесійний файл cookie
this.sessionCookieName = "PHPSESSID";
this.sessionCookie = '';
}
// ініціалізація сесії
async initSession() {
// параметри запиту HHTP [get /main.php?action=init-session&type=json]
const options = {
method: "GET",
// параметри запиту URL
params: {
action: 'init-session',
type: 'json'
}
};
// виконання запиту HTTP
return await this.getRemoteData(options);
}
async authentifierUtilisateur(user, password) {
// параметри запиту HHTP [post /main.php?action=authentifier-utilisateur]
const options = {
method: "POST",
headers: {
'«Content-type»: «application/x-www-form-urlencoded»,
},
// тіло POST
data: qs.stringify({
user: user,
password: password
}),
// параметри запиту URL
params: {
action: 'authentifier-utilisateur'
}
};
// виконання запиту HTTP
return await this.getRemoteData(options);
}
async getAdminData() {
// параметри запиту HHTP [get /main.php?action=get-admindata]
const options = {
method: "GET",
// параметри запиту URL
params: {
action: 'get-admindata'
}
};
// виконання запиту HTTP
const data = await this.getRemoteData(options);
// результат
return data;
}
async getRemoteData(options) {
// для сесійного файлу cookie
if (!options.headers) {
options.headers = {};
}
options.headers.Cookie = this.sessionCookie;
// виконання запиту HTTP
let response;
try {
// асинхронний запит
response = await this.axios.request('main.php', options);
} catch (error) {
// параметр [error] є екземпляром винятку — він може мати різні форми
if (error.response) {
// відповідь сервера міститься в [error.response]
response = error.response;
} else {
// помилка повторюється
throw error;
}
}
// response — це повна відповідь HTTP від сервера (заголовки HTTP + сама відповідь)
// отримуємо сесійний файл cookie, якщо він існує
const setCookie = response.headers['set-cookie'];
if (setCookie) {
// setCookie — це масив
// шукаємо сесійний файл cookie в цьому масиві
let trouvé = false;
let i = 0;
while (!trouvé && i < setCookie.length) {
// шукаємо сесійний файл cookie
const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
if (results) {
// зберігаємо сесійний файл cookie
// eslint-disable-next-line require-atomic-updates
this.sessionCookie = results[1];
// знайдено
trouvé = true;
} else {
// наступний елемент
i++;
}
}
}
// відповідь сервера знаходиться в [response.data]
return response.data;
}
}
// експорт класу
export default Dao2;
Коментарі
- клас [Dao2] реалізує лише три з можливих запитів до сервера розрахунку податків:
- [init-session] (рядки 17–29): для ініціалізації сеансу jSON;
- [authentifier-utilisateur] (рядки 31–50): для аутентифікації;
- [get-admindata] (рядки 52–65): для отримання даних від податкової адміністрації, які дозволять виконати розрахунки податку на стороні клієнта;
- рядки 52–65: ми вводимо нову дію [get-admindata] на сервері. Ця дія досі не була реалізована. Ми робимо це зараз.
14.3.3. Модифікація сервера розрахунку податку
Сервер розрахунку податку повинен реалізувати нову дію. Ми зробимо це у версії 14 сервера. Дія, яку потрібно реалізувати, має такі характеристики:
- вона викликається операцією [get /main.php?action=get-admindata];
- вона повертає рядок jSON об’єкта, що інкапсулює дані податкової адміністрації;
Ми розглянемо, як додати дію до нашого сервера.
Зміни будуть внесені в NetBeans:

У [2] ми змінюємо файл [config.json], щоб додати нову дію:
{
"databaseFilename": "Config/database.json",
"corsAllowed": true,
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-14",
"relativeDependencies": [
"/Entities/BaseEntity.php",
"/Entities/Simulation.php",
"/Entities/Database.php",
"/Entities/TaxAdminData.php",
"/Entities/ExceptionImpots.php",
"/Utilities/Logger.php",
"/Utilities/SendAdminMail.php",
"/Model/InterfaceServerDao.php",
"/Model/ServerDao.php",
"/Model/ServerDaoWithSession.php",
"/Model/InterfaceServerMetier.php",
"/Model/ServerMetier.php",
"/Responses/InterfaceResponse.php",
"/Responses/ParentResponse.php",
"/Responses/JsonResponse.php",
"/Responses/XmlResponse.php",
"/Responses/HtmlResponse.php",
"/Controllers/InterfaceController.php",
"/Controllers/InitSessionController.php",
"/Controllers/ListerSimulationsController.php",
"/Controllers/AuthentifierUtilisateurController.php",
"/Controllers/CalculerImpotController.php",
"/Controllers/SupprimerSimulationController.php",
"/Controllers/FinSessionController.php",
"/Controllers/AfficherCalculImpotController.php",
"/Controllers/AdminDataController.php"
],
"absoluteDependencies": [
"C:/myprograms/laragon-lite/www/vendor/autoload.php",
"C:/myprograms/laragon-lite/www/vendor/predis/predis/autoload.php"
],
"users": [
{
"login": "admin",
"passwd": "admin"
}
],
"adminMail": {
"smtp-server": "localhost",
"smtp-port": "25",
"from": "guest@localhost",
"to": "guest@localhost",
"subject": "plantage du serveur de calcul d'impôts",
"tls": "FALSE",
"attachments": []
},
"logsFilename": "Logs/logs.txt",
"actions":
{
"init-session": "\\InitSessionController",
"authentifier-utilisateur": "\\AuthentifierUtilisateurController",
"calculer-impot": "\\CalculerImpotController",
"lister-simulations": "\\ListerSimulationsController",
"supprimer-simulation": "\\SupprimerSimulationController",
"fin-session": "\\FinSessionController",
"afficher-calcul-impot": "\\AfficherCalculImpotController",
"get-admindata": "\\AdminDataController"
},
"types": {
"json": "\\JsonResponse",
"html": "\\HtmlResponse",
"xml": "\\XmlResponse"
},
"vues": {
"vue-authentification.php": [700, 221, 400],
"vue-calcul-impot.php": [200, 300, 341, 350, 800],
"vue-liste-simulations.php": [500, 600]
},
"vue-erreurs": "vue-erreurs.php"
}
Зміна полягає в наступному:
- рядок 67: додати дію [get-admindata] та пов’язати її з контролером;
- рядок 36: оголосити цей контролер у списку класів, які має завантажити додаток PHP;
Наступним етапом є реалізація контролера [AdminDataController] [3]:
<?php
namespace Application;
// залежності Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// псевдонім шару [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
class AdminDataController implements InterfaceController {
// $config — це конфігурація додатка
// обробка запиту Request
// використовує сесію Session і може її змінювати
// $infos — це додаткова інформація, характерна для кожного контролера
// повертає масив [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// повинен містити лише один параметр GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 1;
if ($erreur) {
// фіксуємо помилку
$message = "il faut utiliser la méthode [get] avec l'unique paramètre [action] dans l'URL";
$état = 1001;
// результат повертається головному контролеру
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// можна працювати
// Redis
\Predis\Autoloader::register();
try {
// клієнт [predis]
$redis = new \Predis\Client();
// підключаємося до сервера, щоб перевірити, чи він працює
$redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
// щось пішло не так
// повернення результату з помилкою до головного контролера
$état = 1050;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
}
// отримання даних від податкової служби
// спочатку шукаємо в кеші [redis]
if (!$redis->get("taxAdminData")) {
try {
// запитуються податкові дані з бази даних
$dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
// taxAdminData
$taxAdminData = $dao->getTaxAdminData();
// отримані дані заносимо в Redis
$redis->set("taxAdminData", $taxAdminData);
} catch (\RuntimeException $ex) {
// сталася помилка
// повертаємо результат з помилкою головному контролеру
$état = 1041;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => utf8_encode($ex->getMessage())], []];
}
} else {
// фіскальні дані беруться з пам'яті [redis] з областю дії [application]
$arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
// створено екземпляр об’єкта [TaxAdminData] на основі попереднього масиву атрибутів
$taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
}
// повертає результат головному контролеру
$état = 1000;
return [Response::HTTP_OK, $état, ["réponse" => $taxAdminData], []];
}
}
Коментарі
- рядок 12: як і інші контролери сервера, [AdminDataController] реалізує інтерфейс [InterfaceController], що складається з методу [execute] у рядках 19–79;
- рядок 78: як і в інших контролерах сервера, метод [AdminDataController.execute] повертає масив [$status, $état, [‘réponse’=>$response]] із:
- [$status]: код статусу відповіді HTTP;
- [$état] — внутрішній код додатка, що відображає стан сервера після виконання запиту клієнта;
- [$response]: масив, що містить відповідь, яку потрібно надіслати клієнту. У цьому випадку цей масив згодом буде перетворено на рядок jSON;
- рядки 25–34: перевіряється, чи дія [get-admindata] клієнта є синтаксично правильною;
- рядки 37–74: отримується об’єкт [TaxAdminData], знайдений або:
- рядки 56–59: у базі даних, якщо його не знайдено в кеші [redis];
- рядки 70–73: у кеші [redis];
Цей код повторює код контролера [CalculerImpotController], описаний у статті за посиланням. Дійсно, цей контролер також мав отримати об’єкт [TaxAdminData], що інкапсулює дані податкової адміністрації.
Під час тестування клієнта JavaScript форма jSON об’єкта [TaxAdminData] спричинила проблему, коли цей об’єкт було знайдено в кеші [redis]. Щоб це зрозуміти, розглянемо, у якому вигляді цей об’єкт зберігається в [redis]:


- У [5-7] видно, що числові значення були збережені у вигляді символьних рядків. PHP впорався з цим, оскільки оператор + у обчисленнях між числами та рядками неявним чином спричиняє зміну типу рядка на число. Але JavaScript діє навпаки: оператор + у обчисленнях між числами та рядками неявним чином спричиняє зміну типу з числа на рядок. Отже, обчислення класу JavaScript [Métier] є помилковими;
Щоб вирішити цю проблему, ми змінюємо метод [TaxAdminData.setFromArrayOfAttributes], який використовується в рядку 71 контролера, щоб створити екземпляр об’єкта [TaxAdminData] (див. статтю) на основі рядка jSON, знайденого в кеші [redis]:
<?php
namespace Application;
class TaxAdminData extends BaseEntity {
// податкові шкали
protected $limites;
protected $coeffR;
protected $coeffN;
// константи для розрахунку податку
protected $plafondQfDemiPart;
protected $plafondRevenusCelibatairePourReduction;
protected $plafondRevenusCouplePourReduction;
protected $valeurReducDemiPart;
protected $plafondDecoteCelibataire;
protected $plafondDecoteCouple;
protected $plafondImpotCouplePourDecote;
protected $plafondImpotCelibatairePourDecote;
protected $abattementDixPourcentMax;
protected $abattementDixPourcentMin;
// ініціалізація
public function setFromJsonFile(string $taxAdminDataFilename) {
// батьківський елемент
parent::setFromJsonFile($taxAdminDataFilename);
// перевіряються значення атрибутів
$this->checkAttributes();
// повертається об’єкт
return $this;
}
protected function check($value): \stdClass {
// $value — це масив елементів типу string або єдиний елемент
if (!\is_array($value)) {
$tableau = [$value];
} else {
$tableau = $value;
}
// масив рядків перетворюється на масив дійсних чисел
$newTableau = [];
$result = new \stdClass();
// елементи масиву мають бути додатними або нульовими десятковими числами
$modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
for ($i = 0; $i < count($tableau); $i ++) {
if (preg_match($modèle, $tableau[$i])) {
// поміщаємо число типу float у newTableau
$newTableau[] = (float) $tableau[$i];
} else {
// фіксуємо помилку
$result->erreur = TRUE;
// програма завершується
return $result;
}
}
// повертаємо результат
$result->erreur = FALSE;
if (!\is_array($value)) {
// одне значення
$result->value = $newTableau[0];
} else {
// список значень
$result->value = $newTableau;
}
return $result;
}
// ініціалізація за допомогою масиву атрибутів
public function setFromArrayOfAttributes(array $arrayOfAttributes) {
// батьківський елемент
parent::setFromArrayOfAttributes($arrayOfAttributes);
// перевіряються значення атрибутів
$this->checkAttributes();
// повертається об’єкт
return $this;
}
// перевірка значень атрибутів
protected function checkAttributes() {
// перевіряється, чи значення атрибутів є дійсними числами >=0
foreach ($this as $key => $value) {
if (is_string($value)) {
// $value має бути дійсним числом >=0 або масивом дійсних чисел >=0
$result = $this->check($value);
// помилка?
if ($result->erreur) {
// генерується виняток
throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
} else {
// записується значення
$this->$key = $result->value;
}
}
}
// повертається об’єкт
return $this;
}
// методи getter та setter
...
}
Коментарі
- рядок 5: клас [TaxAdminData] успадковує клас [BaseEntity], який вже має метод [setFromArrayOfAttributes]. Оскільки цей метод не підходить, ми перевизначаємо його у рядках 67–75;
- рядок 70: спочатку використовується метод [setFromArrayOfAttributes] батьківського класу для ініціалізації атрибутів класу;
- рядок 72: метод [checkAttributes] перевіряє, чи пов’язані значення дійсно є числами. Якщо це рядки, їх перетворюють на числа;
- рядок 74: об’єкт [$this], що створюється, є об’єктом з атрибутами, які мають числові значення;
- рядки 78–93: метод [checkAttributes] перевіряє, чи значення, пов’язані з атрибутами об’єкта, дійсно є числовими;
- рядок 80: відбувається обхід списку атрибутів;
- рядок 81: якщо значення атрибута має тип [string];
- рядок 83: то перевіряється, чи цей рядок є числом;
- рядок 90: якщо це так, рядок перетворюється на число і присвоюється перевіреному атрибуту;
- рядки 85–86: якщо це не так, генерується виняток;
- рядки 32–65: функція [check] робить трохи більше, ніж потрібно. Вона обробляє як масиви, так і окремі значення. Однак тут її викликають лише для перевірки значення типу [string]. Вона повертає об’єкт із властивостями [erreur, value], де:
- [erreur] — це логічне значення, яке вказує на наявність чи відсутність помилки;
- [value] — це параметр [value] із рядка 32, перетворений на число або масив чисел залежно від випадку;
Клас [BaseEntity], який міг мати атрибут із назвою [arrayOfAttributes], змінено таким чином, щоб він більше не містив цього атрибута: він, а саме, забруднює рядок jSON значенням [TaxAdminData]. Клас переписано таким чином:
<?php
namespace Application;
class BaseEntity {
// ініціалізація з файлу JSON
public function setFromJsonFile(string $jsonFilename) {
// отримуємо вміст файлу податкових даних
$fileContents = \file_get_contents($jsonFilename);
$erreur = FALSE;
// помилка?
if (!$fileContents) {
// фіксуємо помилку
$erreur = TRUE;
$message = "Le fichier des données [$jsonFilename] n'existe pas";
}
if (!$erreur) {
// код JSON з конфігураційного файлу заноситься в асоціативний масив
$arrayOfAttributes = \json_decode($fileContents, true);
// помилка?
if ($arrayOfAttributes === FALSE) {
// зафіксовано помилку
$erreur = TRUE;
$message = "Le fichier de données JSON [$jsonFilename] n'a pu être exploité correctement";
}
}
// помилка?
if ($erreur) {
// генерується виняток
throw new ExceptionImpots($message);
}
// ініціалізація атрибутів класу
foreach ($arrayOfAttributes as $key => $value) {
$this->$key = $value;
}
// перевіряється наявність усіх атрибутів
$this->checkForAllAttributes($arrayOfAttributes);
// повертається об’єкт
return $this;
}
public function checkForAllAttributes($arrayOfAttributes) {
// перевіряється, чи всі ключі були ініціалізовані
foreach (\array_keys($arrayOfAttributes) as $key) {
if (!isset($this->$key)) {
throw new ExceptionImpots("L'attribut [$key] de la classe "
. get_class($this) . " n'a pas été initialisé");
}
}
}
public function setFromArrayOfAttributes(array $arrayOfAttributes) {
// ініціалізація деяких атрибутів класу (не обов’язково всіх)
foreach ($arrayOfAttributes as $key => $value) {
$this->$key = $value;
}
// повертається об’єкт
return $this;
}
// toString
public function __toString() {
// атрибути об’єкта
$arrayOfAttributes = \get_object_vars($this);
// рядок jSON об’єкта
return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
}
}
Коментарі
- рядок 20: атрибут [$this→arrayOfAttributes] перетворено на змінну, яку тепер потрібно передавати методу [checkForAllAttributes], рядок 38, який раніше оперував атрибутом [$this→arrayOfAttributes];
Через цю зміну в [BaseEntity] клас [Database] також потрібно дещо змінити:
<?php
namespace Application;
class Database extends BaseEntity {
// атрибути
protected $dsn;
protected $id;
protected $pwd;
protected $tableTranches;
protected $colLimites;
protected $colCoeffR;
protected $colCoeffN;
protected $tableConstantes;
protected $colPlafondQfDemiPart;
protected $colPlafondRevenusCelibatairePourReduction;
protected $colPlafondRevenusCouplePourReduction;
protected $colValeurReducDemiPart;
protected $colPlafondDecoteCelibataire;
protected $colPlafondDecoteCouple;
protected $colPlafondImpotCelibatairePourDecote;
protected $colPlafondImpotCouplePourDecote;
protected $colAbattementDixPourcentMax;
protected $colAbattementDixPourcentMin;
// сетер
// ініціалізація
public function setFromJsonFile(string $jsonFilename) {
// батьківський об’єкт
parent::setFromJsonFile($jsonFilename);
// повертається об’єкт
return $this;
}
// геттери та сеттери
...
}
Коментарі
- у вихідному коді після рядка 30 викликався метод [parent::checkForAllAttributes]. Тепер цього робити не потрібно, оскільки це автоматично забезпечується методом [parent::setFromJsonFile($jsonFilename)];
14.3.4. Тестування методу [Postman] на сервері
[Postman] було представлено у статті за посиланням.
Ми використовуємо такі тести Postman:



Результат jSON цього останнього запиту такий:

- у [5-8] можна помітити, що атрибути рядка jSON дійсно мають числові значення (а не символьні рядки). Цей результат дозволить класу JavaScript [Métier] нормально виконатися;
14.3.5. Головний скрипт [main]

Основний скрипт [main] клієнта JavaScript має такий вигляд:
// імпорт
import axios from 'axios';
// імпорт
import Dao from './Dao2';
import Métier from './Métier';
// асинхронна функція [main]
async function main() {
// конфігурація axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
// інстанціювання шару [dao]
const dao = new Dao(axios);
// запити HTTP
let taxAdminData;
try {
// ініціалізація сеансу
log("-----------init-session");
let response = await dao.initSession();
log(response);
// аутентифікація
log("-----------authentifier-utilisateur");
response = await dao.authentifierUtilisateur("admin", "admin");
log(response);
// податкові дані
log("-----------get-admindata");
response = await dao.getAdminData();
log(response);
taxAdminData = response.réponse;
} catch (error) {
// помилка записується в журнал
console.log("erreur=", error.message);
// завершення
return;
}
// інстанціювання шару [métier]
const métier = new Métier(taxAdminData);
// розрахунок податку
log("-----------calculer-impot x 3");
const simulations = [];
simulations.push(métier.calculerImpot("oui", 2, 45000));
simulations.push(métier.calculerImpot("non", 2, 45000));
simulations.push(métier.calculerImpot("non", 1, 30000));
// список моделювань
log("-----------liste-des-simulations");
log(simulations);
// видалення моделювання
log("-----------suppression simulation n° 1");
simulations.splice(1, 1);
log(simulations);
}
// журнал jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// виконання
main();
Коментарі
- рядки 5–6: імпорт класів [Dao] та [Métier];
- рядок 9: асинхронна функція [main], яка організовує взаємодію з сервером за допомогою класу [Dao] та доручає класу [Métier] виконати розрахунки податку;
- рядки 10–36: скрипт послідовно та у блокуючому режимі викликає методи [initSession, authentifierUtilisateur, getAdminData] з шару [dao];
- рядок 38: шар [dao] більше не потрібен. Ми маємо всі елементи для запуску шару [métier] клієнтського JavaScript;
- рядки 41–46: виконуємо три розрахунки податку, результати яких підсумовуємо в масиві [simulations];
- рядок 49: виводимо масив симуляцій;
- рядок 52: видаляємо один із них;
Результати виконання головного скрипта такі:
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\client impôts\client http 2\main2.js"
"-----------init-session"
{
"action": "init-session",
"état": 700,
"réponse": "session démarrée avec type [json]"
}
"-----------authentifier-utilisateur"
{
"action": "authentifier-utilisateur",
"état": 200,
"réponse": "Authentification réussie [admin, admin]"
}
"-----------get-admindata"
{
"action": "get-admindata",
"état": 1000,
"réponse": {
"limites": [
9964,
27519,
73779,
156244,
0
],
"coeffR": [
0,
0.14,
0.3,
0.41,
0.45
],
"coeffN": [
0,
1394.96,
5798,
13913.69,
20163.45
],
"plafondQfDemiPart": 1551,
"plafondRevenusCelibatairePourReduction": 21037,
"plafondRevenusCouplePourReduction": 42074,
"valeurReducDemiPart": 3797,
"plafondDecoteCelibataire": 1196,
"plafondDecoteCouple": 1970,
"plafondImpotCouplePourDecote": 2627,
"plafondImpotCelibatairePourDecote": 1595,
"abattementDixPourcentMax": 12502,
"abattementDixPourcentMin": 437
}
}
"-----------calculer-impot x 3"
"-----------liste-des-simulations"
[
{
"impôt": 502,
"surcôte": 0,
"décôte": 857,
"réduction": 126,
"taux": 0.14
},
{
"impôt": 3250,
"surcôte": 370,
"décôte": 0,
"réduction": 0,
"taux": 0.3
},
{
"impôt": 1687,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.14
}
]
"-----------suppression simulation n° 1"
[
{
"impôt": 502,
"surcôte": 0,
"décôte": 857,
"réduction": 126,
"taux": 0.14
},
{
"impôt": 1687,
"surcôte": 0,
"décôte": 0,
"réduction": 0,
"taux": 0.14
}
]
[Done] exited with code=0 in 0.583 seconds
14.4. Клієнт HTTP 3

У цьому розділі ми запускаємо додаток [Client HTTP 2] у браузері за такою архітектурою:

Перенесення відбувається не відразу. Хоча [node.js] вміє виконувати JavaScript ES6, це, як правило, не характерно для браузерів. Тому доводиться використовувати інструменти, які перетворюють код ES6 на код ES5, зрозумілий сучасним браузерам. На щастя, ці інструменти є одночасно потужними та досить простими у використанні.
Тут ми керувалися статтею [How to write ES6 code that’s safe to run in the browser - Web Developer's Journal].
У папку [client HTTP 3/src] ми помістили елементи [main.js, Métier.js, Dao2.js] додатка [Client Http 2], який ми щойно розробили.
14.4.1. Ініціалізація проєкту
Ми будемо працювати в папці [client http 3]. Відкриваємо термінал у [VSCode] і переходимо до цієї папки:

Ініціалізуємо цей проєкт за допомогою команди [npm init] і приймаємо запропоновані за замовчуванням відповіді на поставлені запитання:

- у [4-5] — файл конфігурації проєкту [package.json], згенерований на основі різних наданих відповідей;
14.4.2. Встановлення залежностей проекту
Ми встановимо такі залежності:
- [@babel/core]: ядро інструменту [Babel] [https://babeljs.io], яке перетворює код ES 2015+ на код, що виконується в сучасних і старіших браузерах;
- [@babel/preset-env]: входить до складу набору інструментів Babel. Застосовується перед транспіляцією ES6 → ES5;
- [babel-loader]: ця залежність дозволяє інструменту [webpack] викликати інструмент [Babel];
- [webpack]: головний координатор. Саме [webpack] викликає Babel для транспіляції коду ES6 → ES5, а потім об’єднує всі отримані файли в один;
- [webpack-cli]: необхідний для роботи [webpack];
- [@webpack-cli/init]: використовується для налаштування [webpack];
- [webpack-dev-server]: надає веб-сервер для розробки, який за замовчуванням працює на порту 8080. При внесенні змін до вихідних файлів автоматично перезавантажує веб-додаток;
Залежності проекту встановлюються в терміналі [VSCode] наступним чином:
npm --save-dev install @babel/core @babel/preset-env babel-loader webpack webpack-cli webpack-dev-server @webpack-cli/init

Після встановлення залежностей файл [package.json] змінився наступним чином:
{
"name": "client-http-3",
"version": "1.0.0",
"description": "client jS du serveur de calcul de l'impôt",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "serge.tahe@gmail.com",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.6.0",
"@babel/preset-env": "^7.6.0",
"@webpack-cli/init": "^0.2.2",
"babel-loader": "^8.0.6",
"cross-env": "^6.0.0",
"webpack": "^4.40.2",
"webpack-cli": "^3.3.9",
"webpack-dev-server": "^3.8.1"
}
}
- рядки 12–19: залежності проєкту — це [devDependencies]: вони потрібні на етапі розробки, але не потрібні на етапі виробництва. Адже у виробничому середовищі використовується файл [dist/main.js]. Він написаний на ES5 і більше не потребує інструментів для транспіляції коду з ES6 у код ES5;
Нам потрібно додати до проєкту дві залежності:
- [core-js]: містить «поліфіли» для ECMAScript 2019. Поліфіл дозволяє виконувати новітній код, такий як ECMAScript 2019 (вересень 2019 р.), у старих браузерах;
- [regenerator-runtime]: згідно з веб-сайтом бібліотеки --> [Source transformer enabling ECMAScript 6 generator functions in JavaScript-of-today];
Ці дві залежності, починаючи з Babel 7, замінюють залежність [@babel/polyfill], яка раніше виконувала цю роль і яка зараз (вересень 2019 року) є застарілою. Їх встановлюють таким чином:

Файл [package.json] змінюється таким чином:
{
"name": "client-http-3",
"version": "1.0.0",
"description": "My webpack project",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "webpack-dev-server"
},
"author": "serge.tahe@gmail.com",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.6.0",
"@babel/preset-env": "^7.6.0",
"@webpack-cli/init": "^0.2.2",
"babel-loader": "^8.0.6",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"html-webpack-plugin": "^3.2.0",
"webpack": "^4.40.2",
"webpack-cli": "^3.3.9",
"webpack-dev-server": "^3.8.1"
},
"dependencies": {
"core-js": "^3.2.1",
"regenerator-runtime": "^0.13.3"
}
}
Використання залежностей [core-js, regenerator-runtime] вимагає додавання наступних [imports] (рядки 3–4) до головного скрипта [src/main.js]:
// імпорт
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
// імпорт
import Dao from './Dao2';
import Métier from './Métier';
14.4.3. Налаштування [webpack]
[webpack] — це інструмент, який керуватиме:
- транспіляцію ES6 → ES5 усіх файлів JavaScript у проєкті;
- об’єднання згенерованих файлів в один файл;
Цей інструмент керується конфігураційним файлом [webpack.config.js], який можна згенерувати за допомогою залежності під назвою [@webpack-cli/init] (вересень 2019 р.). Її було встановлено разом з іншими, зазначеними у розділі «Посилання».
Виконуємо команду [npx webpack-cli init] у терміналі [VSCode]:

Після відповідей на різні запитання (де більшість запропонованих за замовчуванням відповідей можна прийняти) у кореневому каталозі проєкту [4] створюється файл [webpack.config.js]:
Файл [webpack.config.js] виглядає так:
/* eslint-disable */
const path = require('path');
const webpack = require('webpack');
/*
* SplitChunksPlugin is enabled by default and replaced
* deprecated CommonsChunkPlugin. It automatically identifies modules which
* should be splitted of chunk by heuristics using module duplication count and
* module category (i. e. node_modules). And splits the chunks…
*
* It is safe to remove "splitChunks" from the generated configuration
* and was added as an educational example.
*
* https://webpack.js.org/plugins/split-chunks-plugin/
*
*/
const HtmlWebpackPlugin = require('html-webpack-plugin');
/*
* We've enabled HtmlWebpackPlugin for you! This generates a html
* page for you when you compile webpack, which will make you start
* developing and prototyping faster.
*
* https://github.com/jantimon/html-webpack-plugin
*
*/
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: '[name].[chunkhash].js',
path: path.resolve(__dirname, 'dist')
},
plugins: [new webpack.ProgressPlugin(), new HtmlWebpackPlugin()],
module: {
rules: [
{
test: /.(js|jsx)$/,
include: [path.resolve(__dirname, 'src')],
loader: 'babel-loader',
options: {
plugins: ['syntax-dynamic-import'],
presets: [
[
'@babel/preset-env',
{
modules: false
}
]
]
}
}
]
},
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
priority: -10,
test: /[\\/]node_modules[\\/]/
}
},
chunks: 'async',
minChunks: 1,
minSize: 30000,
name: true
}
},
devServer: {
open: true
}
};
Я не розумію всіх деталей цього файлу, але можна відзначити кілька моментів:
- рядок 1: файл не містить коду ES6. Тому [Eslint] повідомляє про помилки, які походять аж із кореневого каталогу проєкту [javascript]. Це створює незручності. Щоб Eslint не аналізував файл, достатньо прокоментувати рядок 1;
- рядок 31: ми працюємо в режимі [développement];
- рядок 32: вхідний скрипт тут — [src/index.js]. Нам доведеться це змінити;
- рядок 36: папкою, куди будуть записані результати роботи [webpack], буде папка [dist];
- рядок 46: бачимо, що [webpack] використовує [babel-loader] — одну з залежностей, які ми встановили;
- рядок 54: бачимо, що [webpack] використовує [@babel-preset/env] — одну з залежностей, які ми встановили;
Ініціалізація [webpack] змінила файл [package.json] (просить дозволу):
{
"name": "client-http-3",
"version": "1.0.0",
"description": "My webpack project",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "webpack-dev-server"
},
"author": "serge.tahe@gmail.com",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.6.0",
"@babel/preset-env": "^7.6.0",
"@webpack-cli/init": "^0.2.2",
"babel-loader": "^8.0.6",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"html-webpack-plugin": "^3.2.0",
"webpack": "^4.40.2",
"webpack-cli": "^3.3.9",
"webpack-dev-server": "^3.8.1"
},
"dependencies": {
"core-js": "^3.2.1",
"regenerator-runtime": "^0.13.3"
}
}
- рядок 4: його було змінено;
- рядки 8–9, 18–19: їх було додано;
- рядок 8: завдання [npm], яке дозволяє скомпілювати проект;
- рядок 9: завдання [npm], яке дозволяє його виконати;
- рядок 18: ?
- рядок 19: дозволяє створити файл [dist/index.html], який автоматично вбудовує скрипт [dist/main.js], згенерований [webpack], і саме цей скрипт використовується під час виконання проєкту;
Нарешті, конфігурація [webpack] згенерувала файл [src/index.js]:

Вміст файлу [index.js] є таким (вересень 2019 року):
console.log("Hello World from your main file!");
14.4.4. Компіляція та виконання проєкту
Файл [package.json] містить три завдання [npm]:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "webpack-dev-server"
},
Ці завдання входять до складу [VSCode], який запускає їх на виконання:

- у [1-3] відбувається компіляція проєкту;
- у [4]: проект, скомпільований у [dist/main.hash.js], створює сторінку [dist/index.html];
Створена сторінка [index.html] має такий вигляд:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Webpack App</title>
</head>
<body>
<script type="text/javascript" src="main.87afc226fd6d648e7dea.js"></script></body>
</html>
Отже, ця сторінка лише інкапсулює файл [main.hash.js], згенерований [webpack].
Проєкт виконується завданням [start]:

Сторінка [dist/index.html] завантажується на сервер, що належить до набору [webpack], який працює на порту 8080 локальної машини, і відображається браузером за замовчуванням цієї машини:

- у [2] — це порт служби веб-сервера [webpack];
- на [3], тіло сторінки [dist/index.html] є порожнім;
- у [4] — вкладка [console] у інструментах розробки браузера, у даному випадку Firefox (F12);
- на [5] — результат виконання файлу [src/index.js]. Нагадаємо, що його вміст був таким:
Тепер замінимо цей вміст на такий рядок:
Автоматично (без перекомпіляції) генеруються нові файли [main.js, index.html], а новий файл [index.html] завантажується у браузер:

Немає необхідності виконувати завдання [build] перед завданням [start]: останнє спочатку виконує компіляцію проєкту. Воно не зберігає результати цієї компіляції у папці [dist]. Щоб у цьому переконатися, достатньо видалити цю папку. Тоді стане видно, що завдання [start] компілює та виконує проект, не створюючи папку [dist]. Здається, що вона зберігає результати [index.html, main.hash.js] у окремій папці, пов’язаній із [webpackdev-server]. Такої поведінки достатньо для наших тестів.
Коли запускається сервер розробки, будь-яка збережена зміна в одному з файлів проєкту спричиняє перекомпіляцію. З цієї причини ми вимикаємо режим [Auto Save] у [VSCode]. Адже ми не хочемо, щоб відбувалася перекомпіляція щоразу, коли ми вводимо символи в один із файлів проєкту. Ми хочемо, щоб перекомпіляція відбувалася лише під час збереження змін:

- у [2] опція [Auto Save] не повинна бути відмічена;
14.4.5. Тестування клієнта JavaScript сервера розрахунку податку
Щоб протестувати клієнтський JavaScript сервера розрахунку податку, потрібно вказати [main.js] [1] як точку входу в проект у файлі [webpack.config.js] [2-3]:

Не забуваймо, що скрипт [main.js] повинен містити два додаткові імпорти порівняно з його версією у [Client http 2]:

Крім того, ми дещо змінили код для обробки помилок, які може надсилати сервер:
// імпортує
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
// імпорт
import Dao from './Dao2';
import Métier from './Métier';
// асинхронна функція [main]
async function main() {
// конфігурація axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
// інстанціювання шару [dao]
const dao = new Dao(axios);
// запити HTTP
let taxAdminData;
try {
// ініціалізація сеансу
log("-----------init-session");
let response = await dao.initSession();
log(response);
if (response.état != 700) {
throw new Error(JSON.stringify(response.réponse));
}
// аутентифікація
log("-----------authentifier-utilisateur");
response = await dao.authentifierUtilisateur("admin", "admin");
log(response);
if (response.état != 200) {
throw new Error(JSON.stringify(response.réponse));
}
// податкові дані
log("-----------get-admindata");
response = await dao.getAdminData();
log(response);
if (response.état != 1000) {
throw new Error(JSON.stringify(response.réponse));
}
taxAdminData = response.réponse;
} catch (error) {
// помилка в журналі
console.log("erreur=", error.message);
// завершення
return;
}
// інстанціювання шару [métier]
const métier = new Métier(taxAdminData);
// розрахунок податку
log("-----------calculer-impot x 3");
const simulations = [];
simulations.push(métier.calculerImpot("oui", 2, 45000));
simulations.push(métier.calculerImpot("non", 2, 45000));
simulations.push(métier.calculerImpot("non", 1, 30000));
// список моделювань
log("-----------liste-des-simulations");
log(simulations);
// видалення моделювання
log("-----------suppression simulation n° 1");
simulations.splice(1, 1);
log(simulations);
}
// журнал jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// виконання
main();
Коментарі
- у рядках [24-26], [31-33], [38-40] перевіряється код [response.état], надісланий у відповіді jSON від сервера. Якщо цей код вказує на помилку, генерується виняток із повідомленням про помилку у вигляді рядка jSON із відповіді сервера [response.réponse];
Після цього ми виконуємо проект [5-6].
Після цього сторінка [index.html] генерується та завантажується у браузері:

- у [7] видно, що дія [init-session] не змогла завершитися через проблему [CORS] (Cross-Origin Resource Sharing);
Проблема CORS пов’язана з взаємодією клієнт/сервер:
- наш клієнт JavaScript було завантажено на комп’ютер [http://localhost:8080];
- сервер розрахунку податку працює на машині [http://localhost:80];
- отже, клієнт і сервер не знаходяться в одних і тих самих доменах (одна машина, але не один і той самий порт);
- браузер, який виконує клієнтський JavaScript, завантажений з машини [http://localhost:8080], блокує будь-який запит, який не спрямований на [http://localhost:80]. Це захід безпеки. Тому він також блокує запит клієнта до сервера, що працює на машині [http://localhost:80];
Насправді браузер не блокує запит повністю. Він чекає, поки сервер «повідомить» йому, що приймає міждоменні запити. Якщо він отримає цей дозвіл, браузер передасть міждоменний запит.
Сервер надає дозвіл, надсилаючи спеціальні заголовки HTTP:
- рядок 1: клієнт JavaScript працює в домені [http://localhost:8080]. Сервер повинен явно відповісти, що він приймає цей домен;
- рядок 2: клієнт JavaScript використовуватиме у своїх запитах заголовки HTTP та [Accept, Content-Type]:
- [Accept]: цей заголовок надсилається у кожному запиті;
- [Content-Type]: цей заголовок використовується в операціях POST для вказівки типу параметрів POST;
Сервер повинен явно прийняти ці два заголовки HTTP;
- рядок 3: клієнт JavaScript використовуватиме запити GET та POST. Сервер повинен явно прийняти ці два типи запитів;
- рядок 4: клієнт JavaScript надсилатиме сесійні файли cookie. Сервер приймає їх із заголовком з рядка 4;
Отже, нам потрібно внести зміни до сервера. Ми робимо це у файлі [Netbeans]. Проблема з CORS виникає виключно в режимі розробки. У виробничому середовищі клієнт і сервер працюватимуть у тому самому домені [http://localhost:80], і проблеми CORS не виникне. Отже, нам потрібен спосіб дозволити або заборонити запити CORS шляхом налаштування сервера.

Зміни на сервері вносяться у трьох місцях:
- [1, 4]: у файлі конфігурації [config.json], щоб додати туди логічне значення, яке контролюватиме, чи приймаються міждоменні запити;
- [2]: у класі [ParentResponse], який надсилає відповідь клієнту JavaScript. Саме цей клас надсилатиме заголовки CORS, яких очікує клієнтський браузер;
- [3]: у класах [HtmlResponse, JsonResponse, XmlResponse], які генерують відповіді відповідно для сесій [html, json, xml]. Ці класи повинні передати своєму батьківському класу [2] булеве значення [corsAllowed], яке міститься у [4]. Це здійснюється у [5] шляхом передачі масиву зображень із файлу jSON [2];
Клас [ParentResponse] [2] змінюється наступним чином:
<?php
namespace Application;
// залежності Symfony
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
class ParentResponse {
// int $statusCode: код статусу відповіді HTTP
// рядок $content: тіло відповіді, що надсилається
// залежно від випадку — це рядок JSON, XML, HTML
// масив $headers: заголовки HTTP, які потрібно додати до відповіді
public function sendResponse(
Request $request,
int $statusCode,
string $content,
array $headers,
array $config): void {
// підготовка текстової відповіді сервера
$response = new Response();
$response->setCharset("utf-8");
// код статусу
$response->setStatusCode($statusCode);
// заголовки для міждоменних запитів
if ($config['corsAllowed']) {
$origin = $request->headers->get("origin");
if (strpos($origin, "http://localhost") === 0) {
$headers = array_merge($headers,
["Access-Control-Allow-Origin" => $origin,
"Access-Control-Allow-Headers" => "Accept, Content-Type",
"Access-Control-Allow-Methods" => "GET, POST",
"Access-Control-Allow-Credentials" => "true"
]);
}
}
foreach ($headers as $text => $value) {
$response->headers->set($text, $value);
}
// особливий випадок методу [OPTIONS]
// у цьому випадку важливі лише заголовки
$method = strtolower($request->getMethod());
if ($method === "options") {
$content = "";
$response->setStatusCode(Response::HTTP_OK);
}
// надсилається відповідь
$response->setContent($content);
$response->send();
}
}
- рядок 29: перевіряється, чи потрібно обробляти міждоменні запити. Якщо так, то генеруються заголовки HTTP CORS (рядки 33–37), навіть якщо поточний запит не є міждоменним. В останньому випадку заголовки CORS будуть непотрібними і не будуть використані клієнтом;
- рядок 30: у міждоменному запиті браузер клієнта, який звертається до сервера, надсилає заголовки HTTP та [Origin: http://localhost:8080] (у конкретному випадку нашого клієнта JavaScript). У рядку 30 цей заголовок HTTP отримується із запиту [$request];
- у рядку 31: ми будемо приймати міждоменні запити виключно з комп’ютера [http://localhost]. Нагадуємо, що такі запити відбуваються лише в режимі розробки проєкту;
- рядки 32–36: додаємо заголовки CORS до заголовків, що вже є в таблиці [$headers];
- рядки 45–49: спосіб, у який клієнтський браузер запитує дозволи CORS, може відрізнятися залежно від використовуваного клієнта. Іноді трапляється, що клієнтський браузер запитує ці дозволи за допомогою команди HTTP [OPTIONS]. Це нововведення для нашого сервера, який був створений для обслуговування виключно команд [GET, POST]. У разі команди [OPTIONS] сервер наразі генерує відповідь про помилку. У рядках 46–49 ми виправляємо це в останній момент: якщо в рядку 46 ми виявляємо, що поточна команда є командою [OPTIONS], то для клієнта генерується:
- у рядках 47, 51 — порожню відповідь [$content];
- у рядку 48 — код статусу 200, що вказує на успішне виконання запиту. Єдине, що важливо для цього запиту, — це надсилання заголовків CORS із рядків 33–36. Саме цього очікує браузер клієнта;
Після внесення відповідних виправлень у сервер клієнтський JavaScript працює краще, але з’являється нова помилка:

- у [1] сесія jSON ініціалізується правильно;
- у [2] дія [authentifier-utilisateur] завершується з помилкою: сервер повідомляє, що поточної сесії немає. Це означає, що клієнт JavaScript не надіслав серверу належним чином сесійний файл cookie, який він відправив під час дії [init-session];
Розглянемо мережеві обміни, що відбулися:

- у [4] — запит [init-session]. Він пройшов успішно зі статусом відповіді 200;
- у [5] — запит [authentifier-utilisateur]. Цей запит завершився невдачею з кодом 400 (Bad Request) [6] у статусі відповіді;
Якщо розглянути заголовки HTTP та [7] запиту [5], можна побачити, що клієнт JavaScript не надіслав заголовки HTTP та [Cookie], які дозволили б йому повернути сесійний файл cookie, спочатку надісланий сервером. Саме тому сервер повідомляє, що сесія відсутня.
Щоб клієнт надіслав сесійний файл cookie, потрібно додати налаштування до об’єкта [axios]:
// імпорт
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
// імпорт
import Dao from './Dao2';
import Métier from './Métier';
// асинхронна функція [main]
async function main() {
// конфігурація axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
axios.defaults.withCredentials = true;
// інстанціювання шару [dao]
const dao = new Dao(axios);
// запити HTTP
let taxAdminData;
...
У рядку 15 вказано, що файли cookie мають бути включені в заголовки HTTP запиту [axios]. Зазначимо, що в середовищі [node.js] це не було необхідним. Отже, між цими двома середовищами існують відмінності в коді.
Після виправлення цієї помилки JavaScript-клієнт працює нормально:


14.5. Вдосконалення клієнта HTTP 3
Коли попередній клас [Dao2] виконується в браузері, управління сесійним файлом cookie не потрібне. Адже саме браузер, який розміщує рівень [dao], управляє сесійним файлом cookie: він автоматично повертає будь-який файл cookie, який йому надсилає сервер. Отже, клас [Dao2] можна переписати у такий клас [Dao3]:
"use strict";
// імпорт
import qs from "qs";
class Dao3 {
// конструктор
constructor(axios) {
this.axios = axios;
}
// ініціалізація сеансу
async initSession() {
// параметри запиту HHTP [get /main.php?action=init-session&type=json]
const options = {
method: "GET",
// параметри URL
params: {
action: "init-session",
type: "json"
}
};
// виконання запиту HTTP
return await this.getRemoteData(options);
}
async authentifierUtilisateur(user, password) {
// параметри запиту HHTP [post /main.php?action=authentifier-utilisateur]
const options = {
method: "POST",
headers: {
"Content-type": "application/x-www-form-urlencoded"
},
// тіло запиту POST
data: qs.stringify({
user: user,
password: password
}),
// параметри URL
params: {
action: "authentifier-utilisateur"
}
};
// виконання запиту HTTP
return await this.getRemoteData(options);
}
async getAdminData() {
// параметри запиту HHTP [get /main.php?action=get-admindata]
const options = {
method: "GET",
// параметри запиту URL
params: {
action: "get-admindata"
}
};
// виконання запиту HTTP
const data = await this.getRemoteData(options);
// результат
return data;
}
async getRemoteData(options) {
// виконання запиту HTTP
let response;
try {
// асинхронний запит
response = await this.axios.request("main.php", options);
} catch (error) {
// параметр [error] є екземпляром винятку — він може мати різні форми
if (error.response) {
// відповідь сервера міститься в [error.response]
response = error.response;
} else {
// помилка повторюється
throw error;
}
}
// response — це повна відповідь сервера у форматі HTTP (заголовки HTTP + сама відповідь)
// відповідь сервера міститься в [response.data]
return response.data;
}
}
// експорт класу
export default Dao3;
Усе, що стосувалося управління файлом cookie управління, зникло.
Ми змінюємо попередній проект наступним чином:

У папці [src] ми додали два файли:
- клас [Dao3], який ми щойно представили;
- файл [main3], який відповідає за запуск нової версії;
Файл [main3] залишається ідентичним файлу [main] з попередньої версії, але тепер він використовує клас [Dao3]:
// імпорт
import axios from "axios";
import "core-js/stable";
import "regenerator-runtime/runtime";
// імпорт
import Dao from "./Dao3";
import Métier from "./Métier";
// асинхронна функція [main]
async function main() {
// конфігурація Axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL =
"http://localhost/php7/scripts-web/impots/version-14";
axios.defaults.withCredentials = true;
// інстанціювання шару [dao]
const dao = new Dao(axios);
// запити HTTP
...
}
// журнал jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// виконання
main();
Файл [webpack.config] змінено, щоб тепер запускати скрипт [main3]:
/* eslint-disable */
const path = require("path");
const webpack = require("webpack");
/*
* SplitChunksPlugin is enabled by default and replaced
* deprecated CommonsChunkPlugin. It automatically identifies modules which
* should be splitted of chunk by heuristics using module duplication count and
* module category (i. e. node_modules). And splits the chunks…
*
* It is safe to remove "splitChunks" from the generated configuration
* and was added as an educational example.
*
* https://webpack.js.org/plugins/split-chunks-plugin/
*
*/
const HtmlWebpackPlugin = require("html-webpack-plugin");
/*
* We've enabled HtmlWebpackPlugin for you! This generates a html
* page for you when you compile webpack, which will make you start
* developing and prototyping faster.
*
* https://github.com/jantimon/html-webpack-plugin
*
*/
module.exports = {
mode: "development",
//запис: "./src/mainjs",
entry: "./src/main3.js",
output: {
filename: "[name].[chunkhash].js",
path: path.resolve(__dirname, "dist")
},
plugins: [new webpack.ProgressPlugin(), new HtmlWebpackPlugin()],
...
};
Після цього запускаємо проект після запуску сервера розрахунку податку:

Результати, отримані в консолі браузера, ідентичні результатам попередньої версії.
14.6. Conclusion
Тепер ми маємо всі необхідні інструменти для розробки JavaScript-коду веб-додатку. Ми можемо:
- використовувати найновіший код ECMAScript;
- тестувати окремі елементи цього коду в середовищі [node.js], яке є простішим для налагодження та тестування;
- потім переносити цей код у браузер за допомогою інструментів [babel] та [webpack];