14. Clientes HTTP JavaScript del servicio de cálculo de impuestos
14.1. Introduction
Aquí nos proponemos escribir un cliente [node.js] de la versión 14 del servicio de cálculo de impuestos. La arquitectura cliente/servidor será la siguiente:

Analizaremos dos versiones del cliente:
- la versión 1 del cliente tendrá la siguiente estructura por capas: [main, dao]:

- la versión 2 del cliente tendrá una estructura [main, métier, dao]. La capa [métier] del servidor se trasladará al cliente:

14.2. Cliente HTTP 1

Como ya hemos dicho, el cliente HTTP 1 implementa la siguiente arquitectura cliente/servidor:

Implementaremos:
- la capa [dao] en forma de clase;
- la capa [main] en forma de script que utilice dicha clase;
14.2.1. La capa [dao]
La capa [dao] se implementará mediante la siguiente clase [Dao1.js]:
'«use strict»;
// importaciones
import qs from 'qs'
class Dao1 {
// constructor
constructor(axios) {
// biblioteca axios para realizar las peticiones HTTP
this.axios = axios;
// cookie de sesión
this.sessionCookieName = "PHPSESSID";
this.sessionCookie = '';
}
// iniciar sesión
async initSession() {
// opciones de la consulta HHTP [get /main.php?action=init-session&type=json]
const options = {
method: "GET",
// parámetros de la consulta URL
params: {
action: 'init-session',
type: 'json'
}
};
// ejecución de la consulta HTTP
return await this.getRemoteData(options);
}
async authentifierUtilisateur(user, password) {
// opciones de la solicitud HHTP [post /main.php?action=authentifier-utilisateur]
const options = {
method: "POST",
headers: {
'«Content-type»: «application/x-www-form-urlencoded»,
},
// cuerpo del POST
data: qs.stringify({
user: user,
password: password
}),
// parámetros de la URL
params: {
action: 'authentifier-utilisateur'
}
};
// ejecución de la consulta HTTP
return await this.getRemoteData(options);
}
// cálculo del impuesto
async calculerImpot(marié, enfants, salaire) {
// opciones de la consulta HHTP [post /main.php?action=calculer-impot]
const options = {
method: "POST",
headers: {
'«Content-type»: «application/x-www-form-urlencoded»,
},
// cuerpo de POST [marié, enfants, salaire]
data: qs.stringify({
marié: marié,
enfants: enfants,
salaire: salaire
}),
// parámetros de la URL
params: {
action: 'calculer-impot'
}
};
// ejecución de la consulta HTTP
const data = await this.getRemoteData(options);
// resultado
return data;
}
// lista de simulaciones
async listeSimulations() {
// opciones de la consulta HHTP [get /main.php?action=lister-simulations]
const options = {
method: "GET",
// parámetros de la consulta URL
params: {
action: 'lister-simulations'
},
};
// ejecución de la consulta HTTP
const data = await this.getRemoteData(options);
// resultado
return data;
}
// lista de simulaciones
async supprimerSimulation(index) {
// opciones de la consulta HHTP [get /main.php?action=supprimer-simulation&numéro=index]
const options = {
method: "GET",
// parámetros de la consulta URL
params: {
action: 'supprimer-simulation',
numéro: index
},
};
// ejecución de la consulta HTTP
const data = await this.getRemoteData(options);
// resultado
return data;
}
async getRemoteData(options) {
// para la cookie de sesión
if (!options.headers) {
options.headers = {};
}
options.headers.Cookie = this.sessionCookie;
// ejecución de la consulta HTTP
let response;
try {
// solicitud asíncrona
response = await this.axios.request('main.php', options);
} catch (error) {
// el parámetro [error] es una instancia de excepción; puede adoptar diversas formas
if (error.response) {
// la respuesta del servidor se encuentra en [error.response]
response = error.response;
} else {
// Se vuelve a generar el error
throw error;
}
}
//: la respuesta es el conjunto completo de la respuesta HTTP del servidor (encabezados HTTP + la propia respuesta)
// se recupera la cookie de sesión, si existe
const setCookie = response.headers['set-cookie'];
if (setCookie) {
// setCookie es una matriz
//: se busca la cookie de sesión en este array
let trouvé = false;
let i = 0;
while (!trouvé && i < setCookie.length) {
// se busca la cookie de sesión
const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
if (results) {
// se almacena la cookie de sesión
// eslint-disable-next-line require-atomic-updates
this.sessionCookie = results[1];
// la hemos encontrado
trouvé = true;
} else {
// el siguiente elemento
i++;
}
}
}
// la respuesta del servidor está en [response.data]
return response.data;
}
}
// exportación de la clase
export default Dao1;
- Aquí utilizamos lo que hemos aprendido en el apartado «enlace», donde presentamos la biblioteca [axios], que permite realizar consultas HTTP tanto en [node.js] como en un navegador. Analizaremos en particular el script del apartado «enlace»;
- líneas 9-15: el constructor de la clase. Esta tendrá tres propiedades:
- [axios]: el objeto [axios] que permite realizar las consultas HTTP. Este es transmitido por el código que realiza la llamada;
- [sessionCookieName]: dependiendo de los servidores, la cookie de sesión puede tener diferentes nombres. En este caso, es [PHPSESSID];
- [sessionCookie]: la cookie de sesión enviada por el servidor y almacenada por el cliente;
- líneas 53-76: la función asíncrona [calculerImpot] realiza la solicitud [post /main.php?action=calculer-impot] enviando los parámetros [marié, enfants, salaire]. Devuelve la cadena jSON transmitida por el servidor en forma de objeto JavaScript;
- líneas 79-92: la función asíncrona [listeSimulations] realiza la solicitud [get /main.php?action=lister-simulations. Devuelve la cadena jSON enviada por el servidor en forma de objeto JavaScript;
- líneas 95-109: la función asíncrona [supprimerSimulation] realiza la solicitud [get /main.php?action=supprimer-simulation&numéro=index]. Devuelve la cadena jSON enviada por el servidor en forma de objeto JavaScript;
- línea 121: se utiliza la notación [this.axios] porque, en este caso, el objeto [axios] enviado al constructor se ha almacenado en la propiedad [this.axios];
- línea 161: se exporta la clase [Dao1] para poder utilizarla;
14.2.2. El script [main1.js]
El script [main1.js] realiza una serie de llamadas al servidor utilizando la clase [Dao1]:
- inicialización de una sesión jSON;
- autenticación con [admin, admin];
- solicita tres cálculos de impuestos;
- solicita la lista de simulaciones;
- elimina una de ellas;
El código es el siguiente:
// importación de Axios
import axios from 'axios';
// importación de la clase Dao1
import Dao from './Dao1';
// función asíncrona [main]
async function main() {
// Configuración de Axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
// instanciación de la capa [dao]
const dao = new Dao(axios);
// uso de la capa [dao]
try {
// inicialización de sesión
log("-----------init-session");
let response = await dao.initSession();
log(response);
// autenticación
log("-----------authentifier-utilisateur");
response = await dao.authentifierUtilisateur("admin", "admin");
log(response);
// cálculos de impuestos
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);
// Lista de simulaciones
log("-----------liste-des-simulations");
response = await dao.listeSimulations();
log(response);
// eliminación de una simulación
log("-----------suppression simulation n° 1");
response = await dao.supprimerSimulation(1);
log(response);
} catch (error) {
// se registra el error
console.log("erreur=", error.message);
}
}
// registro jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// ejecución
main();
Comentarios
- línea 2: se importa la biblioteca [axios];
- línea 4: se importa la clase [Dao];
- línea 7: la función [main], que se comunica con el servidor, es asíncrona;
- líneas 9-10: configuración por defecto de las consultas HTTP que se realizarán al servidor:
- línea 9: [timeout] de 2 segundos;
- línea 10: todas las URL tienen como prefijo la URL básica de la versión 14 del servidor de cálculo de impuestos;
- línea 12: se crea la capa [Dao]. Ya se puede utilizar;
- líneas 46-48: la función [log] tiene como objetivo mostrar la cadena jSON de un objeto JavaScript en un formato mejorado: en formato vertical con una sangría de dos espacios (tercer parámetro);
- líneas 15-18: inicialización de la sesión jSON;
- líneas 19-22: autenticación;
- líneas 23-30: se solicitan tres cálculos de impuestos en paralelo. Gracias a [await Promise.all], la ejecución queda bloqueada hasta que se hayan obtenido los tres resultados;
- líneas 31-34: lista de simulaciones;
- líneas 35-38: eliminación de una simulación;
- líneas 39-42: gestión de posibles excepciones;
Los resultados de la ejecución son los siguientes:
[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. Cliente HTTP 2

La arquitectura del cliente HTTP2 es la siguiente:

Se ha trasladado la capa [métier] del servidor al cliente JavaScript. A diferencia de lo que hicimos en el curso PHP7, la capa [main] no tendrá que pasar aquí por la capa [métier] para llegar a la capa [dao]. Utilizaremos estas dos capas como centros de competencias:
- la capa [main] pasa por la capa [dao] en cuanto necesita datos que se encuentran en el servidor;
- la capa [main] solicita a la capa [métier] que realice los cálculos del impuesto;
- la capa [métier] es independiente de la capa [dao] y nunca recurre a ella;
14.3.1. La clase JavaScript [Métier]
La esencia de la clase [Métier] en PHP se ha descrito en el artículo enlazado. Se trata de un código bastante complejo que recordamos aquí, no para explicarlo, sino para poder traducirlo a JavaScript:
<?php
// espacio de nombres
namespace Application;
class Metier implements InterfaceMetier {
// capa DAO
private $dao;
// datos de la administración tributaria
private $taxAdminData;
//---------------------------------------------
// configurador de la capa [dao]
public function setDao(InterfaceDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceDao $dao) {
// se almacena una referencia en la capa [dao]
$this->dao = $dao;
// se recuperan los datos que permiten calcular el impuesto
// el método [getTaxAdminData] puede lanzar una excepción ExceptionImpots
// a continuación, se permite que se transmita al código llamante
$this->taxAdminData = $this->dao->getTaxAdminData();
}
// cálculo del impuesto
// --------------------------------------------------------------------------
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// $marié: sí, no
// $enfants: número de hijos
// $salaire: salario anual
// $this->taxAdminData: datos de la administración tributaria
//
// se comprueba que se dispone de los datos de la administración tributaria
if ($this->taxAdminData === NULL) {
$this->taxAdminData = $this->getTaxAdminData();
}
// cálculo del impuesto con hijos
$result1 = $this->calculerImpot2($marié, $enfants, $salaire);
$impot1 = $result1["impôt"];
// cálculo del impuesto sin hijos
if ($enfants != 0) {
$result2 = $this->calculerImpot2($marié, 0, $salaire);
$impot2 = $result2["impôt"];
// aplicación del límite máximo del coeficiente familiar
$plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
if ($enfants < 3) {
// $PLAFOND_QF_DEMI_PART euros para los dos primeros hijos
$impot2 = $impot2 - $enfants * $plafonDemiPart;
} else {
// $PLAFOND_QF_DEMI_PART euros para los dos primeros hijos, el doble para los siguientes
$impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
}
} else {
$impot2 = $impot1;
$result2 = $result1;
}
// se aplica el tipo impositivo más alto
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"];
}
// cálculo de una posible deducción
$décôte = $this->getDecôte($marié, $salaire, $impot);
$impot -= $décôte;
// cálculo de una posible reducción de impuestos
$réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
$impot -= $réduction;
// resultado
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é: sí, no
// $enfants: número de hijos
// $salaire: salario anual
// $this->taxAdminData: datos de la administración tributaria
//
// número de participaciones
$marié = strtolower($marié);
if ($marié === "oui") {
$nbParts = $enfants / 2 + 2;
} else {
$nbParts = $enfants / 2 + 1;
}
// 1 participación por hijo a partir del tercero
if ($enfants >= 3) {
// media participación adicional por cada hijo a partir del tercero
$nbParts += 0.5 * ($enfants - 2);
}
// renta imponible
$revenuImposable = $this->getRevenuImposable($salaire);
// recargo
$surcôte = floor($revenuImposable - 0.9 * $salaire);
// por problemas de redondeo
if ($surcôte < 0) {
$surcôte = 0;
}
// coeficiente familiar
$quotient = $revenuImposable / $nbParts;
// cálculo del impuesto
$limites = $this->taxAdminData->getLimites();
$coeffR = $this->taxAdminData->getCoeffR();
$coeffN = $this->taxAdminData->getCoeffN();
// se coloca al final de la tabla de límites para detener el bucle siguiente
$limites[count($limites) - 1] = $quotient;
// búsqueda del tipo impositivo
$i = 0;
while ($quotient > $limites[$i]) {
$i++;
}
// debido a que se ha colocado $quotient al final de la tabla $limites, el bucle anterior
// no puede salirse de los límites de la tabla $limites
// ahora podemos calcular el impuesto
$impôt = floor($revenuImposable * $coeffR[$i] - $nbParts * $coeffN[$i]);
// resultado
return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
}
// revenuImposable = salarioAnual - deducción
// la deducción tiene un mínimo y un máximo
private function getRevenuImposable(float $salaire): float {
// descuento del 10 % del salario
$abattement = 0.1 * $salaire;
// esta deducción no puede superar $this->taxAdminData->getAbattementDixPourCentMax()
if ($abattement > $this->taxAdminData->getAbattementDixPourCentMax()) {
$abattement = $this->taxAdminData->getAbattementDixPourcentMax();
}
// la deducción no puede ser inferior a $this->taxAdminData->getAbattementDixPourcentMin()
if ($abattement < $this->taxAdminData->getAbattementDixPourcentMin()) {
$abattement = $this->taxAdminData->getAbattementDixPourcentMin();
}
// renta imponible
$revenuImposable = $salaire - $abattement;
// resultado
return floor($revenuImposable);
}
// calcula un posible descuento
private function getDecôte(string $marié, float $salaire, float $impots): float {
// inicialmente, un descuento nulo
$décôte = 0;
// importe máximo del impuesto para poder aplicar la reducción
$plafondImpôtPourDécôte = $marié === "oui" ?
$this->taxAdminData->getPlafondImpotCouplePourDecote() :
$this->taxAdminData->getPlafondImpotCelibatairePourDecote();
if ($impots < $plafondImpôtPourDécôte) {
// importe máximo de la reducción
$plafondDécôte = $marié === "oui" ?
$this->taxAdminData->getPlafondDecoteCouple() :
$this->taxAdminData->getPlafondDecoteCelibataire();
// descuento teórico
$décôte = $plafondDécôte - 0.75 * $impots;
// la deducción no puede superar el importe del impuesto
if ($décôte > $impots) {
$décôte = $impots;
}
// no hay descuento <0
if ($décôte < 0) {
$décôte = 0;
}
}
// resultado
return ceil($décôte);
}
// calcula una posible reducción
private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
// el límite máximo de ingresos para tener derecho a la reducción del 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();
}
// ingresos imponibles
$revenuImposable = $this->getRevenuImposable($salaire);
// descuento
$réduction = 0;
if ($revenuImposable < $plafondRevenuPourRéduction) {
// reducción del 20 %
$réduction = 0.2 * $impots;
}
// resultado
return ceil($réduction);
}
// cálculo de impuestos en modo por lotes
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// se permiten las excepciones procedentes de la capa [dao]
// se recuperan los datos de los contribuyentes
$taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// tabla de resultados
$results = [];
// se procesan
foreach ($taxPayersData as $taxPayerData) {
// se calcula el impuesto
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// se completa [$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"]);
// se introduce el resultado en la tabla de resultados
$results [] = $taxPayerData;
}
// almacenamiento de los resultados
$this->dao->saveResults($resultsFileName, $results);
}
}
- líneas 19-26: el constructor de la clase PHP. Como hemos dicho que construimos una capa [métier] independiente de la capa [dao], realizaremos en JavaScript dos modificaciones en este constructor:
- no recibirá una instancia de la capa [dao] (ya no la necesita);
- no solicitará los datos fiscales de la administración [taxAdminData] a la capa [dao]: será el código llamante el que transmita estos datos al constructor;
- líneas 197-122: no implementaremos el método [executeBatchImpots], cuyo objetivo final era guardar los resultados de las simulaciones en un archivo de texto. Queremos un código que funcione tanto en [node.js] como en un navegador. Sin embargo, no es posible guardar datos en el sistema de archivos del equipo en el que se ejecuta el navegador cliente;
Teniendo en cuenta estas restricciones, el código de la clase JavaScript [Métier] es el siguiente:
'use strict';
// clase «Métier»
class Métier {
// constructor
constructor(taxAdmindata) {
// this.taxAdminData: datos de la administración tributaria
this.taxAdminData = taxAdmindata;
}
// cálculo del impuesto
// --------------------------------------------------------------------------
calculerImpot(marié, enfants, salaire) {
// casado: sí, no
// hijos: número de hijos
// salario: salario anual
// this.taxAdminData: datos de la administración tributaria
//
// cálculo del impuesto con hijos
const result1 = this.calculerImpot2(marié, enfants, salaire);
const impot1 = result1["impôt"];
// cálculo del impuesto sin hijos
let result2, impot2, plafondDemiPart;
if (enfants !== 0) {
result2 = this.calculerImpot2(marié, 0, salaire);
impot2 = result2["impôt"];
// aplicación del límite máximo del coeficiente familiar
plafondDemiPart = this.taxAdminData.plafondQfDemiPart;
if (enfants < 3) {
// PLAFOND_QF_DEMI_PART euros para los dos primeros hijos
impot2 = impot2 - enfants * plafondDemiPart;
} else {
// PLAFOND_QF_DEMI_PART euros para los dos primeros hijos, el doble para los siguientes
impot2 = impot2 - 2 * plafondDemiPart - (enfants - 2) * 2 * plafondDemiPart;
}
} else {
// no se vuelve a calcular el impuesto
impot2 = impot1;
result2 = result1;
}
// se toma el impuesto más alto de [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"];
}
// cálculo de una posible deducción
const décôte = this.getDecôte(marié, impot);
impot -= décôte;
// cálculo de una posible reducción de impuestos
const réduction = this.getRéduction(marié, salaire, enfants, impot);
impot -= réduction;
// resultado
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) {
// casado: sí, no
// hijos: número de hijos
// salario: salario anual
// this->taxAdminData: datos de la administración tributaria
//
// número de participaciones
marié = marié.toLowerCase();
let nbParts;
if (marié === "oui") {
nbParts = enfants / 2 + 2;
} else {
nbParts = enfants / 2 + 1;
}
// 1 parte por hijo a partir del tercero
if (enfants >= 3) {
// media participación adicional por cada hijo a partir del tercero
nbParts += 0.5 * (enfants - 2);
}
// renta imponible
const revenuImposable = this.getRevenuImposable(salaire);
// recargo
let surcôte = Math.floor(revenuImposable - 0.9 * salaire);
// por problemas de redondeo
if (surcôte < 0) {
surcôte = 0;
}
// coeficiente familiar
const quotient = revenuImposable / nbParts;
// cálculo del impuesto
const limites = this.taxAdminData.limites;
const coeffR = this.taxAdminData.coeffR;
const coeffN = this.taxAdminData.coeffN;
// se coloca al final de la tabla de límites para detener el bucle siguiente
limites[limites.length - 1] = quotient;
// búsqueda del tipo impositivo
let i = 0;
while (quotient > limites[i]) {
i++;
}
// dado que se ha colocado el cociente al final de la tabla de límites, el bucle anterior
// no puede sobrepasar los límites de la tabla
// ahora se puede calcular el impuesto
const impôt = Math.floor(revenuImposable * coeffR[i] - nbParts * coeffN[i]);
// resultado
return { "impôt": impôt, "surcôte": surcôte, "taux": coeffR[i] };
}
// revenuImposable = salarioAnual - deducción
// la deducción tiene un mínimo y un máximo
getRevenuImposable(salaire) {
// descuento del 10 % del salario
let abattement = 0.1 * salaire;
// esta deducción no puede superar taxAdminData.getAbattementDixPourCentMax()
if (abattement > this.taxAdminData.abattementDixPourCentMax) {
abattement = this.taxAdminData.abattementDixPourcentMax;
}
// la deducción no puede ser inferior a taxAdminData.getAbattementDixPourcentMin()
if (abattement < this.taxAdminData.abattementDixPourcentMin) {
abattement = this.taxAdminData.abattementDixPourcentMin;
}
// renta imponible
const revenuImposable = salaire - abattement;
// resultado
return Math.floor(revenuImposable);
}
// calcula una posible reducción
getDecôte(marié, impots) {
// inicialmente, un descuento nulo
let décôte = 0;
// importe máximo del impuesto para poder aplicar la reducción
let plafondImpôtPourDécôte = marié === "oui" ?
this.taxAdminData.plafondImpotCouplePourDecote :
this.taxAdminData.plafondImpotCelibatairePourDecote;
let plafondDécôte;
if (impots < plafondImpôtPourDécôte) {
// importe máximo de la reducción
plafondDécôte = marié === "oui" ?
this.taxAdminData.plafondDecoteCouple :
this.taxAdminData.plafondDecoteCelibataire;
// descuento teórico
décôte = plafondDécôte - 0.75 * impots;
// la deducción no puede superar el importe del impuesto
if (décôte > impots) {
décôte = impots;
}
// no hay descuento <0
if (décôte < 0) {
décôte = 0;
}
}
// resultado
return Math.ceil(décôte);
}
// calcula una posible reducción
getRéduction(marié, salaire, enfants, impots) {
// límite máximo de ingresos para tener derecho a la reducción del 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;
}
// ingresos imponibles
const revenuImposable = this.getRevenuImposable(salaire);
// descuento
let réduction = 0;
if (revenuImposable < plafondRevenuPourRéduction) {
// reducción del 20 %
réduction = 0.2 * impots;
}
// resultado
return Math.ceil(réduction);
}
}
// exportación de la clase
export default Métier;
- El código JavaScript sigue al pie de la letra el código PHP;
- Se exporta la clase [Métier], línea 187;
14.3.2. La clase de JavaScript [Dao2]

