Skip to content

14. 税费计算服务的 JavaScript HTTP 客户端

14.1. 简介

在此,我们计划为第 14 版税费计算服务编写一个 [Node.js] 客户端。客户端/服务器架构如下:

Image

我们将探讨两种版本的客户端:

  • 客户端第 1 版将采用以下 [主层、DAO] 分层结构:

Image

  • 客户端的第 2 版将采用 [主层、业务逻辑层、DAO] 结构。服务器的 [业务逻辑] 层将移至客户端:

Image

14.2. HTTP 客户端 1

Image

如前所述,HTTP 1 客户端实现了以下客户端/服务器架构:

Image

我们将实现:

  • [DAO] 层实现为一个类;
  • [main] 层实现为使用该类的脚本;

14.2.1. [dao]

[dao]层将由以下类[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;
  • 这里我们运用了链接章节中学习的内容,该章节介绍了 [axios] 库,它允许我们在 [node.js] 和浏览器中发送 HTTP 请求。我们将重点关注链接章节中的脚本;
  • 第 9–15 行:类构造函数。该类将包含三个属性:
    • [axios]:用于发起 HTTP 请求的 [axios] 对象。该对象由调用代码传入;
    • [sessionCookieName]:根据服务器的不同,会话 Cookie 的名称各异。此处为 [PHPSESSID]
    • [sessionCookie]:由服务器发送并由客户端存储的会话 Cookie;
  • 第 53–76 行:异步函数 [calculateTax] 通过提交参数 [married, children, salary] 发起请求 [post /main.php?action=calculate-tax]。它将服务器返回的 JSON 字符串转换为 JavaScript 对象;
  • 第 79–92 行:异步函数 [listSimulations] 发起请求 [get /main.php?action=list-simulations]。它将服务器返回的 JSON 字符串转换为 JavaScript 对象;
  • 第 95–109 行:异步函数 [deleteSimulation] 发起请求 [get /main.php?action=delete-simulation&number=index]。它将服务器返回的 JSON 字符串转换为 JavaScript 对象;
  • 第 121 行:使用 [this.axios] 这种写法,是因为在此处,传递给构造函数的 [axios] 对象已被存储在 [this.axios] 属性中;
  • 第 161 行:导出 [Dao1] 类以便使用;

14.2.2. [main1.js] 脚本

[main1.js] 脚本使用 [Dao1] 类向服务器发起了一系列调用:

  • 初始化一个 JSON 会话;
  • 使用 [admin, admin] 进行身份验证;
  • 请求三项税费计算;
  • 请求模拟列表;
  • 删除其中一个;

代码如下:


// 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();

注释

  • 第 2 行:导入 [axios] 库;
  • 第 4 行:导入 [Dao] 类;
  • 第 7 行:与服务器通信的 [main] 函数是异步的;
  • 第 9-10 行:发送至服务器的 HTTP 请求的默认配置:
    • 第 9 行:超时 [timeout] 设置为 2 秒;
    • 第 10 行:所有 URL 均以税费计算服务器第 14 版的基准 URL 为前缀;
  • 第 12 行:构建 [Dao] 层。现在可以使用它了;
  • 第46–48行:使用[log]函数以格式化方式显示JavaScript对象的JSON字符串:垂直排列并使用两个空格缩进(第3个参数);
  • 第 15–18 行:初始化 JSON 会话;
  • 第19–22行:身份验证;
  • 第 23–30 行:并行请求了三项税费计算。得益于 [await Promise.all],执行将阻塞直至获得全部三项结果;
  • 第 31–34 行:模拟列表;
  • 第 35–38 行:删除一个模拟;
  • 第 39–42 行:异常处理;

执行结果如下:


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\client impôts\client http 1\main1.js"
"-----------init-session"
{
  "action": "init-session",
  "état": 700,
  "réponse": "session démarrée avec type [json]"
}
"-----------authentifier-utilisateur"
{
  "action": "authentifier-utilisateur",
  "état": 200,
  "réponse": "Authentification réussie [admin, admin]"
}
"-----------calculer-impot x 3"
[
  {
    "action": "calculer-impot",
    "état": 300,
    "réponse": {
      "marié": "oui",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 502,
      "surcôte": 0,
      "décôte": 857,
      "réduction": 126,
      "taux": 0.14
    }
  },
  {
    "action": "calculer-impot",
    "état": 300,
    "réponse": {
      "marié": "non",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 3250,
      "surcôte": 370,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.3
    }
  },
  {
    "action": "calculer-impot",
    "état": 300,
    "réponse": {
      "marié": "non",
      "enfants": "1",
      "salaire": "30000",
      "impôt": 1687,
      "surcôte": 0,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.14
    }
  }
]
"-----------liste-des-simulations"
{
  "action": "lister-simulations",
  "état": 500,
  "réponse": [
    {
      "marié": "oui",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 502,
      "surcôte": 0,
      "décôte": 857,
      "réduction": 126,
      "taux": 0.14,
      "arrayOfAttributes": null
    },
    {
      "marié": "non",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 3250,
      "surcôte": 370,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.3,
      "arrayOfAttributes": null
    },
    {
      "marié": "non",
      "enfants": "1",
      "salaire": "30000",
      "impôt": 1687,
      "surcôte": 0,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.14,
      "arrayOfAttributes": null
    }
  ]
}
"-----------suppression simulation n° 1"
{
  "action": "supprimer-simulation",
  "état": 600,
  "réponse": [
    {
      "marié": "oui",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 502,
      "surcôte": 0,
      "décôte": 857,
      "réduction": 126,
      "taux": 0.14,
      "arrayOfAttributes": null
    },
    {
      "marié": "non",
      "enfants": "1",
      "salaire": "30000",
      "impôt": 1687,
      "surcôte": 0,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.14,
      "arrayOfAttributes": null
    }
  ]
}
 
[Done] exited with code=0 in 0.516 seconds

14.3. HTTP 2 客户端

Image

HTTP2 客户端的架构如下:

Image

我们将 [business] 层从服务器移到了 JavaScript 客户端。与 PHP7 课程中的做法不同,[main] 层无需通过 [business] 层即可直接访问 [DAO] 层。我们将把这两个层作为专用组件使用:

  • [main] 层需要服务器端数据时,会通过 [DAO] 层进行调用;
  • [主]层会请求[业务]层执行税费计算;
  • [业务]层独立于[DAO]层,且绝不调用它;

14.3.1. JavaScript [Business]

PHP 中 [Business] 类的本质已在链接的文章中描述。这是一段相当复杂的代码,我们在此重提它,并非为了解释其原理,而是为了将其转换为 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);
  }
 
}
  • 第 19–26 行:PHP 类的构造函数。既然我们提到要构建一个独立于 [DAO] 层的 [业务] 层,因此我们将对 JavaScript 中的这个构造函数进行两处修改:
    • 它将不再接收 [DAO] 层的实例(它不再需要该实例);
    • 它将不再向 [dao] 层的 [taxAdminData] 管理模块请求税费数据:调用代码会将这些数据传递给构造函数;
  • 第 197–122 行:我们将不实现 [executeBatchImports] 方法,该方法的最终目的是将模拟结果保存到文本文件中。我们希望代码既能在 [node.js] 中运行,也能在浏览器中运行。然而,将数据保存到运行客户端浏览器的机器的文件系统中是不可能的;

