14. HTTP مشتریان: جاوااسکریپت برای سرویس محاسبه مالیات
14.1. Introduction
در اینجا پیشنهاد میکنیم یک کلاینت [node.js] برای نسخهٔ ۱۴ سرویس محاسبه مالیات بنویسیم. معماری کلاینت/سرور به شرح زیر خواهد بود:

ما دو نسخه از کلاینت را بررسی خواهیم کرد:
- نسخهٔ ۱ کلاینت ساختار لایهای زیر را خواهد داشت: [main, dao]:

- نسخهٔ ۲ کلاینت ساختار [main, métier, dao] را خواهد داشت. لایهٔ سرور [métier] به کلاینت واگذار خواهد شد:

14.2. کلاینت HTTP 1

همانطور که گفته شد، کلاینت HTTP 1 معماری کلاینت/سرور زیر را پیادهسازی میکند:

ما پیادهسازی خواهیم کرد:
- لایه [dao] به صورت یک کلاس؛
- لایه [main] را به صورت یک اسکریپت با استفاده از این کلاس پیادهسازی خواهیم کرد؛
14.2.1. لایه [dao]
لایه [dao] توسط کلاس زیر، [Dao1.js]، پیادهسازی خواهد شد:
''use strict';
// وارداتها
import qs from 'qs'
class Dao1 {
// سازنده
constructor(axios) {
//کتابخانه axios برای ارسال درخواستها HTTP
this.axios = axios;
// کوکی جلسه
this.sessionCookieName = "PHPSESSID";
this.sessionCookie = '';
}
// راهاندازی جلسه
async initSession() {
// گزینههای درخواست HHTP [get /main.php?action=init-session&type=json]
const options = {
method: "GET",
//URL پارامترها
params: {
action: 'init-session',
type: 'json'
}
};
//اجرای پرسوجو HTTP
return await this.getRemoteData(options);
}
async authentifierUtilisateur(user, password) {
// گزینههای درخواست HHTP [post /main.php?action=authentifier-utilisateur]
const options = {
method: "POST",
headers: {
'Content-type': 'application/x-www-form-urlencoded',
},
// بدنه POST
data: qs.stringify({
user: user,
password: password
}),
// پارامترهای URL
params: {
action: 'authentifier-utilisateur'
}
};
//اجرای پرسوجوی HTTP
return await this.getRemoteData(options);
}
//محاسبه مالیات
async calculerImpot(marié, enfants, salaire) {
// گزینههای پرسوجو HHTP [post /main.php?action=calculer-impot]
const options = {
method: "POST",
headers: {
'Content-type': 'application/x-www-form-urlencoded',
},
// بدنهٔ POST [marié, enfants, salaire]
data: qs.stringify({
marié: marié,
enfants: enfants,
salaire: salaire
}),
//پارامترهای URL
params: {
action: 'calculer-impot'
}
};
//اجرای پرسوجوی HTTP
const data = await this.getRemoteData(options);
// نتیجه
return data;
}
// فهرست شبیهسازیها
async listeSimulations() {
// گزینهها برای پرسوجوی HHTP [get /main.php?action=lister-simulations]
const options = {
method: "GET",
//URL پارامترها
params: {
action: 'lister-simulations'
},
};
//اجرای پرسوجوی HTTP
const data = await this.getRemoteData(options);
// نتیجه
return data;
}
// فهرست شبیهسازیها
async supprimerSimulation(index) {
// گزینههای پرسوجو HHTP [get /main.php?action=supprimer-simulation&numéro=index]
const options = {
method: "GET",
//URL پارامترها
params: {
action: 'supprimer-simulation',
numéro: index
},
};
//اجرای پرسوجوی HTTP
const data = await this.getRemoteData(options);
// نتیجه
return data;
}
async getRemoteData(options) {
//برای کوکی جلسه
if (!options.headers) {
options.headers = {};
}
options.headers.Cookie = this.sessionCookie;
//اجرای درخواست HTTP
let response;
try {
// درخواست ناهمزمان
response = await this.axios.request('main.php', options);
} catch (error) {
// پارامتر [error] یک نمونه استثنا است – میتواند اشکال مختلفی داشته باشد
if (error.response) {
//پاسخ سرور در [error.response] است
response = error.response;
} else {
//خطا مجدداً فعال میشود
throw error;
}
}
//پاسخ کل پاسخ HTTP از سرور است (سربرگهای HTTP + خود پاسخ)
//کوکی جلسه را در صورت وجود بازیابی میکند
const setCookie = response.headers['set-cookie'];
if (setCookie) {
// setCookie یک آرایه است
// کوکی جلسه در این آرایه جستجو میشود
let trouvé = false;
let i = 0;
while (!trouvé && i < setCookie.length) {
//جستجو برای کوکی جلسه
const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
if (results) {
// کوکی جلسه ذخیره میشود
//eslint-disable-next-line require-atomic-updates
this.sessionCookie = results[1];
// یافت شد
trouvé = true;
} else {
// عنصر بعدی
i++;
}
}
}
// پاسخ سرور در [response.data] است
return response.data;
}
}
// صادرات کلاس
export default Dao1;
- در اینجا ما آنچه را که در بخش مرتبط آموختهایم به کار میبریم، جایی که کتابخانه [axios] را معرفی کردیم، که امکان انجام پرسوجوهای HTTP را هم تحت [node.js] و هم در یک مرورگر فراهم میکند. ما به طور خاص به اسکریپت در بخش مرتبط نگاه خواهیم کرد؛
- خطوط ۹–۱۵: سازنده کلاس. این کلاس سه ویژگی خواهد داشت:
- [axios]: شیء [axios] که برای ارسال درخواستهای HTTP استفاده میشود. این شیء توسط کد فراخوانیکننده ارسال میشود؛
- [sessionCookieName]: بسته به سرور، کوکی جلسه ممکن است نامهای متفاوتی داشته باشد. در اینجا، این [PHPSESSID] است؛
- [sessionCookie]: کوکی جلسه که توسط سرور ارسال و توسط کلاینت ذخیره میشود؛
- خطوط ۵۳–۷۶: تابع غیرهمزمان [calculerImpot] با ارسال پارامترهای [marié, enfants, salaire]، درخواست [post /main.php?action=calculer-impot] را انجام میدهد. این تابع رشته jSON را که توسط سرور در قالب یک شی JavaScript ارسال شده است، بازمیگرداند؛
- خطوط ۷۹–۹۲: تابع غیرهمزمان [listeSimulations] درخواست [get /main.php?action=lister-simulations. این تابع رشته jSON ارسالشده توسط سرور را بهعنوان یک شی JavaScript بازمیگرداند؛
- خطوط ۹۵–۱۰۹: تابع غیرهمزمان [supprimerSimulation] درخواست [get /main.php?action=supprimer-simulation&numéro=index] را انجام میدهد. این تابع رشته jSON ارسالشده توسط سرور را به شکل یک شیء جاوااسکریپت بازمیگرداند؛
- خط ۱۲۱: نشانه [this.axios] استفاده میشود زیرا در این مورد، شی [axios] که به سازنده (constructor) پاس شده، در ویژگی [this.axios] ذخیره شده است؛
- خط ۱۶۱: کلاس [Dao1] صادر شده است تا بتوان از آن استفاده کرد؛
14.2.2. اسکریپت [main1.js]
اسکریپت [main1.js] با استفاده از کلاس [Dao1] مجموعهای از فراخوانیها را به سرور انجام میدهد:
- ابتداییسازی یک جلسه jSON؛
- احراز هویت با استفاده از [admin, admin];
- درخواست سه محاسبه مالیات؛
- فهرست شبیهسازیها را درخواست میکند؛
- یکی از آنها را حذف میکند؛
کد به شرح زیر است:
// وارد کردن Axios
import axios from 'axios';
//وارد کردن کلاس Dao1
import Dao from './Dao1';
// تابع غیرهمزمان [main]
async function main() {
//پیکربندی Axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
// مثالسازی لایه [dao]
const dao = new Dao(axios);
// با استفاده از لایه [dao]
try {
// ابتدای جلسه
log("-----------init-session");
let response = await dao.initSession();
log(response);
// احراز هویت
log("-----------authentifier-utilisateur");
response = await dao.authentifierUtilisateur("admin", "admin");
log(response);
//محاسبات مالیاتی
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);
// فهرست شبیهسازیها
log("-----------liste-des-simulations");
response = await dao.listeSimulations();
log(response);
// حذف یک شبیهسازی
log("-----------suppression simulation n° 1");
response = await dao.supprimerSimulation(1);
log(response);
} catch (error) {
//ثبت خطا
console.log("erreur=", error.message);
}
}
//ثبت jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// اجرا
main();
توضیحات
- خط ۲: کتابخانه [axios] وارد میشود؛
- خط ۴: کلاس [Dao] وارد میشود؛
- خط ۷: تابع [main] که با سرور ارتباط برقرار میکند، غیرهمزمان است؛
- خطوط ۹–۱۰: پیکربندی پیشفرض درخواستهای HTTP برای ارسال به سرور:
- خط ۹: [timeout] با زمانانتظار ۲ ثانیه؛
- خط ۱۰: تمام درخواستهای URL با پیشوند پایه URL از نسخه ۱۴ سرور محاسبه مالیات شروع میشوند؛
- خط ۱۲: لایه [Dao] ساخته میشود. اکنون میتوان از آن استفاده کرد؛
- خطوط ۴۶–۴۸: هدف تابع [log] نمایش رشته jSON از یک شیء جاوااسکریپت به صورت قالببندیشده است: به صورت عمودی، با یک عقبنشینی دو فاصلهای (پارامتر سوم);
- خطوط ۱۵–۱۸: راهاندازی جلسه jSON؛
- خطوط ۱۹–۲۲: احراز هویت؛
- خطوط ۲۳–۳۰: سه محاسبه مالیاتی به طور موازی درخواست میشود. به لطف [await Promise.all]، اجرا تا زمانی که هر سه نتیجه به دست نیامدهاند، مسدود میشود؛
- خطوط ۳۱–۳۴: فهرست شبیهسازیها؛
- خطوط ۳۵–۳۸: حذف یک شبیهسازی؛
- خطوط ۳۹–۴۲: رسیدگی به هرگونه استثنا؛
نتایج اجرای برنامه به شرح زیر است:
[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

معماری کلاینت HTTP2 به شرح زیر است:

لایه [métier] از سرور به کلاینت جاوااسکریپت منتقل شده است. برخلاف کاری که در ماژول PHP7 انجام دادیم، لایه [main] نیازی ندارد از طریق لایه [métier] عبور کند تا به لایه [dao] برسد. ما از این دو لایه بهعنوان مراکز تخصص استفاده خواهیم کرد:
- لایه [main] هر زمان که به دادههای ذخیرهشده روی سرور نیاز داشته باشد، از طریق لایه [dao] عبور میکند؛
- لایه [main] از لایه [métier] میخواهد محاسبات مالیاتی را انجام دهد؛
- لایه [métier] مستقل از لایه [dao] است و هرگز از آن فراخوانی نمیکند؛
14.3.1. کلاس جاوااسکریپت [Métier]
ماهیت کلاس [Métier] در PHP در مقاله مرتبط توضیح داده شده است. این قطعه کد نسبتاً پیچیدهای است که ما اینجا آن را به یاد میآوریم، نه برای توضیح آن، بلکه برای اینکه بتوانیم آن را به جاوااسکریپت تبدیل کنیم:
<?php
// فضای نام
namespace Application;
class Metier implements InterfaceMetier {
// لایه DAO
private $dao;
// دادههای اداره مالیات
private $taxAdminData;
//---------------------------------------------
// تنظیمکننده لایه [dao]
public function setDao(InterfaceDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceDao $dao) {
// یک مرجع در لایه [dao] ذخیره میشود
$this->dao = $dao;
// بازیابی دادههای مورد نیاز برای محاسبه مالیات
// روش [getTaxAdminData] ممکن است یک استثنا پرتاب کند ExceptionImpots
//سپس اجازه داده میشود که به کد فراخواننده بازتاب داده شود
$this->taxAdminData = $this->dao->getTaxAdminData();
}
//محاسبه مالیات
// --------------------------------------------------------------------------
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// $marié: بله، نه
// $enfants: تعداد فرزندان
// $salaire: حقوق سالانه
// $this->taxAdminData: دادههای سازمان مالیاتی
//
// ما بررسی میکنیم که دادههای سازمان مالیاتی را در اختیار داریم
if ($this->taxAdminData === NULL) {
$this->taxAdminData = $this->getTaxAdminData();
}
//محاسبه مالیات با فرزندان
$result1 = $this->calculerImpot2($marié, $enfants, $salaire);
$impot1 = $result1["impôt"];
//محاسبه مالیات بدون احتساب فرزندان
if ($enfants != 0) {
$result2 = $this->calculerImpot2($marié, 0, $salaire);
$impot2 = $result2["impôt"];
// اعمال سقف ضریب خانوادگی
$plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
if ($enfants < 3) {
//$PLAFOND_QF_DEMI_PART یورو برای دو فرزند اول
$impot2 = $impot2 - $enfants * $plafonDemiPart;
} else {
// $PLAFOND_QF_DEMI_PART یورو برای دو فرزند اول، دو برابر آن مبلغ برای فرزندان بعدی
$impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
}
} else {
$impot2 = $impot1;
$result2 = $result1;
}
//بالاترین نرخ مالیات اعمال میشود
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"];
}
//محاسبه هرگونه تخفیف مالیاتی
$décôte = $this->getDecôte($marié, $salaire, $impot);
$impot -= $décôte;
//محاسبه هرگونه معافیت مالیاتی
$réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
$impot -= $réduction;
//نتیجه
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é: بله، نه
// $enfants: تعداد فرزندان
// $salaire: حقوق سالانه
// $this->taxAdminData: دادههای اداره مالیات
//
//تعداد سهام
$marié = strtolower($marié);
if ($marié === "oui") {
$nbParts = $enfants / 2 + 2;
} else {
$nbParts = $enfants / 2 + 1;
}
// به ازای هر فرزند از سوم به بعد یک سهم
if ($enfants >= 3) {
// نیم سهم اضافی برای هر فرزند از فرزند سوم به بعد
$nbParts += 0.5 * ($enfants - 2);
}
// درآمد مشمول مالیات
$revenuImposable = $this->getRevenuImposable($salaire);
// سربار
$surcôte = floor($revenuImposable - 0.9 * $salaire);
//برای مسائل گرد کردن
if ($surcôte < 0) {
$surcôte = 0;
}
// ضریب خانوادگی
$quotient = $revenuImposable / $nbParts;
//محاسبه مالیات
$limites = $this->taxAdminData->getLimites();
$coeffR = $this->taxAdminData->getCoeffR();
$coeffN = $this->taxAdminData->getCoeffN();
//در انتهای جدول محدودیتها قرار میگیرد تا حلقه زیر را خاتمه دهد
$limites[count($limites) - 1] = $quotient;
//نرخ مالیات را جستجو میکند
$i = 0;
while ($quotient > $limites[$i]) {
$i++;
}
// زیرا $quotient در انتهای جدول $limites قرار گرفته است، حلقه قبلی
//نمیتواند از آرایه $limites فراتر رود
//اکنون میتوانیم مالیات را محاسبه کنیم
$impôt = floor($revenuImposable * $coeffR[$i] - $nbParts * $coeffN[$i]);
// نتیجه
return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
}
// revenuImposable = حقوق سالانه – کسورات
// معافیت مالیاتی حداقل و حداکثری دارد
private function getRevenuImposable(float $salaire): float {
// مبنای ۱۰٪ از حقوق
$abattement = 0.1 * $salaire;
//این کسورات نمیتواند از $this->taxAdminData تجاوز کند->getAbattementDixPourCentMax()
if ($abattement > $this->taxAdminData->getAbattementDixPourCentMax()) {
$abattement = $this->taxAdminData->getAbattementDixPourcentMax();
}
// این کمکهزینه نباید کمتر از $this->taxAdminData باشد->getAbattementDixPourcentMin()
if ($abattement < $this->taxAdminData->getAbattementDixPourcentMin()) {
$abattement = $this->taxAdminData->getAbattementDixPourcentMin();
}
// درآمد مشمول مالیات
$revenuImposable = $salaire - $abattement;
//سود
return floor($revenuImposable);
}
// هرگونه تخفیف را محاسبه میکند
private function getDecôte(string $marié, float $salaire, float $impots): float {
// در ابتدا، تخفیف صفر
$décôte = 0;
// حداکثر مبلغ مالیات برای واجد شرایط شدن جهت معافیت مالیاتی
$plafondImpôtPourDécôte = $marié === "oui" ?
$this->taxAdminData->getPlafondImpotCouplePourDecote() :
$this->taxAdminData->getPlafondImpotCelibatairePourDecote();
if ($impots < $plafondImpôtPourDécôte) {
// حداکثر مبلغ تخفیف
$plafondDécôte = $marié === "oui" ?
$this->taxAdminData->getPlafondDecoteCouple() :
$this->taxAdminData->getPlafondDecoteCelibataire();
// تخفیف نظری
$décôte = $plafondDécôte - 0.75 * $impots;
// معافیت مالیاتی نمیتواند از مبلغ مالیات تجاوز کند
if ($décôte > $impots) {
$décôte = $impots;
}
// هیچ تخفیفی <0
if ($décôte < 0) {
$décôte = 0;
}
}
// نتیجه
return ceil($décôte);
}
//هرگونه کاهش را محاسبه میکند
private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
// آستانه درآمد برای برخورداری از کاهش ۲۰٪
$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();
}
// درآمد مشمول مالیات
$revenuImposable = $this->getRevenuImposable($salaire);
// کاهش
$réduction = 0;
if ($revenuImposable < $plafondRevenuPourRéduction) {
// کاهش ۲۰ درصدی
$réduction = 0.2 * $impots;
}
// نتیجه
return ceil($réduction);
}
//محاسبه مالیات دستهای
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
//اجازه میدهد استثناهای ناشی از لایه [dao] منتقل شوند
//بازیابی دادههای مودی مالیاتی
$taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// جدول نتایج
$results = [];
// پردازش نتایج
foreach ($taxPayersData as $taxPayerData) {
// مالیات را محاسبه میکند
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// فرم را تکمیل کنید [$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"]);
// ما نتیجه را در جدول نتایج وارد میکنیم
$results [] = $taxPayerData;
}
//ذخیره نتایج
$this->dao->saveResults($resultsFileName, $results);
}
}
- خطوط ۱۹–۲۶: سازنده کلاس PHP. از آنجا که اعلام کردهایم در حال ساخت لایهای از [métier] مستقل از لایه [dao] هستیم، دو تغییر در این سازنده در جاوااسکریپت اعمال خواهیم کرد:
- این کلاس دیگر نمونه لایه [dao] را دریافت نخواهد کرد (دیگر به آن نیازی ندارد)؛
- این کلاس دیگر از لایه [dao] برای درخواست دادههای مالیاتی از مدیریت [taxAdminData] استفاده نخواهد کرد: کد فراخوانی این دادهها را به سازنده منتقل خواهد کرد؛
- خطوط 197–122: ما متد [executeBatchImpots] را پیادهسازی نخواهیم کرد، که هدف نهایی آن ذخیره نتایج شبیهسازی در یک فایل متنی بود. ما کدی میخواهیم که هم تحت [node.js] و هم در یک مرورگر کار کند. با این حال، ذخیره دادهها در سیستم فایل دستگاهی که مرورگر کلاینت را اجرا میکند، امکانپذیر نیست؛
با توجه به این محدودیتها، کد کلاس JavaScript [Métier] به شرح زیر است:
'use strict';
//کلاس تجاری
class Métier {
// سازنده
constructor(taxAdmindata) {
// QZXW2HTMLCdGhpcy.0YXhBZG1pbkRhdGEZQX: دادههای مرجع مالیاتی
this.taxAdminData = taxAdmindata;
}
// محاسبه مالیات
// --------------------------------------------------------------------------
calculerImpot(marié, enfants, salaire) {
// متأهل: بله، خیر
// فرزندان: تعداد فرزندان
// حقوق: حقوق سالانه
// QZXW2HTMLCdGhpcy.0YXhBZG1pbkRhdGEZQX: دادههای اداره مالیات
//
// محاسبه مالیات شامل فرزندان
const result1 = this.calculerImpot2(marié, enfants, salaire);
const impot1 = result1["impôt"];
// محاسبه مالیات بدون احتساب فرزندان
let result2, impot2, plafondDemiPart;
if (enfants !== 0) {
result2 = this.calculerImpot2(marié, 0, salaire);
impot2 = result2["impôt"];
// اعمال سقف ضریب خانوادگی
plafondDemiPart = this.taxAdminData.plafondQfDemiPart;
if (enfants < 3) {
// PLAFOND_QF_DEMI_PART یورو برای دو فرزند اول
impot2 = impot2 - enfants * plafondDemiPart;
} else {
//PLAFOND_QF_DEMI_PART یورو برای دو فرزند اول، دو برابر آن مبلغ برای فرزندان بعدی
impot2 = impot2 - 2 * plafondDemiPart - (enfants - 2) * 2 * plafondDemiPart;
}
} else {
// بدون بازمحاسبه مالیات
impot2 = impot1;
result2 = result1;
}
// بالاترین نرخ مالیات در [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"];
}
//محاسبه هرگونه تخفیف مالیاتی
const décôte = this.getDecôte(marié, impot);
impot -= décôte;
//محاسبه هرگونه معافیت مالیاتی
const réduction = this.getRéduction(marié, salaire, enfants, impot);
impot -= réduction;
// نتیجه
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) {
// متأهل: بله، خیر
// فرزندان: تعداد فرزندان
// حقوق: حقوق سالانه
// this->taxAdminData: دادهها از مراجع مالیاتی
//
// تعداد سهام
marié = marié.toLowerCase();
let nbParts;
if (marié === "oui") {
nbParts = enfants / 2 + 2;
} else {
nbParts = enfants / 2 + 1;
}
// به ازای هر فرزند، از سومین فرزند به بعد یک سهم
if (enfants >= 3) {
// نیمه معافیت اضافی برای هر فرزند از سوم به بعد
nbParts += 0.5 * (enfants - 2);
}
// درآمد مشمول مالیات
const revenuImposable = this.getRevenuImposable(salaire);
// عوارض اضافی
let surcôte = Math.floor(revenuImposable - 0.9 * salaire);
//برای مشکلات گرد کردن
if (surcôte < 0) {
surcôte = 0;
}
// مستمری خانوادگی
const quotient = revenuImposable / nbParts;
//محاسبه مالیات
const limites = this.taxAdminData.limites;
const coeffR = this.taxAdminData.coeffR;
const coeffN = this.taxAdminData.coeffN;
//در انتهای جدول محدودیتها قرار میگیرد تا حلقه زیر را خاتمه دهد
limites[limites.length - 1] = quotient;
//نرخ مالیات را جستجو میکند
let i = 0;
while (quotient > limites[i]) {
i++;
}
// از آنجا که کسر خانواده در انتهای جدول محدودهها قرار داده شده است، حلقه قبلی
// نمیتواند از محدودههای آرایه فراتر رود
//اکنون میتوانیم مالیات را محاسبه کنیم
const impôt = Math.floor(revenuImposable * coeffR[i] - nbParts * coeffN[i]);
// نتیجه
return { "impôt": impôt, "surcôte": surcôte, "taux": coeffR[i] };
}
//revenuImposable = حقوق سالانه – معافیت
// مبلغ معافیت حداقل و حداکثری دارد
getRevenuImposable(salaire) {
// مبنای معافیت ۱۰٪ از حقوق
let abattement = 0.1 * salaire;
//این کسورات نمیتواند از taxAdminData.getAbattementDixPourCentMax() فراتر رود
if (abattement > this.taxAdminData.abattementDixPourCentMax) {
abattement = this.taxAdminData.abattementDixPourcentMax;
}
// کسر نباید کمتر از taxAdminData.getAbattementDixPourcentMin() باشد
if (abattement < this.taxAdminData.abattementDixPourcentMin) {
abattement = this.taxAdminData.abattementDixPourcentMin;
}
//درآمد مشمول مالیات
const revenuImposable = salaire - abattement;
// سود
return Math.floor(revenuImposable);
}
//هرگونه تخفیف را محاسبه میکند
getDecôte(marié, impots) {
// در ابتدا، تخفیف صفر
let décôte = 0;
// حداکثر مبلغ مالیات برای واجد شرایط شدن جهت تخفیف
let plafondImpôtPourDécôte = marié === "oui" ?
this.taxAdminData.plafondImpotCouplePourDecote :
this.taxAdminData.plafondImpotCelibatairePourDecote;
let plafondDécôte;
if (impots < plafondImpôtPourDécôte) {
// حداکثر مبلغ تخفیف
plafondDécôte = marié === "oui" ?
this.taxAdminData.plafondDecoteCouple :
this.taxAdminData.plafondDecoteCelibataire;
// تخفیف نظری
décôte = plafondDécôte - 0.75 * impots;
// تخفیف نمیتواند از مبلغ مالیات تجاوز کند
if (décôte > impots) {
décôte = impots;
}
// هیچ تخفیفی <0
if (décôte < 0) {
décôte = 0;
}
}
// نتیجه
return Math.ceil(décôte);
}
//هرگونه کاهش را محاسبه میکند
getRéduction(marié, salaire, enfants, impots) {
// آستانه درآمد برای برخورداری از کاهش ۲۰٪
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;
}
// درآمد مشمول مالیات
const revenuImposable = this.getRevenuImposable(salaire);
// کاهش
let réduction = 0;
if (revenuImposable < plafondRevenuPourRéduction) {
// کاهش ۲۰ درصدی
réduction = 0.2 * impots;
}
// نتیجه
return Math.ceil(réduction);
}
}
// صادرات کلاس
export default Métier;
- کد جاوااسکریپت به طور دقیق از کد PHP پیروی میکند؛
- کلاس [Métier] صادر شده است، خط ۱۸۷؛
14.3.2. کلاس جاوااسکریپت [Dao2]

