14. Clientes HTTP JavaScript do serviço de cálculo de impostos
14.1. Introduction
Propomos aqui escrever um cliente [node.js] da versão 14 do serviço de cálculo de impostos. A arquitetura cliente/servidor será a seguinte:

Iremos analisar duas versões do cliente:
- a versão 1 do cliente terá a seguinte estrutura em camadas [main, dao]:

- a versão 2 do cliente terá a estrutura [main, métier, dao]. A camada [métier] do servidor será transferida para o cliente:

14.2. Cliente HTTP 1

Como já referimos, o cliente HTTP 1 implementa a seguinte arquitetura cliente/servidor:

Iremos implementar:
- a camada [dao] sob a forma de uma classe;
- a camada [main] sob a forma de um script que utiliza essa classe;
14.2.1. A camada [dao]
A camada [dao] será implementada pela seguinte classe [Dao1.js]:
'use strict';
// importações
import qs from 'qs'
class Dao1 {
// construtor
constructor(axios) {
// biblioteca axios para efetuar as solicitações HTTP
this.axios = axios;
// cookie de sessão
this.sessionCookieName = "PHPSESSID";
this.sessionCookie = '';
}
// iniciar sessão
async initSession() {
// opções da consulta HHTP [get /main.php?action=init-session&type=json]
const options = {
method: "GET",
// parâmetros da URL
params: {
action: 'init-session',
type: 'json'
}
};
// execução da consulta HTTP
return await this.getRemoteData(options);
}
async authentifierUtilisateur(user, password) {
// opções da consulta HHTP [post /main.php?action=authentifier-utilisateur]
const options = {
method: "POST",
headers: {
'Content-type': 'application/x-www-form-urlencoded',
},
// corpo do POST
data: qs.stringify({
user: user,
password: password
}),
// parâmetros do URL
params: {
action: 'authentifier-utilisateur'
}
};
// execução da consulta HTTP
return await this.getRemoteData(options);
}
// cálculo do imposto
async calculerImpot(marié, enfants, salaire) {
// opções da consulta HHTP [post /main.php?action=calculer-impot]
const options = {
method: "POST",
headers: {
'Content-type': 'application/x-www-form-urlencoded',
},
// corpo do POST [marié, enfants, salaire]
data: qs.stringify({
marié: marié,
enfants: enfants,
salaire: salaire
}),
// parâmetros do URL
params: {
action: 'calculer-impot'
}
};
// execução da consulta HTTP
const data = await this.getRemoteData(options);
// resultado
return data;
}
// lista de simulações
async listeSimulations() {
// opções da consulta HHTP [get /main.php?action=lister-simulations]
const options = {
method: "GET",
// parâmetros do URL
params: {
action: 'lister-simulations'
},
};
// execução da consulta HTTP
const data = await this.getRemoteData(options);
// resultado
return data;
}
// lista de simulações
async supprimerSimulation(index) {
// opções da consulta HHTP [get /main.php?action=supprimer-simulation&numéro=index]
const options = {
method: "GET",
// parâmetros da consulta URL
params: {
action: 'supprimer-simulation',
numéro: index
},
};
// execução da consulta HTTP
const data = await this.getRemoteData(options);
// resultado
return data;
}
async getRemoteData(options) {
// para o cookie de sessão
if (!options.headers) {
options.headers = {};
}
options.headers.Cookie = this.sessionCookie;
// execução da consulta HTTP
let response;
try {
// solicitação assíncrona
response = await this.axios.request('main.php', options);
} catch (error) {
// o parâmetro [error] é uma instância de exceção — pode assumir várias formas
if (error.response) {
// a resposta do servidor está em [error.response]
response = error.response;
} else {
// o erro é reenviado
throw error;
}
}
// a resposta é o conjunto completo da resposta HTTP do servidor (cabeçalhos HTTP + a própria resposta)
// recupera-se o cookie de sessão, caso exista
const setCookie = response.headers['set-cookie'];
if (setCookie) {
// setCookie é um array
// procura-se o cookie de sessão neste array
let trouvé = false;
let i = 0;
while (!trouvé && i < setCookie.length) {
// procura-se o cookie de sessão
const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
if (results) {
// guarda-se o cookie de sessão
// eslint-disable-next-line require-atomic-updates
this.sessionCookie = results[1];
// encontrado
trouvé = true;
} else {
// elemento seguinte
i++;
}
}
}
// a resposta do servidor está em [response.data]
return response.data;
}
}
// exportação da classe
export default Dao1;
- Aqui, utilizamos o que aprendemos no parágrafo «ligação», onde apresentámos a biblioteca [axios], que permite efetuar requisições HTTP tanto no [node.js] como num navegador. Analisaremos, em particular, o script do parágrafo «ligação»;
- linhas 9-15: o construtor da classe. Esta terá três propriedades:
- [axios]: o objeto [axios] que permite efetuar as consultas HTTP. Este é transmitido pelo código chamador;
- [sessionCookieName]: dependendo dos servidores, o cookie de sessão tem nomes diferentes. Neste caso, é [PHPSESSID];
- [sessionCookie]: o cookie de sessão enviado pelo servidor e armazenado pelo cliente;
- linhas 53-76: a função assíncrona [calculerImpot] efetua a solicitação [post /main.php?action=calculer-impot], enviando os parâmetros [marié, enfants, salaire]. Esta função devolve a cadeia jSON transmitida pelo servidor sob a forma de um objeto JavaScript;
- linhas 79-92: a função assíncrona [listeSimulations] efetua a solicitação [get /main.php?action=lister-simulations. Ela converte a cadeia jSON transmitida pelo servidor na forma de um objeto JavaScript;
- linhas 95-109: a função assíncrona [supprimerSimulation] efetua a solicitação [get /main.php?action=supprimer-simulation&numéro=index]. Ela retorna a cadeia jSON transmitida pelo servidor na forma de um objeto JavaScript;
- linha 121: utiliza-se a notação [this.axios] porque, neste caso, o objeto [axios] transmitido ao construtor foi armazenado na propriedade [this.axios];
- linha 161: a classe [Dao1] é exportada para poder ser utilizada;
14.2.2. O script [main1.js]
O script [main1.js] efetua uma série de chamadas ao servidor utilizando a classe [Dao1]:
- inicialização de uma sessão jSON;
- autenticação com [admin, admin];
- solicita três cálculos de impostos;
- solicita a lista de simulações;
- elimina uma delas;
O código é o seguinte:
// importação do axios
import axios from 'axios';
// importação da classe Dao1
import Dao from './Dao1';
// função assíncrona [main]
async function main() {
// configuração do axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/versão-14';
// instanciação da camada [dao]
const dao = new Dao(axios);
// utilização da camada [dao]
try {
// inicialização da sessão
log("-----------init-session");
let response = await dao.initSession();
log(response);
// autenticação
log("-----------authentifier-utilisateur");
response = await dao.authentifierUtilisateur("admin", "admin");
log(response);
// cálculos de impostos
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 simulações
log("-----------liste-des-simulations");
response = await dao.listeSimulations();
log(response);
// eliminação de uma simulação
log("-----------suppression simulation n° 1");
response = await dao.supprimerSimulation(1);
log(response);
} catch (error) {
// registo do erro
console.log("erreur=", error.message);
}
}
// registo jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// execução
main();
Comentários
- linha 2: importa-se a biblioteca [axios];
- linha 4: importa-se a classe [Dao];
- linha 7: a função [main], que comunica com o servidor, é assíncrona;
- linhas 9-10: configuração por predefinição das requisições HTTP que serão enviadas ao servidor:
- linha 9: [timeout] com duração de 2 segundos;
- linha 10: todas as URL têm como prefixo a URL, base da versão 14 do servidor de cálculo de impostos;
- linha 12: a camada [Dao] é criada. Já é possível utilizá-la;
- linhas 46-48: a função [log] tem como objetivo apresentar a cadeia jSON de um objeto JavaScript de forma mais elegante: na forma vertical com um recuo de dois espaços (3.º parâmetro);
- linhas 15-18: inicialização da sessão jSON;
- linhas 19-22: autenticação;
- linhas 23-30: são solicitados três cálculos de impostos em paralelo. Graças ao [await Promise.all], a execução fica bloqueada enquanto não forem obtidos todos os três resultados;
- linhas 31-34: lista de simulações;
- linhas 35-38: eliminação de uma simulação;
- linhas 39-42: gestão de uma eventual exceção;
Os resultados da execução são os seguintes:
[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

A arquitetura do cliente HTTP2 é a seguinte:

A camada [métier] foi transferida do servidor para o cliente JavaScript. Ao contrário do que fizemos no curso PHP7, a camada [main] não terá aqui de passar pela camada [métier] para chegar à camada [dao]. Utilizaremos estas duas camadas como centros de competências:
- a camada [main] passa pela camada [dao] assim que necessita de dados que se encontram no servidor;
- a camada [main] solicita à camada [métier] que efetue os cálculos do imposto;
- a camada [métier] é independente da camada [dao] e nunca recorre a ela;
14.3.1. A classe JavaScript [Métier]
A essência da classe [Métier] em PHP foi descrita no artigo cujo link se encontra aqui. Trata-se de um código bastante complexo que aqui recordamos, não para o explicar, mas para o poder traduzir para JavaScript:
<?php
// espaço de nomes
namespace Application;
class Metier implements InterfaceMetier {
// camada DAO
private $dao;
// dados da administração fiscal
private $taxAdminData;
//---------------------------------------------
// setter da camada [dao]
public function setDao(InterfaceDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceDao $dao) {
// armazena-se uma referência na camada [dao]
$this->dao = $dao;
// recuperam-se os dados que permitem o cálculo do imposto
// o método [getTaxAdminData] pode lançar uma exceção ExceptionImpots
// deixa-se então que a exceção seja propagada para o código chamador
$this->taxAdminData = $this->dao->getTaxAdminData();
}
// cálculo do imposto
// --------------------------------------------------------------------------
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// $marié: sim, não
// $enfants: número de filhos
// $salaire: salário anual
// $this->taxAdminData: dados da administração fiscal
//
// verifica-se se os dados da administração fiscal estão corretos
if ($this->taxAdminData === NULL) {
$this->taxAdminData = $this->getTaxAdminData();
}
// cálculo do imposto com filhos
$result1 = $this->calculerImpot2($marié, $enfants, $salaire);
$impot1 = $result1["impôt"];
// cálculo do imposto sem filhos
if ($enfants != 0) {
$result2 = $this->calculerImpot2($marié, 0, $salaire);
$impot2 = $result2["impôt"];
// aplicação do limite máximo do quociente familiar
$plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
if ($enfants < 3) {
// $PLAFOND_QF_DEMI_PART euros para os dois primeiros filhos
$impot2 = $impot2 - $enfants * $plafonDemiPart;
} else {
// $PLAFOND_QF_DEMI_PART euros para os dois primeiros filhos, o dobro para os seguintes
$impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
}
} else {
$impot2 = $impot1;
$result2 = $result1;
}
// aplica-se a taxa de imposto mais elevada
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 uma eventual dedução
$décôte = $this->getDecôte($marié, $salaire, $impot);
$impot -= $décôte;
// cálculo de uma eventual redução de impostos
$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é: sim, não
// $enfants: número de filhos
// $salaire: salário anual
// $this->taxAdminData: dados da administração fiscal
//
// número de quotas
$marié = strtolower($marié);
if ($marié === "oui") {
$nbParts = $enfants / 2 + 2;
} else {
$nbParts = $enfants / 2 + 1;
}
// 1 quota por filho a partir do terceiro
if ($enfants >= 3) {
// meia quota adicional por cada filho a partir do terceiro
$nbParts += 0.5 * ($enfants - 2);
}
// rendimento tributável
$revenuImposable = $this->getRevenuImposable($salaire);
// sobretaxa
$surcôte = floor($revenuImposable - 0.9 * $salaire);
// para problemas de arredondamento
if ($surcôte < 0) {
$surcôte = 0;
}
// quociente familiar
$quotient = $revenuImposable / $nbParts;
// cálculo do imposto
$limites = $this->taxAdminData->getLimites();
$coeffR = $this->taxAdminData->getCoeffR();
$coeffN = $this->taxAdminData->getCoeffN();
// é colocado no final da tabela de limites para interromper o ciclo seguinte
$limites[count($limites) - 1] = $quotient;
// procura da taxa de imposto
$i = 0;
while ($quotient > $limites[$i]) {
$i++;
}
// uma vez que se colocou $quotient no final da tabela $limites, o ciclo anterior
// não pode ultrapassar os limites da tabela $limites
// agora podemos calcular o imposto
$impôt = floor($revenuImposable * $coeffR[$i] - $nbParts * $coeffN[$i]);
// resultado
return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
}
// revenuImposable = salárioAnual - dedução
// a dedução tem um valor mínimo e um valor máximo
private function getRevenuImposable(float $salaire): float {
// abatimento de 10% do salário
$abattement = 0.1 * $salaire;
// esta dedução não pode exceder $this->taxAdminData->getAbattementDixPourCentMax()
if ($abattement > $this->taxAdminData->getAbattementDixPourCentMax()) {
$abattement = $this->taxAdminData->getAbattementDixPourcentMax();
}
// a dedução não pode ser inferior a $this->taxAdminData->getAbattementDixPourcentMin()
if ($abattement < $this->taxAdminData->getAbattementDixPourcentMin()) {
$abattement = $this->taxAdminData->getAbattementDixPourcentMin();
}
// rendimento tributável
$revenuImposable = $salaire - $abattement;
// resultado
return floor($revenuImposable);
}
// calcula uma eventual redução
private function getDecôte(string $marié, float $salaire, float $impots): float {
// inicialmente, uma dedução nula
$décôte = 0;
// montante máximo de imposto para beneficiar da redução
$plafondImpôtPourDécôte = $marié === "oui" ?
$this->taxAdminData->getPlafondImpotCouplePourDecote() :
$this->taxAdminData->getPlafondImpotCelibatairePourDecote();
if ($impots < $plafondImpôtPourDécôte) {
// montante máximo do desconto
$plafondDécôte = $marié === "oui" ?
$this->taxAdminData->getPlafondDecoteCouple() :
$this->taxAdminData->getPlafondDecoteCelibataire();
// abatimento teórico
$décôte = $plafondDécôte - 0.75 * $impots;
// a dedução não pode exceder o montante do imposto
if ($décôte > $impots) {
$décôte = $impots;
}
// não há dedução <0
if ($décôte < 0) {
$décôte = 0;
}
}
// resultado
return ceil($décôte);
}
// calcula uma eventual redução
private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
// o limite máximo de rendimentos para ter direito à redução de 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();
}
// rendimento tributável
$revenuImposable = $this->getRevenuImposable($salaire);
// redução
$réduction = 0;
if ($revenuImposable < $plafondRevenuPourRéduction) {
// redução de 20%
$réduction = 0.2 * $impots;
}
// resultado
return ceil($réduction);
}
// cálculo de impostos em modo batch
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// permitem-se as exceções provenientes da camada [dao]
// recuperam-se os dados dos contribuintes
$taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// tabela de resultados
$results = [];
// analisam-se os resultados
foreach ($taxPayersData as $taxPayerData) {
// calcula-se o imposto
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// preenche-se [$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"]);
// insere-se o resultado na tabela de resultados
$results [] = $taxPayerData;
}
// registo dos resultados
$this->dao->saveResults($resultsFileName, $results);
}
}
- linhas 19-26: o construtor da classe PHP. Como referimos que estávamos a construir uma camada [métier] independente da camada [dao], iremos efetuar duas alterações a este construtor em JavaScript:
- não receberá uma instância da camada [dao] (já não precisa dela);
- não solicitará os dados fiscais da administração [taxAdminData] à camada [dao]: será o código chamador que transmitirá esses dados ao construtor;
- linhas 197-122: não iremos implementar o método [executeBatchImpots], cujo objetivo final era registar os resultados das simulações num ficheiro de texto. Queremos um código que funcione tanto no [node.js] como num navegador. No entanto, não é possível guardar dados no sistema de ficheiros do computador que executa o navegador do cliente;
Com estas restrições, o código da classe JavaScript [Métier] é o seguinte:
'use strict';
// classe Métier
class Métier {
// construtor
constructor(taxAdmindata) {
// this.taxAdminData: dados da administração fiscal
this.taxAdminData = taxAdmindata;
}
// cálculo do imposto
// --------------------------------------------------------------------------
calculerImpot(marié, enfants, salaire) {
// casado: sim, não
// filhos: número de filhos
// salário: salário anual
// this.taxAdminData: dados da administração fiscal
//
// cálculo do imposto com filhos
const result1 = this.calculerImpot2(marié, enfants, salaire);
const impot1 = result1["impôt"];
// cálculo do imposto sem filhos
let result2, impot2, plafondDemiPart;
if (enfants !== 0) {
result2 = this.calculerImpot2(marié, 0, salaire);
impot2 = result2["impôt"];
// aplicação do limite máximo do quociente familiar
plafondDemiPart = this.taxAdminData.plafondQfDemiPart;
if (enfants < 3) {
// PLAFOND_QF_DEMI_PART euros para os dois primeiros filhos
impot2 = impot2 - enfants * plafondDemiPart;
} else {
// PLAFOND_QF_DEMI_PART euros para os dois primeiros filhos, o dobro para os seguintes
impot2 = impot2 - 2 * plafondDemiPart - (enfants - 2) * 2 * plafondDemiPart;
}
} else {
// não há recálculo do imposto
impot2 = impot1;
result2 = result1;
}
// considera-se o imposto mais elevado em [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 uma eventual dedução
const décôte = this.getDecôte(marié, impot);
impot -= décôte;
// cálculo de uma eventual redução de impostos
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: sim, não
// filhos: número de filhos
// salário: salário anual
// this->taxAdminData: dados da administração fiscal
//
// número de quotas
marié = marié.toLowerCase();
let nbParts;
if (marié === "oui") {
nbParts = enfants / 2 + 2;
} else {
nbParts = enfants / 2 + 1;
}
// 1 quota por filho a partir do terceiro
if (enfants >= 3) {
// meia quota adicional por cada filho a partir do terceiro
nbParts += 0.5 * (enfants - 2);
}
// rendimento tributável
const revenuImposable = this.getRevenuImposable(salaire);
// majoração
let surcôte = Math.floor(revenuImposable - 0.9 * salaire);
// para problemas de arredondamento
if (surcôte < 0) {
surcôte = 0;
}
// quociente familiar
const quotient = revenuImposable / nbParts;
// cálculo do imposto
const limites = this.taxAdminData.limites;
const coeffR = this.taxAdminData.coeffR;
const coeffN = this.taxAdminData.coeffN;
// é colocado no final da tabela de limites para interromper o ciclo seguinte
limites[limites.length - 1] = quotient;
// procura da taxa de imposto
let i = 0;
while (quotient > limites[i]) {
i++;
}
// uma vez que o quociente familiar foi colocado no final da tabela de limites, o ciclo anterior
// não pode ultrapassar os limites da tabela
// agora é possível calcular o imposto
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 = salárioAnual - dedução
// a dedução tem um valor mínimo e um valor máximo
getRevenuImposable(salaire) {
// abatimento de 10% do salário
let abattement = 0.1 * salaire;
// esta dedução não pode exceder taxAdminData.getAbattementDixPourCentMax()
if (abattement > this.taxAdminData.abattementDixPourCentMax) {
abattement = this.taxAdminData.abattementDixPourcentMax;
}
// a dedução não pode ser inferior a taxAdminData.getAbattementDixPourcentMin()
if (abattement < this.taxAdminData.abattementDixPourcentMin) {
abattement = this.taxAdminData.abattementDixPourcentMin;
}
// rendimento tributável
const revenuImposable = salaire - abattement;
// resultado
return Math.floor(revenuImposable);
}
// calcula uma eventual redução
getDecôte(marié, impots) {
// inicialmente, um abatimento nulo
let décôte = 0;
// montante máximo de imposto para beneficiar da redução
let plafondImpôtPourDécôte = marié === "oui" ?
this.taxAdminData.plafondImpotCouplePourDecote :
this.taxAdminData.plafondImpotCelibatairePourDecote;
let plafondDécôte;
if (impots < plafondImpôtPourDécôte) {
// montante máximo da redução
plafondDécôte = marié === "oui" ?
this.taxAdminData.plafondDecoteCouple :
this.taxAdminData.plafondDecoteCelibataire;
// abatimento teórico
décôte = plafondDécôte - 0.75 * impots;
// a dedução não pode exceder o montante do imposto
if (décôte > impots) {
décôte = impots;
}
// não há dedução <0
if (décôte < 0) {
décôte = 0;
}
}
// resultado
return Math.ceil(décôte);
}
// calcula uma eventual redução
getRéduction(marié, salaire, enfants, impots) {
// o limite máximo de rendimentos para ter direito à redução de 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;
}
// rendimento tributável
const revenuImposable = this.getRevenuImposable(salaire);
// redução
let réduction = 0;
if (revenuImposable < plafondRevenuPourRéduction) {
// redução de 20%
réduction = 0.2 * impots;
}
// resultado
return Math.ceil(réduction);
}
}
// exportação da classe
export default Métier;
- o código JavaScript segue rigorosamente o código PHP;
- A classe [Métier] é exportada, linha 187;
14.3.2. A classe JavaScript [Dao2]