La clase [Dao2] implementa la capa [dao] del cliente de JavaScript anterior de la siguiente manera:
'use strict';
// importaciones
import qs from 'qs'
class Dao2 {
// constructor
constructor(axios) {
this.axios = axios;
// cookie de sesión
this.sessionCookieName = "PHPSESSID";
this.sessionCookie = '';
}
// inicialización de sesión
async initSession() {
// opciones de la solicitud HHTP [get /main.php?action=init-session&type=json]
const options = {
method: "GET",
// parámetros de la consulta URL
params: {
action: 'init-session',
type: 'json'
}
};
// ejecución de la consulta HTTP
return await this.getRemoteData(options);
}
async authentifierUtilisateur(user, password) {
// opciones de la solicitud HHTP [post /main.php?action=authentifier-utilisateur]
const options = {
method: "POST",
headers: {
'«Content-type»: «application/x-www-form-urlencoded»,
},
// cuerpo del POST
data: qs.stringify({
user: user,
password: password
}),
// parámetros de la URL
params: {
action: 'authentifier-utilisateur'
}
};
// ejecución de la consulta HTTP
return await this.getRemoteData(options);
}
async getAdminData() {
// opciones de la consulta HHTP [get /main.php?action=get-admindata]
const options = {
method: "GET",
// parámetros de la consulta URL
params: {
action: 'get-admindata'
}
};
// ejecución de la consulta HTTP
const data = await this.getRemoteData(options);
// resultado
return data;
}
async getRemoteData(options) {
// para la cookie de sesión
if (!options.headers) {
options.headers = {};
}
options.headers.Cookie = this.sessionCookie;
// ejecución de la consulta HTTP
let response;
try {
// solicitud asíncrona
response = await this.axios.request('main.php', options);
} catch (error) {
// el parámetro [error] es una instancia de excepción; puede adoptar diversas formas
if (error.response) {
// la respuesta del servidor se encuentra en [error.response]
response = error.response;
} else {
// Se vuelve a generar el error
throw error;
}
}
//: la respuesta es el conjunto completo de la respuesta HTTP del servidor (encabezados HTTP + la propia respuesta)
// se recupera la cookie de sesión, si existe
const setCookie = response.headers['set-cookie'];
if (setCookie) {
// setCookie es una matriz
//: se busca la cookie de sesión en este array
let trouvé = false;
let i = 0;
while (!trouvé && i < setCookie.length) {
// se busca la cookie de sesión
const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
if (results) {
// se almacena la cookie de sesión
// eslint-disable-next-line require-atomic-updates
this.sessionCookie = results[1];
// la hemos encontrado
trouvé = true;
} else {
// el siguiente elemento
i++;
}
}
}
// la respuesta del servidor está en [response.data]
return response.data;
}
}
// exportación de la clase
export default Dao2;
Comentarios
- La clase [Dao2] solo implementa tres de las posibles solicitudes al servidor de cálculo de impuestos:
- [init-session] (líneas 17-29): para inicializar la sesión jSON;
- [authentifier-utilisateur] (líneas 31-50): para autenticarse;
- [get-admindata] (líneas 52-65): para obtener los datos de la administración tributaria que permitirán realizar los cálculos del impuesto, en el lado del cliente;
- líneas 52-65: introducimos una nueva acción [get-admindata] hacia el servidor. Esta acción no se había implementado hasta ahora. Lo hacemos ahora.
14.3.3. Modificación del servidor de cálculo de impuestos
El servidor de cálculo de impuestos debe implementar una nueva acción. Lo haremos en la versión 14 del servidor. La acción que hay que implementar tiene las siguientes características:
- la solicita una operación [get /main.php?action=get-admindata];
- devuelve la cadena jSON de un objeto que encapsula los datos de la administración tributaria;
Vamos a repasar cómo añadir una acción a nuestro servidor.
La modificación se realizará en NetBeans:

En [2], modificamos el archivo [config.json] para añadir la nueva acción:
{
"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"
}
La modificación consiste en:
- línea 67: añadir la acción [get-admindata] y asociarla a un controlador;
- línea 36: declarar este controlador en la lista de clases que debe cargar la aplicación PHP;
El siguiente paso es implementar el controlador [AdminDataController] [3]:
<?php
namespace Application;
// dependencias de Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// alias de la capa [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
class AdminDataController implements InterfaceController {
// $config es la configuración de la aplicación
// procesamiento de una solicitud Request
// utiliza la sesión «Session» y puede modificarla
// $infos son datos adicionales específicos de cada controlador
// devuelve un array [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
//: debe haber un único parámetro GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 1;
if ($erreur) {
// se observa el error
$message = "il faut utiliser la méthode [get] avec l'unique paramètre [action] dans l'URL";
$état = 1001;
// se devuelve el resultado al controlador principal
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// se puede trabajar
// Redis
\Predis\Autoloader::register();
try {
// cliente [predis]
$redis = new \Predis\Client();
// nos conectamos al servidor para comprobar si está disponible
$redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
// ha salido mal
// se ha devuelto el resultado con un error al controlador principal
$état = 1050;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
}
// Recuperación de datos de la administración tributaria
// Primero se busca en la caché [redis]
if (!$redis->get("taxAdminData")) {
try {
// se van a buscar los datos fiscales en la base de datos
$dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
// taxAdminData
$taxAdminData = $dao->getTaxAdminData();
// se guardan en Redis los datos recuperados
$redis->set("taxAdminData", $taxAdminData);
} catch (\RuntimeException $ex) {
// Ha salido mal
// Se devuelve el resultado con un error al controlador principal
$état = 1041;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => utf8_encode($ex->getMessage())], []];
}
} else {
// los datos fiscales se almacenan en la memoria [redis] del ámbito [application]
$arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
// Se instancia un objeto [TaxAdminData] a partir de la tabla de atributos anterior
$taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
}
// se devuelve el resultado al controlador principal
$état = 1000;
return [Response::HTTP_OK, $état, ["réponse" => $taxAdminData], []];
}
}
Comentarios
- línea 12: al igual que los demás controladores del servidor, [AdminDataController] implementa la interfaz [InterfaceController], constituida por el método [execute] de las líneas 19-79;
- línea 78: al igual que los demás controladores del servidor, el método [AdminDataController.execute] devuelve una matriz [$status, $état, [‘réponse’=>$response]] con:
- [$status]: el código de estado de la respuesta HTTP;
- [$état]: un código interno de la aplicación que representa el estado en el que se encuentra el servidor tras la ejecución de la solicitud del cliente;
- [$response]: un array que encapsula la respuesta que se enviará al cliente. En este caso, dicho array se transformará posteriormente en la cadena jSON;
- líneas 25-34: se comprueba que la acción [get-admindata] del cliente sea sintácticamente correcta;
- líneas 37-74: se recupera un objeto [TaxAdminData] que se encuentra:
- líneas 56-59: en la base de datos si no se ha encontrado en la caché [redis];
- líneas 70-73: en la caché [redis];
Este código retoma el del controlador [CalculerImpotController] explicado en el artículo enlazado. De hecho, este controlador también debía recuperar el objeto [TaxAdminData] que encapsula los datos de la administración tributaria.
Durante las pruebas del cliente JavaScript, la forma jSON de [TaxAdminData] planteó un problema cuando este objeto se encontraba en la caché [redis]. Para entenderlo, veamos en qué formato se almacena este objeto en [redis]:


- En [5-7], se observa que los valores numéricos se han almacenado como cadenas de caracteres. PHP lo ha aceptado porque el operador + en los cálculos entre números y cadenas provoca implícitamente un cambio de tipo de la cadena a un número. Sin embargo, JavaScript hace lo contrario: el operador + en los cálculos entre números y cadenas provoca implícitamente un cambio de tipo del número a una cadena de caracteres. Por lo tanto, los cálculos de la clase JavaScript [Métier] son erróneos;
Para solucionar este problema, modificamos el método [TaxAdminData.setFromArrayOfAttributes] utilizado en la línea 71 del controlador para instanciar un objeto [TaxAdminData] (véase el artículo) a partir de la cadena jSON que se encuentra en la caché [redis]:
<?php
namespace Application;
class TaxAdminData extends BaseEntity {
// tramos impositivos
protected $limites;
protected $coeffR;
protected $coeffN;
// constantes para el cálculo del impuesto
protected $plafondQfDemiPart;
protected $plafondRevenusCelibatairePourReduction;
protected $plafondRevenusCouplePourReduction;
protected $valeurReducDemiPart;
protected $plafondDecoteCelibataire;
protected $plafondDecoteCouple;
protected $plafondImpotCouplePourDecote;
protected $plafondImpotCelibatairePourDecote;
protected $abattementDixPourcentMax;
protected $abattementDixPourcentMin;
// inicialización
public function setFromJsonFile(string $taxAdminDataFilename) {
// padre
parent::setFromJsonFile($taxAdminDataFilename);
// se comprueban los valores de los atributos
$this->checkAttributes();
// se devuelve el objeto
return $this;
}
protected function check($value): \stdClass {
// $value es una matriz de elementos de tipo cadena o un único elemento
if (!\is_array($value)) {
$tableau = [$value];
} else {
$tableau = $value;
}
// se transforma el array de cadenas en un array de números reales
$newTableau = [];
$result = new \stdClass();
// los elementos del array deben ser números decimales positivos o nulos
$modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
for ($i = 0; $i < count($tableau); $i ++) {
if (preg_match($modèle, $tableau[$i])) {
// se coloca el número flotante en newTableau
$newTableau[] = (float) $tableau[$i];
} else {
// se anota el error
$result->erreur = TRUE;
// salimos
return $result;
}
}
// se devuelve el resultado
$result->erreur = FALSE;
if (!\is_array($value)) {
// un único valor
$result->value = $newTableau[0];
} else {
// una lista de valores
$result->value = $newTableau;
}
return $result;
}
// inicialización mediante una matriz de atributos
public function setFromArrayOfAttributes(array $arrayOfAttributes) {
// padre
parent::setFromArrayOfAttributes($arrayOfAttributes);
// se comprueban los valores de los atributos
$this->checkAttributes();
// se devuelve el objeto
return $this;
}
// comprobación de los valores de los atributos
protected function checkAttributes() {
// se comprueba que los valores de los atributos sean números reales >=0
foreach ($this as $key => $value) {
if (is_string($value)) {
// $value debe ser un número real >=0 o una matriz de números reales >=0
$result = $this->check($value);
// ¿Error?
if ($result->erreur) {
// Se lanza una excepción
throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
} else {
// se anota el valor
$this->$key = $result->value;
}
}
}
// se devuelve el objeto
return $this;
}
// métodos getter y setter
...
}
Comentarios
- línea 5: la clase [TaxAdminData] hereda de la clase [BaseEntity], que ya cuenta con el método [setFromArrayOfAttributes]. Dado que este no es adecuado, lo redefinimos en las líneas 67-75;
- línea 70: primero se utiliza el método [setFromArrayOfAttributes] de la clase padre para inicializar los atributos de la clase;
- línea 72: el método [checkAttributes] comprueba que los valores asociados sean efectivamente números. Si son cadenas, se convierten en números;
- línea 74: el objeto [$this] resultante es, por tanto, un objeto con atributos de valores numéricos;
- líneas 78-93: el método [checkAttributes] comprueba que los valores asociados a los atributos del objeto sean efectivamente numéricos;
- línea 80: se recorre la lista de atributos;
- línea 81: si el valor de un atributo es de tipo [string];
- línea 83: entonces se comprueba que esa cadena representa un número;
- línea 90: si es así, la cadena se convierte en un número y se asigna al atributo comprobado;
- líneas 85-86: si no es así, se lanza una excepción;
- líneas 32-65: la función [check] hace algo más de lo necesario. Procesa tanto matrices como valores únicos. Sin embargo, aquí solo se invoca para comprobar un valor de tipo [string]. Devuelve un objeto con las propiedades [erreur, value], donde:
- [erreur] es un valor booleano que indica si hay un error o no;
- [value] es el parámetro [value] de la línea 32, transformado en un número o en una matriz de números, según el caso;
La clase [BaseEntity], que podía tener un atributo denominado [arrayOfAttributes], se modifica para que ya no lo tenga: de hecho, este contamina la cadena jSON de [TaxAdminData]. La clase se reescribe de la siguiente manera:
<?php
namespace Application;
class BaseEntity {
// inicialización a partir de un archivo JSON
public function setFromJsonFile(string $jsonFilename) {
// se recupera el contenido del archivo de datos fiscales
$fileContents = \file_get_contents($jsonFilename);
$erreur = FALSE;
// ¿Error?
if (!$fileContents) {
// se registra el error
$erreur = TRUE;
$message = "Le fichier des données [$jsonFilename] n'existe pas";
}
if (!$erreur) {
// se recupera el código JSON del archivo de configuración en una tabla asociativa
$arrayOfAttributes = \json_decode($fileContents, true);
// ¿Error?
if ($arrayOfAttributes === FALSE) {
// se registra el error
$erreur = TRUE;
$message = "Le fichier de données JSON [$jsonFilename] n'a pu être exploité correctement";
}
}
// ¿Error?
if ($erreur) {
// se lanza una excepción
throw new ExceptionImpots($message);
}
// inicialización de los atributos de la clase
foreach ($arrayOfAttributes as $key => $value) {
$this->$key = $value;
}
// se comprueba que estén presentes todos los atributos
$this->checkForAllAttributes($arrayOfAttributes);
// se devuelve el objeto
return $this;
}
public function checkForAllAttributes($arrayOfAttributes) {
// se comprueba que se hayan inicializado todas las claves
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) {
// se inicializan algunos atributos de la clase (no necesariamente todos)
foreach ($arrayOfAttributes as $key => $value) {
$this->$key = $value;
}
// se devuelve el objeto
return $this;
}
// toString
public function __toString() {
// atributos del objeto
$arrayOfAttributes = \get_object_vars($this);
// cadena jSON del objeto
return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
}
}
Comentarios
- línea 20: el atributo [$this→arrayOfAttributes] se ha transformado en una variable que, a partir de ahora, debe pasarse al método [checkForAllAttributes], línea 38, que anteriormente operaba sobre el atributo [$this→arrayOfAttributes];
Debido a este cambio en [BaseEntity], la clase [Database] también debe modificarse ligeramente:
<?php
namespace Application;
class Database extends BaseEntity {
// atributos
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;
// setter
// inicialización
public function setFromJsonFile(string $jsonFilename) {
// padre
parent::setFromJsonFile($jsonFilename);
// devuelve el objeto
return $this;
}
// getters y setters
...
}
Comentarios
- En el código original, después de la línea 30, se llamaba al método [parent::checkForAllAttributes]. Ya no es necesario hacerlo, ya que ahora el método [parent::setFromJsonFile($jsonFilename)] se encarga de ello automáticamente;
14.3.4. Pruebas del servidor con [Postman]
[Postman] se presentó en el artículo enlace.
Utilizamos las siguientes pruebas de Postman:



El resultado jSON de esta última consulta es el siguiente:

- En [5-8], se puede observar que los atributos de la cadena jSON tienen valores numéricos (y no cadenas de caracteres). Este resultado permitirá que la clase de JavaScript [Métier] se ejecute con normalidad;
14.3.5. El script principal [main]

El script principal [main] del cliente JavaScript es el siguiente:
// importaciones
import axios from 'axios';
// importaciones
import Dao from './Dao2';
import Métier from './Métier';
// función asíncrona [main]
async function main() {
// configuración de Axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
// instanciación de la capa [dao]
const dao = new Dao(axios);
// solicitudes HTTP
let taxAdminData;
try {
// inicialización de sesión
log("-----------init-session");
let response = await dao.initSession();
log(response);
// autenticación
log("-----------authentifier-utilisateur");
response = await dao.authentifierUtilisateur("admin", "admin");
log(response);
// datos fiscales
log("-----------get-admindata");
response = await dao.getAdminData();
log(response);
taxAdminData = response.réponse;
} catch (error) {
// se registra el error
console.log("erreur=", error.message);
// fin
return;
}
// instanciación de la capa [métier]
const métier = new Métier(taxAdminData);
// cálculos de impuestos
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));
// lista de simulaciones
log("-----------liste-des-simulations");
log(simulations);
// eliminación de una simulación
log("-----------suppression simulation n° 1");
simulations.splice(1, 1);
log(simulations);
}
// registro jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// ejecución
main();
Comentarios
- líneas 5-6: importaciones de las clases [Dao] y [Métier];
- línea 9: la función asíncrona [main], que se encargará de gestionar la comunicación con el servidor mediante la clase [Dao] y solicitará a la clase [Métier] que realice los cálculos fiscales;
- líneas 10-36: el script llama sucesivamente y de forma bloqueante a los métodos [initSession, authentifierUtilisateur, getAdminData] de la capa [dao];
- línea 38: ya no se necesita la capa [dao]. Se dispone de todos los elementos para que funcione la capa [métier] del cliente JavaScript;
- líneas 41-46: se realizan tres cálculos de impuestos y se acumulan los resultados en una tabla [simulations];
- línea 49: se muestra la tabla de simulaciones;
- línea 52: se elimina una de ellas;
Los resultados de la ejecución del script principal son los siguientes:
[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. Cliente HTTP 3

En esta sección, ejecutamos la aplicación [Client HTTP 2] en un navegador siguiendo la siguiente arquitectura:

La adaptación no es inmediata. Aunque [node.js] es capaz de ejecutar JavaScript ES6, no suele ser el caso de los navegadores. Por lo tanto, hay que utilizar herramientas que traduzcan el código ES6 a código ES5 comprensible para los navegadores recientes. Afortunadamente, estas herramientas son potentes y bastante fáciles de usar.
En este caso, hemos seguido el artículo [How to write ES6 code that’s safe to run in the browser - Web Developer's Journal].
En la carpeta [client HTTP 3/src], hemos colocado los elementos [main.js, Métier.js, Dao2.js] de la aplicación [Client Http 2] que acabamos de desarrollar.
14.4.1. Inicialización del proyecto
Vamos a trabajar en la carpeta [client http 3]. Abrimos un terminal en [VSCode] y nos situamos en esta carpeta:

Inicializamos este proyecto con el comando [npm init] y aceptamos las respuestas predeterminadas a las preguntas que se nos plantean:

- en [4-5], el archivo de configuración del proyecto [package.json] generado a partir de las diferentes respuestas dadas;
14.4.2. Instalación de las dependencias del proyecto
Vamos a instalar las siguientes dependencias:
- [@babel/core]: el núcleo de la herramienta [Babel] [https://babeljs.io], que transforma el código ES 2015+ en código ejecutable en navegadores recientes y más antiguos;
- [@babel/preset-env]: forma parte del conjunto de herramientas Babel. Interviene antes de la transpilación de ES6 → ES5;
- [babel-loader]: esta dependencia permite que la herramienta [webpack] utilice la herramienta [Babel];
- [webpack]: el «director de orquesta». Es [webpack] la que recurre a Babel para realizar la transpilación de los códigos ES6 → ES5 y, a continuación, es ella la que ensambla todos los archivos resultantes en un único archivo;
- [webpack-cli]: necesario para [webpack];
- [@webpack-cli/init]: se utiliza para configurar [webpack];
- [webpack-dev-server]: proporciona un servidor web de desarrollo que funciona por defecto en el puerto 8080. Cuando se modifican los archivos fuente, recarga automáticamente la aplicación web;
Las dependencias del proyecto se instalan de la siguiente manera en un terminal de [VSCode]:
npm --save-dev install @babel/core @babel/preset-env babel-loader webpack webpack-cli webpack-dev-server @webpack-cli/init

Tras la instalación de las dependencias, el archivo [package.json] ha cambiado de la siguiente manera:
{
"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"
}
}
- líneas 12-19: las dependencias del proyecto son [devDependencies]: son necesarias durante la fase de desarrollo, pero ya no en la fase de producción. De hecho, en producción se utiliza el archivo [dist/main.js]. Está codificado en ES5 y ya no necesita las herramientas de transpilación de código de ES6 a código ES5;
Tenemos que añadir dos dependencias al proyecto:
- [core-js]: contiene «polyfills» para ECMAScript 2019. Un «polyfill» permite ejecutar código reciente, como ECMAScript 2019 (septiembre de 2019), en navegadores antiguos;
- [regenerator-runtime]: según la página web de la biblioteca --> [Source transformer enabling ECMAScript 6 generator functions in JavaScript-of-today];
Estas dos dependencias sustituyen, a partir de Babel 7, a la dependencia [@babel/polyfill], que antes desempeñaba esta función y que ahora (septiembre de 2019) está obsoleta. Se instalan de la siguiente manera:

El archivo [package.json] evoluciona entonces de la siguiente manera:
{
"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"
}
}
El uso de las dependencias [core-js, regenerator-runtime] obliga a incluir los siguientes [imports] (líneas 3-4) en el script principal [src/main.js]:
// importaciones
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
// importaciones
import Dao from './Dao2';
import Métier from './Métier';
14.4.3. Configuración de [webpack]
[webpack] es la herramienta que se encargará de:
- la transpilación de ES6 a ES5 de todos los archivos JavaScript del proyecto;
- la combinación de los archivos generados en un único archivo;
Esta herramienta se controla mediante un archivo de configuración [webpack.config.js] que se puede generar gracias a una dependencia denominada [@webpack-cli/init] (septiembre de 2019). Esta se ha instalado junto con las demás en el apartado «Enlace».
Ejecutamos el comando [npx webpack-cli init] en un terminal [VSCode]:

Tras responder a las distintas preguntas (en las que se pueden aceptar la mayoría de las respuestas propuestas por defecto), se genera un archivo [webpack.config.js] en la raíz del proyecto [4]:
El archivo [webpack.config.js] tiene el siguiente aspecto:
/* 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
}
};
No entiendo todos los detalles de este archivo, pero se pueden observar algunos puntos:
- línea 1: el archivo no contiene código ES6. Por lo tanto, [Eslint] reporta errores que se remontan hasta la raíz del proyecto [javascript]. Esto resulta molesto. Para evitar que ESLint analice un archivo, basta con comentar la línea 1;
- línea 31: trabajamos en modo [développement];
- línea 32: el script de entrada es aquí [src/index.js]. Tendremos que cambiar esto;
- línea 36: la carpeta donde se guardarán los productos de [webpack] será la carpeta [dist];
- línea 46: vemos que [webpack] utiliza [babel-loader], una de las dependencias que hemos instalado;
- línea 54: vemos que [webpack] utiliza [@babel-preset/env], una de las dependencias que hemos instalado;
La inicialización de [webpack] ha modificado el archivo [package.json] (solicita autorización):
{
"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"
}
}
- línea 4: se ha modificado;
- líneas 8-9, 18-19: se han añadido;
- línea 8: la tarea [npm], que permite compilar el proyecto;
- línea 9: la tarea [npm], que permite ejecutarlo;
- línea 18: ¿?
- línea 19: permite generar un archivo [dist/index.html] que incorpora automáticamente el script [dist/main.js] generado por [webpack], y es este el que se utiliza cuando se ejecuta el proyecto;
Por último, la configuración de [webpack] ha generado un archivo [src/index.js]:

El contenido de [index.js] es el siguiente (septiembre de 2019):
console.log("Hello World from your main file!");
14.4.4. Compilación y ejecución del proyecto
El archivo [package.json] contiene tres tareas [npm]:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "webpack-dev-server"
},
Estas tareas están incluidas en [VSCode], que las propone para su ejecución:

- en [1-3], se compila el proyecto;
- en [4]: el proyecto se compila en [dist/main.hash.js] y se crea una página [dist/index.html];
La página [index.html] generada es la siguiente:
<!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>
Por lo tanto, esta página se limita a encapsular el archivo [main.hash.js] generado por [webpack].
El proyecto se ejecuta mediante la tarea [start]:

A continuación, la página [dist/index.html] se carga en un servidor, que forma parte de la suite [webpack], que opera en el puerto 8080 del equipo local, y se muestra en el navegador predeterminado del equipo:

- en [2], el puerto de servicio del servidor web de [webpack];
- en [3], el cuerpo de la página [dist/index.html] está vacío;
- en [4], la pestaña [console] de las herramientas de desarrollo del navegador, en este caso Firefox (F12);
- en [5], el resultado de la ejecución del archivo [src/index.js]. Recordemos que el contenido de este era el siguiente:
Ahora, cambiemos ese contenido por la siguiente línea:
Automáticamente (sin necesidad de recompilar), se generan nuevos archivos [main.js, index.html] y el nuevo archivo [index.html] se carga en el navegador:

No es necesario ejecutar la tarea [build] antes de la tarea [start]: esta última se encarga primero de compilar el proyecto. No almacena los resultados de dicha compilación en la carpeta [dist]. Para comprobarlo, basta con eliminar esa carpeta. Entonces se observará que la tarea [start] compila y ejecuta el proyecto sin crear la carpeta [dist]. Parece que almacena los resultados de [index.html, main.hash.js] en una carpeta específica de [webpackdev-server]. Este comportamiento es suficiente para nuestras pruebas.
Cuando se inicia el servidor de desarrollo, cualquier modificación guardada en uno de los archivos del proyecto provoca una recompilación. Por este motivo, desactivamos el modo [Auto Save] de [VSCode]. De hecho, no queremos que se produzca una recompilación cada vez que se escriban caracteres en uno de los archivos del proyecto. Solo queremos que se produzca una recompilación en el momento de guardar los cambios:

- en [2], la opción [Auto Save] no debe estar marcada;
14.4.5. Pruebas del cliente JavaScript del servidor de cálculo de impuestos
Para probar el cliente JavaScript del servidor de cálculo de impuestos, hay que designar [main.js] [1] como punto de entrada del proyecto en el archivo [webpack.config.js] [2-3]:

No olvidemos que el script [main.js] debe incluir dos importaciones adicionales con respecto a su versión en [Client http 2]:

Además, hemos modificado ligeramente el código para gestionar los errores que pueda enviar el servidor:
// importaciones
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
// importaciones
import Dao from './Dao2';
import Métier from './Métier';
// función asíncrona [main]
async function main() {
// configuración de Axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
// instanciación de la capa [dao]
const dao = new Dao(axios);
// solicitudes HTTP
let taxAdminData;
try {
// inicialización de sesión
log("-----------init-session");
let response = await dao.initSession();
log(response);
if (response.état != 700) {
throw new Error(JSON.stringify(response.réponse));
}
// autenticación
log("-----------authentifier-utilisateur");
response = await dao.authentifierUtilisateur("admin", "admin");
log(response);
if (response.état != 200) {
throw new Error(JSON.stringify(response.réponse));
}
// datos fiscales
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) {
// se registra el error
console.log("erreur=", error.message);
// fin
return;
}
// instanciación de la capa [métier]
const métier = new Métier(taxAdminData);
// cálculos de impuestos
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));
// lista de simulaciones
log("-----------liste-des-simulations");
log(simulations);
// eliminación de una simulación
log("-----------suppression simulation n° 1");
simulations.splice(1, 1);
log(simulations);
}
// registro jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// ejecución
main();
Comentarios
- En las líneas [24-26], [31-33] y [38-40], se comprueba el código [response.état] enviado en la respuesta jSON del servidor. Si este código indica un error, se lanza una excepción con el mensaje de error «jSON» de la respuesta del servidor [response.réponse];
Una vez hecho esto, ejecutamos el proyecto [5-6].
A continuación, se genera la página [index.html] y se carga en el navegador:

- En [7], vemos que la acción [init-session] no se ha podido completar debido a un problema [CORS] (Cross-Origin Resource Sharing);
El problema CORS se debe a la relación cliente/servidor:
- nuestro cliente JavaScript se ha descargado en el equipo [http://localhost:8080];
- el servidor de cálculo de impuestos se ejecuta en la máquina [http://localhost:80];
- por lo tanto, el cliente y el servidor no se encuentran en los mismos dominios (mismo equipo, pero no el mismo puerto);
- el navegador que ejecuta el cliente JavaScript cargado desde la máquina [http://localhost:8080] bloquea cualquier solicitud que no tenga como destino [http://localhost:80]. Se trata de una medida de seguridad. Por ello, bloquea la solicitud del cliente al servidor que opera en la máquina [http://localhost:80];
De hecho, el navegador no bloquea totalmente la solicitud. En realidad, espera a que el servidor le «indique» que acepta las solicitudes entre dominios. Si obtiene esta autorización, el navegador transmitirá entonces la solicitud entre dominios.
El servidor da su autorización enviando unos encabezados HTTP específicos:
- línea 1: el cliente JavaScript opera en el dominio [http://localhost:8080]. El servidor debe responder explícitamente que acepta este dominio;
- línea 2: el cliente JavaScript utilizará en sus solicitudes los encabezados HTTP y [Accept, Content-Type]:
- [Accept]: este encabezado se envía en todas las solicitudes;
- [Content-Type]: este encabezado se utiliza en las operaciones POST para indicar el tipo de los parámetros de POST;
El servidor debe aceptar explícitamente estas dos cabeceras HTTP;
- línea 3: el cliente JavaScript utilizará las solicitudes GET y POST. El servidor debe aceptar explícitamente estos dos tipos de solicitudes;
- línea 4: el cliente JavaScript enviará cookies de sesión. El servidor las acepta con el encabezado de la línea 4;
Por lo tanto, tenemos que modificar el servidor. Lo hacemos en [Netbeans]. El problema de CORS es un problema que solo se produce en modo de desarrollo. En producción, el cliente y el servidor funcionarán en el mismo dominio [http://localhost:80] y no habrá ningún problema CORS. Por lo tanto, necesitamos una forma de autorizar o no las solicitudes CORS mediante la configuración del servidor.

Los cambios en el servidor se realizan en tres lugares:
- [1, 4]: en el archivo de configuración [config.json], para introducir un valor booleano que controle si se aceptan o no las solicitudes entre dominios;
- [2]: en la clase [ParentResponse], que envía la respuesta al cliente JavaScript. Es esta clase la que enviará los encabezados CORS que espera el navegador del cliente;
- [3]: en las clases [HtmlResponse, JsonResponse, XmlResponse] que generan las respuestas para las sesiones [html, json, xml], respectivamente. Estas clases deben pasar a su clase padre [2] el valor booleano [corsAllowed] que se encuentra en [4]. Esto se realiza en [5], pasando la matriz de imágenes del archivo jSON a [2];
La clase [ParentResponse] [2] evoluciona de la siguiente manera:
<?php
namespace Application;
// dependencias de Symfony
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
class ParentResponse {
// int $statusCode: el código HTTP del estado de la respuesta
// cadena $content: el cuerpo de la respuesta que se va a enviar
// dependiendo del caso, es una cadena JSON, XML o HTML
// matriz $headers: los encabezados HTTP que hay que añadir a la respuesta
public function sendResponse(
Request $request,
int $statusCode,
string $content,
array $headers,
array $config): void {
// preparación de la respuesta de texto del servidor
$response = new Response();
$response->setCharset("utf-8");
// código de estado
$response->setStatusCode($statusCode);
// encabezados para las solicitudes entre dominios
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);
}
//: caso particular del método [OPTIONS]
// en este caso solo son importantes los encabezados
$method = strtolower($request->getMethod());
if ($method === "options") {
$content = "";
$response->setStatusCode(Response::HTTP_OK);
}
// se envía la respuesta
$response->setContent($content);
$response->send();
}
}
- línea 29: se comprueba si hay que gestionar las solicitudes entre dominios. En caso afirmativo, se generarán los encabezados HTTP y CORS (líneas 33-37), aunque la solicitud actual no sea una solicitud entre dominios. En este último caso, los encabezados CORS serán innecesarios y el cliente no los utilizará;
- línea 30: en una solicitud entre dominios, el navegador del cliente que consulta al servidor envía un encabezado HTTP [Origin: http://localhost:8080] (en el caso concreto de nuestro cliente JavaScript). En la línea 30, se recupera este encabezado HTTP en la solicitud [$request];
- línea 31: solo se aceptarán solicitudes entre dominios procedentes de la máquina [http://localhost]. Recordamos que estas solicitudes solo tienen lugar en el modo de desarrollo del proyecto;
- líneas 32-36: se añaden los encabezados CORS a los encabezados ya presentes en la tabla [$headers];
- líneas 45-49: la forma en que el navegador del cliente solicita los permisos CORS puede variar en función del cliente que se ejecute. En ocasiones, el navegador del cliente solicita estos permisos con un comando HTTP [OPTIONS]. Esto supone una novedad para nuestro servidor, que se diseñó para atender únicamente los comandos [GET, POST]. En el caso de un comando [OPTIONS], el servidor genera actualmente una respuesta de error. En las líneas 46-49, corregimos esto en el último momento: si en la línea 46 observamos que el comando actual es un comando [OPTIONS], entonces generamos para el cliente:
- líneas 47 y 51: una respuesta [$content] vacía;
- línea 48: un código de estado 200 que indica que el comando se ha ejecutado correctamente. Lo único importante para este comando es el envío de los encabezados CORS de las líneas 33-36. Eso es lo que espera el navegador del cliente;
Una vez corregido el servidor de esta forma, el cliente JavaScript funciona mejor, pero aparece un nuevo error:

- en [1], la sesión jSON se inicializa correctamente;
- en [2], la acción [authentifier-utilisateur] falla: el servidor indica que no hay ninguna sesión activa. Esto significa que el cliente JavaScript no le ha devuelto correctamente la cookie de sesión que envió durante la acción [init-session];
Analicemos los intercambios de red que han tenido lugar:

- en [4], la solicitud [init-session]. Se ha ejecutado correctamente con un código 200 como estado de la respuesta;
- en [5], la solicitud [authentifier-utilisateur]. Esta falla con un código 400 (Bad Request) [6] como estado de la respuesta;
Si examinamos los encabezados HTTP y [7] de la solicitud [5], se puede observar que el cliente JavaScript no ha enviado los encabezados HTTP y [Cookie], que le habrían permitido devolver la cookie de sesión enviada inicialmente por el servidor. Por este motivo, el servidor indica que no hay sesión.
Para que el cliente envíe la cookie de sesión, hay que añadir una configuración al objeto [axios]:
// importaciones
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
// importaciones
import Dao from './Dao2';
import Métier from './Métier';
// función asíncrona [main]
async function main() {
// configuración de Axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
axios.defaults.withCredentials = true;
// instanciación de la capa [dao]
const dao = new Dao(axios);
// solicitudes HTTP
let taxAdminData;
...
La línea 15 indica que las cookies se incluyan en los encabezados HTTP de la solicitud [axios]. Cabe señalar que esto no había sido necesario en el entorno [node.js]. Por lo tanto, existen diferencias de código entre ambos entornos.
Una vez corregido este error, el cliente JavaScript funciona con normalidad:


14.5. Mejora del cliente HTTP 3
Cuando la clase [Dao2] anterior se ejecuta en un navegador, no es necesario gestionar la cookie de sesión. De hecho, es el navegador el que aloja la capa [dao] y gestiona la cookie de sesión: devuelve automáticamente cualquier cookie que le envíe el servidor. Por lo tanto, la clase [Dao2] puede reescribirse como la siguiente clase [Dao3]:
"use strict";
// importaciones
import qs from "qs";
class Dao3 {
// constructor
constructor(axios) {
this.axios = axios;
}
// inicialización de sesión
async initSession() {
// opciones de la solicitud HHTP [get /main.php?action=init-session&type=json]
const options = {
method: "GET",
// parámetros de la consulta URL
params: {
action: "init-session",
type: "json"
}
};
// ejecución de la consulta HTTP
return await this.getRemoteData(options);
}
async authentifierUtilisateur(user, password) {
// opciones de la consulta HHTP [post /main.php?action=authentifier-utilisateur]
const options = {
method: "POST",
headers: {
"Content-type": "application/x-www-form-urlencoded"
},
// cuerpo de POST
data: qs.stringify({
user: user,
password: password
}),
// parámetros de la URL
params: {
action: "authentifier-utilisateur"
}
};
// ejecución de la consulta HTTP
return await this.getRemoteData(options);
}
async getAdminData() {
// opciones de la consulta HHTP [get /main.php?action=get-admindata]
const options = {
method: "GET",
// parámetros de la consulta URL
params: {
action: "get-admindata"
}
};
// ejecución de la consulta HTTP
const data = await this.getRemoteData(options);
// resultado
return data;
}
async getRemoteData(options) {
// ejecución de la consulta HTTP
let response;
try {
// consulta asíncrona
response = await this.axios.request("main.php", options);
} catch (error) {
// el parámetro [error] es una instancia de excepción; puede adoptar diversas formas
if (error.response) {
// la respuesta del servidor se encuentra en [error.response]
response = error.response;
} else {
// Se vuelve a generar el error
throw error;
}
}
// la respuesta es el conjunto completo de la respuesta HTTP del servidor (encabezados HTTP + la propia respuesta)
// la respuesta del servidor se encuentra en [response.data]
return response.data;
}
}
// exportación de la clase
export default Dao3;
Todo lo relacionado con la gestión de la cookie de gestión ha desaparecido.
Modificamos el proyecto anterior de la siguiente manera:

En la carpeta [src], hemos añadido dos archivos:
- la clase [Dao3] que acabamos de presentar;
- el archivo [main3], encargado de iniciar la nueva versión;
El archivo [main3] sigue siendo idéntico al archivo [main] de la versión anterior, pero ahora utiliza la clase [Dao3]:
// importaciones
import axios from "axios";
import "core-js/stable";
import "regenerator-runtime/runtime";
// importaciones
import Dao from "./Dao3";
import Métier from "./Métier";
// función asíncrona [main]
async function main() {
// configuración de Axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL =
"http://localhost/php7/scripts-web/impots/version-14";
axios.defaults.withCredentials = true;
// instanciación de la capa [dao]
const dao = new Dao(axios);
// solicitudes HTTP
...
}
// registro jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// ejecución
main();
El archivo [webpack.config] se ha modificado para que ahora ejecute el script [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",
//entrada: «./src/mainjs»,
entry: "./src/main3.js",
output: {
filename: "[name].[chunkhash].js",
path: path.resolve(__dirname, "dist")
},
plugins: [new webpack.ProgressPlugin(), new HtmlWebpackPlugin()],
...
};
Una vez hecho esto, se ejecuta el proyecto tras iniciar el servidor de cálculo de impuestos:

Los resultados obtenidos en la consola del navegador son idénticos a los de la versión anterior.
14.6. Conclusion
Ahora ya disponemos de todas las herramientas necesarias para desarrollar el código JavaScript de una aplicación web. Podemos:
- utilizar el código ECMAScript más reciente;
- probar elementos aislados de este código en un entorno [node.js] más sencillo para la depuración y las pruebas;
- llevar posteriormente este código a un navegador gracias a las herramientas [babel] y [webpack];