کلاس [Dao2] لایه [dao] از کلاینت جاوااسکریپت فوق را به شرح زیر پیادهسازی میکند:
'use strict';
// وارداتها
import qs from 'qs'
class Dao2 {
// سازنده
constructor(axios) {
this.axios = axios;
// کوکی جلسه
this.sessionCookieName = "PHPSESSID";
this.sessionCookie = '';
}
// راهاندازی جلسه
async initSession() {
// گزینههای درخواست HHTP [get /main.php?action=init-session&type=json]
const options = {
method: "GET",
//URL پارامترها
params: {
action: 'init-session',
type: 'json'
}
};
//اجرای پرسوجو HTTP
return await this.getRemoteData(options);
}
async authentifierUtilisateur(user, password) {
// گزینههای درخواست HHTP [post /main.php?action=authentifier-utilisateur]
const options = {
method: "POST",
headers: {
'Content-type': 'application/x-www-form-urlencoded',
},
// بدنهٔ POST
data: qs.stringify({
user: user,
password: password
}),
//پارامترهای URL
params: {
action: 'authentifier-utilisateur'
}
};
// اجرای پرسوجو HTTP
return await this.getRemoteData(options);
}
async getAdminData() {
// گزینهها برای پرسوجوی HHTP [get /main.php?action=get-admindata]
const options = {
method: "GET",
//پارامترها برای URL
params: {
action: 'get-admindata'
}
};
// اجرای پرسوجو HTTP
const data = await this.getRemoteData(options);
// نتیجه
return data;
}
async getRemoteData(options) {
//برای کوکی جلسه
if (!options.headers) {
options.headers = {};
}
options.headers.Cookie = this.sessionCookie;
//اجرای پرسوجوی HTTP
let response;
try {
// درخواست ناهمزمان
response = await this.axios.request('main.php', options);
} catch (error) {
// پارامتر [error] یک نمونه استثنا است – میتواند اشکال مختلفی داشته باشد
if (error.response) {
//پاسخ سرور در [error.response] است
response = error.response;
} else {
//خطا مجدداً فعال میشود
throw error;
}
}
//پاسخ کل پاسخ HTTP از سرور است (سربرگهای HTTP + خود پاسخ)
//کوکی جلسه را در صورت وجود بازیابی میکند
const setCookie = response.headers['set-cookie'];
if (setCookie) {
// setCookie یک آرایه است
//کوکی جلسه در این آرایه جستجو میشود
let trouvé = false;
let i = 0;
while (!trouvé && i < setCookie.length) {
//جستجو برای کوکی جلسه
const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
if (results) {
// کوکی جلسه ذخیره میشود
//eslint-disable-next-line require-atomic-updates
this.sessionCookie = results[1];
// پیدا شد
trouvé = true;
} else {
// عنصر بعدی
i++;
}
}
}
// پاسخ سرور در [response.data] است
return response.data;
}
}
// صادرات کلاس
export default Dao2;
نظرات
- کلاس [Dao2] تنها سه مورد از درخواستهای ممکن به سرور محاسبه مالیات را پیادهسازی میکند:
- [init-session] (خطوط 17–29): برای راهاندازی جلسه jSON؛
- [authentifier-utilisateur] (خطوط ۳۱–۵۰): برای احراز هویت؛
- [get-admindata] (خطوط ۵۲–۶۵): برای بازیابی دادهها از مرجع مالیاتی مورد نیاز برای انجام محاسبات مالیاتی در سمت کلاینت؛
- خطوط ۵۲–۶۵: ما یک اقدام جدید، [get-admindata]، به سرور معرفی میکنیم. این اقدام قبلاً پیادهسازی نشده بود. ما اکنون این کار را انجام میدهیم.
14.3.3. اصلاحات در سرور محاسبه مالیات
سرور محاسبه مالیات باید یک اقدام جدید را پیادهسازی کند. ما این کار را در نسخه 14 سرور انجام خواهیم داد. اقدامی که باید پیادهسازی شود دارای ویژگیهای زیر است:
- این عملیات توسط عملیات [get /main.php?action=get-admindata] درخواست میشود؛
- این اکشن رشته jSON را از ابجکتی که دادههای مقامات مالیاتی را در بر میگیرد، بازمیگرداند؛
اکنون به نحوه افزودن یک اقدام به سرور خود میپردازیم.
این تغییر در NetBeans انجام خواهد شد:

در [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"
}
این تغییر شامل موارد زیر است:
- خط ۶۷: اکشن [get-admindata] را اضافه کرده و آن را به یک کنترلر مرتبط کنید؛
- خط ۳۶: این کنترلر را در فهرست کلاسهایی که باید توسط برنامه PHP بارگذاری شوند، اعلام کنید؛
گام بعدی پیادهسازی کنترلر [AdminDataController] [3] است:
<?php
namespace Application;
//وابستگیهای Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
//نام مستعار برای لایه [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;
class AdminDataController implements InterfaceController {
// $config پیکربندی برنامه است
//پردازش یک درخواست
//از Session استفاده میکند و میتواند آن را تغییر دهد
// $infos جزئیات اضافی مختص هر کنترلکننده هستند
//یک آرایه را بازمیگرداند [$statusCode, $état, $content, $headers]
public function execute(
array $config,
Request $request,
Session $session,
array $infos = NULL): array {
//باید یک پارامتر واحد GET وجود داشته باشد
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 1;
if ($erreur) {
//خطا ثبت شد
$message = "il faut utiliser la méthode [get] avec l'unique paramètre [action] dans l'URL";
$état = 1001;
// نتیجه را به کنترلر اصلی بازگردانید
return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
}
// میتوانیم ادامه دهیم
// ردیس
\Predis\Autoloader::register();
try {
// کلاینت [predis]
$redis = new \Predis\Client();
// اتصال به سرور برای بررسی در دسترس بودن آن
$redis->connect();
} catch (\Predis\Connection\ConnectionException $ex) {
// خراب شد
//نتیجه با خطا به کنترلر اصلی بازگردانده شد
$état = 1050;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
}
//بازیابی دادهها از مراجع مالیاتی
//ابتدا کش را بررسی میکند [redis]
if (!$redis->get("taxAdminData")) {
try {
//بازیابی دادههای مالیاتی از پایگاه داده
$dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
//taxAdminData
$taxAdminData = $dao->getTaxAdminData();
//دادههای بازیابیشده در Redis ذخیره میشوند
$redis->set("taxAdminData", $taxAdminData);
} catch (\RuntimeException $ex) {
// چیزی اشتباه پیش رفت
// نتیجه همراه با خطا به کنترلر اصلی بازگردانده میشود
$état = 1041;
return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
["réponse" => utf8_encode($ex->getMessage())], []];
}
} else {
//دادههای مالیاتی از حافظه [redis] در محدوده [application] بازیابی میشوند
$arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
//یک شیء [TaxAdminData] از آرایهٔ ویژگیهای قبلی نمونهسازی میشود
$taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
}
//نتیجه را به کنترلکننده اصلی بازمیگرداند
$état = 1000;
return [Response::HTTP_OK, $état, ["réponse" => $taxAdminData], []];
}
}
نظرات
- خط ۱۲: مانند سایر کنترلکنندههای سرور، [AdminDataController] رابط [InterfaceController] را پیادهسازی میکند که شامل متد [execute] در خطوط ۱۹–۷۹ است؛
- خط ۷۸: مانند سایر کنترلکنندههای سرور، متد [AdminDataController.execute] آرایهای [$status, $état, [‘réponse’=>$response]] را بازمیگرداند که شامل:
- [$status]: کد وضعیت پاسخ به HTTP;
- [$état]: یک کد داخلی برنامه که وضعیت سرور را پس از اجرای درخواست کلاینت نشان میدهد؛
- [$response]: آرایهای که پاسخ ارسالشده به کلاینت را در بر میگیرد. در اینجا، این آرایه متعاقباً به رشته jSON تبدیل خواهد شد؛
- خطوط ۲۵–۳۴: اقدام کلاینت [get-admindata] بررسی میشود تا از صحت نحوی آن اطمینان حاصل شود؛
- خطوط ۳۷–۷۴: یک شیء [TaxAdminData] بازیابی میشود که به یکی از دو صورت زیر یافت میشود:
- خطوط ۵۶–۵۹: از پایگاه داده اگر در کش [redis] یافت نشود؛
- خطوط ۷۰–۷۳: از کش [redis];
این کد مشابه کد کنترلکننده [CalculerImpotController] است که در مقاله مرتبط توضیح داده شده است. در واقع، این کنترلکننده همچنین برای بازیابی شیء [TaxAdminData] که دادههای مرجع مالیاتی را در بر میگیرد، مورد نیاز بود.
در حین آزمایش کلاینت جاوااسکریپت، فرم jSON از [TaxAdminData] زمانی که این شیء در کش [redis] یافت میشد، مشکلساز شد. برای درک این موضوع، بیایید فرمت ذخیرهسازی این شیء در [redis] را بررسی کنیم:


- در [5-7] میبینیم که مقادیر عددی بهصورت رشتههای کاراکتری ذخیره شدهاند. PHP این موضوع را مدیریت کرد، زیرا عملگر + در محاسبات شامل اعداد و رشتهها بهطور ضمنی باعث تبدیل رشته به عدد میشود. با این حال، جاوااسکریپت برعکس عمل میکند: عملگر + در محاسباتی که شامل اعداد و رشتههاست، بهطور ضمنی نوع را از عدد به رشته تبدیل میکند. بنابراین، محاسبات در کلاس جاوااسکریپت [Métier] نادرست هستند؛
برای حل این مشکل، ما متد [TaxAdminData.setFromArrayOfAttributes] را که در خط ۷۱ کنترلر برای نمونهسازی یک شیء [TaxAdminData] استفاده میشود، اصلاح میکنیم (به مقاله مراجعه کنید) از رشته jSON که در کش [redis] یافت میشود:
<?php
namespace Application;
class TaxAdminData extends BaseEntity {
// مراتب مالیاتی
protected $limites;
protected $coeffR;
protected $coeffN;
// ثوابت محاسبه مالیات
protected $plafondQfDemiPart;
protected $plafondRevenusCelibatairePourReduction;
protected $plafondRevenusCouplePourReduction;
protected $valeurReducDemiPart;
protected $plafondDecoteCelibataire;
protected $plafondDecoteCouple;
protected $plafondImpotCouplePourDecote;
protected $plafondImpotCelibatairePourDecote;
protected $abattementDixPourcentMax;
protected $abattementDixPourcentMin;
// ابتداییسازی
public function setFromJsonFile(string $taxAdminDataFilename) {
// والد
parent::setFromJsonFile($taxAdminDataFilename);
//مقادیر ویژگیها بررسی میشوند
$this->checkAttributes();
// شیء را بازمیگرداند
return $this;
}
protected function check($value): \stdClass {
// $value یک آرایه از عناصر رشتهای یا یک عنصر واحد است
if (!\is_array($value)) {
$tableau = [$value];
} else {
$tableau = $value;
}
// آرایه رشتهها به آرایهای از اعداد حقیقی تبدیل میشود
$newTableau = [];
$result = new \stdClass();
// عناصر آرایه باید اعداد اعشاری مثبت یا صفر باشند
$modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
for ($i = 0; $i < count($tableau); $i ++) {
if (preg_match($modèle, $tableau[$i])) {
//عدد اعشاری در newTableau قرار میگیرد
$newTableau[] = (float) $tableau[$i];
} else {
//خطا ثبت شده است
$result->erreur = TRUE;
//خروج
return $result;
}
}
// نتیجه را بازگردانید
$result->erreur = FALSE;
if (!\is_array($value)) {
//یک مقدار واحد
$result->value = $newTableau[0];
} else {
// فهرستی از مقادیر
$result->value = $newTableau;
}
return $result;
}
// ابتداییسازی با استفاده از آرایهای از ویژگیها
public function setFromArrayOfAttributes(array $arrayOfAttributes) {
// والد
parent::setFromArrayOfAttributes($arrayOfAttributes);
// مقادیر ویژگیها بررسی میشوند
$this->checkAttributes();
// بازگرداندن شیء
return $this;
}
//بررسی مقادیر ویژگی
protected function checkAttributes() {
//بررسی کنید که مقادیر ویژگیها اعداد حقیقی ≥ 0 هستند
foreach ($this as $key => $value) {
if (is_string($value)) {
// $value باید یک عدد حقیقی ≥0 یا یک آرایه از اعداد حقیقی ≥0 باشد
$result = $this->check($value);
// خطا؟
if ($result->erreur) {
//یک استثنا پرتاب میشود
throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
} else {
//مقدار ثبت میشود
$this->$key = $result->value;
}
}
}
// شیء را بازگردانید
return $this;
}
// گیرنده و تنظیمکننده
...
}
توضیحات
- خط ۵: کلاس [TaxAdminData] از کلاس [BaseEntity] که از قبل متد [setFromArrayOfAttributes] را دارد، ارث میبرد. از آنجایی که این مناسب نیست، ما آن را در خطوط ۶۷–۷۵ مجدداً تعریف میکنیم؛
- خط ۷۰: متد [setFromArrayOfAttributes] از کلاس والد ابتدا برای مقداردهی اولیه ویژگیهای کلاس استفاده میشود؛
- خط ۷۲: متد [checkAttributes] بررسی میکند که مقادیر مرتبط در واقع اعداد هستند. اگر آنها رشتهها باشند، به اعداد تبدیل میشوند؛
- خط ۷۴: شیء حاصل [$this] سپس شیئی با ویژگیهایی حاوی مقادیر عددی است؛
- خطوط ۷۸–۹۳: متد [checkAttributes] بررسی میکند که مقادیر مرتبط با ویژگیهای شیء واقعاً عددی هستند؛
- خط ۸۰: فهرست ویژگیها بهصورت متوالی بررسی میشود؛
- خط ۸۱: اگر مقدار یک ویژگی از نوع [string] باشد؛
- خط ۸۳: سپس بررسی میشود که آیا این رشته نمایانگر یک عدد است؛
- خط ۹۰: اگر چنین باشد، رشته به عدد تبدیل شده و به ویژگی در حال بررسی اختصاص داده میشود؛
- خطوط ۸۵–۸۶: اگر اینطور نباشد، یک استثنا پرتاب میشود؛
- خطوط ۳۲–۶۵: تابع [check] کمی بیش از آنچه لازم است انجام میدهد. این تابع هم آرایهها و هم مقادیر منفرد را مدیریت میکند. با این حال، در اینجا فقط برای بررسی مقداری از نوع [string] فراخوانی میشود. این یک شیء را با ویژگیهای [erreur, value] بازمیگرداند، که در آن:
- [erreur] یک مقدار بولی است که نشان میدهد آیا خطایی رخ داده است یا خیر؛
- [value] پارامتر [value] از خط ۳۲ است که بسته به مورد به یک عدد یا یک آرایه از اعداد تبدیل شده است؛
کلاس [BaseEntity] که میتوانست دارای صفتی به نام [arrayOfAttributes] باشد، اصلاح شده است تا دیگر این صفت را شامل نشود: این کلاس عملاً رشته jSON را با [TaxAdminData] آلوده میکند. کلاس به صورت زیر بازنویسی شده است:
<?php
namespace Application;
class BaseEntity {
// ابتکاریسازی از یک فایل JSON
public function setFromJsonFile(string $jsonFilename) {
//بازیابی محتویات فایل دادههای مالیاتی
$fileContents = \file_get_contents($jsonFilename);
$erreur = FALSE;
// خطا؟
if (!$fileContents) {
//ثبت خطا
$erreur = TRUE;
$message = "Le fichier des données [$jsonFilename] n'existe pas";
}
if (!$erreur) {
// کد JSON را از فایل پیکربندی در یک آرایهٔ asociative بارگذاری میکند
$arrayOfAttributes = \json_decode($fileContents, true);
//خطا؟
if ($arrayOfAttributes === FALSE) {
//خطا ثبت میشود
$erreur = TRUE;
$message = "Le fichier de données JSON [$jsonFilename] n'a pu être exploité correctement";
}
}
//خطا؟
if ($erreur) {
//یک استثنا پرتاب میشود
throw new ExceptionImpots($message);
}
// ویژگیهای کلاس را مقداردهی اولیه میکند
foreach ($arrayOfAttributes as $key => $value) {
$this->$key = $value;
}
// بررسی میکند که همه ویژگیها موجود هستند
$this->checkForAllAttributes($arrayOfAttributes);
// بازگرداندن شیء
return $this;
}
public function checkForAllAttributes($arrayOfAttributes) {
//بررسی میکند که همه کلیدها مقداردهی شدهاند
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) {
// ابتداییسازی برخی ویژگیهای کلاس (نه لزوماً همهٔ آنها)
foreach ($arrayOfAttributes as $key => $value) {
$this->$key = $value;
}
// شیء را بازمیگرداند
return $this;
}
//toString
public function __toString() {
// ویژگیهای شیء
$arrayOfAttributes = \get_object_vars($this);
//رشتهٔ شیء jSON
return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
}
}
نظرات
- خط ۲۰: ویژگی [$this→arrayOfAttributes] به یک متغیر تبدیل شده است که اکنون باید به متد [checkForAllAttributes] در خط ۳۸ ارسال شود، متدی که قبلاً روی ویژگی [$this→arrayOfAttributes] عمل میکرد؛
به دلیل این تغییر در [BaseEntity]، کلاس [Database] نیز باید اندکی اصلاح شود:
<?php
namespace Application;
class Database extends BaseEntity {
// ویژگیها
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
// ابتکاری
public function setFromJsonFile(string $jsonFilename) {
// پدر
parent::setFromJsonFile($jsonFilename);
// شیء را برمیگرداند
return $this;
}
// گیرنده و تنظیمکننده
...
}
توضیحات
- در کد اصلی، پس از خط ۳۰، متد [parent::checkForAllAttributes] فراخوانی میشد. این کار دیگر لازم نیست زیرا اکنون توسط متد [parent::setFromJsonFile($jsonFilename)] به طور خودکار انجام میشود؛
14.3.4. تستهای سرور برای [Postman]
[Postman] در مقالهٔ پیوندی ارائه شد.
ما از تستهای Postman زیر استفاده میکنیم:



نتیجه jSON از این درخواست آخر به شرح زیر است:

- در [5-8]، میتوان مشاهده کرد که ویژگیهای رشته jSON در واقع دارای مقادیر عددی هستند (نه رشتههای کاراکتری). این نتیجه به کلاس جاوااسکریپت [Métier] اجازه میدهد تا به طور معمول اجرا شود؛
14.3.5. اسکریپت اصلی [main]

اسکریپت اصلی [main] برای کلاینت جاوااسکریپت به شرح زیر است:
// واردات
import axios from 'axios';
// واردات
import Dao from './Dao2';
import Métier from './Métier';
// تابع غیرهمزمان [main]
async function main() {
//پیکربندی Axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
// [dao] ایجاد لایه
const dao = new Dao(axios);
//درخواستها HTTP
let taxAdminData;
try {
// ابتدای جلسه
log("-----------init-session");
let response = await dao.initSession();
log(response);
// احراز هویت
log("-----------authentifier-utilisateur");
response = await dao.authentifierUtilisateur("admin", "admin");
log(response);
// دادههای مالیاتی
log("-----------get-admindata");
response = await dao.getAdminData();
log(response);
taxAdminData = response.réponse;
} catch (error) {
// خطا ثبت شد
console.log("erreur=", error.message);
// پایان
return;
}
// آشکارسازی لایه [métier]
const métier = new Métier(taxAdminData);
// محاسبات مالیاتی
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));
// فهرست شبیهسازیها
log("-----------liste-des-simulations");
log(simulations);
// حذف یک شبیهسازی
log("-----------suppression simulation n° 1");
simulations.splice(1, 1);
log(simulations);
}
// لاگ jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// اجرا
main();
توضیحات
- خطوط ۵–۶: وارد کردن کلاسهای [Dao] و [Métier]؛
- خط ۹: تابع غیرهمزمان [main]، که ارتباط با سرور را با استفاده از کلاس [Dao] مدیریت میکند و به کلاس [Métier] دستور میدهد تا محاسبات مالیات را انجام دهد؛
- خطوط ۱۰–۳۶: اسکریپت متدهای [initSession, authentifierUtilisateur, getAdminData] لایه [dao] را به ترتیب و به صورت مسدودکننده (blocking) فراخوانی میکند؛
- خط ۳۸: لایه [dao] دیگر مورد نیاز نیست. اکنون تمام عناصر لازم برای اجرای لایه [métier] سمت کلاینت جاوااسکریپت را در اختیار داریم؛
- خطوط ۴۱–۴۶: ما سه محاسبه مالیاتی را انجام میدهیم و نتایج را در آرایه [simulations] تجمیع میکنیم؛
- خط ۴۹: آرایه شبیهسازی را نمایش میدهیم؛
- خط ۵۲: یکی از آنها را حذف میکنیم؛
نتایج اجرای اسکریپت اصلی به شرح زیر است:
[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

در این بخش، ما برنامه [Client HTTP 2] را بر اساس معماری زیر در یک مرورگر بارگذاری میکنیم:

پرت کردن (Porting) یک فرآیند فوری نیست. اگرچه [node.js] قادر به اجرای جاوا اسکریپت (ES6) است، اما این امر به طور کلی در مورد مرورگرها صادق نیست. بنابراین لازم است از ابزارهایی استفاده شود که کد ES6 را به کدی ES5 تبدیل کنند که توسط مرورگرهای مدرن قابل درک باشد. خوشبختانه، این ابزارها هم قدرتمند هستند و هم استفاده از آنها نسبتاً ساده است.
در اینجا، ما دستورالعملهای مقاله [How to write ES6 code that’s safe to run in the browser - Web Developer's Journal] را دنبال کردهایم.
در پوشه [client HTTP 3/src]، ما عناصر [main.js, Métier.js, Dao2.js] را از برنامه [Client Http 2] که بهتازگی توسعه دادهایم، قرار دادهایم.
14.4.1. راهاندازی پروژه
ما در پوشه [client http 3] کار خواهیم کرد. یک ترمینال را در [VSCode] باز میکنیم و به این پوشه میرویم:

ما این پروژه را با استفاده از دستور [npm init] راهاندازی میکنیم و پاسخهای پیشفرض را برای سؤالات مطرحشده میپذیریم:

- در [4-5]، فایل پیکربندی پروژه [package.json] بر اساس پاسخهای مختلفی که ارائه شده است، ایجاد میشود؛
14.4.2. نصب وابستگیهای پروژه
ما وابستگیهای زیر را نصب خواهیم کرد:
- [@babel/core]: هسته ابزار [Babel] [https://babeljs.io]، که کد ES 2015+ را به کدی تبدیل میکند که هم روی مرورگرهای جدید و هم روی مرورگرهای قدیمیتر قابل اجرا است؛
- [@babel/preset-env]: بخشی از مجموعه ابزار Babel است. این بسته قبل از تبدیل (transpilation) از ES6 به ES5 استفاده میشود؛
- [babel-loader]: این وابستگی به ابزار [webpack] امکان میدهد تا ابزار [Babel] را فراخوانی کند؛
- [webpack]: هماهنگکننده. این ابزار [webpack] است که از Babel میخواهد کد را از ES6 به ES5 تبدیل (transpile) کند، و همین ابزار است که سپس تمام فایلهای حاصل را در یک فایل واحد مونتاژ میکند؛
- [webpack-cli]: مورد نیاز [webpack];
- [@webpack-cli/init]: برای پیکربندی [webpack] استفاده میشود؛
- [webpack-dev-server]: یک سرور وب توسعه را فراهم میکند که بهطور پیشفرض روی پورت 8080 اجرا میشود. هنگامی که فایلهای منبع تغییر میکنند، بهطور خودکار برنامه وب را مجدداً بارگذاری میکند؛
وابستگیهای پروژه به صورت زیر در ترمینال [VSCode] نصب میشوند:
npm --save-dev install @babel/core @babel/preset-env babel-loader webpack webpack-cli webpack-dev-server @webpack-cli/init

پس از نصب وابستگیها، فایل [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"
}
}
- خطوط ۱۲–۱۹: وابستگیهای پروژه [devDependencies] هستند: اینها در فاز توسعه مورد نیاز هستند اما در فاز تولید دیگر لازم نیستند. در واقع، در تولید، از فایل [dist/main.js] استفاده میشود. این فایل در ES5 کدگذاری شده است و دیگر به ابزارهای تبدیل کد از ES6 به ES5 نیاز ندارد؛
ما باید دو وابستگی به پروژه اضافه کنیم:
- [core-js]: شامل «پلیفیلها» برای ECMAScript 2019 است. یک پلیفیل امکان اجرای کدهای جدیدتر، مانند ECMAScript 2019 (سپتامبر 2019)، را روی مرورگرهای قدیمیتر فراهم میکند؛
- [regenerator-runtime]: طبق وبسایت کتابخانه --> [Source transformer enabling ECMAScript 6 generator functions in JavaScript-of-today];
از Babel 7 به بعد، این دو وابستگی جایگزین وابستگی [@babel/polyfill] میشوند که قبلاً این هدف را برآورده میکرد و اکنون (سپتامبر 2019) منسوخ شده است. آنها به شرح زیر نصب میشوند:

سپس فایل [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] مستلزم آن است که فایلهای زیر [imports] (خطوط ۳–۴) در اسکریپت اصلی [src/main.js] گنجانده شوند:
// وارداتها
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
// واردات
import Dao from './Dao2';
import Métier from './Métier';
14.4.3. پیکربندی [webpack]
[webpack] ابزاری است که مدیریت خواهد کرد:
- ترانسپیلکیشن (تبدیل) تمام فایلهای جاوااسکریپت پروژه از ES6 به ES5؛
- ادغام فایلهای تولیدشده در یک فایل واحد؛
این ابزار توسط یک فایل پیکربندی به نام [webpack.config.js] کنترل میشود که میتوان آن را با استفاده از یک وابستگی به نام [@webpack-cli/init] (سپتامبر ۲۰۱۹) تولید کرد. این وابستگی به همراه موارد دیگر ذکر شده در بخش «لینک» نصب شده است.
ما دستور [npx webpack-cli init] را در یک ترمینال [VSCode] اجرا میکنیم:

پس از پاسخ دادن به سؤالات مختلف (که برای اکثر آنها میتوانیم پاسخهای پیشفرض را بپذیریم)، فایلی با نام [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
}
};
من تمام جزئیات این فایل را کاملاً درک نمیکنم، اما چند نکته قابل توجه است:
- خط ۱: این فایل حاوی کد ES6 نیست. بنابراین [Eslint] خطاهایی را گزارش میکند که به ریشه پروژه [javascript] برمیگردند. این یک مشکل است. برای جلوگیری از تحلیل یک فایل توسط ESLint، کافی است خط ۱ را به صورت نظر (comment) درآورید؛
- خط ۳۱: ما در حالت [développement] کار میکنیم؛
- خط ۳۲: اسکریپت ورودی اینجا [src/index.js] است. باید این را تغییر دهیم؛
- خط ۳۶: پوشهای که خروجیهای [webpack] در آن ذخیره میشوند، پوشه [dist] است؛
- خط ۴۶: میبینیم که [webpack] از [babel-loader]، یکی از وابستگیهایی که نصب کردهایم، استفاده میکند؛
- خط ۵۴: میبینیم که [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"
}
}
- خط ۴: تغییر کرده است؛
- خطوط ۸–۹، ۱۸–۱۹: این موارد اضافه شدهاند؛
- خط ۸: وظیفه [npm] که پروژه را کامپایل میکند؛
- خط ۹: وظیفه [npm] که پروژه را اجرا میکند؛
- خط ۱۸: ؟
- خط ۱۹: فایلی به نام [dist/index.html] تولید میکند که به طور خودکار اسکریپت [dist/main.js] تولید شده توسط [webpack] را در خود جای میدهد، و این اسکریپت است که هنگام اجرای پروژه استفاده میشود؛
در نهایت، پیکربندی [webpack] فایلی به نام [src/index.js] ایجاد کرد:

محتویات [index.js] به شرح زیر است (سپتامبر ۲۰۱۹):
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] قرار دارند که آنها را برای اجرا ارسال میکند:

- در [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>
بنابراین این صفحه صرفاً فایل [main.hash.js] تولیدشده توسط [webpack] را در خود جای میدهد.
پروژه توسط وظیفه [start] اجرا میشود:

صفحه [dist/index.html] سپس روی یک سرور که بخشی از مجموعه [webpack] است و روی پورت 8080 ماشین محلی در حال اجراست، بارگذاری شده و توسط مرورگر پیشفرض ماشین نمایش داده میشود:

- در [2]، پورت سرویس وبسرور [webpack]؛
- به [3]، بدنه صفحه [dist/index.html] خالی است؛
- در [4]، زبانه [console] در ابزارهای توسعهدهنده مرورگر، در این مورد فایرفاکس (F12)؛
- در [5]، نتیجه اجرای فایل [src/index.js]. شایان ذکر است که محتویات این فایل به شرح زیر بود:
اکنون، بیایید این محتوا را به خط زیر تغییر دهیم:
بهطور خودکار (بدون کامپایل مجدد)، فایلهای جدید [main.js, index.html] ایجاد میشوند و فایل جدید [index.html] در مرورگر بارگذاری میشود:

اجرای وظیفه [build] قبل از وظیفه [start] ضروری نیست: وظیفه دوم ابتدا پروژه را کامپایل میکند. این کامپایل، خروجی را در پوشه [dist] ذخیره نمیکند. برای اینکه خودتان این موضوع را ببینید، کافی است این پوشه را حذف کنید. در این صورت خواهید دید که وظیفه [start] پروژه را بدون ایجاد پوشه [dist] کامپایل و اجرا میکند. به نظر میرسد که خروجیهای [index.html, main.hash.js] خود را در پوشهای که مخصوص [webpackdev-server] است، ذخیره میکند. این رفتار برای آزمایشهای ما کافی است.
وقتی سرور توسعه در حال اجراست، هر تغییری که در یکی از فایلهای پروژه ذخیره شود، باعث بازکامپایل شدن میشود. به همین دلیل، ما حالت [Auto Save] را در [VSCode] غیرفعال میکنیم. زیرا نمیخواهیم هر بار که کاراکترها در یکی از فایلهای پروژه تایپ میشوند، بازکامپایل انجام شود. ما فقط میخواهیم زمانی که تغییرات ذخیره میشوند، بازکامپایل انجام شود:

- در [2]، گزینه [Auto Save] نباید تیک خورده باشد؛
14.4.5. آزمایش کلاینت جاوااسکریپت برای سرور محاسبه مالیات
برای آزمایش کلاینت جاوااسکریپت سرور محاسبه مالیات، باید [main.js] [1] را بهعنوان نقطه ورود پروژه در فایل [webpack.config.js] [2-3] مشخص کنید:

به یاد داشته باشید که اسکریپت [main.js] باید در مقایسه با نسخه خود در [Client http 2]، دو واردکردن (import) اضافی را شامل شود:

علاوه بر این، ما کد را کمی تغییر دادهایم تا هرگونه خطایی را که ممکن است سرور بازگرداند، مدیریت کند:
// وارداتها
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
// وارداتها
import Dao from './Dao2';
import Métier from './Métier';
// تابع غیرهمزمان [main]
async function main() {
//پیکربندی axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
// [dao] نمونهسازی لایه
const dao = new Dao(axios);
//درخواستها HTTP
let taxAdminData;
try {
// ابتدای جلسه
log("-----------init-session");
let response = await dao.initSession();
log(response);
if (response.état != 700) {
throw new Error(JSON.stringify(response.réponse));
}
// احراز هویت
log("-----------authentifier-utilisateur");
response = await dao.authentifierUtilisateur("admin", "admin");
log(response);
if (response.état != 200) {
throw new Error(JSON.stringify(response.réponse));
}
// دادههای مالیاتی
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) {
// خطا ثبت شد
console.log("erreur=", error.message);
// پایان
return;
}
// مثالسازی لایه [métier]
const métier = new Métier(taxAdminData);
//محاسبات مالیاتی
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));
// فهرست شبیهسازیها
log("-----------liste-des-simulations");
log(simulations);
// حذف یک شبیهسازی
log("-----------suppression simulation n° 1");
simulations.splice(1, 1);
log(simulations);
}
// لاگ jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// اجرا
main();
توضیحات
- در خطوط [24-26]، [31-33] و [38-40]، کد [response.état] را که در پاسخ سرور jSON ارسال شده است، بررسی میکنیم. اگر این کد خطایی را نشان دهد، یک استثنا با پیام خطای 'jSON' از پاسخ سرور '[response.réponse]' پرتاب میشود؛
پس از انجام این کار، پروژه [5-6] را اجرا میکنیم.
سپس صفحه [index.html] تولید و در مرورگر بارگذاری میشود:

- در [7]، میبینیم که اقدام [init-session] به دلیل مشکل [CORS] (اشتراکگذاری منابع فرامنشأ) تکمیل نشد؛
مسئله CORS از رابطه کلاینت/سرور ناشی میشود:
- کلاینت جاوااسکریپت ما روی ماشین [http://localhost:8080] دانلود شده بود؛
- سرور محاسبه مالیات روی ماشین [http://localhost:80] در حال اجرا است؛
- بنابراین کلاینت و سرور در دامنه یکسانی قرار ندارند (یک ماشین اما پورتهای متفاوت)؛
- مرورگری که کلاینت جاوااسکریپت را از ماشین [http://localhost:8080] بارگذاری میکند، هر درخواستی را که به [http://localhost:80] هدایت نشده باشد، مسدود میکند. این یک اقدام امنیتی است. بنابراین، این درخواست کلاینت به سرورِ در حال اجرا روی ماشین [http://localhost:80] را مسدود میکند؛
در واقع، مرورگر درخواست را به طور کامل مسدود نمیکند. بلکه در واقع منتظر میماند تا سرور به آن «بگوید» که درخواستهای بیندامنهای را میپذیرد. اگر این مجوز را دریافت کند، مرورگر سپس درخواست بیندامنهای را ارسال خواهد کرد.
سرور با ارسال هدرهای خاص HTTP، مجوز خود را اعطا میکند:
- خط ۱: کلاینت جاوا اسکریپت در دامنه [http://localhost:8080] در حال فعالیت است. سرور باید صراحتاً پاسخ دهد که این دامنه را میپذیرد؛
- خط ۲: کلاینت جاوااسکریپت از هدرهای HTTP و [Accept, Content-Type] در درخواستهای خود استفاده خواهد کرد:
- [Accept]: این هدر در هر درخواست ارسال میشود؛
- [Content-Type]: این هدر در عملیات POST برای نشان دادن نوع پارامترها در POST استفاده میشود؛
سرور باید صراحتاً این دو هدر را بپذیرد: HTTP;
- خط ۳: کلاینت جاوااسکریپت از درخواستهای GET و POST استفاده خواهد کرد. سرور باید صراحتاً این دو نوع درخواست را بپذیرد؛
- خط ۴: کلاینت جاوااسکریپت کوکیهای جلسه را ارسال خواهد کرد. سرور این کوکیها را با هدر در خط ۴ میپذیرد؛
بنابراین نیاز داریم سرور را اصلاح کنیم. این کار را در [Netbeans] انجام میدهیم. مشکل مربوط به CORS تنها در حالت توسعه رخ میدهد. در محیط تولید، کلاینت و سرور در همان دامنه [http://localhost:80] عمل خواهند کرد و هیچ مشکلی با CORS وجود نخواهد داشت. بنابراین ما به روشی نیاز داریم تا درخواستهای CORS را از طریق پیکربندی سرور مجاز یا رد کنیم.

تغییرات روی سرور در سه مکان اعمال میشوند:
- [1, 4]: در فایل پیکربندی [config.json]، برای افزودن یک مقدار بولی که کنترل میکند آیا درخواستهای بیندامنهای پذیرفته شوند یا خیر؛
- [2]: در کلاس [ParentResponse] که پاسخ را به کلاینت جاوااسکریپت ارسال میکند. این کلاس سربرگهای CORS مورد انتظار مرورگر کلاینت را ارسال خواهد کرد؛
- [3]: در داخل کلاسهای [HtmlResponse, JsonResponse, XmlResponse] که به ترتیب پاسخها را برای جلسات [html, json, xml] تولید میکنند. این کلاسها باید مقدار بولی [corsAllowed] را که در [4] یافت میشود، به کلاس والد خود [2] ارسال کنند. این کار در [5] با ارسال آرایه تصویر از فایل jSON به [2] انجام میشود؛
کلاس [ParentResponse] [2] به شرح زیر تکامل مییابد:
<?php
namespace Application;
// وابستگیهای Symfony
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
class ParentResponse {
// int $statusCode: کد وضعیت پاسخ HTTP
// رشته $content: بدنه پاسخ ارسالی
//بسته به مورد، این یک رشته است: JSON, XML, HTML
//آرایه $headers: سربرگهای HTTP که باید به پاسخ اضافه شوند
public function sendResponse(
Request $request,
int $statusCode,
string $content,
array $headers,
array $config): void {
//آمادهسازی پاسخ متنی سرور
$response = new Response();
$response->setCharset("utf-8");
// کد وضعیت
$response->setStatusCode($statusCode);
// سربرگها برای درخواستهای بیندامنهای
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);
}
//حالت ویژهٔ روش [OPTIONS]
// در این مورد تنها سربرگها مهم هستند
$method = strtolower($request->getMethod());
if ($method === "options") {
$content = "";
$response->setStatusCode(Response::HTTP_OK);
}
//پاسخ ارسال میشود
$response->setContent($content);
$response->send();
}
}
- خط ۲۹: بررسی میکنیم که آیا نیاز به رسیدگی به درخواستهای بیندامنهای داریم یا خیر. در صورت لزوم، سربرگهای HTTP و CORS (خطوط ۳۳–۳۷) را تولید میکنیم، حتی اگر درخواست فعلی یک درخواست بیندامنهای نباشد. در مورد دوم، سربرگهای CORS غیرضروری خواهند بود و توسط کلاینت استفاده نخواهند شد؛
- خط ۳۰: در یک درخواست بیندامنهای، مرورگر کلاینت که از سرور پرسوجو میکند، یک هدر HTTP [Origin: http://localhost:8080] (در مورد خاص کلاینت جاوااسکریپت ما) ارسال میکند. خط ۳۰: ما این هدر HTTP را از درخواست [$request] بازیابی میکنیم؛
- خط ۳۱: ما فقط درخواستهای بیندامنهای را که از ماشین [http://localhost] سرچشمه میگیرند، میپذیریم. توجه داشته باشید که این درخواستها فقط در حالت توسعه پروژه رخ میدهند؛
- خطوط ۳۲–۳۶: سربرگهای CORS به سربرگهای موجود در آرایه [$headers] اضافه میشوند؛
- خطوط ۴۵–۴۹: روشی که مرورگر کلاینت برای درخواست مجوزهای CORS استفاده میکند ممکن است بسته به کلاینت مورد استفاده متفاوت باشد. گاهی اوقات، مرورگر کلاینت این مجوزها را با استفاده از یک فرمان HTTP [OPTIONS] درخواست میکند. این یک ویژگی جدید برای سرور ما است که برای پردازش تنها دستورات [GET, POST] ساخته شده است. در مورد فرمان [OPTIONS]، سرور در حال حاضر یک پاسخ خطا تولید میکند. در خطوط ۴۶–۴۹، ما این مشکل را در لحظه آخر برطرف میکنیم: اگر در خط ۴۶ متوجه شویم که فرمان فعلی یک فرمان [OPTIONS] است، آنگاه موارد زیر را برای کلاینت تولید میکنیم:
- خطوط ۴۷ و ۵۱: یک پاسخ خالی [$content]؛
- خط ۴۸: یک کد وضعیت ۲۰۰ که نشان میدهد فرمان با موفقیت انجام شده است. تنها نکته مهم برای این فرمان، ارسال سربرگهای CORS از خطوط ۳۳ تا ۳۶ است. این همان چیزی است که مرورگر کلاینت انتظار دارد؛
پس از اینکه سرور به این روش اصلاح شد، کلاینت جاوا اسکریپت روانتر اجرا میشود اما خطای جدیدی نمایش میدهد:

- در [1]، جلسه jSON به درستی اولیه میشود؛
- در [2]، اقدام [authentifier-utilisateur] ناموفق است: سرور نشان میدهد که هیچ جلسه فعالی وجود ندارد. این بدان معناست که کلاینت جاوااسکریپت کوکی جلسه را که در طول اقدام [init-session] ارسال کرده بود، به درستی بازنگردانده است؛
بیایید ترافیک شبکهای که رخ داده است را بررسی کنیم:

- در [4]، درخواست [init-session]. این با کد وضعیت پاسخ 200 با موفقیت تکمیل شد؛
- به [5]، درخواست [authentifier-utilisateur]. این یکی با کد وضعیت 400 (درخواست نامعتبر) [6] شکست خورد؛
اگر سربرگهای HTTP و [7] درخواست [5] را بررسی کنیم، میتوانیم ببینیم که کلاینت جاوااسکریپت سربرگهای HTTP و [Cookie] را ارسال نکرده است، که این امر میتوانست به آن امکان دهد تا کوکی جلسه را که در ابتدا توسط سرور ارسال شده بود، بازگرداند. به همین دلیل است که سرور گزارش میدهد که هیچ جلسهای وجود ندارد.
برای اینکه کلاینت کوکی جلسه را ارسال کند، باید یک پیکربندی به شیء [axios] اضافه شود:
// واردات
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";
//وارداتی
import Dao from './Dao2';
import Métier from './Métier';
// تابع غیرهمزمان [main]
async function main() {
//پیکربندی axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
axios.defaults.withCredentials = true;
// [dao] نمونهسازی لایه
const dao = new Dao(axios);
// درخواستها HTTP
let taxAdminData;
...
خط ۱۵ مشخص میکند که کوکیها باید در هدرهای HTTP درخواست [axios] گنجانده شوند. توجه داشته باشید که این کار در محیط [node.js] ضروری نبود. بنابراین تفاوتهایی در کد بین این دو محیط وجود دارد.
پس از رفع این خطا، کلاینت جاوا اسکریپت به طور معمول اجرا میشود:


14.5. HTTP ۳ بهبود مشتری
وقتی کلاس قبلی [Dao2] در یک مرورگر اجرا میشود، مدیریت کوکی جلسه غیرضروری است. زیرا مرورگر میزبان لایه [dao] است که کوکی جلسه را مدیریت میکند: آن بهطور خودکار هر کوکی را که توسط سرور ارسال شده بازمیگرداند. در نتیجه، کلاس [Dao2] را میتوان به کلاس زیر [Dao3] بازنویسی کرد:
"use strict";
// واردات
import qs from "qs";
class Dao3 {
// سازنده
constructor(axios) {
this.axios = axios;
}
// ابتدای جلسه
async initSession() {
// گزینههای درخواست HHTP [get /main.php?action=init-session&type=json]
const options = {
method: "GET",
//URL پارامترها
params: {
action: "init-session",
type: "json"
}
};
//اجرای پرسوجو HTTP
return await this.getRemoteData(options);
}
async authentifierUtilisateur(user, password) {
// گزینهها برای پرسوجوی HHTP [post /main.php?action=authentifier-utilisateur]
const options = {
method: "POST",
headers: {
"Content-type": "application/x-www-form-urlencoded"
},
// بدنهٔ POST
data: qs.stringify({
user: user,
password: password
}),
//پارامترهای URL
params: {
action: "authentifier-utilisateur"
}
};
// اجرای پرسوجو HTTP
return await this.getRemoteData(options);
}
async getAdminData() {
// گزینهها برای پرسوجوی HHTP [get /main.php?action=get-admindata]
const options = {
method: "GET",
//پارامترها برای URL
params: {
action: "get-admindata"
}
};
// اجرای پرسوجو HTTP
const data = await this.getRemoteData(options);
// نتیجه
return data;
}
async getRemoteData(options) {
//اجرای پرسوجوی HTTP
let response;
try {
//درخواست ناهمزمان
response = await this.axios.request("main.php", options);
} catch (error) {
//پارامتر [error] یک نمونه استثنا است – ممکن است اشکال مختلفی داشته باشد
if (error.response) {
//پاسخ سرور در [error.response] است
response = error.response;
} else {
//خطا مجدداً فعال میشود
throw error;
}
}
//پاسخ، پاسخ کامل HTTP از سرور است (سربرگهای HTTP + خود پاسخ)
//پاسخ سرور در [response.data] قرار دارد
return response.data;
}
}
// خروجی کلاس
export default Dao3;
تمام موارد مربوط به مدیریت کوکی مدیریت حذف شده است.
ما پروژه قبلی را به شرح زیر اصلاح میکنیم:

در پوشه [src]، دو فایل اضافه کردهایم:
- کلاس [Dao3]، که همیناکنون آن را توصیف کردیم؛
- فایل [main3] که مسئول راهاندازی نسخه جدید است؛
فایل [main3] همچنان مشابه فایل [main] نسخه قبلی است، اما اکنون از کلاس [Dao3] استفاده میکند:
//وارداتی
import axios from "axios";
import "core-js/stable";
import "regenerator-runtime/runtime";
// وارداتها
import Dao from "./Dao3";
import Métier from "./Métier";
// تابع غیرهمزمان [main]
async function main() {
// پیکربندی Axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL =
"http://localhost/php7/scripts-web/impots/version-14";
axios.defaults.withCredentials = true;
// [dao] ایجاد لایه
const dao = new Dao(axios);
// درخواستها HTTP
...
}
// لاگ jSON
function log(object) {
console.log(JSON.stringify(object, null, 2));
}
// اجرا
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://QZXW2HTMLCZ2l0aHViLmNvbQZX/jantimon/html-webpack-plugin
*
*/
module.exports = {
mode: "development",
//ورودی: "./src/mainjs",
entry: "./src/main3.js",
output: {
filename: "[name].[chunkhash].js",
path: path.resolve(__dirname, "dist")
},
plugins: [new webpack.ProgressPlugin(), new HtmlWebpackPlugin()],
...
};
پس از انجام این کار، پروژه پس از راهاندازی سرور محاسبه مالیات اجرا میشود:

نتایج نمایش داده شده در کنسول مرورگر با نتایج نسخه قبلی یکسان هستند.
14.6. Conclusion
اکنون تمام ابزارهای لازم برای توسعه کد جاوااسکریپت برای یک برنامه وب را در اختیار داریم. ما میتوانیم:
- از آخرین کد ECMAScript استفاده کنید؛
- اجزای جداگانه این کد را در محیطی سادهتر برای عیبیابی و آزمایش بررسی کنیم؛
- سپس این کد را با استفاده از ابزارهای [babel] و [webpack] به یک مرورگر منتقل کنیم؛