鉴于这些限制,JavaScript [Métier] 类的代码如下:


'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;
  • JavaScript 代码与 PHP 代码非常接近;
  • 第 187 行导出了 [Business] 类;

14.3.2. JavaScript 类 [Dao2]

Image

[Dao2] 类实现了上述 JavaScript 客户端的 [dao] 层,具体如下:


'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;

注释

  • [Dao2] 类仅实现了向税费计算服务器发送的三种可能请求:
    • [init-session](第 17–29 行):用于初始化 JSON 会话;
    • [authenticate-user](第 31–50 行):用于身份验证;
    • [get-admindata](第 52–65 行):用于检索在客户端执行税务计算所需的税务管理数据;
  • 第 52–65 行:我们向服务器引入了一个新的操作 [get-admindata]。该操作此前尚未实现,现将其实现。

14.3.3. 税费计算服务器的修改

税务计算服务器必须实现一个新操作。我们将在服务器的第 14 版中完成此项工作。待实现的操作具有以下特征:

  • 由操作 [get /main.php?action=get-admindata] 调用
  • 它返回一个封装税务管理数据的对象的 JSON 字符串;

我们将探讨如何向服务器添加该操作。

该修改将在 NetBeans 中进行:

Image

[2] 中,我们修改 [config.json] 文件以添加新操作:


{
    "databaseFilename": "Config/database.json",
    "corsAllowed": true,
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-14",
    "relativeDependencies": [
 
        "/Entities/BaseEntity.php",
        "/Entities/Simulation.php",
        "/Entities/Database.php",
        "/Entities/TaxAdminData.php",
        "/Entities/ExceptionImpots.php",
 
        "/Utilities/Logger.php",
        "/Utilities/SendAdminMail.php",        
 
        "/Model/InterfaceServerDao.php",
        "/Model/ServerDao.php",
        "/Model/ServerDaoWithSession.php",
        "/Model/InterfaceServerMetier.php",
        "/Model/ServerMetier.php",
 
        "/Responses/InterfaceResponse.php",
        "/Responses/ParentResponse.php",
        "/Responses/JsonResponse.php",
        "/Responses/XmlResponse.php",
        "/Responses/HtmlResponse.php",
 
        "/Controllers/InterfaceController.php",
        "/Controllers/InitSessionController.php",
        "/Controllers/ListerSimulationsController.php",
        "/Controllers/AuthentifierUtilisateurController.php",
        "/Controllers/CalculerImpotController.php",
        "/Controllers/SupprimerSimulationController.php",
        "/Controllers/FinSessionController.php",
        "/Controllers/AfficherCalculImpotController.php",
        "/Controllers/AdminDataController.php"
    ],
    "absoluteDependencies": [
        "C:/myprograms/laragon-lite/www/vendor/autoload.php",
        "C:/myprograms/laragon-lite/www/vendor/predis/predis/autoload.php"
    ],
    "users": [
        {
            "login": "admin",
            "passwd": "admin"
        }
    ],
    "adminMail": {
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "plantage du serveur de calcul d'impôts",
        "tls": "FALSE",
        "attachments": []
    },
    "logsFilename": "Logs/logs.txt",
    "actions":
            {
                "init-session": "\\InitSessionController",
                "authentifier-utilisateur": "\\AuthentifierUtilisateurController",
                "calculer-impot": "\\CalculerImpotController",
                "lister-simulations": "\\ListerSimulationsController",
                "supprimer-simulation": "\\SupprimerSimulationController",
                "fin-session": "\\FinSessionController",
                "afficher-calcul-impot": "\\AfficherCalculImpotController",
                "get-admindata": "\\AdminDataController"
            },
    "types": {
        "json": "\\JsonResponse",
        "html": "\\HtmlResponse",
        "xml": "\\XmlResponse"
    },
    "vues": {
        "vue-authentification.php": [700, 221, 400],
        "vue-calcul-impot.php": [200, 300, 341, 350, 800],
        "vue-liste-simulations.php": [500, 600]
    },
    "vue-erreurs": "vue-erreurs.php"
}

修改内容包括:

  • 第 67 行:添加 [get-admindata] 操作并将其关联到一个控制器;
  • 第 36 行:在 PHP 应用程序需加载的类列表中声明此控制器;

下一步是实现 [AdminDataController] 控制器 [3]


<?php
 
namespace Application;
 
// symfony 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], []];
  }
 
}

评论

  • 第 12 行:与服务器的其他控制器一样,[AdminDataController] 实现了 [InterfaceController] 接口,该接口包含第 19–79 行中的 [execute] 方法;
  • 第 78 行:与服务器上的其他控制器一样,[AdminDataController.execute] 方法返回一个数组 [$status, $status, [‘response’=>$response]],其中:
    • [$status]:HTTP 响应状态码;
    • [$état]:一个内部应用程序代码,表示执行客户端请求后服务器的状态;
    • [$response]:封装了将发送给客户端的响应的数组。此处,该数组稍后将被转换为 JSON 字符串;
  • 第 25–34 行:我们验证客户端的 [get-admindata] 操作在语法上是否正确;
  • 第 37–74 行:检索 [TaxAdminData] 对象,该对象来自:
    • 第 56–59 行:若未在 [redis] 缓存中找到,则从数据库中检索;
    • 第 70–73 行:若在 [redis] 缓存中找到;

此代码摘自链接文章中介绍的 [CalculerImpotController] 控制器。实际上,该控制器还需要检索封装税务管理数据的 [TaxAdminData] 对象。

在测试 JavaScript 客户端时,当 [redis] 缓存中存在 [TaxAdminData] 对象时,其 JSON 格式会引发问题。为了解原因,让我们来分析该对象在 [redis] 中的存储方式:

Image

Image

  • [5-7]中,我们可以看到数值被存储为字符串。PHP能够处理这种情况,因为在涉及数字和字符串的计算中,+运算符会隐式地将字符串转换为数字。但JavaScript的做法恰恰相反:在涉及数字和字符串的计算中,+运算符会隐式地将数字转换为字符串。因此,JavaScript [Métier]类中的计算结果是不正确的;

要解决此问题,我们需要修改控制器第 71 行中使用的 [TaxAdminData.setFromArrayOfAttributes] 方法,使其能够根据 [redis] 缓存中的 JSON 字符串实例化一个 [TaxAdminData] 对象(参见文章):


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

评论

  • 第 5 行:[TaxAdminData] 类继承自 [BaseEntity] 类,而 [BaseEntity] 类本身已包含 [setFromArrayOfAttributes] 方法。由于该方法不适用,我们在第 67–75 行对其进行了重定义;
  • 第 70 行:首先使用父类的 [setFromArrayOfAttributes] 方法来初始化该类的属性;
  • 第 72 行:[checkAttributes] 方法验证关联的值是否确实为数字。如果是字符串,则将其转换为数字;
  • 第 74 行:生成的 [$this] 对象此时已成为一个属性值为数值的对象;
  • 第 78–93 行:[checkAttributes] 方法验证对象属性的关联值是否确实为数值;
  • 第 80 行:遍历属性列表;
  • 第 81 行:如果某个属性的值类型为 [string]
  • 第 83 行:则检查该字符串是否表示一个数字;
  • 第 90 行:如果是,则将该字符串转换为数字并赋值给正在测试的属性;
  • 第 85–86 行:若非如此,则抛出异常;
  • 第 32–65 行:[check] 函数的功能略多于实际所需。它既处理数组也处理单个值。然而,此处调用它仅是为了检查 [string] 类型的值。它返回一个包含 [error, value] 属性的对象,其中:
    • [error] 是一个布尔值,表示是否发生错误;
    • [value] 是第 32 行传入的 [value] 参数,根据情况转换为数字或数字数组;

