Skip to content

14. Clients HTTP Javascript from the tax calculation service

14.1. Introduction

Here, we propose to write a client [node.js] for version 14 of the tax calculation service. The client/server architecture will be as follows:

Image

We will examine two versions of the client:

  • Client version 1 will have the following layered structure:

Image

  • Client version 2 will have a [main, métier, dao] structure. The server’s [métier] layer will be offloaded to the client:

Image

14.2. Client HTTP 1

Image

As mentioned, the client HTTP 1 implements the following client/server architecture:

Image

We will implement:

  • the [dao] layer as a class;
  • the [main] layer as a script using this class;

14.2.1. The [dao] layer

The [dao] layer will be implemented by the following class [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) {
      // parameter [error] 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;
  • Here we use what we learned in the linked section, where we introduced the [axios] library that allows you to make HTTP requests both in [node.js] and in a browser. We will look in particular at the script in the linked section;
  • lines 9–15: the class constructor. This class will have three properties:
    • [axios]: the [axios] object used to make HTTP requests. This is passed by the calling code;
    • [sessionCookieName]: depending on the server, the session cookie has different names. Here, it is [PHPSESSID];
    • [sessionCookie]: the session cookie sent by the server and stored by the client;
  • lines 53–76: the asynchronous function [calculerImpot] makes the request [post /main.php?action=calculer-impot] by posting the parameters [marié, enfants, salaire]. It returns the string jSON transmitted by the server in the form of an object Javascript;
  • lines 79–92: the asynchronous function [listeSimulations] makes the request [get /main.php?action=lister-simulations. It returns the string jSON transmitted by the server in the form of an object Javascript;
  • Lines 95–109: The asynchronous function [supprimerSimulation] makes the request [get /main.php?action=supprimer-simulation&numéro=index]. It returns the string jSON transmitted by the server in the form of an object Javascript;
  • line 121: the notation [this.axios] is used because here, the [axios] object passed to the constructor has been stored in the [this.axios] property;
  • line 161: the class [Dao1] is exported so it can be used;

14.2.2. The [main1.js] script

The script [main1.js] makes a series of calls to the server using the class [Dao1]:

  • initialization of a session jSON;
  • authentication with [admin, admin];
  • requests three tax calculations;
  • requests the list of simulations;
  • deletes one of them;

The code is as follows:


// 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';
  // instantiation layer [dao]
  const dao = new Dao(axios);
  // use of 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();

Comments

  • line 2: import the [axios] library;
  • line 4: import the [Dao] class;
  • line 7: the [main] function that communicates with the server is asynchronous;
  • lines 9-10: default configuration of the HTTP requests that will be sent to the server:
    • line 9: [timeout] with a 2-second timeout;
    • line 10: all URL requests are prefixed with the base URL of the tax calculation server’s version 14;
  • line 12: the [Dao] layer is built. It can now be used;
  • lines 46–48: the function [log] is designed to display the string jSON from an object Javascript in a formatted manner: vertically with a two-space indentation (3rd parameter);
  • lines 15–18: initialization of the jSON session;
  • lines 19–22: authentication;
  • lines 23–30: three tax calculations are requested in parallel. Thanks to [await Promise.all], execution is blocked until all three results have been obtained;
  • lines 31–34: list of simulations;
  • lines 35–38: deletion of a simulation;
  • lines 39–42: handling of any exceptions;

The execution results are as follows:


[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. Client HTTP 2

Image

The architecture of the HTTP2 client is as follows:

Image

The [métier] layer has been moved from the server to the Javascript client. Unlike what we did in the PHP7 course, the [main] layer will not have to pass through the [métier] layer to reach the [dao] layer. We will use these two layers as centers of expertise:

  • the [main] layer goes through the [dao] layer whenever it needs data that is on the server;
  • the [main] layer asks the [métier] layer to perform the tax calculations;
  • The [métier] layer is independent of the [dao] layer and never calls upon it;

14.3.1. The class Javascript [Métier]

The essence of the class [Métier] in PHP was described in the linked article. It is a rather complex code that we recall here, not to explain it, but to be able to translate it into Javascript:


<?php
 
// namespace
namespace Application;
 
class Metier implements InterfaceMetier {
  // layer Dao
  private $dao;
  // tax administration data
  private $taxAdminData;
 
  //---------------------------------------------
  // setter layer [dao]
  public function setDao(InterfaceDao $dao) {
    $this->dao = $dao;
    return $this;
  }
 
  public function __construct(InterfaceDao $dao) {
    // we store a reference 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 table 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 {
    // exceptions from the [dao] layer are allowed to bubble up
    // 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);
  }
 
}
  • lines 19–26: the constructor of the PHP class. Since we specified that we were constructing a [métier] layer independent of the [dao] layer, we will make two changes to this constructor in Javascript:
    • it will not receive an instance of the [dao] layer (it no longer needs it);
    • it will not request tax data from the [taxAdminData] administration layer to the [dao] layer: the calling code will transmit this data to the constructor;
  • lines 197–122: we will not implement the [executeBatchImpots] method, whose ultimate purpose was to save simulation results to a text file. We want code that works both under [node.js] and in a browser. However, saving data to the file system of the machine running the client browser is not possible;

Given these restrictions, the code for the Javascript and [Métier] classes is as follows:


'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;
  • The code Javascript closely follows the code PHP;
  • The class [Métier] is exported on line 187;

14.3.2. The class Javascript [Dao2]

Image

The class [Dao2] implements the [dao] layer of the above client Javascript as follows:


'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) {
      // parameter [error] 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;

Comments

  • The [Dao2] class implements only three of the possible requests to the tax calculation server:
    • [init-session] (lines 17–29): to initialize the jSON session;
    • [authentifier-utilisateur] (lines 31–50): to authenticate;
    • [get-admindata] (lines 52–65): to retrieve data from the tax authority that will enable tax calculations on the client side;
  • lines 52-65: we are introducing a new action, [get-admindata], to the server. This action had not been implemented until now. We are doing so now.

14.3.3. Modification of the tax calculation server

The tax calculation server must implement a new action. We will do this on the server’s version 14. The action to be implemented has the following characteristics:

  • it is requested by a [get /main.php?action=get-admindata] operation;
  • it returns the jSON string of an object encapsulating tax administration data;

We will review how to add an action to our server.

The modification will be made under Netbeans:

Image

In [2], we modify the file [config.json] to add the new action:


{
    "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"
}

The modification consists of:

  • line 67: add the action [get-admindata] and associate it with a controller;
  • line 36: declare this controller in the list of classes to be loaded by the PHP application;

The next step is to implement the [AdminDataController] [3] controller:


<?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
  // request processing
  // 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], []];
  }
 
}

Comments

  • line 12: like the server’s other controllers, [AdminDataController] implements the [InterfaceController] interface consisting of the [execute] method on lines 19–79;
  • line 78: as with the server’s other controllers, the [AdminDataController.execute] method returns an array [$status, $état, [‘réponse’=>$response]] containing:
    • [$status]: the response status code HTTP;
    • [$état]: an internal application code representing the server’s state after executing the client’s request;
    • [$response]: an array encapsulating the response to be sent to the client. Here, this array will later be converted into the string jSON;
  • lines 25–34: we verify that the client’s action [get-admindata] is syntactically correct;
  • lines 37–74: an object named [TaxAdminData] is retrieved; if it is found:
    • lines 56–59: from the database if it was not found in the cache [redis];
    • lines 70–73: from the cache [redis];

This code is the same as that of the [CalculerImpotController] controller explained in the linked article. In fact, this controller also had to retrieve the [TaxAdminData] object encapsulating the tax administration data.

During testing of the Javascript client, the jSON form of [TaxAdminData] caused an issue when this object was found in the [redis] cache. To understand this, let’s examine the format in which this object is stored in [redis]:

Image

Image

  • In [5-7], we see that numeric values were stored as character strings. PHP handled this because the + operator in calculations involving numbers and strings implicitly causes a type conversion from string to number. But Javascript does the opposite: the + operator in calculations involving numbers and strings implicitly causes a type cast from the number to a string. The calculations in the Javascript and [Métier] classes are therefore incorrect;

To fix this issue, we modify the [TaxAdminData.setFromArrayOfAttributes] method used on line 71 of the controller to instantiate a [TaxAdminData] object (see article) from the string jSON found in the 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 elements of type string 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
  ...
 
}

