14. Clientes HTTP JavaScript do serviço de cálculo de impostos
14.1. Introdução
Aqui, propomos escrever um cliente [Node.js] para a 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á uma estrutura [main, business logic, DAO]. A camada [business logic] do servidor será transferida para o cliente:

14.2. Cliente HTTP 1

Como mencionado, o cliente HTTP 1 implementa a seguinte arquitetura cliente/servidor:

Iremos implementar:
- a camada [DAO] como uma classe;
- a camada [main] como um script que utiliza esta classe;
14.2.1. A camada [dao]
A camada [dao] será implementada pela seguinte classe [Dao1.js]:
'use strict';
// imports
import qs from 'qs'
class Dao1 {
// manufacturer
constructor(axios) {
// axios library for queries HTTP
this.axios = axios;
// session cookie
this.sessionCookieName = "PHPSESSID";
this.sessionCookie = '';
}
// init session
async initSession() {
// query options HHTP [get /main.php?action=init-session&type=json]
const options = {
method: "GET",
// URL parameters
params: {
action: 'init-session',
type: 'json'
}
};
// execute query HTTP
return await this.getRemoteData(options);
}
async authentifierUtilisateur(user, password) {
// query options HHTP [post /main.php?action=authenticate-user]
const options = {
method: "POST",
headers: {
'Content-type': 'application/x-www-form-urlencoded',
},
// body of POST
data: qs.stringify({
user: user,
password: password
}),
// URL parameters
params: {
action: 'authentifier-utilisateur'
}
};
// execute query HTTP
return await this.getRemoteData(options);
}
// tAX CALCULATION
async calculerImpot(marié, enfants, salaire) {
// query options HHTP [post /main.php?action=calculate-tax]
const options = {
method: "POST",
headers: {
'Content-type': 'application/x-www-form-urlencoded',
},
// body of POST [married, children, salary]
data: qs.stringify({
marié: marié,
enfants: enfants,
salaire: salaire
}),
// URL parameters
params: {
action: 'calculer-impot'
}
};
// execute query HTTP
const data = await this.getRemoteData(options);
// result
return data;
}
// list of simulations
async listeSimulations() {
// query options HHTP [get /main.php?action=lister-simulations]
const options = {
method: "GET",
// URL parameters
params: {
action: 'lister-simulations'
},
};
// execute query HTTP
const data = await this.getRemoteData(options);
// result
return data;
}
// list of simulations
async supprimerSimulation(index) {
// query options HHTP [get /main.php?action=suppress-simulation&number=index]
const options = {
method: "GET",
// URL parameters
params: {
action: 'supprimer-simulation',
numéro: index
},
};
// execute query HTTP
const data = await this.getRemoteData(options);
// result
return data;
}
async getRemoteData(options) {
// for the session cookie
if (!options.headers) {
options.headers = {};
}
options.headers.Cookie = this.sessionCookie;
// execute query HTTP
let response;
try {
// asynchronous request
response = await this.axios.request('main.php', options);
} catch (error) {
// the [error] parameter is an exception instance - it can take various forms
if (error.response) {
// the server response is in [error.response]
response = error.response;
} else {
// error restart
throw error;
}
}
// response is the entire HTTP response from the server (HTTP headers + response itself)
// retrieve the session cookie if it exists
const setCookie = response.headers['set-cookie'];
if (setCookie) {
// setCookie is an array
// look for the session cookie in this table
let trouvé = false;
let i = 0;
while (!trouvé && i < setCookie.length) {
// look for the session cookie
const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
if (results) {
// the session cookie is stored
// eslint-disable-next-line require-atomic-updates
this.sessionCookie = results[1];
// we found
trouvé = true;
} else {
// next item
i++;
}
}
}
// the server response is in [response.data]
return response.data;
}
}
// class export
export default Dao1;
- Aqui estamos a utilizar o que aprendemos na secção em destaque, onde apresentámos a biblioteca [axios], que nos permite fazer pedidos HTTP tanto no [node.js] como num navegador. Iremos analisar especificamente o script na secção em destaque;
- linhas 9–15: o construtor da classe. Esta classe terá três propriedades:
- [axios]: o objeto [axios] utilizado para efetuar pedidos HTTP. Este é passado pelo código de chamada;
- [sessionCookieName]: dependendo do servidor, o cookie de sessão tem nomes diferentes. Aqui, é [PHPSESSID];
- [sessionCookie]: o cookie de sessão enviado pelo servidor e armazenado pelo cliente;
- linhas 53–76: a função assíncrona [calculateTax] faz a solicitação [post /main.php?action=calculate-tax] enviando os parâmetros [married, children, salary]. Ela retorna a string JSON enviada pelo servidor como um objeto JavaScript;
- linhas 79–92: a função assíncrona [listSimulations] faz a solicitação [get /main.php?action=list-simulations]. Ela retorna a string JSON enviada pelo servidor como um objeto JavaScript;
- linhas 95–109: A função assíncrona [deleteSimulation] faz a solicitação [get /main.php?action=delete-simulation&number=index]. Ela retorna a string JSON enviada pelo servidor como um objeto JavaScript;
- linha 121: a notação [this.axios] é utilizada porque, neste caso, o objeto [axios] passado ao construtor foi armazenado na propriedade [this.axios];
- linha 161: a classe [Dao1] é exportada para que possa 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:
// import axios
import axios from 'axios';
// dao1 class import
import Dao from './Dao1';
// asynchronous function [main]
async function main() {
// axios configuration
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
// layer instantiation [dao]
const dao = new Dao(axios);
// using the [dao] layer
try {
// init session
log("-----------init-session");
let response = await dao.initSession();
log(response);
// authentication
log("-----------authentifier-utilisateur");
response = await dao.authentifierUtilisateur("admin", "admin");
log(response);
// tax calculations
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);
// list of simulations
log("-----------liste-des-simulations");
response = await dao.listeSimulations();
log(response);
// deleting a simulation
log("-----------suppression simulation n° 1");
response = await dao.supprimerSimulation(1);
log(response);
} catch (error) {
// we log the error
console.log("erreur=", error.message);
}
}
// log jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// execution
main();
Comentários
- linha 2: importar a biblioteca [axios];
- linha 4: importar a classe [Dao];
- linha 7: a função [main] que comunica com o servidor é assíncrona;
- linhas 9-10: configuração padrão para os pedidos HTTP a enviar para o servidor:
- linha 9: [timeout] de 2 segundos;
- linha 10: todas as URLs têm como prefixo a URL base da versão 14 do servidor de cálculo de impostos;
- linha 12: a camada [Dao] está construída. Agora pode ser utilizada;
- linhas 46–48: a função [log] é utilizada para apresentar a cadeia JSON de um objeto JavaScript de forma formatada: verticalmente com 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 a [await Promise.all], a execução fica bloqueada até que os três resultados tenham sido obtidos;
- linhas 31–34: lista de simulações;
- linhas 35–38: eliminação de uma simulação;
- linhas 39–42: tratamento de eventuais exceções;
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:

Transferimos a camada [business] do servidor para o cliente JavaScript. Ao contrário do que fizemos no curso de PHP7, a camada [main] não precisará passar pela camada [business] para chegar à camada [DAO]. Utilizaremos estas duas camadas como componentes especializados:
- a camada [main] passa pela camada [DAO] sempre que precisa de dados que se encontram no servidor;
- a camada [main] solicita à camada [business] que execute os cálculos de impostos;
- a camada [business] é independente da camada [DAO] e nunca a invoca;
14.3.1. A classe [Business] em JavaScript
A essência da classe [Business] em PHP foi descrita no artigo em link. Trata-se de um trecho de código bastante complexo que estamos a recordar aqui, não para o explicar, mas para o podermos traduzir para JavaScript:
<?php
// namespace
namespace Application;
class Metier implements InterfaceMetier {
// dao layer
private $dao;
// tax administration data
private $taxAdminData;
//---------------------------------------------
// setter couche [dao]
public function setDao(InterfaceDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceDao $dao) {
// a reference is stored on the [dao] layer
$this->dao = $dao;
// recover data for tax calculation
// method [getTaxAdminData] may throw a ExceptionImpots exception
// we then let it go back to the calling code
$this->taxAdminData = $this->dao->getTaxAdminData();
}
// tAX CALCULATION
// --------------------------------------------------------------------------
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// $marié : yes, no
// $enfants : number of children
// $salaire: annual salary
// $this->taxAdminData: tax administration data
//
// we check that we have the correct data from the tax authorities
if ($this->taxAdminData === NULL) {
$this->taxAdminData = $this->getTaxAdminData();
}
// tax calculation with children
$result1 = $this->calculerImpot2($marié, $enfants, $salaire);
$impot1 = $result1["impôt"];
// tax calculation without children
if ($enfants != 0) {
$result2 = $this->calculerImpot2($marié, 0, $salaire);
$impot2 = $result2["impôt"];
// application of the family allowance ceiling
$plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
if ($enfants < 3) {
// $PLAFOND_QF_DEMI_PART euros for the first 2 children
$impot2 = $impot2 - $enfants * $plafonDemiPart;
} else {
// $PLAFOND_QF_DEMI_PART euros for the first 2 children, double for subsequent children
$impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
}
} else {
$impot2 = $impot1;
$result2 = $result1;
}
// we take the highest tax
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"];
}
// calculation of any discount
$décôte = $this->getDecôte($marié, $salaire, $impot);
$impot -= $décôte;
// calculation of any tax reduction
$réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
$impot -= $réduction;
// result
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é : yes, no
// $enfants : number of children
// $salaire: annual salary
// $this->taxAdminData: tax administration data
//
// number of shares
$marié = strtolower($marié);
if ($marié === "oui") {
$nbParts = $enfants / 2 + 2;
} else {
$nbParts = $enfants / 2 + 1;
}
// 1 part per child from the 3rd
if ($enfants >= 3) {
// an additional half share for each child from the 3rd onwards
$nbParts += 0.5 * ($enfants - 2);
}
// taxable income
$revenuImposable = $this->getRevenuImposable($salaire);
// surcharge
$surcôte = floor($revenuImposable - 0.9 * $salaire);
// for rounding problems
if ($surcôte < 0) {
$surcôte = 0;
}
// family quotient
$quotient = $revenuImposable / $nbParts;
// tAX CALCULATION
$limites = $this->taxAdminData->getLimites();
$coeffR = $this->taxAdminData->getCoeffR();
$coeffN = $this->taxAdminData->getCoeffN();
// is set at the end of the limit array to stop the following loop
$limites[count($limites) - 1] = $quotient;
// tax rate search
$i = 0;
while ($quotient > $limites[$i]) {
$i++;
}
// because $quotient has been placed at the end of the $limites array, the previous loop
// cannot exceed the table $limites
// now we can calculate the tax
$impôt = floor($revenuImposable * $coeffR[$i] - $nbParts * $coeffN[$i]);
// result
return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
}
// revenuImposable=annualwage-discount
// the allowance has a minimum and a maximum
private function getRevenuImposable(float $salaire): float {
// 10% salary deduction
$abattement = 0.1 * $salaire;
// this allowance cannot exceed $this->taxAdminData->getAbattementDixPourCentMax()
if ($abattement > $this->taxAdminData->getAbattementDixPourCentMax()) {
$abattement = $this->taxAdminData->getAbattementDixPourcentMax();
}
// the allowance cannot be less than $this->taxAdminData->getAbattementDixPourcentMin()
if ($abattement < $this->taxAdminData->getAbattementDixPourcentMin()) {
$abattement = $this->taxAdminData->getAbattementDixPourcentMin();
}
// taxable income
$revenuImposable = $salaire - $abattement;
// result
return floor($revenuImposable);
}
// calculates any discount
private function getDecôte(string $marié, float $salaire, float $impots): float {
// at the outset, a zero discount
$décôte = 0;
// maximum tax amount to qualify for discount
$plafondImpôtPourDécôte = $marié === "oui" ?
$this->taxAdminData->getPlafondImpotCouplePourDecote() :
$this->taxAdminData->getPlafondImpotCelibatairePourDecote();
if ($impots < $plafondImpôtPourDécôte) {
// maximum discount
$plafondDécôte = $marié === "oui" ?
$this->taxAdminData->getPlafondDecoteCouple() :
$this->taxAdminData->getPlafondDecoteCelibataire();
// theoretical discount
$décôte = $plafondDécôte - 0.75 * $impots;
// the discount cannot exceed the amount of tax due
if ($décôte > $impots) {
$décôte = $impots;
}
// no discount <0
if ($décôte < 0) {
$décôte = 0;
}
}
// result
return ceil($décôte);
}
// calculates any reduction
private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
// the income ceiling to qualify for the 20% reduction
$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();
}
// taxable income
$revenuImposable = $this->getRevenuImposable($salaire);
// reduction
$réduction = 0;
if ($revenuImposable < $plafondRevenuPourRéduction) {
// 20% discount
$réduction = 0.2 * $impots;
}
// result
return ceil($réduction);
}
// batch mode tax calculation
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// we let the exceptions coming from the [dao] layer flow upwards
// retrieve taxpayer data
$taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// results table
$results = [];
// we exploit them
foreach ($taxPayersData as $taxPayerData) {
// tax calculation
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// complete [$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"]);
// put the result in the results table
$results [] = $taxPayerData;
}
// recording results
$this->dao->saveResults($resultsFileName, $results);
}
}
- linhas 19–26: o construtor da classe PHP. Como dissemos que estávamos a construir uma camada [de negócios] independente da camada [DAO], faremos duas alterações a este construtor em JavaScript:
- não receberá uma instância da camada [DAO] (já não precisa de uma);
- não solicitará dados fiscais da administração [taxAdminData] da camada [dao]: o código de chamada passará esses dados para o construtor;
- Linhas 197–122: Não iremos implementar o método [executeBatchImpots], cujo objetivo final era guardar os resultados da simulação num ficheiro de texto. Queremos código que funcione tanto em [node.js] como num navegador. No entanto, não é possível guardar dados no sistema de ficheiros da máquina que executa o navegador do cliente;
Dadas estas restrições, o código para a classe [Métier] em JavaScript é o seguinte:
'use strict';
// job class
class Métier {
// manufacturer
constructor(taxAdmindata) {
// this.taxAdminData: tax administration data
this.taxAdminData = taxAdmindata;
}
// tAX CALCULATION
// --------------------------------------------------------------------------
calculerImpot(marié, enfants, salaire) {
// married: yes, no
// children: number of children
// salary: annual salary
// this.taxAdminData: tax administration data
//
// tax calculation with children
const result1 = this.calculerImpot2(marié, enfants, salaire);
const impot1 = result1["impôt"];
// tax calculation without children
let result2, impot2, plafondDemiPart;
if (enfants !== 0) {
result2 = this.calculerImpot2(marié, 0, salaire);
impot2 = result2["impôt"];
// application of the family allowance ceiling
plafondDemiPart = this.taxAdminData.plafondQfDemiPart;
if (enfants < 3) {
// PLAFOND_QF_DEMI_PART euros for the first 2 children
impot2 = impot2 - enfants * plafondDemiPart;
} else {
// PLAFOND_QF_DEMI_PART euros for the first 2 children, double for subsequent children
impot2 = impot2 - 2 * plafondDemiPart - (enfants - 2) * 2 * plafondDemiPart;
}
} else {
// no tax recalculation
impot2 = impot1;
result2 = result1;
}
// we take the highest tax in [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"];
}
// calculation of any discount
const décôte = this.getDecôte(marié, impot);
impot -= décôte;
// calculation of any tax reduction
const réduction = this.getRéduction(marié, salaire, enfants, impot);
impot -= réduction;
// result
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) {
// married: yes, no
// children: number of children
// salary: annual salary
// this->taxAdminData: tax administration data
//
// number of shares
marié = marié.toLowerCase();
let nbParts;
if (marié === "oui") {
nbParts = enfants / 2 + 2;
} else {
nbParts = enfants / 2 + 1;
}
// 1 part per child from the 3rd
if (enfants >= 3) {
// an additional half share for each child from the 3rd onwards
nbParts += 0.5 * (enfants - 2);
}
// taxable income
const revenuImposable = this.getRevenuImposable(salaire);
// surcharge
let surcôte = Math.floor(revenuImposable - 0.9 * salaire);
// for rounding problems
if (surcôte < 0) {
surcôte = 0;
}
// family quotient
const quotient = revenuImposable / nbParts;
// tAX CALCULATION
const limites = this.taxAdminData.limites;
const coeffR = this.taxAdminData.coeffR;
const coeffN = this.taxAdminData.coeffN;
// is set at the end of the limit table to stop the following loop
limites[limites.length - 1] = quotient;
// tax rate search
let i = 0;
while (quotient > limites[i]) {
i++;
}
// because we've placed quotient at the end of the limit array, the previous loop
// cannot go beyond the limit table
// now we can calculate the tax
const impôt = Math.floor(revenuImposable * coeffR[i] - nbParts * coeffN[i]);
// result
return { "impôt": impôt, "surcôte": surcôte, "taux": coeffR[i] };
}
// revenuImposable=annualwage-discount
// the allowance has a minimum and a maximum
getRevenuImposable(salaire) {
// 10% salary deduction
let abattement = 0.1 * salaire;
// this allowance cannot exceed taxAdminData.getAbattementDixPourCentMax()
if (abattement > this.taxAdminData.abattementDixPourCentMax) {
abattement = this.taxAdminData.abattementDixPourcentMax;
}
// the allowance cannot be less than taxAdminData.getAbattementDixPourcentMin()
if (abattement < this.taxAdminData.abattementDixPourcentMin) {
abattement = this.taxAdminData.abattementDixPourcentMin;
}
// taxable income
const revenuImposable = salaire - abattement;
// result
return Math.floor(revenuImposable);
}
// calculates any discount
getDecôte(marié, impots) {
// at the outset, a zero discount
let décôte = 0;
// maximum tax amount to qualify for discount
let plafondImpôtPourDécôte = marié === "oui" ?
this.taxAdminData.plafondImpotCouplePourDecote :
this.taxAdminData.plafondImpotCelibatairePourDecote;
let plafondDécôte;
if (impots < plafondImpôtPourDécôte) {
// maximum discount
plafondDécôte = marié === "oui" ?
this.taxAdminData.plafondDecoteCouple :
this.taxAdminData.plafondDecoteCelibataire;
// theoretical discount
décôte = plafondDécôte - 0.75 * impots;
// the discount cannot exceed the amount of tax due
if (décôte > impots) {
décôte = impots;
}
// no discount <0
if (décôte < 0) {
décôte = 0;
}
}
// result
return Math.ceil(décôte);
}
// calculates any reduction
getRéduction(marié, salaire, enfants, impots) {
// the income ceiling to qualify for the 20% reduction
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;
}
// taxable income
const revenuImposable = this.getRevenuImposable(salaire);
// reduction
let réduction = 0;
if (revenuImposable < plafondRevenuPourRéduction) {
// 20% discount
réduction = 0.2 * impots;
}
// result
return Math.ceil(réduction);
}
}
// class export
export default Métier;
- O código JavaScript segue de perto o código PHP;
- A classe [Business] é 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';
// imports
import qs from 'qs'
class Dao2 {
// manufacturer
constructor(axios) {
this.axios = axios;
// session cookie
this.sessionCookieName = "PHPSESSID";
this.sessionCookie = '';
}
// init session
async initSession() {
// query options HHTP [get /main.php?action=init-session&type=json]
const options = {
method: "GET",
// URL parameters
params: {
action: 'init-session',
type: 'json'
}
};
// execute query HTTP
return await this.getRemoteData(options);
}
async authentifierUtilisateur(user, password) {
// query options HHTP [post /main.php?action=authenticate-user]
const options = {
method: "POST",
headers: {
'Content-type': 'application/x-www-form-urlencoded',
},
// body of POST
data: qs.stringify({
user: user,
password: password
}),
// URL parameters
params: {
action: 'authentifier-utilisateur'
}
};
// execute query HTTP
return await this.getRemoteData(options);
}
async getAdminData() {
// query options HHTP [get /main.php?action=get-admindata]
const options = {
method: "GET",
// URL parameters
params: {
action: 'get-admindata'
}
};
// execute query HTTP
const data = await this.getRemoteData(options);
// result
return data;
}
async getRemoteData(options) {
// for the session cookie
if (!options.headers) {
options.headers = {};
}
options.headers.Cookie = this.sessionCookie;
// execute query HTTP
let response;
try {
// asynchronous request
response = await this.axios.request('main.php', options);
} catch (error) {
// the [error] parameter is an exception instance - it can take various forms
if (error.response) {
// the server response is in [error.response]
response = error.response;
} else {
// error restart
throw error;
}
}
// response is the entire HTTP response from the server (HTTP headers + response itself)
// retrieve the session cookie if it exists
const setCookie = response.headers['set-cookie'];
if (setCookie) {
// setCookie is an array
// look for the session cookie in this table
let trouvé = false;
let i = 0;
while (!trouvé && i < setCookie.length) {
// look for the session cookie
const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
if (results) {
// the session cookie is stored
// eslint-disable-next-line require-atomic-updates
this.sessionCookie = results[1];
// we found
trouvé = true;
} else {
// next item
i++;
}
}
}
// the server response is in [response.data]
return response.data;
}
}
// class export
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;
- [authenticate-user] (linhas 31–50): para autenticar;
- [get-admindata] (linhas 52–65): para recuperar os dados de administração fiscal necessários para realizar cálculos de impostos no lado do cliente;
- linhas 52–65: introduzimos uma nova ação [get-admindata] no servidor. Esta ação não tinha sido implementada até agora. Estamos a fazê-lo agora.
14.3.3. Modificação do servidor de cálculo de impostos
O servidor de cálculo de impostos deve implementar uma nova ação. Faremos isso na versão 14 do servidor. A ação a ser implementada 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 de administração fiscal;
Iremos rever como adicionar uma ação ao nosso servidor.
A modificação será feita 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 modificaçã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 serem carregadas pela aplicação PHP;
O próximo passo é implementar o controlador [AdminDataController] [3]:
<?php
namespace Application;
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// layer alias [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
class AdminDataController implements InterfaceController {
// $config is the application configuration
// traitement d'une requête Request
// session and can modify it
// $infos is additional information specific to each controller
// renders an array [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
// you must have a single parameter GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 1;
if ($erreur) {
// we note the error
$message = "il faut utiliser la méthode [get] avec l'unique paramètre [action] dans l'URL";
$état = 1001;
// return result to main controller
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// we can work
// Redis
\Predis\Autoloader::register();
try {
// customer [predis]
$redis = new \Predis\Client();
// connect to the server to see if it's there
$redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
// it didn't go well
// return result with error to main controller
$état = 1050;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
}
// data recovery from tax authorities
// first search the cache [redis]
if (!$redis->get("taxAdminData")) {
try {
// retrieve tax data from the database
$dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
// taxAdminData
$taxAdminData = $dao->getTaxAdminData();
// put the recovered data into redis
$redis->set("taxAdminData", $taxAdminData);
} catch (\RuntimeException $ex) {
// it didn't go well
// return result with error to main controller
$état = 1041;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => utf8_encode($ex->getMessage())], []];
}
} else {
// tax data are taken from the [redis] memory of the [application] scope
$arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
// we instantiate an object [TaxAdminData] from the previous attribute array
$taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
}
// return result to main controller
$é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], que consiste no método [execute] nas linhas 19–79;
- linha 78: tal como acontece com os outros controladores do servidor, o método [AdminDataController.execute] devolve uma matriz [$status, $status, [‘response’=>$response]] com:
- [$status]: o código de estado da resposta HTTP;
- [$état]: um código interno da aplicação que representa o estado do servidor após a execução do pedido do cliente;
- [$response]: uma matriz que encapsula a resposta a ser enviada ao cliente. Aqui, esta matriz será posteriormente convertida numa cadeia JSON;
- linhas 25–34: verificamos se a ação [get-admindata] do cliente está sintaticamente correta;
- linhas 37–74: recuperamos um objeto [TaxAdminData] encontrado:
- linhas 56–59: da base de dados, caso não tenha sido encontrado no cache [redis];
- linhas 70–73: no cache [redis];
Este código foi retirado do controlador [CalculerImpotController] explicado no artigo em link. Na verdade, este controlador também precisava de recuperar o objeto [TaxAdminData] que encapsula os dados da administração fiscal.
Durante os testes do cliente JavaScript, o formato JSON de [TaxAdminData] causou problemas quando este objeto foi encontrado no cache [redis]. Para compreender porquê, vamos examinar como este objeto é armazenado no [redis]:


- Em [5-7], vemos que os valores numéricos foram armazenados como cadeias de caracteres. O PHP lidou com isto porque o operador + em cálculos que envolvem números e cadeias de caracteres provoca implicitamente uma conversão de tipo de cadeia de caracteres para número. Mas o JavaScript faz o oposto: o operador + em cálculos que envolvem números e cadeias de caracteres provoca implicitamente uma conversão de tipo de número para cadeia de caracteres. Os cálculos na classe [Métier] do JavaScript estão, portanto, incorretos;
Para resolver este problema, modificamos 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 {
// tax brackets
protected $limites;
protected $coeffR;
protected $coeffN;
// tax calculation constants
protected $plafondQfDemiPart;
protected $plafondRevenusCelibatairePourReduction;
protected $plafondRevenusCouplePourReduction;
protected $valeurReducDemiPart;
protected $plafondDecoteCelibataire;
protected $plafondDecoteCouple;
protected $plafondImpotCouplePourDecote;
protected $plafondImpotCelibatairePourDecote;
protected $abattementDixPourcentMax;
protected $abattementDixPourcentMin;
// initialization
public function setFromJsonFile(string $taxAdminDataFilename) {
// parent
parent::setFromJsonFile($taxAdminDataFilename);
// check attribute values
$this->checkAttributes();
// we return the object
return $this;
}
protected function check($value): \stdClass {
// $value is an array of string elements or a single element
if (!\is_array($value)) {
$tableau = [$value];
} else {
$tableau = $value;
}
// transform the array of strings into an array of reals
$newTableau = [];
$result = new \stdClass();
// table elements must be positive or zero decimal numbers
$modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
for ($i = 0; $i < count($tableau); $i ++) {
if (preg_match($modèle, $tableau[$i])) {
// put the float in newTableau
$newTableau[] = (float) $tableau[$i];
} else {
// we note the error
$result->erreur = TRUE;
// we leave
return $result;
}
}
// we return the result
$result->erreur = FALSE;
if (!\is_array($value)) {
// a single value
$result->value = $newTableau[0];
} else {
// a list of values
$result->value = $newTableau;
}
return $result;
}
// initialization by an array of attributes
public function setFromArrayOfAttributes(array $arrayOfAttributes) {
// parent
parent::setFromArrayOfAttributes($arrayOfAttributes);
// check attribute values
$this->checkAttributes();
// we return the object
return $this;
}
// checking attribute values
protected function checkAttributes() {
// check that attribute values are real >=0
foreach ($this as $key => $value) {
if (is_string($value)) {
// $value must be a real number >=0 or an array of reals >=0
$result = $this->check($value);
// mistake?
if ($result->erreur) {
// throw an exception
throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
} else {
// we note the value
$this->$key = $result->value;
}
}
}
// we return the object
return $this;
}
// getters and setters
...
}
Comentários
- linha 5: a classe [TaxAdminData] estende a classe [BaseEntity], que já possui o método [setFromArrayOfAttributes]. Como este método não é adequado, redefinimo-lo nas linhas 67–75;
- linha 70: o método [setFromArrayOfAttributes] da classe pai é utilizado primeiro para inicializar os atributos da classe;
- linha 72: o método [checkAttributes] verifica se os valores associados são, de facto, números. Se forem cadeias de caracteres, são convertidos em números;
- linha 74: o objeto [$this] resultante é, então, um objeto com atributos que possuem valores 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: a lista de atributos é percorrida;
- linha 81: se o valor de um atributo for do tipo [string];
- linha 83: então verificamos se essa cadeia de caracteres representa um número;
- linha 90: se for o caso, a string é convertida num número e atribuída ao atributo que está a ser testado;
- linhas 85–86: caso contrário, é lançada uma exceção;
- linhas 32–65: a função [check] faz um pouco mais do que o necessário. Ela lida tanto com matrizes como com valores únicos. No entanto, aqui é chamada apenas para verificar um valor do tipo [string]. Ela retorna um objeto com as propriedades [error, value], onde:
- [error] é um booleano que indica se ocorreu ou não um erro;
- [value] é o parâmetro [value] da linha 32, convertido num número ou numa matriz de números, conforme apropriado;
A classe [BaseEntity], que anteriormente tinha um atributo chamado [arrayOfAttributes], foi modificada para remover este atributo: estava a causar problemas com a cadeia JSON [TaxAdminData]. A classe foi reescrita da seguinte forma:
<?php
namespace Application;
class BaseEntity {
// initialization from a JSON file
public function setFromJsonFile(string $jsonFilename) {
// retrieve the contents of the tax data file
$fileContents = \file_get_contents($jsonFilename);
$erreur = FALSE;
// mistake?
if (!$fileContents) {
// we note the error
$erreur = TRUE;
$message = "Le fichier des données [$jsonFilename] n'existe pas";
}
if (!$erreur) {
// retrieve the JSON code from the configuration file in an associative array
$arrayOfAttributes = \json_decode($fileContents, true);
// mistake?
if ($arrayOfAttributes === FALSE) {
// we note the error
$erreur = TRUE;
$message = "Le fichier de données JSON [$jsonFilename] n'a pu être exploité correctement";
}
}
// mistake?
if ($erreur) {
// throw an exception
throw new ExceptionImpots($message);
}
// initialization of class attributes
foreach ($arrayOfAttributes as $key => $value) {
$this->$key = $value;
}
// we check the presence of all attributes
$this->checkForAllAttributes($arrayOfAttributes);
// we return the object
return $this;
}
public function checkForAllAttributes($arrayOfAttributes) {
// check that all keys have been initialized
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) {
// initialize certain class attributes (not necessarily all)
foreach ($arrayOfAttributes as $key => $value) {
$this->$key = $value;
}
// object is returned
return $this;
}
// toString
public function __toString() {
// object attributes
$arrayOfAttributes = \get_object_vars($this);
// string jSON of object
return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
}
}
Comentários
- linha 20: o atributo [$this→arrayOfAttributes] foi convertido numa variável que agora deve ser passada para o método [checkForAllAttributes] na linha 38, que anteriormente operava sobre o atributo [$this→arrayOfAttributes];
Devido a esta alteração em [BaseEntity], a classe [Database] também deve ser ligeiramente modificada:
<?php
namespace Application;
class Database extends BaseEntity {
// attributes
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
// initialization
public function setFromJsonFile(string $jsonFilename) {
// parent
parent::setFromJsonFile($jsonFilename);
// object is returned
return $this;
}
// getters and setters
...
}
Comentários
- No código original, após a linha 30, era chamado o método [parent::checkForAllAttributes]. Isto já não é necessário, uma vez que agora é tratado automaticamente pelo método [parent::setFromJsonFile($jsonFilename)];
14.3.4. Testes de servidor [Postman]
O [Postman] foi apresentado no artigo em link.
Utilizamos os seguintes testes do Postman:



O resultado JSON desta última solicitação é o seguinte:

- Em [5-8], pode ver-se que os atributos na cadeia JSON têm, de facto, valores numéricos (e não cadeias de caracteres). Este resultado permitirá que a classe JavaScript [Business] seja executada normalmente;
14.3.5. O script principal [main]

O script principal [main] do cliente JavaScript é o seguinte:
// imports
import axios from 'axios';
// imports
import Dao from './Dao2';
import Métier from './Métier';
// asynchronous function [main]
async function main() {
// axios configuration
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
// layer instantiation [dao]
const dao = new Dao(axios);
// requests HTTP
let taxAdminData;
try {
// init session
log("-----------init-session");
let response = await dao.initSession();
log(response);
// authentication
log("-----------authentifier-utilisateur");
response = await dao.authentifierUtilisateur("admin", "admin");
log(response);
// tax information
log("-----------get-admindata");
response = await dao.getAdminData();
log(response);
taxAdminData = response.réponse;
} catch (error) {
// we log the error
console.log("erreur=", error.message);
// end
return;
}
// instantiation layer [business]
const métier = new Métier(taxAdminData);
// tax calculations
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));
// list of simulations
log("-----------liste-des-simulations");
log(simulations);
// deleting a simulation
log("-----------suppression simulation n° 1");
simulations.splice(1, 1);
log(simulations);
}
// log jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// execution
main();
Comentários
- linhas 5-6: importações das classes [Dao] e [Business];
- linha 9: a função assíncrona [main] que irá gerir a comunicação com o servidor utilizando a classe [Dao] e solicitar à classe [Business] que efetue os cálculos de impostos;
- linhas 10-36: o script chama os métodos [initSession, authenticateUser, getAdminData] da camada [Dao] sequencialmente e de forma bloqueante;
- linha 38: já não precisamos da camada [dao]. Temos todos os elementos necessários para executar a camada [business] do cliente JavaScript;
- linhas 41–46: realizamos três cálculos de impostos e armazenamos os resultados numa matriz [simulations];
- linha 49: exibimos a matriz de simulações;
- linha 52: removemos 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, portamos a aplicação [Cliente HTTP 2] para um navegador utilizando a seguinte arquitetura:

O processo de portabilidade não é simples. Embora o [node.js] possa executar JavaScript ES6, isso geralmente não acontece nos navegadores. Por isso, temos de utilizar ferramentas que traduzam o código ES6 para código ES5, compreendido pelos navegadores modernos. Felizmente, estas ferramentas são poderosas e bastante fáceis de utilizar.
Aqui, seguimos o artigo [Como escrever código ES6 que seja seguro para executar no navegador - Web Developer's Journal].
Na pasta [client HTTP 3/src], colocámos os ficheiros [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
Iremos 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 padrão às perguntas feitas:

- em [4-5], o ficheiro de configuração do projeto [package.json] gerado a partir das várias respostas fornecidas;
14.4.2. Instalação das dependências do projeto
Iremos 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 tanto em navegadores modernos como em navegadores mais antigos;
- [@babel/preset-env]: faz parte do conjunto de ferramentas do Babel. É executado antes da transpilagem de ES6 para ES5;
- [babel-loader]: esta dependência permite que a ferramenta [webpack] recorra à ferramenta [Babel];
- [webpack]: o orquestrador. É o [webpack] que invoca o Babel para transcompilar código ES6 para ES5 e, em seguida, reúne 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 origem são modificados, recarrega automaticamente a aplicação web;
As dependências do projeto são instaladas da seguinte forma num terminal [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] ficou alterado da seguinte forma:
{
"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]: precisamos delas durante a fase de desenvolvimento, mas não na fase de produção. Em produção, é utilizado o ficheiro [dist/main.js]. Este está escrito em ES5 e já não requer ferramentas para transpilá-lo de ES6 para ES5;
Precisamos de adicionar duas dependências ao projeto:
- [core-js]: contém «polyfills» para o ECMAScript 2019. Um polyfill permite que código recente, como o ECMAScript 2019 (setembro de 2019), seja executado em navegadores mais antigos;
- [regenerator-runtime]: de acordo com o site da biblioteca --> [Transformador de código-fonte que permite funções geradoras do ECMAScript 6 no JavaScript atual];
A partir do Babel 7, estas duas dependências substituem a dependência [@babel/polyfill], que anteriormente servia este propósito e está agora (setembro de 2019) 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"
}
}
Para utilizar as dependências [core-js, regenerator-runtime], é necessário adicionar as seguintes [imports] (linhas 3–4) ao script principal [src/main.js]:
// imports
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
// imports
import Dao from './Dao2';
import Métier from './Métier';
14.4.3. Configuração do [webpack]
[webpack] é a ferramenta que irá tratar:
- a transpilagem de todos os ficheiros JavaScript do projeto de ES6 para ES5;
- o agrupamento dos ficheiros gerados num único ficheiro;
Esta ferramenta é controlada por um ficheiro de configuração [webpack.config.js], que pode ser gerado utilizando uma dependência chamada [@webpack-cli/init] (setembro de 2019). Esta dependência foi instalada juntamente com as outras mencionadas na secção «Link».
Executamos o comando [npx webpack-cli init] num terminal do [VSCode]:

Após responder às várias perguntas (para as quais podemos aceitar a maioria das respostas predefinidas), é 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 há alguns aspetos que se destacam:
- linha 1: o ficheiro não contém código ES6. O [Eslint] reporta então erros que se propagam até à raiz do projeto [javascript]. Isto é irritante. Para impedir que o Eslint analise um ficheiro, basta comentar a linha 1;
- linha 31: estamos a trabalhar no modo [development];
- linha 32: o script de entrada está aqui [src/index.js]. Teremos de alterar isto;
- linha 36: a pasta onde a saída do [webpack] será colocada é a pasta [dist];
- linha 46: podemos ver que o [webpack] usa o [babel-loader], uma das dependências que instalámos;
- linha 54: vemos que o [webpack] usa o [@babel-preset/env], uma das dependências que instalámos;
A inicialização do [webpack] modificou o ficheiro [package.json] (pede permissã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 modificada;
- linhas 8–9, 18–19: foram adicionadas;
- linha 8: a tarefa [npm] que compila o projeto;
- linha 9: a tarefa [npm] que a executa;
- linha 18: ?
- linha 19: gera um ficheiro [dist/index.html] que incorpora automaticamente o script [dist/main.js] gerado pelo [webpack], e é este que é utilizado quando o projeto é executado;
Por fim, a configuração [webpack] gerou um ficheiro [src/index.js]:

O conteúdo de [index.js] é o seguinte (setembro de 2019):
console.log("Hello World from your main file!");
14.4.4. Compilar e executar o 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 reconhecidas pelo [VSCode], que as disponibiliza para execução:

- em [1-3], o projeto é compilado;
- em [4]: o projeto é compilado para [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 simplesmente encapsula o ficheiro [main.hash.js] gerado pelo [webpack].
O projeto é executado pela tarefa [start]:

A página [dist/index.html] é então carregada num servidor, parte do conjunto [webpack], a funcionar 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 [webpack];
- em [3], o corpo da página [dist/index.html] está vazio;
- em [4], o separador [console] das ferramentas de programador do navegador, neste caso o Firefox (F12);
- em [5], o resultado da execução do ficheiro [src/index.js]. Recorde-se que o seu conteúdo era o seguinte:
Agora, vamos alterar este 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. Ela não armazena o resultado dessa compilação na pasta [dist]. Para verificar isso, basta eliminar essa pasta. Veremos então que a tarefa [start] compila e executa o projeto sem criar a pasta [dist]. Parece que armazena o seu resultado [index.html, main.hash.js] numa pasta específica do [webpackdev-server]. Este comportamento é suficiente para os nossos testes.
Quando o servidor de desenvolvimento está em execução, quaisquer alterações guardadas num ficheiro do projeto desencadeiam uma recompilação. Por este motivo, desativamos o modo [Auto Save] do [VSCode]. Não queremos que o projeto seja recompilado sempre que digitamos caracteres num ficheiro do projeto. Queremos que a recompilação ocorra apenas quando as alterações são guardadas:

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

Lembre-se de que o script [main.js] deve incluir duas importações adicionais em comparação com a sua versão em [Cliente HTTP 2]:

Além disso, modificámos ligeiramente o código para lidar com os erros que o servidor possa enviar:
// imports
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
// imports
import Dao from './Dao2';
import Métier from './Métier';
// asynchronous function [main]
async function main() {
// axios configuration
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
// layer instantiation [dao]
const dao = new Dao(axios);
// requests HTTP
let taxAdminData;
try {
// init session
log("-----------init-session");
let response = await dao.initSession();
log(response);
if (response.état != 700) {
throw new Error(JSON.stringify(response.réponse));
}
// authentication
log("-----------authentifier-utilisateur");
response = await dao.authentifierUtilisateur("admin", "admin");
log(response);
if (response.état != 200) {
throw new Error(JSON.stringify(response.réponse));
}
// tax information
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) {
// we log the error
console.log("erreur=", error.message);
// end
return;
}
// instantiation layer [business]
const métier = new Métier(taxAdminData);
// tax calculations
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));
// list of simulations
log("-----------liste-des-simulations");
log(simulations);
// deleting a simulation
log("-----------suppression simulation n° 1");
simulations.splice(1, 1);
log(simulations);
}
// log jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// execution
main();
Comentários
- Nas linhas [24-26], [31-33] e [38-40], verificamos o código [response.status] enviado na resposta JSON do servidor. Se este código indicar um erro, é lançada uma exceção com a cadeia JSON da resposta do servidor [response.response] como mensagem de erro;
Depois de feito isso, executamos o projeto [5-6].
A página [index.html] é então gerada e carregada no navegador:

- Em [7], vemos que a ação [init-session] não pôde ser concluída devido a um problema de [CORS] (Cross-Origin Resource Sharing);
O problema de 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 funciona na máquina [http://localhost:80];
- o cliente e o servidor não estão, portanto, no mesmo domínio (mesma máquina, mas portas diferentes);
- 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 conseguinte, bloqueia o pedido do cliente ao servidor que opera na máquina [http://localhost:80];
Na verdade, o navegador não bloqueia completamente a solicitação. Na realidade, ele aguarda que o servidor lhe «indique» que aceita solicitações entre domínios. Se receber 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 utilizará os cabeçalhos HTTP [Accept, Content-Type] nas suas solicitações:
- [Accept]: este cabeçalho é enviado em todas as solicitações;
- [Content-Type]: este cabeçalho é utilizado em operações POST para especificar o tipo dos parâmetros POST;
O servidor deve aceitar explicitamente estes dois cabeçalhos HTTP;
- Linha 3: O cliente JavaScript utilizará pedidos GET e POST. O servidor deve aceitar explicitamente estes dois tipos de pedidos;
- Linha 4: O cliente JavaScript enviará cookies de sessão. O servidor aceita-os com o cabeçalho na linha 4;
Por isso, precisamos de alterar a configuração do servidor. Fazemos isso no [NetBeans]. O problema relacionado com o CORS 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á problemas relacionados com o CORS. Por isso, precisamos de uma forma de ativar ou desativar os pedidos CORS através da configuração do servidor.

As modificações no servidor são feitas em três locais:
- [1, 4]: no ficheiro de configuração [config.json] para definir um valor booleano que controla 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 enviará os cabeçalhos CORS esperados pelo navegador do cliente;
- [3]: nas classes [HtmlResponse, JsonResponse, XmlResponse] que geram respostas para as sessões [html, json, xml], respetivamente. Estas classes devem passar o valor booleano [corsAllowed] encontrado em [4] para a sua classe pai [2]. Isto é feito em [5], passando a matriz de imagens do ficheiro JSON [2];
A classe [ParentResponse] [2] evolui da seguinte forma:
<?php
namespace Application;
// symfony dependencies
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
class ParentResponse {
// int $statusCode: HTTP response status code
// string $content: the body of the response to be sent
// depending on the case, this is a JSON, XML, HTML string
// array $headers: HTTP headers to be added to the response
public function sendResponse(
Request $request,
int $statusCode,
string $content,
array $headers,
array $config): void {
// preparing the server's text response
$response = new Response();
$response->setCharset("utf-8");
// status code
$response->setStatusCode($statusCode);
// headers for cross-domain requests
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);
}
// special case of the [OPTIONS] method
// only the headers are important in this case
$method = strtolower($request->getMethod());
if ($method === "options") {
$content = "";
$response->setStatusCode(Response::HTTP_OK);
}
// we send the answer
$response->setContent($content);
$response->send();
}
}
- linha 29: verificamos se precisamos de lidar com pedidos entre domínios. Se for o caso, geramos 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 consulta 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 da solicitação [$request];
- linha 31: só aceitaremos pedidos entre domínios originários da máquina [http://localhost]. Note-se que estes pedidos só ocorrem no modo de desenvolvimento do projeto;
- Linhas 32–36: Adicionamos os cabeçalhos CORS aos cabeçalhos já presentes na matriz [$headers];
- Linhas 45–49: A forma como o navegador do cliente solicita permissões CORS pode variar dependendo do navegador utilizado. Por vezes, o navegador do cliente solicita estas permissões utilizando uma solicitação HTTP [OPTIONS]. Este é um novo cenário para o nosso servidor, que foi construído para lidar apenas com solicitações [GET] e [POST]. No caso de uma solicitação [OPTIONS], o servidor gera atualmente uma resposta de erro. Linhas 46–49: corrigimos isto no último momento: se, na linha 46, determinarmos que a solicitação atual é uma solicitação [OPTIONS], então geramos o seguinte para o cliente:
- linhas 47, 51: uma resposta [$content] vazia;
- linha 48: um código de estado 200 indicando que a solicitação foi bem-sucedida. A única coisa importante para esta solicitação é enviar os cabeçalhos CORS nas linhas 33–36. É isso que o navegador do cliente espera;
Depois de o servidor ter sido corrigido desta forma, o cliente JavaScript funciona melhor, mas apresenta um novo erro:

- em [1], a sessão JSON é inicializada corretamente;
- Em [2], a ação [authenticate-user] falha: o servidor indica que não existe nenhuma sessão ativa. Isto significa que o cliente JavaScript não devolveu corretamente o cookie de sessão que enviou durante a ação [init-session];
Vamos analisar as interações que ocorreram na rede:

- em [4], o pedido [init-session]. Foi concluído com sucesso com um código de estado 200 na resposta;
- em [5], o pedido [authenticate-user]. Este pedido falha com um código de estado 400 (Bad Request) [6] na resposta;
Se analisarmos os cabeçalhos HTTP [7] da solicitação [5], podemos ver que o cliente JavaScript não enviou o cabeçalho HTTP [Cookie], o que lhe teria permitido devolver o cookie de sessão inicialmente enviado pelo servidor. É por isso que o servidor informa que não há 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";
// imports
import Dao from './Dao2';
import Métier from './Métier';
// asynchronous function [main]
async function main() {
// axios configuration
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
axios.defaults.withCredentials = true;
// layer instantiation [dao]
const dao = new Dao(axios);
// requests HTTP
let taxAdminData;
...
A linha 15 solicita que os cookies sejam incluídos nos cabeçalhos HTTP da solicitação [axios]. Note que isto não era 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 no cliente HTTP 3
Quando a classe [Dao2] anterior é executada num navegador, a gestão de cookies de sessão torna-se desnecessária. Isto porque o navegador que hospeda a camada [dao] gere o cookie de sessão: reenvia automaticamente qualquer cookie que o servidor lhe envie. Consequentemente, a classe [Dao2] pode ser reescrita como a seguinte classe [Dao3]:
"use strict";
// imports
import qs from "qs";
class Dao3 {
// manufacturer
constructor(axios) {
this.axios = axios;
}
// init session
async initSession() {
// query options HHTP [get /main.php?action=init-session&type=json]
const options = {
method: "GET",
// URL parameters
params: {
action: "init-session",
type: "json"
}
};
// execute query HTTP
return await this.getRemoteData(options);
}
async authentifierUtilisateur(user, password) {
// query options HHTP [post /main.php?action=authenticate-user]
const options = {
method: "POST",
headers: {
"Content-type": "application/x-www-form-urlencoded"
},
// body of POST
data: qs.stringify({
user: user,
password: password
}),
// URL parameters
params: {
action: "authentifier-utilisateur"
}
};
// execute query HTTP
return await this.getRemoteData(options);
}
async getAdminData() {
// query options HHTP [get /main.php?action=get-admindata]
const options = {
method: "GET",
// URL parameters
params: {
action: "get-admindata"
}
};
// execute query HTTP
const data = await this.getRemoteData(options);
// result
return data;
}
async getRemoteData(options) {
// execute query HTTP
let response;
try {
// asynchronous request
response = await this.axios.request("main.php", options);
} catch (error) {
// the [error] parameter is an exception instance - it can take various forms
if (error.response) {
// the server response is in [error.response]
response = error.response;
} else {
// error restart
throw error;
}
}
// response is the entire HTTP response from the server (HTTP headers + response itself)
// the server response is in [response.data]
return response.data;
}
}
// class export
export default Dao3;
Tudo o que está relacionado com a gestão do cookie de gestão desapareceu.
Modificamos o projeto anterior da seguinte forma:

Na pasta [src], adicionámos dois ficheiros:
- a classe [Dao3] que acabámos de introduzir;
- 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 agora utiliza a classe [Dao3]:
// imports
import axios from "axios";
import "core-js/stable";
import "regenerator-runtime/runtime";
// imports
import Dao from "./Dao3";
import Métier from "./Métier";
// asynchronous function [main]
async function main() {
// axios configuration
axios.defaults.timeout = 2000;
axios.defaults.baseURL =
"http://localhost/php7/scripts-web/impots/version-14";
axios.defaults.withCredentials = true;
// layer instantiation [dao]
const dao = new Dao(axios);
// requests HTTP
...
}
// log jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// execution
main();
O ficheiro [webpack.config] foi modificado 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",
//entry: "./src/mainjs",
entry: "./src/main3.js",
output: {
filename: "[name].[chunkhash].js",
path: path.resolve(__dirname, "dist")
},
plugins: [new webpack.ProgressPlugin(), new HtmlWebpackPlugin()],
...
};
Depois de fazer isto, executamos o projeto após iniciar o servidor de cálculo de impostos:

Os resultados apresentados na consola do navegador são idênticos aos da versão anterior.
14.6. Conclusão
Agora dispomos de todas as ferramentas necessárias para desenvolver código JavaScript para uma aplicação web. Podemos:
- utilizar o código ECMAScript mais recente;
- testar partes isoladas deste código num ambiente [node.js], o que é mais simples para depuração e testes;
- depois portar esse código para um navegador utilizando [babel] e [webpack];