此前拥有名为 [arrayOfAttributes] 属性的 [BaseEntity] 类已进行修改,删除了该属性:该属性曾导致 [TaxAdminData] JSON 字符串出现问题。该类已重写如下:


<?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);
  }
 
}

评论

  • 第 20 行:属性 [$this→arrayOfAttributes] 已被转换为一个变量,现在必须将其传递给第 38 行的 [checkForAllAttributes] 方法,该方法此前操作的是属性 [$this→arrayOfAttributes]

由于 [BaseEntity] 的这一变更,[Database] 类也必须稍作修改:


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

注释

  • 在原始代码中,第 30 行之后调用了 [parent::checkForAllAttributes] 方法。由于该操作现已被 [parent::setFromJsonFile($jsonFilename)] 方法自动处理,因此不再需要此调用;

14.3.4. [Postman] 服务器测试

相关文章中介绍了 [Postman]

我们使用以下 Postman 测试:

Image

Image

Image

最后一次请求的 JSON 结果如下:

Image

  • [5-8] 中,您可以看到 JSON 字符串中的属性确实具有数值(而非字符串)。此结果将使 JavaScript [Business] 类能够正常执行;

14.3.5. 主脚本 [main]

Image

JavaScript 客户端的主脚本 [main] 如下所示:


// 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();

注释

  • 第 5-6 行:导入 [Dao] [Business] 类;
  • 第 9 行:异步函数 [main],该函数将使用 [Dao] 类处理与服务器的通信,并请求 [Business] 类执行税费计算;
  • 第10-36行:脚本以阻塞方式依次调用[Dao]层的[initSession、authenticateUser、getAdminData]方法;
  • 第 38 行:我们不再需要 [dao] 层。现在已具备运行 JavaScript 客户端 [business] 层所需的所有元素;
  • 第 41–46 行:我们执行三项税费计算,并将结果存储在 [simulations] 数组中;
  • 第 49 行:我们显示 simulations 数组;
  • 第 52 行:移除其中一项;

运行主脚本的结果如下:


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\client impôts\client http 2\main2.js"
"-----------init-session"
{
  "action": "init-session",
  "état": 700,
  "réponse": "session démarrée avec type [json]"
}
"-----------authentifier-utilisateur"
{
  "action": "authentifier-utilisateur",
  "état": 200,
  "réponse": "Authentification réussie [admin, admin]"
}
"-----------get-admindata"
{
  "action": "get-admindata",
  "état": 1000,
  "réponse": {
    "limites": [
      9964,
      27519,
      73779,
      156244,
      0
    ],
    "coeffR": [
      0,
      0.14,
      0.3,
      0.41,
      0.45
    ],
    "coeffN": [
      0,
      1394.96,
      5798,
      13913.69,
      20163.45
    ],
    "plafondQfDemiPart": 1551,
    "plafondRevenusCelibatairePourReduction": 21037,
    "plafondRevenusCouplePourReduction": 42074,
    "valeurReducDemiPart": 3797,
    "plafondDecoteCelibataire": 1196,
    "plafondDecoteCouple": 1970,
    "plafondImpotCouplePourDecote": 2627,
    "plafondImpotCelibatairePourDecote": 1595,
    "abattementDixPourcentMax": 12502,
    "abattementDixPourcentMin": 437
  }
}
"-----------calculer-impot x 3"
"-----------liste-des-simulations"
[
  {
    "impôt": 502,
    "surcôte": 0,
    "décôte": 857,
    "réduction": 126,
    "taux": 0.14
  },
  {
    "impôt": 3250,
    "surcôte": 370,
    "décôte": 0,
    "réduction": 0,
    "taux": 0.3
  },
  {
    "impôt": 1687,
    "surcôte": 0,
    "décôte": 0,
    "réduction": 0,
    "taux": 0.14
  }
]
"-----------suppression simulation n° 1"
[
  {
    "impôt": 502,
    "surcôte": 0,
    "décôte": 857,
    "réduction": 126,
    "taux": 0.14
  },
  {
    "impôt": 1687,
    "surcôte": 0,
    "décôte": 0,
    "réduction": 0,
    "taux": 0.14
  }
]
 
[Done] exited with code=0 in 0.583 seconds

14.4. HTTP 3 客户端

Image

在本节中,我们将使用以下架构将 [HTTP 客户端 2] 应用程序移植到浏览器上:

Image