A classe [Dao2] implementa a camada [dao] do cliente JavaScript acima da seguinte forma:
'use strict';
// importações
import qs from 'qs'
class Dao2 {
// construtor
constructor(axios) {
this.axios = axios;
// cookie de sessão
this.sessionCookieName = "PHPSESSID";
this.sessionCookie = '';
}
// inicialização da sessão
async initSession() {
// opções da solicitação HHTP [get /main.php?action=init-session&type=json]
const options = {
method: "GET",
// parâmetros da URL
params: {
action: 'init-session',
type: 'json'
}
};
// execução da consulta HTTP
return await this.getRemoteData(options);
}
async authentifierUtilisateur(user, password) {
// opções da consulta HHTP [post /main.php?action=authentifier-utilisateur]
const options = {
method: "POST",
headers: {
'«Content-type»: «application/x-www-form-urlencoded»,
},
// corpo do POST
data: qs.stringify({
user: user,
password: password
}),
// parâmetros do URL
params: {
action: 'authentifier-utilisateur'
}
};
// execução da consulta HTTP
return await this.getRemoteData(options);
}
async getAdminData() {
// opções da consulta HHTP [get /main.php?action=get-admindata]
const options = {
method: "GET",
// parâmetros da consulta URL
params: {
action: 'get-admindata'
}
};
// execução da consulta HTTP
const data = await this.getRemoteData(options);
// resultado
return data;
}
async getRemoteData(options) {
// para o cookie de sessão
if (!options.headers) {
options.headers = {};
}
options.headers.Cookie = this.sessionCookie;
// execução da consulta HTTP
let response;
try {
// consulta assíncrona
response = await this.axios.request('main.php', options);
} catch (error) {
// o parâmetro [error] é uma instância de exceção — pode assumir várias formas
if (error.response) {
// a resposta do servidor está em [error.response]
response = error.response;
} else {
// o erro é reenviado
throw error;
}
}
// a resposta é o conjunto completo da resposta HTTP do servidor (cabeçalhos HTTP + a própria resposta)
// recupera-se o cookie de sessão, caso exista
const setCookie = response.headers['set-cookie'];
if (setCookie) {
// setCookie é um array
// procura-se o cookie de sessão neste array
let trouvé = false;
let i = 0;
while (!trouvé && i < setCookie.length) {
// procura-se o cookie de sessão
const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
if (results) {
// guarda-se o cookie de sessão
// eslint-disable-next-line require-atomic-updates
this.sessionCookie = results[1];
// encontrado
trouvé = true;
} else {
// elemento seguinte
i++;
}
}
}
// a resposta do servidor está em [response.data]
return response.data;
}
}
// exportação da classe
export default Dao2;
Comentários
- A classe [Dao2] implementa apenas três das possíveis solicitações ao servidor de cálculo de impostos:
- [init-session] (linhas 17-29): para inicializar a sessão jSON;
- [authentifier-utilisateur] (linhas 31-50): para autenticar-se;
- [get-admindata] (linhas 52-65): para obter os dados da administração fiscal que permitirão efetuar os cálculos do imposto, do lado do cliente;
- linhas 52-65: introduzimos uma nova ação [get-admindata] para o servidor. Esta ação ainda não tinha sido implementada. Fazemo-lo agora.
14.3.3. Alteração do servidor de cálculo do imposto
O servidor de cálculo do imposto deve implementar uma nova ação. Vamos fazê-lo na versão 14 do servidor. A ação a implementar tem as seguintes características:
- é solicitada por uma operação [get /main.php?action=get-admindata];
- retorna a cadeia jSON de um objeto que encapsula os dados da administração fiscal;
Vamos rever como adicionar uma ação ao nosso servidor.
A alteração será efetuada no NetBeans:

Em [2], modificamos o ficheiro [config.json] para adicionar a nova ação:
{
"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"
}
A alteração consiste em:
- linha 67: adicionar a ação [get-admindata] e associá-la a um controlador;
- linha 36: declarar este controlador na lista de classes a carregar pela aplicação PHP;
A fase seguinte consiste em implementar o controlador [AdminDataController] [3]:
<?php
namespace Application;
// dependências do Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// alias da camada [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
class AdminDataController implements InterfaceController {
// $config é a configuração da aplicação
// processamento de um pedido Request
// utiliza a sessão Session e pode alterá-la
// $infos são informações adicionais específicas de cada controlador
// retorna um array [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// deve existir um único parâmetro GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 1;
if ($erreur) {
// regista-se o erro
$message = "il faut utiliser la méthode [get] avec l'unique paramètre [action] dans l'URL";
$état = 1001;
// envio do resultado ao controlador principal
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// é possível continuar a trabalhar
// Redis
\Predis\Autoloader::register();
try {
// cliente [predis]
$redis = new \Predis\Client();
// estabelece-se ligação ao servidor para verificar se este está disponível
$redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
// correu mal
// resultado devolvido com erro ao controlador principal
$état = 1050;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
}
// recuperação dos dados da administração fiscal
// procura-se primeiro no cache [redis]
if (!$redis->get("taxAdminData")) {
try {
// os dados fiscais são obtidos da base de dados
$dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
// taxAdminData
$taxAdminData = $dao->getTaxAdminData();
// colocamos os dados recuperados no Redis
$redis->set("taxAdminData", $taxAdminData);
} catch (\RuntimeException $ex) {
// correu mal
// Envio do resultado com erro para o controlador principal
$état = 1041;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => utf8_encode($ex->getMessage())], []];
}
} else {
// os dados fiscais são obtidos da memória [redis], com o âmbito [application]
$arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
// é instanciado um objeto [TaxAdminData] a partir da tabela de atributos anterior
$taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
}
// retorno do resultado ao controlador principal
$état = 1000;
return [Response::HTTP_OK, $état, ["réponse" => $taxAdminData], []];
}
}
Comentários
- linha 12: tal como os outros controladores do servidor, o [AdminDataController] implementa a interface [InterfaceController], constituída pelo método [execute] das linhas 19-79;
- linha 78: tal como acontece com os outros controladores do servidor, o método [AdminDataController.execute] devolve um array [$status, $état, [‘réponse’=>$response]] com:
- [$status]: o código de estado da resposta HTTP;
- [$état]: um código interno da aplicação que representa o estado em que se encontra o servidor após a execução do pedido do cliente;
- [$response]: um array que encapsula a resposta a enviar ao cliente. Aqui, este array será posteriormente transformado na cadeia jSON;
- linhas 25-34: verifica-se se a ação [get-admindata] do cliente está sintaticamente correta;
- linhas 37-74: recupera-se um objeto [TaxAdminData] encontrado:
- linhas 56-59: na base de dados, caso não tenha sido encontrado no cache [redis];
- linhas 70-73: no cache [redis];
Este código retoma o do controlador [CalculerImpotController] explicado no artigo (ligação). Com efeito, este controlador também devia recuperar o objeto [TaxAdminData] que encapsula os dados da administração fiscal.
Durante os testes do cliente JavaScript, a forma jSON de [TaxAdminData] causou problemas quando este objeto foi encontrado no cache [redis]. Para compreender isto, vamos analisar de que forma este objeto é armazenado em [redis]:


- No [5-7], verifica-se que os valores numéricos foram armazenados sob a forma de cadeias de caracteres. O PHP adaptou-se a esta situação, uma vez que o operador + nos cálculos entre números e cadeias de caracteres provoca implicitamente uma conversão do tipo da cadeia de caracteres para um número. Mas o JavaScript faz o contrário: o operador + nos cálculos entre números e cadeias de caracteres provoca implicitamente uma mudança de tipo do número para uma cadeia de caracteres. Os cálculos da classe JavaScript [Métier] estão, portanto, errados;
Para resolver este problema, alteramos o método [TaxAdminData.setFromArrayOfAttributes] utilizado na linha 71 do controlador para instanciar um objeto [TaxAdminData] (ver artigo) a partir da cadeia jSON encontrada no cache [redis]:
<?php
namespace Application;
class TaxAdminData extends BaseEntity {
// faixas de imposto
protected $limites;
protected $coeffR;
protected $coeffN;
// constantes de cálculo do imposto
protected $plafondQfDemiPart;
protected $plafondRevenusCelibatairePourReduction;
protected $plafondRevenusCouplePourReduction;
protected $valeurReducDemiPart;
protected $plafondDecoteCelibataire;
protected $plafondDecoteCouple;
protected $plafondImpotCouplePourDecote;
protected $plafondImpotCelibatairePourDecote;
protected $abattementDixPourcentMax;
protected $abattementDixPourcentMin;
// inicialização
public function setFromJsonFile(string $taxAdminDataFilename) {
// pai
parent::setFromJsonFile($taxAdminDataFilename);
// verifica-se os valores dos atributos
$this->checkAttributes();
// retorna o objeto
return $this;
}
protected function check($value): \stdClass {
// $value é um tabulácio de elementos do tipo string ou um único elemento
if (!\is_array($value)) {
$tableau = [$value];
} else {
$tableau = $value;
}
// transforma-se a matriz de strings numa matriz de números reais
$newTableau = [];
$result = new \stdClass();
// os elementos da matriz devem ser números decimais positivos ou nulos
$modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
for ($i = 0; $i < count($tableau); $i ++) {
if (preg_match($modèle, $tableau[$i])) {
// coloca-se o float em newTableau
$newTableau[] = (float) $tableau[$i];
} else {
// regista-se o erro
$result->erreur = TRUE;
// sai-se
return $result;
}
}
// retornamos o resultado
$result->erreur = FALSE;
if (!\is_array($value)) {
// um único valor
$result->value = $newTableau[0];
} else {
// uma lista de valores
$result->value = $newTableau;
}
return $result;
}
// inicialização através de um tabuleiro de atributos
public function setFromArrayOfAttributes(array $arrayOfAttributes) {
// pai
parent::setFromArrayOfAttributes($arrayOfAttributes);
// verificam-se os valores dos atributos
$this->checkAttributes();
// retorna o objeto
return $this;
}
// verificação dos valores dos atributos
protected function checkAttributes() {
// verifica-se se os valores dos atributos são números reais >=0
foreach ($this as $key => $value) {
if (is_string($value)) {
// $value deve ser um número real >=0 ou uma matriz de números reais >=0
$result = $this->check($value);
// erro?
if ($result->erreur) {
// é lançada uma exceção
throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
} else {
// regista-se o valor
$this->$key = $result->value;
}
}
}
// retorna-se o objeto
return $this;
}
// getters e setters
...
}
Comentários
- linha 5: a classe [TaxAdminData] estende a classe [BaseEntity], que já possui o método [setFromArrayOfAttributes]. Como este não é adequado, redefinimo-lo nas linhas 67-75;
- linha 70: o método [setFromArrayOfAttributes] da classe pai é utilizado em primeiro lugar para inicializar os atributos da classe;
- linha 72: o método [checkAttributes] verifica se os valores associados são efetivamente números. Se forem cadeias de caracteres, estas são convertidas em números;
- linha 74: o objeto [$this] resultante é, então, um objeto com atributos cujos valores são numéricos;
- linhas 78-93: o método [checkAttributes] verifica se os valores associados aos atributos do objeto são efetivamente numéricos;
- linha 80: percorre-se a lista de atributos;
- linha 81: se o valor de um atributo for do tipo [string];
- linha 83: verifica-se então se essa cadeia de caracteres representa um número;
- linha 90: se for esse o caso, a cadeia é convertida num número e atribuída ao atributo em análise;
- linhas 85-86: se não for esse o caso, é lançada uma exceção;
- linhas 32-65: a função [check] faz um pouco mais do que o necessário. Ela processa tanto tabelas como valores únicos. No entanto, neste caso, é chamada apenas para verificar um valor do tipo [string]. Devolve um objeto com as propriedades [erreur, value], em que:
- [erreur] é um valor booleano que indica se há ou não um erro;
- [value] é o parâmetro [value] da linha 32, transformado num número ou num tabuleiro de números, conforme o caso;
A classe [BaseEntity], que podia ter um atributo denominado [arrayOfAttributes], é alterada para deixar de o ter: na verdade, este atributo contamina a cadeia jSON de [TaxAdminData]. A classe é reescrita da seguinte forma:
<?php
namespace Application;
class BaseEntity {
// inicialização a partir de um ficheiro JSON
public function setFromJsonFile(string $jsonFilename) {
// recupera-se o conteúdo do ficheiro de dados fiscais
$fileContents = \file_get_contents($jsonFilename);
$erreur = FALSE;
// erro?
if (!$fileContents) {
// regista-se o erro
$erreur = TRUE;
$message = "Le fichier des données [$jsonFilename] n'existe pas";
}
if (!$erreur) {
// recupera-se o código JSON do ficheiro de configuração numa tabela associativa
$arrayOfAttributes = \json_decode($fileContents, true);
// erro?
if ($arrayOfAttributes === FALSE) {
// regista-se o erro
$erreur = TRUE;
$message = "Le fichier de données JSON [$jsonFilename] n'a pu être exploité correctement";
}
}
// erro?
if ($erreur) {
// lança-se uma exceção
throw new ExceptionImpots($message);
}
// inicialização dos atributos da classe
foreach ($arrayOfAttributes as $key => $value) {
$this->$key = $value;
}
// verifica-se a presença de todos os atributos
$this->checkForAllAttributes($arrayOfAttributes);
// retorna o objeto
return $this;
}
public function checkForAllAttributes($arrayOfAttributes) {
// verifica-se se todas as chaves foram inicializadas
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) {
// inicializam-se alguns atributos da classe (não necessariamente todos)
foreach ($arrayOfAttributes as $key => $value) {
$this->$key = $value;
}
// retorna-se o objeto
return $this;
}
// toString
public function __toString() {
// atributos do objeto
$arrayOfAttributes = \get_object_vars($this);
// cadeia jSON do objeto
return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
}
}
Comentários
- linha 20: o atributo [$this→arrayOfAttributes] foi transformado numa variável que deve agora ser passada para o método [checkForAllAttributes], na linha 38, que anteriormente operava sobre o atributo [$this→arrayOfAttributes];
Devido a esta alteração no [BaseEntity], a classe [Database] também deve ser ligeiramente modificada:
<?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
// inicialização
public function setFromJsonFile(string $jsonFilename) {
// pai
parent::setFromJsonFile($jsonFilename);
// retorna o objeto
return $this;
}
// getters e setters
...
}
Comentários
- no código original, após a linha 30, era chamado o método [parent::checkForAllAttributes]. Isso já não é necessário, uma vez que agora é tratado automaticamente pelo método [parent::setFromJsonFile($jsonFilename)];
14.3.4. Testes do servidor com o método [Postman]
O [Postman] foi apresentado no artigo (ligação).
Utilizamos os seguintes testes do Postman:



O resultado jSON desta última consulta é o seguinte:

- em [5-8], é possível observar que os atributos da cadeia jSON têm, de facto, valores numéricos (e não cadeias de caracteres). Este resultado permitirá que a classe JavaScript [Métier] seja executada normalmente;
14.3.5. O script principal [main]

O script principal [main] do cliente JavaScript é o seguinte:
// importações
import axios from 'axios';
// importações
import Dao from './Dao2';
import Métier from './Métier';
// função assíncrona [main]
async function main() {
// configuração do Axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/versão-14';
// instanciação da camada [dao]
const dao = new Dao(axios);
// pedidos HTTP
let taxAdminData;
try {
// inicialização da sessão
log("-----------init-session");
let response = await dao.initSession();
log(response);
// autenticação
log("-----------authentifier-utilisateur");
response = await dao.authentifierUtilisateur("admin", "admin");
log(response);
// dados fiscais
log("-----------get-admindata");
response = await dao.getAdminData();
log(response);
taxAdminData = response.réponse;
} catch (error) {
// registo do erro
console.log("erreur=", error.message);
// fim
return;
}
// instanciação da camada [métier]
const métier = new Métier(taxAdminData);
// cálculos de impostos
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 simulações
log("-----------liste-des-simulations");
log(simulations);
// eliminação de uma simulação
log("-----------suppression simulation n° 1");
simulations.splice(1, 1);
log(simulations);
}
// registo jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// execução
main();
Comentários
- linhas 5-6: importações das classes [Dao] e [Métier];
- linha 9: a função assíncrona [main], que irá organizar a comunicação com o servidor através da classe [Dao] e solicitar à classe [Métier] que efetue os cálculos de impostos;
- linhas 10-36: o script chama sucessivamente e de forma bloqueante os métodos [initSession, authentifierUtilisateur, getAdminData] da camada [dao];
- linha 38: já não é necessária a camada [dao]. Temos todos os elementos para que a camada [métier] do cliente JavaScript funcione;
- linhas 41-46: efetuamos três cálculos de imposto, cujos resultados são acumulados numa tabela [simulations];
- linha 49: apresentamos a tabela de simulações;
- linha 52: elimina-se uma delas;
Os resultados da execução do script principal são os seguintes:
[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

Nesta secção, abrimos a aplicação [Client HTTP 2] num navegador, de acordo com a seguinte arquitetura:

A portabilidade não é imediata. Embora o [node.js] consiga executar JavaScript ES6, isso não acontece, em geral, com os navegadores. É, portanto, necessário utilizar ferramentas que convertam o código ES6 em código ES5 compreensível pelos navegadores recentes. Felizmente, estas ferramentas são simultaneamente poderosas e bastante simples de utilizar.
Seguimos aqui o artigo [How to write ES6 code that’s safe to run in the browser - Web Developer's Journal].
Na pasta [client HTTP 3/src], colocámos os elementos [main.js, Métier.js, Dao2.js] da aplicação [Client Http 2] que acabámos de desenvolver.
14.4.1. Inicialização do projeto
Vamos trabalhar na pasta [client http 3]. Abrimos um terminal no [VSCode] e navegamos até esta pasta:

Inicializamos este projeto com o comando [npm init] e aceitamos as respostas predefinidas às perguntas apresentadas:

- em [4-5], o ficheiro de configuração do projeto [package.json] gerado a partir das diferentes respostas fornecidas;
14.4.2. Instalação das dependências do projeto
Vamos instalar as seguintes dependências:
- [@babel/core]: o núcleo da ferramenta [Babel] [https://babeljs.io], que transforma código ES 2015+ em código executável em navegadores recentes e mais antigos;
- [@babel/preset-env]: faz parte do conjunto de ferramentas Babel. Intervém antes da transpilagem de ES6 → ES5;
- [babel-loader]: esta dependência permite que a ferramenta [webpack] recorra à ferramenta [Babel];
- [webpack]: o «maestro». É a [webpack] que recorre ao Babel para realizar a transpilagem dos códigos ES6 → ES5 e, em seguida, é ela que agrupa todos os ficheiros resultantes num único ficheiro;
- [webpack-cli]: necessário para o [webpack];
- [@webpack-cli/init]: utilizado para configurar o [webpack];
- [webpack-dev-server]: fornece um servidor web de desenvolvimento que funciona, por predefinição, na porta 8080. Quando os ficheiros de código-fonte são alterados, recarrega automaticamente a aplicação web;
As dependências do projeto são instaladas da seguinte forma num terminal do [VSCode]:
npm --save-dev install @babel/core @babel/preset-env babel-loader webpack webpack-cli webpack-dev-server @webpack-cli/init

Após a instalação das dependências, o ficheiro [package.json] sofreu as seguintes alterações:
{
"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"
}
}
- linhas 12-19: as dependências do projeto são [devDependencies]: são necessárias durante a fase de desenvolvimento, mas já não na fase de produção. Com efeito, em produção, é utilizado o ficheiro [dist/main.js]. Está codificado em ES5 e já não necessita das ferramentas de transpilagem de código ES6 para código ES5;
Temos de adicionar duas dependências ao projeto:
- [core-js]: contém «polyfills» para o ECMAScript 2019. Um polyfill permite executar código recente, como o ECMAScript 2019 (setembro de 2019), em navegadores mais antigos;
- [regenerator-runtime]: de acordo com o site da biblioteca --> [Source transformer enabling ECMAScript 6 generator functions in JavaScript-of-today];
Estas duas dependências substituem, a partir do Babel 7, a dependência [@babel/polyfill], que anteriormente desempenhava essa função e que agora (setembro de 2019) está obsoleta. São instaladas da seguinte forma:

O ficheiro [package.json] passa então a ter a seguinte forma:
{
"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"
}
}
A utilização das dependências [core-js, regenerator-runtime] obriga a incluir os seguintes [imports] (linhas 3-4) no script principal [src/main.js]:
// importações
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
// importações
import Dao from './Dao2';
import Métier from './Métier';
14.4.3. Configuração do [webpack]
O [webpack] é a ferramenta que irá controlar:
- a transpilagem de ES6 → ES5 de todos os ficheiros JavaScript do projeto;
- a compilação dos ficheiros gerados num único ficheiro;
Esta ferramenta é controlada por um ficheiro de configuração [webpack.config.js], que pode ser gerado através de uma dependência denominada [@webpack-cli/init] (setembro de 2019). Esta foi instalada juntamente com as outras, conforme indicado no parágrafo «ligação».
Executamos o comando [npx webpack-cli init] num terminal [VSCode]:

Depois de responder às várias perguntas (para as quais podemos aceitar a maioria das respostas propostas por predefinição), é gerado um ficheiro [webpack.config.js] na raiz do projeto [4]:
O ficheiro [webpack.config.js] tem o seguinte aspeto:
/* 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
}
};
Não compreendo todos os detalhes deste ficheiro, mas é possível destacar alguns pontos:
- linha 1: o ficheiro não contém código ES6. O [Eslint] reporta, então, erros que remontam até à raiz do projeto [javascript]. Isto é incómodo. Para impedir que o Eslint analise um ficheiro, basta colocar a linha 1 entre comentários;
- linha 31: estamos a trabalhar no modo [développement];
- linha 32: o script de entrada é aqui o [src/index.js]. Teremos de alterar isto;
- linha 36: a pasta onde serão guardados os produtos do [webpack] será a pasta [dist];
- linha 46: vemos que o [webpack] utiliza o [babel-loader], uma das dependências que instalámos;
- linha 54: vemos que o [webpack] utiliza o [@babel-preset/env], uma das dependências que instalámos;
A inicialização do [webpack] alterou o ficheiro [package.json] (solicita autorização):
{
"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"
}
}
- linha 4: foi alterada;
- linhas 8-9, 18-19: foram adicionadas;
- linha 8: a tarefa [npm], que permite compilar o projeto;
- linha 9: a tarefa [npm], que permite executá-lo;
- linha 18: ?
- linha 19: permite a geração de um ficheiro [dist/index.html] que incorpora automaticamente o script [dist/main.js] gerado por [webpack], sendo este o script utilizado quando o projeto é executado;
Por fim, a configuração do [webpack] gerou um ficheiro [src/index.js]:

O conteúdo do ficheiro [index.js] é o seguinte (setembro de 2019):
console.log("Hello World from your main file!");
14.4.4. Compilação e execução do projeto
O ficheiro [package.json] contém três tarefas [npm]:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "webpack-dev-server"
},
Estas tarefas são incluídas pelo [VSCode], que as submete à execução:

- em [1-3], o projeto é compilado;
- em [4]: o projeto é compilado em [dist/main.hash.js] e é criada uma página [dist/index.html];
A página [index.html] gerada é a seguinte:
<!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>
Esta página limita-se, portanto, a encapsular o ficheiro [main.hash.js] gerado por [webpack].
O projeto é executado pela tarefa [start]:

A página [dist/index.html] é então carregada num servidor, pertencente à suíte [webpack], a operar na porta 8080 da máquina local e apresentada pelo navegador predefinido da máquina:

- em [2], a porta de serviço do servidor web de [webpack];
- em [3], o corpo da página [dist/index.html] está vazio;
- em [4], o separador [console] das ferramentas de desenvolvimento do navegador, neste caso o Firefox (F12);
- em [5], o resultado da execução do ficheiro [src/index.js]. Recorde-se que o conteúdo deste era o seguinte:
Agora, vamos alterar esse conteúdo para a seguinte linha:
Automaticamente (sem recompilar), são gerados novos ficheiros [main.js, index.html] e o novo ficheiro [index.html] é carregado no navegador:

Não é necessário executar a tarefa [build] antes da tarefa [start]: esta última compila primeiro o projeto. Não armazena os resultados dessa compilação na pasta [dist]. Para se certificar disso, basta eliminar essa pasta. Ver-se-á então que a tarefa [start] compila e executa o projeto sem criar a pasta [dist]. Parece que armazena os seus produtos [index.html, main.hash.js] numa pasta específica da tarefa [webpackdev-server]. Este comportamento é suficiente para os nossos testes.
Quando o servidor de desenvolvimento é iniciado, qualquer alteração guardada num dos ficheiros do projeto provoca uma recompilação. Por esse motivo, desativamos o modo [Auto Save] do [VSCode]. Com efeito, não queremos que haja recompilação sempre que se introduzam caracteres num dos ficheiros do projeto. Só queremos que a recompilação ocorra no momento em que as alterações forem guardadas:

- em [2], a opção [Auto Save] não deve estar marcada;
14.4.5. Testes do cliente JavaScript do servidor de cálculo de impostos
Para testar o cliente JavaScript do servidor de cálculo de impostos, é necessário designar [main.js] [1] como o ponto de entrada do projeto no ficheiro [webpack.config.js] [2-3]:

Não nos esqueçamos de que o script [main.js] deve incluir duas importações adicionais em relação à sua versão em [Client http 2]:

Além disso, alterámos ligeiramente o código para gerir os erros que o servidor possa enviar:
// imports
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
// importações
import Dao from './Dao2';
import Métier from './Métier';
// função assíncrona [main]
async function main() {
// configuração do axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/versão-14';
// instanciação da camada [dao]
const dao = new Dao(axios);
// pedidos HTTP
let taxAdminData;
try {
// inicialização da sessão
log("-----------init-session");
let response = await dao.initSession();
log(response);
if (response.état != 700) {
throw new Error(JSON.stringify(response.réponse));
}
// autenticação
log("-----------authentifier-utilisateur");
response = await dao.authentifierUtilisateur("admin", "admin");
log(response);
if (response.état != 200) {
throw new Error(JSON.stringify(response.réponse));
}
// dados fiscais
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) {
// registo do erro
console.log("erreur=", error.message);
// fim
return;
}
// instanciação da camada [métier]
const métier = new Métier(taxAdminData);
// cálculos de impostos
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 simulações
log("-----------liste-des-simulations");
log(simulations);
// eliminação de uma simulação
log("-----------suppression simulation n° 1");
simulations.splice(1, 1);
log(simulations);
}
// registo jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// execução
main();
Comentários
- nas linhas [24-26], [31-33] e [38-40], verifica-se o código [response.état] enviado na resposta jSON do servidor. Se este código indicar um erro, é lançada uma exceção com a mensagem de erro «jSON» da resposta do servidor [response.réponse];
Feito isto, executamos o projeto [5-6].
A página [index.html] é então gerada e carregada no navegador:

- em [7], verifica-se que a ação [init-session] não pôde ser concluída devido a um problema [CORS] (Cross-Origin Resource Sharing);
O problema CORS decorre da relação cliente/servidor:
- o nosso cliente JavaScript foi descarregado para a máquina [http://localhost:8080];
- o servidor de cálculo de impostos está a ser executado na máquina [http://localhost:80];
- o cliente e o servidor não se encontram, portanto, nos mesmos domínios (mesma máquina, mas porta diferente);
- o navegador que executa o cliente JavaScript carregado a partir da máquina [http://localhost:8080] bloqueia qualquer pedido que não tenha como destino [http://localhost:80]. Trata-se de uma medida de segurança. Por isso, bloqueia a solicitação do cliente para o servidor que opera na máquina [http://localhost:80];
Na verdade, o navegador não bloqueia totalmente a solicitação. Na realidade, aguarda que o servidor lhe «diga» que aceita solicitações entre domínios. Se obtiver essa autorização, o navegador transmitirá então a solicitação entre domínios.
O servidor concede a sua autorização enviando cabeçalhos HTTP específicos:
- linha 1: o cliente JavaScript opera no domínio [http://localhost:8080]. O servidor deve responder explicitamente que aceita este domínio;
- linha 2: o cliente JavaScript irá utilizar nas suas solicitações os cabeçalhos HTTP e [Accept, Content-Type]:
- [Accept]: este cabeçalho é enviado em todas as solicitações;
- [Content-Type]: este cabeçalho é utilizado nas operações POST para indicar o tipo dos parâmetros do POST;
O servidor deve aceitar explicitamente estes dois cabeçalhos HTTP;
- linha 3: o cliente JavaScript irá utilizar as solicitações GET e POST. O servidor deve aceitar explicitamente estes dois tipos de solicitações;
- linha 4: o cliente JavaScript irá enviar cookies de sessão. O servidor aceita-os com o cabeçalho da linha 4;
Portanto, temos de alterar o servidor. Fazemos isso em [Netbeans]. O problema com CORS é um problema que ocorre apenas no modo de desenvolvimento. Em produção, o cliente e o servidor funcionarão no mesmo domínio [http://localhost:80] e não haverá qualquer problema CORS. Por isso, precisamos de uma forma de autorizar ou não as solicitações CORS através da configuração do servidor.

As alterações no servidor são efetuadas em três locais:
- [1, 4]: no ficheiro de configuração [config.json], para incluir um valor booleano que controlará se as solicitações entre domínios são aceites ou não;
- [2]: na classe [ParentResponse], que envia a resposta ao cliente JavaScript. É esta classe que enviará os cabeçalhos CORS esperados pelo navegador do cliente;
- [3]: nas classes [HtmlResponse, JsonResponse, XmlResponse] que geram as respostas para as sessões [html, json, xml], respetivamente. Estas classes devem passar para a sua classe pai [2] o valor booleano [corsAllowed] encontrado em [4]. Isto é feito em [5], passando a matriz de imagens do ficheiro jSON para [2];
A classe [ParentResponse] [2] evolui da seguinte forma:
<?php
namespace Application;
// dependências do Symfony
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
class ParentResponse {
// int $statusCode: o código HTTP do estado da resposta
// cadeia de caracteres $content: o corpo da resposta a enviar
// consoante o caso, trata-se de uma cadeia de caracteres JSON, XML ou HTML
// matriz $headers: os cabeçalhos HTTP a adicionar à resposta
public function sendResponse(
Request $request,
int $statusCode,
string $content,
array $headers,
array $config): void {
// preparação da resposta de texto do servidor
$response = new Response();
$response->setCharset("utf-8");
// código de estado
$response->setStatusCode($statusCode);
// cabeçalhos para pedidos entre domínios
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 específico do método [OPTIONS]
// neste caso, apenas os cabeçalhos são importantes
$method = strtolower($request->getMethod());
if ($method === "options") {
$content = "";
$response->setStatusCode(Response::HTTP_OK);
}
// envia-se a resposta
$response->setContent($content);
$response->send();
}
}
- linha 29: verifica-se se é necessário gerir os pedidos entre domínios. Se for o caso, geram-se os cabeçalhos HTTP CORS (linhas 33-37), mesmo que o pedido atual não seja um pedido entre domínios. Neste último caso, os cabeçalhos CORS serão desnecessários e não serão utilizados pelo cliente;
- linha 30: numa solicitação entre domínios, o navegador do cliente que interroga o servidor envia um cabeçalho HTTP [Origin: http://localhost:8080] (no caso específico do nosso cliente JavaScript). Na linha 30, recuperamos este cabeçalho HTTP na solicitação [$request];
- linha 31: só serão aceites pedidos entre domínios provenientes exclusivamente da máquina [http://localhost]. Recorde-se que estes pedidos só ocorrem no modo de desenvolvimento do projeto;
- linhas 32-36: adicionam-se os cabeçalhos CORS aos cabeçalhos já presentes na tabela [$headers];
- linhas 45-49: a forma como o navegador do cliente solicita as autorizações CORS pode variar consoante o cliente em execução. Por vezes, o navegador do cliente solicita estas autorizações com um comando HTTP [OPTIONS]. Trata-se de uma novidade para o nosso servidor, que foi concebido para atender exclusivamente aos comandos [GET, POST]. No caso de um comando [OPTIONS], o servidor gera atualmente uma resposta de erro. Nas linhas 46-49, corrigimos isso no último momento: se, na linha 46, verificarmos que o comando atual é um comando [OPTIONS], então geramos para o cliente:
- linhas 47 e 51: uma resposta [$content] vazia;
- linha 48: um código de estado 200, indicando que o comando foi bem-sucedido. O único aspeto importante para este comando é o envio dos cabeçalhos CORS das linhas 33-36. É isso que o navegador do cliente espera;
Depois de corrigir o servidor desta forma, o cliente JavaScript funciona melhor, mas surge um novo erro:

- em [1], a sessão jSON é inicializada corretamente;
- em [2], a ação [authentifier-utilisateur] falha: o servidor indica que não existe nenhuma sessão em curso. Isto significa que o cliente JavaScript não lhe devolveu corretamente o cookie de sessão que enviou durante a ação [init-session];
Vamos analisar as trocas de dados de rede que ocorreram:

- em [4], a solicitação [init-session]. Esta decorreu corretamente, com um código 200 como estado da resposta;
- em [5], a solicitação [authentifier-utilisateur]. Esta falhou com um código 400 (Bad Request) [6] como estado da resposta;
Se analisarmos os cabeçalhos HTTP e [7] da solicitação [5], percebe-se que o cliente JavaScript não enviou os cabeçalhos HTTP e [Cookie], que lhe teriam permitido reenviar o cookie de sessão inicialmente enviado pelo servidor. É por esta razão que o servidor declara que não existe nenhuma sessão.
Para que o cliente envie o cookie de sessão, é necessário adicionar uma configuração ao objeto [axios]:
// imports
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
// importações
import Dao from './Dao2';
import Métier from './Métier';
// função assíncrona [main]
async function main() {
// configuração do Axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/versão-14';
axios.defaults.withCredentials = true;
// instanciação da camada [dao]
const dao = new Dao(axios);
// pedidos HTTP
let taxAdminData;
...
A linha 15 solicita que os cookies sejam incluídos nos cabeçalhos HTTP da solicitação [axios]. Note-se que isto não tinha sido necessário no ambiente [node.js]. Existem, portanto, diferenças de código entre os dois ambientes.
Depois de corrigido este erro, o cliente JavaScript funciona normalmente:


14.5. Melhoria do cliente HTTP 3
Quando a classe [Dao2] anterior é executada num navegador, a gestão do cookie de sessão torna-se desnecessária. Com efeito, é o navegador que hospeda a camada [dao] que gere o cookie de sessão: ele reenvia automaticamente qualquer cookie que o servidor lhe envie. Assim, a classe [Dao2] pode ser reescrita na seguinte classe [Dao3]:
"use strict";
// importações
import qs from "qs";
class Dao3 {
// construtor
constructor(axios) {
this.axios = axios;
}
// inicialização da sessão
async initSession() {
// opções da solicitação HHTP [get /main.php?action=init-session&type=json]
const options = {
method: "GET",
// parâmetros da URL
params: {
action: "init-session",
type: "json"
}
};
// execução da consulta HTTP
return await this.getRemoteData(options);
}
async authentifierUtilisateur(user, password) {
// opções da consulta HHTP [post /main.php?action=authentifier-utilisateur]
const options = {
method: "POST",
headers: {
"Content-type": "application/x-www-form-urlencoded"
},
// corpo do POST
data: qs.stringify({
user: user,
password: password
}),
// parâmetros do URL
params: {
action: "authentifier-utilisateur"
}
};
// execução da consulta HTTP
return await this.getRemoteData(options);
}
async getAdminData() {
// opções da consulta HHTP [get /main.php?action=get-admindata]
const options = {
method: "GET",
// parâmetros da consulta URL
params: {
action: "get-admindata"
}
};
// execução da consulta HTTP
const data = await this.getRemoteData(options);
// resultado
return data;
}
async getRemoteData(options) {
// execução da consulta HTTP
let response;
try {
// consulta assíncrona
response = await this.axios.request("main.php", options);
} catch (error) {
// o parâmetro [error] é uma instância de exceção — pode assumir várias formas
if (error.response) {
// a resposta do servidor está em [error.response]
response = error.response;
} else {
// o erro é reenviado
throw error;
}
}
// a resposta é o conjunto completo da resposta HTTP do servidor (cabeçalhos HTTP + a própria resposta)
// a resposta do servidor está em [response.data]
return response.data;
}
}
// exportação da classe
export default Dao3;
Tudo o que se relacionava com a gestão do cookie de gestão desapareceu.
Alteramos o projeto anterior da seguinte forma:

Na pasta [src], adicionámos dois ficheiros:
- a classe [Dao3] que acabámos de apresentar;
- o ficheiro [main3], responsável por iniciar a nova versão;
O ficheiro [main3] permanece idêntico ao ficheiro [main] da versão anterior, mas utiliza agora a classe [Dao3]:
// importações
import axios from "axios";
import "core-js/stable";
import "regenerator-runtime/runtime";
// importações
import Dao from "./Dao3";
import Métier from "./Métier";
// função assíncrona [main]
async function main() {
// configuração do Axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL =
"http://localhost/php7/scripts-web/impots/version-14";
axios.defaults.withCredentials = true;
// instanciação da camada [dao]
const dao = new Dao(axios);
// pedidos HTTP
...
}
// registo jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// execução
main();
O ficheiro [webpack.config] foi alterado para, agora, executar o 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()],
...
};
Feito isto, executa-se o projeto após ter iniciado o servidor de cálculo do imposto:

Os resultados obtidos na consola do navegador são idênticos aos da versão anterior.
14.6. Conclusion
Dispomos agora de todas as ferramentas necessárias para desenvolver o código JavaScript de uma aplicação web. Podemos:
- utilizar o código ECMAScript mais recente;
- testar elementos isolados desse código num ambiente [node.js] mais simples para depuração e testes;
- transferir posteriormente esse código para um navegador graças às ferramentas [babel] e [webpack];