Comments

  • line 5: the [TaxAdminData] class extends the [BaseEntity] class, which already has the [setFromArrayOfAttributes] method. Since this method is not suitable, we redefine it on lines 67–75;
  • line 70: the [setFromArrayOfAttributes] method of the parent class is first used to initialize the class attributes;
  • line 72: the [checkAttributes] method checks that the associated values are indeed numbers. If they are strings, they are converted to numbers;
  • line 74: the resulting [$this] object is then an object with attributes having numeric values;
  • lines 78–93: the [checkAttributes] method verifies that the values associated with the object’s attributes are indeed numeric;
  • line 80: the list of attributes is traversed;
  • line 81: if the value of an attribute is of type [string];
  • line 83: then we verify that this string represents a number;
  • line 90: if so, the string is converted to a number and assigned to the tested attribute;
  • lines 85–86: if not, an exception is thrown;
  • Lines 32–65: The function [check] does a little more than necessary. It handles both arrays and single values. However, here it is called only to check a value of type [string]. It returns an object with the properties [erreur, value], where:
    • [erreur] is a Boolean indicating whether an error occurred or not;
    • [value] is the parameter [value] from line 32, converted to a number or an array of numbers as appropriate;

The class [BaseEntity], which could have had an attribute named [arrayOfAttributes], is modified to no longer have this attribute: it indeed pollutes the string jSON from [TaxAdminData]. The class is rewritten as follows:


<?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) {
    // on initialise certains attributs de la classe (pas forcément tous)
    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);
  }
 
}

Comments

  • line 20: the [$this→arrayOfAttributes] attribute has been converted into a variable that must now be passed to the [checkForAllAttributes] method on line 38, which previously operated on the [$this→arrayOfAttributes] attribute;

Due to this change to [BaseEntity], the [Database] class must also be slightly modified:


<?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
  ...
}

Comments

  • In the original code, after line 30, the method [parent::checkForAllAttributes] was called. This is no longer necessary since it is now automatically handled by the method [parent::setFromJsonFile($jsonFilename)];

14.3.4. [Postman] server tests

[Postman] was presented in the linked article.

We use the following Postman tests:

Image

Image

Image

The result of this last request is as follows:

Image

  • In [5-8], we can see that the attributes of the string jSON do indeed have numeric values (and not character strings). This result will allow the Javascript and [Métier] classes to execute normally;

14.3.5. The main script [main]

Image

The main script [main] for the client Javascript is as follows:


// imports
import axios from 'axios';
 
// imports
import Dao from './Dao2';
import Métier from 'trade';
 
// asynchronous function [main]
async function main() {
  // axios configuration
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // instantiation layer [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();

Comments

  • lines 5-6: imports of the [Dao] and [Métier] classes;
  • line 9: the asynchronous function [main], which will handle communication with the server using the [Dao] class and request the [Métier] class to perform the tax calculations;
  • lines 10–36: the script calls the [initSession, authentifierUtilisateur, getAdminData] methods of the [dao] layer sequentially and in a blocking manner;
  • line 38: the [dao] layer is no longer needed. We have all the elements to run the [métier] layer of the Javascript client;
  • lines 41–46: we perform three tax calculations and aggregate the results in a table [simulations];
  • line 49: we display the simulation table;
  • line 52: one of them is deleted;

The results of running the main script are as follows:


[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. Client HTTP 3

Image

In this section, we port the [Client HTTP 2] application to a browser using the following architecture:

Image

Porting is not immediate. While [node.js] can execute Javascript and ES6, this is generally not the case for browsers. You must therefore use tools that translate the ES6 code into ES5 code understood by modern browsers. Fortunately, these tools are both powerful and fairly easy to use.

Here, we followed the article [How to write ES6 code that’s safe to run in the browser - Web Developer's Journal].

In the [client HTTP 3/src] folder, we placed the [main.js, Métier.js, Dao2.js] elements of the [Client Http 2] application that we just developed.

14.4.1. Initializing the project

We will be working in the [client http 3] folder. We open a terminal in [VSCode] and navigate to this folder:

Image

We initialize this project with the command [npm init] and accept the default answers to the questions asked:

Image

  • In [4-5], the project configuration file [package.json] is generated based on the various answers provided;

14.4.2. Installing project dependencies

We will install the following dependencies:

  • [@babel/core]: the core of the [Babel] [https://babeljs.io] tool that transforms 2015+ ES code into code executable on both modern and older browsers;
  • [@babel/preset-env]: part of the Babel toolkit. Runs before the ES6 → ES5 transpilation;
  • [babel-loader]: this dependency allows the [webpack] tool to call the [Babel] tool;
  • [webpack]: orchestrator. It is [webpack] that calls upon Babel to transpilate the codes ES6 → ES5, and then it assembles all the resulting files into a single file;
  • [webpack-cli]: required by [webpack];
  • [@webpack-cli/init]: used to configure [webpack];
  • [webpack-dev-server]: provides a development web server that runs by default on port 8080. When source files are modified, it automatically reloads the web application;

The project dependencies are installed as follows in a [VSCode] terminal:

npm --save-dev install @babel/core @babel/preset-env babel-loader webpack webpack-cli webpack-dev-server @webpack-cli/init

Image

After installing the dependencies, the [package.json] file has changed as follows:


{
  "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"
  }
}
  • lines 12–19: the project dependencies are [devDependencies]: these are needed during the development phase but not in the production phase. In fact, in production, the file [dist/main.js] is used. It is written in ES5 and no longer requires the tools for transpilating code from ES6 to ES5;

We need to add two dependencies to the project:

  • [core-js]: contains "polyfills" for ECMAScript 2019. A polyfill allows recent code, such as ECMAScript 2019 (Sept. 2019), to run on older browsers;
  • [regenerator-runtime]: according to the library’s website --> [Source transformer enabling ECMAScript 6 generator functions in JavaScript-of-today];

Starting with Babel 7, these two dependencies replace the [@babel/polyfill] dependency, which previously served this purpose and is now (Sept 2019) deprecated. They are installed as follows:

Image

The [package.json] file then changes as follows:


{
  "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"
  }
}

Using the [core-js, regenerator-runtime] dependencies requires adding the following [imports] lines (lines 3-4) to the main [src/main.js] script:


// imports
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
 
// imports
import Dao from './Dao2';
import Métier from 'trade';

14.4.3. Configuration of [webpack]

[webpack] is the tool that will control:

  • the transpilation of all Javascript files in the project from ES6 to ES5;
  • the merging of the generated files into a single file;

This tool is controlled by a configuration file named [webpack.config.js], which can be generated using a dependency named [@webpack-cli/init] (Sept. 2019). This dependency was installed along with the others in the "Link" section.

We run the command [npx webpack-cli init] in a terminal [VSCode]:

Image

After answering the various questions (for which we can accept most of the default answers), a file named [webpack.config.js] is generated at the root of the [4] project:

The [webpack.config.js] file looks like this:


/* 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
    }
};

I don't understand all the details of this file, but a few points stand out:

  • line 1: the file does not contain code ES6. [Eslint] then reports errors that trace back to the root of the project [javascript]. This is annoying. To prevent ESLint from analyzing a file, simply comment out line 1;
  • line 31: we are working in [développement] mode;
  • line 32: the input script here is [src/index.js]. We will need to change this;
  • line 36: the folder where the outputs of [webpack] will be stored is the [dist] folder;
  • line 46: we see that [webpack] uses [babel-loader], one of the dependencies we installed;
  • line 54: we see that [webpack] uses [@babel-preset/env], one of the dependencies we installed;

The initialization of [webpack] has modified the file [package.json] (it is requesting permission):


{
  "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"
  }
}
  • line 4: it has been modified;
  • lines 8–9, 18–19: these have been added;
  • line 8: the [npm] task that compiles the project;
  • line 9: the task [npm], which runs the project;
  • line 18: ?
  • line 19: generates a [dist/index.html] file that automatically embeds the [dist/main.js] script generated by [webpack], and it is this script that is used when the project is executed;

Finally, the configuration of [webpack] generated a file named [src/index.js]:

Image

The contents of [index.js] are as follows (Sept. 2019):


console.log("Hello World from your main file!");

14.4.4. Compiling and running the project

The [package.json] file has three tasks named [npm]:


"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "webpack-dev-server"
},

These tasks are included in [VSCode], which executes them:

Image

  • in [1-3], the project is compiled;
  • in [4]: the project is compiled in [dist/main.hash.js] and a page [dist/index.html] is created;

The generated [index.html] page is as follows:


<!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>

This page simply encapsulates the [main.hash.js] file generated by [webpack].

The project is executed by the [start] task:

Image

The [dist/index.html] page is then loaded onto a server, part of the [webpack] suite, running on port 8080 of the local machine and displayed by the machine’s default browser:

Image

  • in [2], the service port of the [webpack] web server;
  • in [3], the body of the [dist/index.html] page is empty;
  • in [4], the [console] tab of the browser’s developer tools, here Firefox (F12);
  • in [5], the result of executing the file [src/index.js]. Recall that the content of this file was as follows:
console.log("Hello World from your main file!");

Now, let’s change this content to the following line:

console.log("Bonjour le monde");

Automatically (without recompiling), new [main.js, index.html] files are generated and the new [index.html] file is loaded into the browser:

Image

It is not necessary to run the [build] task before the [start] task: the latter first compiles the project. It does not store the output of this compilation in the [dist] folder. To verify this, simply delete that folder. You will then see that the [start] task compiles and runs the project without creating the [dist] folder. It appears to store its [index.html, main.hash.js] outputs in a folder specific to [webpackdev-server]. This behavior is sufficient for our tests.

When the development server is running, any saved changes to one of the project files trigger a recompilation. For this reason, we disable the [Auto Save] mode of [VSCode]. Indeed, we do not want a recompilation to occur every time characters are typed into one of the project files. We only want recompilation to occur when changes are saved:

Image

  • in [2], the option and [Auto Save] options must not be checked;

14.4.5. Testing the Javascript client for the tax calculation server

To test the Javascript client of the tax calculation server, you must designate [main.js] [1] as the project entry point in the file [webpack.config.js] [2-3]:

Image

Let’s not forget that the [main.js] script must include two additional imports compared to its version counterpart in [Client http 2]:

Image

In addition, we have slightly modified the code to handle errors that the server may return:


// imports
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
 
// imports
import Dao from './Dao2';
import Métier from 'trade';
 
// asynchronous function [main]
async function main() {
  // axios configuration
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // instantiation layer [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();

Comments

  • On lines [24-26], [31-33], and [38-40], we validate the code [response.état] sent in the server’s response jSON. If this code indicates an error, an exception is thrown with the error message string jSON from the server response [response.réponse];

Once this is done, we execute the project [5-6].

The page [index.html] is then generated and loaded into the browser:

Image

  • In [7], we see that the [init-session] action could not be completed due to a [CORS] issue (Cross-Origin Resource Sharing);

The CORS issue stems from the client/server relationship:

  • our client Javascript was downloaded to the machine [http://localhost:8080];
  • the tax calculation server is running on the [http://localhost:80] machine;
  • the client and server are therefore not in the same domain (same machine but different ports);
  • The browser running the Javascript client, which was loaded from the [http://localhost:8080] machine, blocks any request that is not directed to [http://localhost:80]. This is a security measure. It therefore blocks the client’s request to the server running on the [http://localhost:80] machine;

In fact, the browser does not completely block the request. It actually waits for the server to “tell” it that it accepts cross-domain requests. If it receives this authorization, the browser will then send the cross-domain request.

The server grants its authorization by sending specific headers:

1
2
3
4
Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Allow-Headers: Accept, Content-Type
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Credentials: true
  • Line 1: The client Javascript operates on the domain [http://localhost:8080]. The server must explicitly respond that it accepts this domain;
  • Line 2: The client Javascript will use the headers HTTP and [Accept, Content-Type] in its requests:
    • [Accept]: this header is sent in every request;
    • [Content-Type]: this header is used in POST operations to indicate the type of parameters in POST;

The server must explicitly accept these two headers HTTP;

  • Line 3: The client Javascript will use requests GET and POST. The server must explicitly accept these two types of requests;
  • line 4: the client Javascript will send session cookies. The server accepts them with the header from line 4;

We therefore need to modify the server. We do this in [Netbeans]. The issue with CORS is a problem encountered only in development mode. In production, the client and server will operate within the same domain [http://localhost:80], and there will be no issue with CORS. We therefore need a way to enable or disable CORS requests via server configuration.

Image

Server modifications are made in three places:

  • [1, 4]: in the configuration file [config.json] to set a boolean that controls whether or not cross-domain requests are accepted;
  • [2]: in the [ParentResponse] class that sends the response to the Javascript client. This class will send the CORS headers expected by the client browser;
  • [3]: within the [HtmlResponse, JsonResponse, XmlResponse] classes that generate responses for the [html, json, xml] sessions, respectively. These classes must pass the boolean [corsAllowed] found in [4] to their parent class [2]. This is done in [5] by passing the image array from the file jSON to [2];

The class [ParentResponse] [2] evolves as follows:


<?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 reply 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();
  }
 
}
  • line 29: we check if we need to handle cross-domain requests. If so, we generate the headers HTTP and CORS (lines 33–37) even if the current request is not a cross-domain request. In the latter case, the CORS headers will be unnecessary and will remain unused by the client;
  • Line 30: In a cross-domain request, the client browser querying the server sends a HTTP [Origin: http://localhost:8080] header (in the specific case of our client, Javascript). Line 30: We retrieve this HTTP header from the [$request] request;
  • Line 31: We will only accept cross-domain requests originating from the machine [http://localhost]. Note that these requests only occur in the project’s development mode;
  • lines 32–36: the headers CORS are added to the headers already present in the table [$headers];
  • Lines 45–49: The way the client browser requests the CORS permissions may vary depending on the client being used. In some cases, the client browser may request these permissions using the HTTP or [OPTIONS] commands. This is a new feature for our server, which was built to handle only [GET, POST] commands. In the case of a [OPTIONS] command, the server currently generates an error response. Lines 46–49: we correct this at the last moment. If, on line 46, we determine that the current command is a [OPTIONS] command, then we generate the following for the client:
    • lines 47, 51: an empty [$content] response;
    • line 48: a status code of 200 indicating that the request was successful. The only important thing for this request is sending the CORS headers from lines 33–36. This is what the client browser expects;

Once the server has been corrected in this way, the client Javascript runs better but displays a new error:

Image

  • In [1], the jSON session is initialized correctly;
  • In [2], the [authentifier-utilisateur] action fails: the server indicates that there is no active session. This means that the client Javascript did not correctly return the session cookie it sent during the action [init-session];

Let’s examine the network traffic that took place:

Image

  • In [4], the request [init-session]. It completed successfully with a 200 status code for the response;
  • In [5], the request [authentifier-utilisateur]. This one fails with a 400 (Bad Request) status code [6] for the response;

If we examine the headers HTTP and [7] of the request [5], we can see that the client Javascript did not send the headers HTTP and [Cookie], which would have allowed it to return the session cookie initially sent by the server. This is why the server reports that there is no session.

To have the client send the session cookie, you must add a configuration to the [axios] object:


// imports
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
 
// imports
import Dao from './Dao2';
import Métier from 'trade';
 
// 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;
  // instantiation layer [dao]
  const dao = new Dao(axios);
  // requests HTTP
  let taxAdminData;
...

Line 15 specifies that cookies should be included in the HTTP headers of the [axios] request. Note that this was not necessary in the [node.js] environment. There are therefore code differences between the two environments.

Once this error is corrected, the Javascript client runs normally:

Image

Image

14.5. Improvement to client HTTP 3

When the previous [Dao2] class runs within a browser, session cookie management is unnecessary. This is because the browser hosting the [dao] layer manages the session cookie: it automatically returns any cookie the server sends to it. Consequently, the [Dao2] class can be rewritten as the following [Dao3] class:


"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) {
      // parameter [error] 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;

Everything related to managing the management cookie has been removed.

We modify the previous project as follows:

Image

In the [src] folder, we have added two files:

  • the [Dao3] class we just introduced;
  • the file [main3], which is responsible for launching the new version;

The [main3] file remains identical to the [main] file from the previous version, but it now uses the [Dao3] class:


// 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;
  // instantiation layer [dao]
  const dao = new Dao(axios);
  // requests HTTP
  ...
}
 
// log jSON
function log(object) {
  console.log(JSON.stringify(object, null, 2));
}
 
// execution
main();

The file [webpack.config] has been modified to now execute the 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()],
...
};

Once this is done, we run the project after starting the tax calculation server:

Image

The results displayed in the browser console are identical to those of the previous version.

14.6. Conclusion

We now have all the tools to develop the Javascript code for a web application. We can:

  • use the latest ECMAScript code;
  • test isolated elements of this code in a simpler [node.js] environment for debugging and testing;
  • then port this code to a browser using the [babel] and [webpack] tools;