移植过程并非一蹴而就。虽然 [node.js] 可以执行 ES6 JavaScript,但浏览器通常无法直接支持。因此,我们必须使用能够将 ES6 代码转换为现代浏览器可识别的 ES5 代码的工具。幸运的是,这些工具既功能强大又相当易于使用。

在此,我们参考了文章 [如何编写可在浏览器中安全运行的 ES6 代码 - Web Developer's Journal]

[client HTTP 3/src] 文件夹中,我们放置了来自刚刚开发的 [Client Http 2] 应用程序的 [main.js、Métier.js、Dao2.js] 文件。

14.4.1. 初始化项目

我们将在 [client http 3] 文件夹中进行操作。在 [VSCode] 中打开终端,并导航至该文件夹:

Image

我们使用 [npm init] 命令初始化该项目,并对提示的问题接受默认选项:

Image

  • [4-5] 中,根据提供的各种答案生成了项目配置文件 [package.json]

14.4.2. 安装项目依赖项

我们将安装以下依赖项:

  • [@babel/core][Babel] 工具 [https://babeljs.io] 的核心组件,用于将 ES 2015+ 代码转换为可在现代及旧版浏览器上运行的代码;
  • [@babel/preset-env]:Babel 工具集的一部分。在 ES6 → ES5 转译之前运行;
  • [babel-loader]:此依赖项使 [webpack] 工具能够调用 [Babel] 工具;
  • [webpack]:协调器。正是 [webpack] 调用 Babel 将 ES6 代码转译为 ES5,然后将所有生成的文件合并为一个文件;
  • [webpack-cli][webpack] 所需;
  • [@webpack-cli/init]:用于配置 [webpack]
  • [webpack-dev-server]:提供一个默认运行在 8080 端口的开发 Web 服务器。当源文件被修改时,它会自动重新加载 Web 应用程序;

[VSCode] 终端中,项目依赖项的安装步骤如下:

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

Image

安装依赖项后,[package.json] 文件已更新如下:


{
  "name": "client-http-3",
  "version": "1.0.0",
  "description": "client jS du serveur de calcul de l'impôt",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "serge.tahe@gmail.com",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.6.0",
    "@babel/preset-env": "^7.6.0",
    "@webpack-cli/init": "^0.2.2",
    "babel-loader": "^8.0.6",
    "cross-env": "^6.0.0",
    "webpack": "^4.40.2",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.1"
  }
}
  • 第 12–19 行:项目的依赖项属于 [devDependencies]:我们在开发阶段需要它们,但在生产环境中则不需要。在生产环境中,使用的是 [dist/main.js] 文件。该文件采用 ES5 编写,因此不再需要工具将 ES6 代码转译为 ES5;

我们需要向该项目添加两个依赖项:

  • [core-js]:包含 ECMAScript 2019 的“polyfills”。polyfill 允许像 ECMAScript 2019(2019 年 9 月)这样的最新代码在旧版浏览器上运行;
  • [regenerator-runtime]:根据该库的官网说明 --> [一种源代码转换器,可在当前的 JavaScript 环境中启用 ECMAScript 6 生成器函数]

从 Babel 7 开始,这两个依赖项取代了 [@babel/polyfill] 依赖项,后者此前曾用于此目的,现已(2019 年 9 月)弃用。安装方法如下:

Image

随后 [package.json] 文件将按以下方式修改:


{
  "name": "client-http-3",
  "version": "1.0.0",
  "description": "My webpack project",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "webpack-dev-server"
  },
  "author": "serge.tahe@gmail.com",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.6.0",
    "@babel/preset-env": "^7.6.0",
    "@webpack-cli/init": "^0.2.2",
    "babel-loader": "^8.0.6",
    "babel-plugin-syntax-dynamic-import": "^6.18.0",
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.40.2",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.1"
  },
  "dependencies": {
    "core-js": "^3.2.1",
    "regenerator-runtime": "^0.13.3"
  }
}

要使用 [core-js, regenerator-runtime] 依赖项,需要在主脚本 [src/main.js] 中添加以下 [imports](第 3–4 行):


// 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. [webpack] 配置

[webpack] 是一个负责:

  • 将项目中所有 JavaScript 文件从 ES6 转译为 ES5;
  • 将生成的文件打包成一个文件;

该工具由配置文件 [webpack.config.js] 控制,可通过名为 [@webpack-cli/init] 的依赖项生成(2019年9月)。该依赖项已与“链接”部分中提到的其他依赖项一同安装。

我们在 [VSCode] 终端中运行命令 [npx webpack-cli init]

Image

回答完各项问题(其中大部分可接受默认答案)后,项目根目录下将生成一个 [webpack.config.js] 文件 [4]

[webpack.config.js] 文件内容如下:


/* eslint-disable */
 
const path = require('path');
const webpack = require('webpack');
 
/*
 * SplitChunksPlugin is enabled by default and replaced
 * deprecated CommonsChunkPlugin. It automatically identifies modules which
 * should be splitted of chunk by heuristics using module duplication count and
 * module category (i. e. node_modules). And splits the chunks…
 *
 * It is safe to remove "splitChunks" from the generated configuration
 * and was added as an educational example.
 *
 * https://webpack.js.org/plugins/split-chunks-plugin/
 *
 */
 
const HtmlWebpackPlugin = require('html-webpack-plugin');
 
/*
 * We've enabled HtmlWebpackPlugin for you! This generates a html
 * page for you when you compile webpack, which will make you start
 * developing and prototyping faster.
 *
 * https://github.com/jantimon/html-webpack-plugin
 *
 */
 
module.exports = {
    mode: 'development',
    entry: './src/index.js',
 
    output: {
        filename: '[name].[chunkhash].js',
        path: path.resolve(__dirname, 'dist')
    },
 
    plugins: [new webpack.ProgressPlugin(), new HtmlWebpackPlugin()],
 
    module: {
        rules: [
            {
                test: /.(js|jsx)$/,
                include: [path.resolve(__dirname, 'src')],
                loader: 'babel-loader',
 
                options: {
                    plugins: ['syntax-dynamic-import'],
 
                    presets: [
                        [
                            '@babel/preset-env',
                            {
                                modules: false
                            }
                        ]
                    ]
                }
            }
        ]
    },
 
    optimization: {
        splitChunks: {
            cacheGroups: {
                vendors: {
                    priority: -10,
                    test: /[\\/]node_modules[\\/]/
                }
            },

            chunks: 'async',
            minChunks: 1,
            minSize: 30000,
            name: true
        }
    },
 
    devServer: {
        open: true
    }
};

我并不完全理解这个文件的所有细节,但有几点特别引人注目:

  • 第 1 行:该文件不包含 ES6 代码。[Eslint] 却报告了错误,这些错误一直传播到了 [javascript] 项目的根目录。这很烦人。要阻止 Eslint 分析某个文件,只需将第 1 行注释掉即可;
  • 第 31 行:我们当前处于 [development] 模式;
  • 第 32 行:入口脚本位于此处 [src/index.js]。我们需要修改这一点;
  • 第 36 行:[webpack] 的输出文件将放置在 [dist] 文件夹中;
  • 第 46 行:可以看到 [webpack] 使用了 [babel-loader],这是我们安装的依赖项之一;
  • 第 54 行:我们看到 [webpack] 使用了 [@babel-preset/env],这是我们安装的依赖项之一;

初始化 [webpack] 已修改了 [package.json] 文件(它会请求权限):


{
  "name": "client-http-3",
  "version": "1.0.0",
  "description": "My webpack project",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "webpack-dev-server"
  },
  "author": "serge.tahe@gmail.com",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.6.0",
    "@babel/preset-env": "^7.6.0",
    "@webpack-cli/init": "^0.2.2",
    "babel-loader": "^8.0.6",
    "babel-plugin-syntax-dynamic-import": "^6.18.0",
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.40.2",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.1"
  },
  "dependencies": {
    "core-js": "^3.2.1",
    "regenerator-runtime": "^0.13.3"
  }
}
  • 第 4 行:已修改;
  • 第 8–9 行、第 18–19 行:这些已被添加;
  • 第 8 行:用于编译项目的 [npm] 任务;
  • 第 9 行:用于运行该项目的 [npm] 任务;
  • 第 18 行:?
  • 第 19 行:生成一个 [dist/index.html] 文件,该文件会自动嵌入由 [webpack] 生成的 [dist/main.js] 脚本,而运行项目时使用的正是该文件;

最后,[webpack] 配置生成了一个文件 [src/index.js]

Image

[index.js] 的内容如下(2019年9月):


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

14.4.4. 编译并运行该项目

[package.json] 文件包含三个 [npm] 任务:


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

这些任务会被 [VSCode] 识别并提供执行选项:

Image

  • [1-3] 中,项目被编译;
  • [4] 中:将项目编译为 [dist/main.hash.js] 并生成 [dist/index.html] 页面;

生成的 [index.html] 页面如下:


<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Webpack App</title>
  </head>
  <body>
  <script type="text/javascript" src="main.87afc226fd6d648e7dea.js"></script></body>
</html>

本页面仅封装了由 [webpack] 生成的 [main.hash.js] 文件。

该项目通过 [start] 任务运行:

Image

随后,[dist/index.html] 页面会被加载到 [webpack] 套件中的一部分——运行在本地机器 8080 端口的服务器上,并通过该机器的默认浏览器进行显示:

Image

  • [2] 中,即 [webpack] Web 服务器的服务端口;
  • [3] 中,[dist/index.html] 页面的主体内容为空;
  • [4] 中,即浏览器开发者工具的 [控制台] 标签页,此处为 Firefox(F12);
  • [5] 中,是执行 [src/index.js] 文件后的结果。回顾一下,其内容如下:
console.log("Hello World from your main file!");

现在,我们将此内容修改为以下这一行:

console.log("Bonjour le monde");

系统会自动(无需重新编译)生成新文件 [main.js, index.html],并将新的 [index.html] 文件加载到浏览器中:

Image

无需在 [start] 任务之前运行 [build] 任务:后者会首先编译项目。它不会将此次编译的输出存储在 [dist] 文件夹中。要验证这一点,只需删除该文件夹即可。 随后我们会发现,[start] 任务在编译并运行项目时并未创建 [dist] 文件夹。其生成的输出文件 [index.html, main.hash.js] 似乎存储在 [webpackdev-server] 专属的文件夹中。这种行为对于我们的测试而言已足够。

当开发服务器运行时,项目文件的任何保存更改都会触发重新编译。因此,我们需要禁用 [VSCode][自动保存] 模式。我们不希望每次在项目文件中输入字符时都重新编译项目,而仅希望在保存更改时才进行重新编译:

Image

  • [2] 中,[自动保存] 选项必须保持未勾选;

14.4.5. 测试税费计算服务器的 JavaScript 客户端

要测试税费计算服务器的 JavaScript 客户端,您必须在 [webpack.config.js] 文件 [2-3] 中将 [main.js] [1] 指定为项目的入口点:

Image

请注意,与 [HTTP Client 2] 中的版本相比,[main.js] 脚本必须额外包含两个导入:

Image

此外,我们还对代码进行了微调,以处理服务器可能发出的错误:


// 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();

注释

  • 在第 [24-26] 行、[31-33] 行和 [38-40] 行中,我们检查了服务器 JSON 响应中发送的代码 [response.status]。如果该代码表示发生错误,则会抛出一个异常,并将服务器响应中的 JSON 字符串 [response.response] 作为错误消息;

完成上述操作后,我们执行项目 [5-6]

随后生成 [index.html] 页面并加载到浏览器中:

Image

  • [7] 中,我们可以看到由于 [CORS](跨源资源共享)问题,[init-session] 操作无法完成;

该 CORS 问题源于客户端与服务端之间的关系:

  • 我们的 JavaScript 客户端已下载到机器 [http://localhost:8080] 上;
  • 税费计算服务器运行在 [http://localhost:80] 这台机器上;
  • 因此客户端和服务器不在同一域名下(同一台机器但端口不同);
  • 运行 JavaScript 客户端的浏览器是从 [http://localhost:8080] 这台机器加载的,它会阻止任何不指向 [http://localhost:80] 的请求。这是一项安全措施。因此,它会阻止客户端向运行在 [http://localhost:80] 这台机器上的服务器发出的请求;

实际上,浏览器并未完全阻止该请求。它实际上是在等待服务器“告知”其接受跨域请求。如果收到此授权,浏览器才会转发该跨域请求。

服务器通过发送特定的 HTTP 头部来授予授权:

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
  • 第 1 行:JavaScript 客户端运行在 [http://localhost:8080] 域名下。服务器必须明确响应表示接受该域名;
  • 第 2 行:JavaScript 客户端将在其请求中使用 [Accept, Content-Type] 这些 HTTP 头部:
    • [Accept]:此标头在每次请求中都会发送;
    • [Content-Type]:此标头用于 POST 操作,用于指定 POST 参数的类型;

服务器必须明确接受这两个 HTTP 头;

  • 第 3 行:JavaScript 客户端将使用 GET 和 POST 请求。服务器必须明确接受这两种类型的请求;
  • 第 4 行:JavaScript 客户端将发送会话 Cookie。服务器通过第 4 行中的标头接受这些 Cookie;

因此,我们需要修改服务器。我们将在 [NetBeans] 中进行此操作。CORS 问题仅在开发模式下才会出现。在生产环境中,客户端和服务器将在同一域名 [http://localhost:80] 内运行,因此不会出现 CORS 问题。因此,我们需要一种通过服务器配置来启用或禁用 CORS 请求的方法。

Image

服务器修改主要涉及以下三个方面:

  • [1, 4]:在配置文件 [config.json] 中设置一个布尔值,用于控制是否接受跨域请求;
  • [2]:在 [ParentResponse] 类中,该类负责将响应发送给 JavaScript 客户端。该类将发送客户端浏览器所期望的 CORS 头部;
  • [3]:在 [HtmlResponse、JsonResponse、XmlResponse] 类中,它们分别用于生成 [html、json、xml] 会话的响应。这些类必须将 [4] 中定义的 [corsAllowed] 布尔值传递给其父类 [2]。这一操作在 [5] 中完成,通过将 JSON 文件 [2] 中的图像数组进行传递;

[ParentResponse][2] 的演变过程如下:


<?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();
  }
 
}
  • 第 29 行:我们检查是否需要处理跨域请求。如果需要,即使当前请求并非跨域请求,我们也会生成 CORS HTTP 头部(第 33–37 行)。在后一种情况下,CORS 头部将不再必要,客户端也不会使用它们;
  • 第 30 行:在跨域请求中,向服务器发送请求的客户端浏览器会发送一个 HTTP 头 [Origin: http://localhost:8080](以我们的 JavaScript 客户端为例)。在第 30 行,我们从请求 [$request] 中获取此 HTTP 头;
  • 第 31 行:我们仅接受源自 [http://localhost] 机器的跨域请求。请注意,此类请求仅在项目的开发模式下发生;
  • 第 32–36 行:我们将 CORS 头部添加到数组 [$headers] 中已有的头部中;
  • 第 45–49 行:客户端浏览器请求 CORS 权限的方式可能因所用浏览器而异。有时客户端浏览器会通过 HTTP [OPTIONS] 请求来获取这些权限。这对我们的服务器来说是一个新场景,因为它原本仅设计用于处理 [GET] 和 [POST] 请求。遇到 [OPTIONS] 请求时,服务器目前会返回错误响应。 第 46–49 行:我们在最后时刻修正了这一问题:如果第 46 行判断出当前请求是 [OPTIONS] 请求,则为客户端生成以下内容:
    • 第 47、51 行:一个空的 [$content] 响应;
    • 第 48 行:返回 200 状态码,表示请求成功。对于此请求,唯一重要的是在第 33–36 行发送 CORS 头部。这是客户端浏览器所期望的;

对服务器进行上述修正后,JavaScript客户端运行更顺畅,但会显示一个新错误:

Image

  • [1] 中,JSON 会话已正确初始化;
  • [2] 中,[authenticate-user] 操作失败:服务器提示不存在活动会话。这意味着 JavaScript 客户端未能正确回传其在 [init-session] 操作期间发送的会话 Cookie;

让我们来看看当时发生的网络交流:

Image

  • [4] 中,[init-session] 请求。该请求成功完成,响应状态码为 200;
  • [5] 中,[authenticate-user] 请求。该请求失败,响应返回 400(请求错误)状态码 [6]

如果我们检查请求 [5] 的 HTTP 头 [7],可以看到 JavaScript 客户端未发送 [Cookie] HTTP 头,而该头本应使其能够返回服务器最初发送的会话 Cookie。这就是服务器报告不存在会话的原因。

要让客户端发送会话 Cookie,你需要在 [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;
...

第 15 行要求将 Cookie 包含在 [axios] 请求的 HTTP 头部中。请注意,在 [node.js] 环境中这并非必要。因此,这两个环境之间的代码存在差异。

修复此错误后,JavaScript 客户端即可正常运行:

Image

Image

14.5. HTTP 客户端的改进 3

当之前的 [Dao2] 类在浏览器中运行时,无需进行会话 Cookie 管理。这是因为托管 [dao] 层的浏览器会自动管理会话 Cookie:它会自动将服务器发送的任何 Cookie 发回给客户端。因此,[Dao2] 类可以重写为以下 [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;

与管理管理 Cookie 相关的所有内容均已消失。

我们将之前的项目修改如下:

Image

在 [src] 文件夹中,我们添加了两个文件:

  • 刚刚引入的 [Dao3] 类;
  • 负责启动新版本的 [main3] 文件;

[main3] 文件与上一版本的 [main] 文件完全相同,但现在使用了 [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();

已修改 [webpack.config] 文件,使其现在运行 [main3] 脚本:


/* eslint-disable */
 
const path = require("path");
const webpack = require("webpack");
 
/*
 * SplitChunksPlugin is enabled by default and replaced
 * deprecated CommonsChunkPlugin. It automatically identifies modules which
 * should be splitted of chunk by heuristics using module duplication count and
 * module category (i. e. node_modules). And splits the chunks…
 *
 * It is safe to remove "splitChunks" from the generated configuration
 * and was added as an educational example.
 *
 * https://webpack.js.org/plugins/split-chunks-plugin/
 *
 */
 
const HtmlWebpackPlugin = require("html-webpack-plugin");
 
/*
 * We've enabled HtmlWebpackPlugin for you! This generates a html
 * page for you when you compile webpack, which will make you start
 * developing and prototyping faster.
 *
 * https://github.com/jantimon/html-webpack-plugin
 *
 */
 
module.exports = {
  mode: "development",
  //entry: "./src/mainjs",
  entry: "./src/main3.js",
  output: {
    filename: "[name].[chunkhash].js",
    path: path.resolve(__dirname, "dist")
  },
 
  plugins: [new webpack.ProgressPlugin(), new HtmlWebpackPlugin()],
...
};

完成上述操作后,我们在启动税务计算服务器后运行该项目:

Image

浏览器控制台中显示的结果与上一版本完全一致。

14.6. 结论

现在,我们已经掌握了开发 Web 应用程序所需的全部 JavaScript 工具。我们可以:

  • 使用最新的 ECMAScript 代码;
  • [node.js] 环境中测试代码的独立部分,这更便于调试和测试;
  • 随后利用 [babel] [webpack] 将代码移植到浏览器中;