Skip to content

18. عميل Vue.js لخادم حساب الضرائب

18.1. البنية

سنقوم بتنفيذ تطبيق عميل/خادم بالبنية التالية:

Image

سيكون خادم حساب الضرائب من الإصدار 14، المطور في المستند |https://tahe.developpez.com/tutoriels-cours/php7|

18.2. طرق عرض التطبيق

طرق عرض التطبيق [vuejs-10] هي تلك الموجودة في الإصدار 13 من المستند |https://tahe.developpez.com/tutoriels-cours/php7| لخادم حساب الضرائب عند استخدامه في وضع HTML. ومع ذلك، في هذا التطبيق، سيتم إنشاء طرق العرض هذه بواسطة عميل JavaScript بدلاً من خادم PHP.

العرض الأول هو عرض المصادقة:

Image

العرض الثاني هو عرض حساب الضريبة:

Image

تعرض طريقة العرض الثالثة قائمة المحاكاة التي أجراها المستخدم:

Image

تُظهر الشاشة أعلاه أنه يمكنك حذف المحاكاة رقم 1. وينتج عن ذلك العرض التالي:

Image

إذا قمت الآن بحذف المحاكاة الأخيرة، فستحصل على العرض الجديد التالي:

Image

18.3. عناصر المشروع [vuejs-20]

تبدو شجرة المشروع [vuejs-20] كما يلي:

Image

وتتمثل عناصر المشروع فيما يلي:

  • [assets/logo.jpg]: شعار المشروع؛
  • [layers]: طبقات [business] و [DAO] للتطبيق؛
  • [plugins]: المكونات الإضافية للتطبيق؛
  • [views]: طرق عرض التطبيق؛
  • [config.js]: تكوين التطبيق؛
  • [router.js]: يحدد توجيه التطبيق؛
  • [store.js]: مخزن [Vuex]؛
  • [main.js]: البرنامج النصي الرئيسي للتطبيق؛

18.3.1. طبقتا [business] و [DAO]

18.3.1.1. طبقة [DAO]

يتم تنفيذ طبقة [DAO] بواسطة فئة [Dao] في القسم |vuejs-10|

18.3.1.2. طبقة [business]

يتم تنفيذ طبقة [business] بواسطة فئة [Business] في المستند |https://tahe.developpez.com/tutoriels-cours/php7|. تمت إضافة الطريقة [setTaxAdminData] التالية إليها:


// constructeur
  constructor(taxAdmindata) {
    // this.taxAdminData : données de l'administration fiscale
    this.taxAdminData = taxAdmindata;
  }
 
  // setter
  setTaxAdminData(taxAdmindata) {
    // this.taxAdminData : données de l'administration fiscale
    this.taxAdminData = taxAdmindata;
}

تقوم طريقة [setTaxAdminData] بنفس وظيفة المنشئ. ويتيح وجودها التسلسل التالي:

  1. إنشاء مثيل لفئة [Métier] باستخدام العبارة [métier=new Métier()] عندما تريد إنشاء مثيل للفئة ولكن لا تتوفر لديك بيانات [taxAdminData] بعد؛
  2. ثم ملء خاصية [taxAdminData] الخاصة بها لاحقًا باستخدام العملية [métier.setTaxAdminData(taxAdmindata)]؛

18.3.2. ملف التكوين [config]

ملف [config.js] هو كما يلي:


// utilisation de la bibliothèque [axios]
const axios = require('axios');
// timeout des requêtes HTTP
axios.defaults.timeout = 2000;
// la base des URL du serveur de calcul de l'impôt
// le schéma [https] pose des problèmes à Firefox parce que le serveur de calcul
// de l'impôt envoie un certificat autosigné. ok avec Chrome et Edge. Safari pas testé.
axios.defaults.baseURL = 'https://localhost/php7/scripts-web/impots/version-14';
// on va utiliser des cookies
axios.defaults.withCredentials = true;
 
// export de la configuration
export default {
  axios: axios
}

هذا التكوين مخصص لمكتبة [axios] التي تستخدمها طبقة [dao] لإجراء طلبات HTTP الخاصة بها. لاحظ في السطر 8 أن الخادم يعمل على منفذ آمن [https].

18.3.3. المكونات الإضافية

تم تصميم المكونات الإضافية [pluginDao، pluginMétier، pluginConfig] لإنشاء ثلاث خصائص جديدة لوظيفة/فئة [Vue]:

  • [$dao]: سيكون له قيمة مثيل لفئة [Dao]؛
  • [$métier]: سيكون له قيمة مثيل لفئة [Métier]؛
  • [$config]: سيتم تعيينها إلى الكائن الذي تم تصديره بواسطة ملف التكوين [config]؛

[pluginDao]


export default {
  install(Vue, dao) {
    // ajoute une propriété [$dao] à la classe Vue
    Object.defineProperty(Vue.prototype, '$dao', {
      // lorsque Vue.$dao est référencé, on rend le 2ième paramètre [dao]
      get: () => dao,
    })
  }
}
 
[pluginMétier]
 
export default {
  install(Vue, métier) {
    // ajoute une propriété [$métier] à la classe Vue
    Object.defineProperty(Vue.prototype, '$métier', {
      // lorsque Vue.$métier est référencé, on rend le 2ième paramètre [métier]
      get: () => métier,
    })
  }
}

[pluginConfig]


export default {
  install(Vue, config) {
    // ajoute une propriété [$config] à la classe vue
    Object.defineProperty(Vue.prototype, '$config', {
      // lorsque Vue.$config est référencé, on rend le 2ième paramètre [config]
      get: () => config,
    })
  }
}

18.3.4. مخزن [Vuex]

يتم تنفيذ مخزن [Vuex] بواسطة ملف [store] التالي:


// plugin Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
 
// store Vuex
const store = new Vuex.Store({
  state: {
    // le tableau des simulations
    simulations: [],
    // le n° de la dernière simulation
    idSimulation: 0
  },
  mutations: {
    // suppression ligne n° index
    deleteSimulation(state, index) {
      // eslint-disable-next-line no-console
      console.log("mutation deleteSimulation");
      // on supprime la ligne n° [index]
      state.simulations.splice(index, 1);
      // eslint-disable-next-line no-console
      console.log("store simulations", state.simulations);
    },
    // ajout d'une simulation
    addSimulation(state, simulation) {
      // eslint-disable-next-line no-console
      console.log("mutation addSimulation");
      // n° de la simulation
      state.idSimulation++;
      simulation.id = state.idSimulation;
      // on ajoute la simulation au tableau des simulations
      state.simulations.push(simulation);
    },
    // nettoyage state
    clear(state) {
      state.simulations = [];
      state.idSimulation = 1;
    }
  }
});
// export de l'objet [store]
export default store;

تعليقات

  • الأسطر 2-4: تم دمج المكون الإضافي [Vuex] في إطار عمل [Vue]؛
  • الأسطر 8-13: نضع العناصر التالية في مخزن [Vuex]:
    • [simulations]: قائمة المحاكاة التي أجراها المستخدم؛
    • [idSimulation]: معرف آخر محاكاة أجراها المستخدم؛

لاحظ أن المخزن سيتم مشاركته بين العروض وأن محتواه تفاعلي: فعندما يتم تعديله، يتم تحديث العروض التي تستخدمه تلقائيًا. في تطبيقنا، لا يحتاج سوى عنصر [simulations] إلى أن يكون تفاعليًا، وليس عنصر [simulationId]. وقد تركنا هذا العنصر في المخزن لتسهيل الأمر؛

  • الأسطر 14–40: التغييرات المسموح بها على كائن [state] من الأسطر 8–13. لاحظ أن هذه تتلقى دائمًا كائن [state] من الأسطر 8–13 كمعاملها الأول؛
    • السطر 16: تسمح لك التغييرات [deleteSimulation] بحذف محاكاة عن طريق تحديد رقم [index] الخاص بها؛
    • السطر 25: تسمح لك عملية التغيير [addSimulation] بإضافة محاكاة جديدة إلى مصفوفة المحاكاة؛
    • السطر 35: تعمل عملية [clear] على إعادة تعيين كائن [state] من الأسطر 8-13؛

18.3.5. ملف التوجيه [router]

ملف التوجيه هو كما يلي:


// imports
import Vue from 'vue'
import VueRouter from 'vue-router'
// les vues
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'
 
// plugin de routage
Vue.use(VueRouter)
 
// les routes de l'application
const routes = [
  // authentification
  {
    path: '/', name: 'authentification', component: Authentification
  },
  // calcul de l'impôt
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot
  },
  // liste des simulations
  {
    path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations
  },
  // fin de session
  {
    path: '/fin-session', name: 'finSession', component: Authentification
  }
]
 
// le routeur
const router = new VueRouter({
  // les routes
  routes,
  // le mode d'affichage des routes dans le navigateur
  mode: 'history',
})
 
// export du router
export default router

تعليقات

  • السطر 16: عند بدء تشغيل التطبيق، يتم عرض طريقة العرض [Authentication] لأن عنوان URL الخاص بها هو الجذر [/]؛
  • السطر 20: يتم عرض طريقة العرض [TaxCalculation] عند طلب عنوان URL [/tax-calculation]؛
  • السطر 24: يتم عرض طريقة العرض [SimulationList] عند طلب عنوان URL [/simulation-list]؛
  • السطر 28: يتم عرض طريقة العرض [Authentication] عند طلب عنوان URL [/end-session]؛
  • الأسطر 33-38: يتم إنشاء كائن [router] باستخدام هذه المسارات (السطر 35) ووضع [history] (السطر 37) لإدارة عناوين URL؛
  • السطر 41: يتم تصدير هذا الموجه؛

18.3.6. النص البرمجي الرئيسي [main.js]

النص البرمجي [main.js] هو كما يلي:


// imports
import Vue from 'vue'
 
// vue principale
import Main from './views/Main.vue'
 
// plugin [bootstrap-vue]
import BootstrapVue from 'bootstrap-vue'
Vue.use(BootstrapVue);
 
// CSS bootstrap
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
 
// routeur
import router from './router'
 
// plugin [config]
import config from './config';
import pluginConfig from './plugins/pluginConfig'
Vue.use(pluginConfig, config)
 
// instanciation couche [dao]
import Dao from './couches/Dao';
const dao = new Dao(config.axios);
 
// plugin [dao]
import pluginDao from './plugins/pluginDao'
Vue.use(pluginDao, dao)
 
// instanciation couche [métier]
import Métier from './couches/Métier';
const métier = new Métier();
 
// plugin [métier]
import pluginMétier from './plugins/pluginMétier'
Vue.use(pluginMétier, métier)
 
// store Vuex
import store from './store'
 
// démarrage de l'UI
new Vue({
  el: '#app',
  // le routeur
  router: router,
  // le store Vuex
  store: store,
  // la vue principale
  render: h => h(Main),
})

لاحظ النقاط التالية:

  • الأسطر 18–21: سيكون الكائن الذي تم تصديره بواسطة البرنامج النصي [./config] متاحًا في الخاصية [Vue.$config]، وبالتالي متاحًا لجميع طرق العرض في التطبيق. لم يكن هذا ضروريًا هنا لأن الكائن [config] لا يستخدمه سوى البرنامج النصي [main] (السطر 25). ومع ذلك، من الشائع أن تكون التهيئة مطلوبة من قبل طرق عرض متعددة. لذلك أردنا الحفاظ على مبدأ إتاحته في سمة عرض؛
  • الأسطر 24-25: إنشاء مثيل لطبقة [dao]. يتم استيراد فئة [Dao] في السطر 24 ثم يتم إنشاء مثيل لها في السطر 25. يأخذ منشئها كائن [axios] — وهو خاصية تكوين — كمعلمة وحيدة له؛
  • الأسطر 27-29: يتم توفير طبقة [dao] في سمة [$dao] لجميع طرق العرض؛
  • الأسطر 31–37: نكرر نفس التسلسل لطبقة [business]. يأخذ منشئ فئة [Business] [taxAdminData] كمعلمة، والتي تمثل بيانات إدارة الضرائب. لا نمتلك هذه البيانات بعد. لذلك، سيتعين ملء كائن [business] في السطر 33 لاحقًا؛
  • السطر 40: نقوم باستيراد مخزن [Vuex]؛
  • الأسطر 43-51: نقوم بإنشاء مثيل للعرض الرئيسي [Main] (السطران 5 و50)، ونمرر له معلمتين:
    • السطر 46: جهاز التوجيه [router] المحدد في السطر 16؛
    • السطر 48: مخزن [Vuex] [store] المحدد في السطر 40؛
    • في كلتا الحالتين، يظهر اسم الخاصية على اليسار وقيمتها على اليمين. يتم تعيين أسماء الخصائص [router، store] بواسطة أطر العمل [vue-router] و[vuex]. يمكن أن تكون القيم المرتبطة بها أي شيء؛

18.4. طرق عرض التطبيق

18.4.1. طريقة العرض الرئيسية [Main]

في كلتا الحالتين، يكون اسم الخاصية على اليسار وقيمتها على اليمين. يتم تعيين أسماء الخصائص [router، store] بواسطة إطاري العمل [vue-router] و [vuex]. يمكن أن تكون القيم المرتبطة بها أي شيء؛


<!-- définition HTML de la vue -->
<template>
  <div class="container">
    <b-card>
      <!-- jumbotron -->
      <b-jumbotron>
        <b-row>
          <b-col cols="4">
            <img src="../assets/logo.jpg" alt="Cerisier en fleurs" />
          </b-col>
          <b-col cols="8">
            <h1>Calculez votre impôt</h1>
          </b-col>
        </b-row>
      </b-jumbotron>
      <!-- erreur requête HTTP -->
      <b-alert
        show
        variant="danger"
        v-if="showError"
      >L'erreur suivante s'est produite : {{error.message}}</b-alert>
      <!-- vue courante -->
      <router-view v-if="showView" @loading="mShowLoading" @error="mShowError" />
      <!-- loading -->
      <b-alert show v-if="showLoading" variant="light">
        <strong>Requête au serveur de calcul d'impôt en cours...</strong>
        <div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
      </b-alert>
    </b-card>
  </div>
</template>
 
<script>
export default {
  // name
  name: "app",
  // inner state
  data() {
    return {
      // controls waiting alert
      showLoading: false,
      // controls error alert
      showError: false,
      // controls the display of the current routing view
      showView: true,
      // an error message
      error: ""
    };
  },
  // event managers
  methods: {
    // asynchronous request error
    mShowError(error) {
      // eslint-disable-next-line
      console.log("Main evt error");
      // error msg is displayed
      this.error = error;
      this.showError = true;
      // hide the routed view
      this.showView = false;
      // we hide the waiting message
      this.showLoading = false;
    },
    // whether or not to display a waiting icon
    mShowLoading(value) {
      // eslint-disable-next-line
      console.log("Main evt showLoading");
      // whether or not to display the waiting alert
      this.showLoading = value;
    }
  }
};
</script>

تعليقات

  • تتولى طريقة العرض [Main] تخطيط طريقة العرض التي تم توجيهها وعرضها. السطر 23:

Image

  • تعرض الأسطر من 5 إلى 15 المنطقة 1؛
  • السطر 23 يعرض طريقة العرض الموجهة [2]؛
  • الأسطر 16-19: تنبيه يُعرض فقط في حالة حدوث خطأ في الاتصال بخادم حساب الضرائب؛
  • الأسطر 25-28: رسالة تحميل تُعرض لكل طلب HTTP يتم إرساله إلى الخادم؛
  • سيتم عرض جميع العروض بهذا التخطيط حيث يتم عرض كل عرض موجه من خلال الأسطر 20-24. يُستخدم العرض [Main] لاستخلاص العناصر التي يمكن مشاركتها بين العروض المختلفة؛
  • السطر 23: يمكن لكل عرض موجه تشغيل ثلاثة أحداث:
    • [loading]: تم إرسال طلب HTTP. يجب عرض رسالة التحميل؛
    • [error]: انتهى طلب HTTP بخطأ. يجب عرض رسالة الخطأ وإخفاء العرض الموجه؛
  • الأسطر 38-49: حالة العرض:
    • السطر 41: يتحكم [showLoading] في عرض الرسالة التي تشير إلى أن طلب HTTP قيد التنفيذ (السطر 25)؛
    • السطر 43: [showError] يتحكم في عرض رسالة الخطأ لطلب HTTP (الأسطر 17-21)؛
    • السطر 45: [showView] يتحكم في عرض العرض الموجه (السطر 23)؛
  • الأسطر 53-63: تعالج طريقة [mShowError] حدث [error] الصادر عن العرض الموجه (السطر 23)؛
  • الأسطر 65-70: تعالج طريقة [mShowLoading] حدث [loading] الذي تصدره طريقة العرض الموجهة (السطر 23)؛
  • السطر 23: سنركز على أحداث [error] و [loading]. يتم اعتراضهما فقط إذا تم عرض العرض الموجه [showView=true]. لهذا السبب يتم عرض العرض الموجه مبدئيًا (السطر 45). ولا يتم إخفاؤه إلا في حالة حدوث خطأ (السطر 60). لتجنب هذه المشكلة، كان بإمكاننا استخدام التوجيه [v-show] بدلاً من [v-if]. والفرق بين هذين التوجيهين هو كما يلي:
    • [v-if=’false’] يخفي الكتلة الخاضعة للتحكم عن طريق إزالتها من HTML العام. وبذلك لا يمكن اعتراض الأحداث من العرض الموجه بعد ذلك؛
    • [v-show=’false’] يخفي الكتلة الخاضعة للتحكم عن طريق معالجة CSS الخاص بها، لكن كود الكتلة يظل موجودًا في HTML العام وبالتالي يمكنه اعتراض الأحداث من العرض الموجه؛

18.4.2. عرض [Layout]

رمز عرض [Layout] هو كما يلي:


<!-- definition HTML of the routed view layout -->
<template>
  <!-- line -->
  <div>
    <b-row>
      <!-- three-column zone on the left -->
      <b-col cols="3" v-if="left">
        <slot name="left" />
      </b-col>
      <!-- nine-column zone on the right -->
      <b-col cols="9" v-if="right">
        <slot name="right" />
      </b-col>
    </b-row>
  </div>
</template>
 
<script>
  export default {
    // paramètres de la vue
    props: {
      // contrôle la colonne de gauche
      left: {
        type: Boolean
      },
      // contrôle la colonne de droite
      right: {
        type: Boolean
      }
    }
  };
</script>

تعليقات

  • تتيح لك طريقة العرض [Layout] تقسيم طريقة العرض الموجهة إلى منطقتين:
    • منطقة Bootstrap مكونة من 3 أعمدة على اليسار (الأسطر 7–9). ستحتوي هذه المنطقة على قائمة التنقل في حالة وجودها؛
    • منطقة مكونة من 9 أعمدة على اليمين (الأسطر 11–13). ستعرض هذه المنطقة المعلومات المقدمة من طريقة العرض الموجهة؛

18.4.3. عرض [Authentication]

عرض المصادقة كما يلي:

Image

يُشتق هذا العرض من [التخطيط] عن طريق إزالة العمود الأيسر لعرض العمود الأيمن فقط.

وإليك كودها:


<!-- définition HTML de la vue -->
<template>
  <Layout :left="false" :right="true">
    <template slot="right">
      <!-- formulaire HTML - on poste ses valeurs avec l'action [authentifier-utilisateur] -->
      <b-form @submit.prevent="login">
        <!-- titre -->
        <b-alert show variant="primary">
          <h4>Bienvenue. Veuillez vous authentifier pour vous connecter</h4>
        </b-alert>
        <!-- 1ère ligne -->
        <b-form-group label="Nom d'utilisateur" label-for="user" label-cols="3">
          <!-- zone de saisie user -->
          <b-col cols="6">
            <b-form-input type="text" id="user" placeholder="Nom d'utilisateur" v-model="user" />
          </b-col>
        </b-form-group>
        <!-- 2ième ligne -->
        <b-form-group label="Mot de passe" label-for="password" label-cols="3">
          <!-- zone de saisie password -->
          <b-col cols="6">
            <b-input type="password" id="password" placeholder="Mot de passe" v-model="password" />
          </b-col>
        </b-form-group>
        <!-- 3ième ligne -->
        <b-alert
          show
          variant="danger"
          v-if="showError"
          class="mt-3"
        >L'erreur suivante s'est produite : {{message}}</b-alert>
        <!-- bouton de type [submit] sur une 3ième ligne -->
        <b-row>
          <b-col cols="2">
            <b-button variant="primary" type="submit" :disabled="!valid">Valider</b-button>
          </b-col>
        </b-row>
      </b-form>
    </template>
  </Layout>
</template>
 
<!-- dynamique de la vue -->
<script>
import Layout from "./Layout";
export default {
  // component status
  data() {
    return {
      // user
      user: "",
      // password
      password: "",
      // controls the display of an error msg
      showError: false,
      // the error message
      message: "",
      // session started
      sessionStarted: false
    };
  },
 
  // components used
  components: {
    Layout
  },
 
  // calculated properties
  computed: {
    // valid entries
    valid() {
      return this.user && this.password && this.sessionStarted;
    }
  },
 
  // event managers
  methods: {
    // ----------- authentication
    async login() {
      try {
        // start waiting
        this.$emit("loading", true);
        // blocking server authentication
        const response = await this.$dao.authentifierUtilisateur(
          this.user,
          this.password
        );
        // end of loading
        this.$emit("loading", false);
        // response analysis
        if (response.état != 200) {
          // error is displayed
          this.message = response.réponse;
          this.showError = true;
          return;
        }
        // no error
        this.showError = false;
        // --------- we now request data from the tax authorities
        // start waiting
        this.$emit("loading", true);
        // blocking request to the server
        const response2 = await this.$dao.getAdminData();
        // end of loading
        this.$emit("loading", false);
        // response analysis
        if (response2.état != 1000) {
          // error is displayed
          this.message = response2.réponse;
          this.showError = true;
          return;
        }
        // no error
        this.showError = false;
        // the received data is stored in the [business] layer
        this.$métier.setTaxAdminData(response2.réponse);
        // we move on to the tax calculation view
        this.$router.push({ name: "calculImpot" });
      } catch (error) {
        // the error is traced back to the main component
        this.$emit("error", error);
      }
    }
  },
  // life cycle: the component has just been created
  created() {
    // eslint-disable-next-line
    console.log("authentification", "created");
    // start a jSON session with the server
    // start waiting
    this.$emit("loading", true);
    // initialize the session with the server - asynchronous request
    // we use the promise rendered by the [dao] layer methods
    this.$dao
      // initialize a jSON session
      .initSession()
      // we got the answer
      .then(response => {
        // end waiting
        this.$emit("loading", false);
        // response analysis
        if (response.état != 700) {
          // error is displayed
          this.message = response.réponse;
          this.showError = true;
          return;
        }
        // the session has started
        this.sessionStarted = true;
      })
      // in case of error
      .catch(error => {
        // the error is traced back to the [Main] view
        this.$emit("error", error);
      });
  }
};
</script>

تعليقات

  • السطر 3: تستخدم طريقة العرض [Authentication] العمود الأيمن فقط من [Layout] (السطران 3 و 4)؛
  • الأسطر 6–38: نموذج Bootstrap الذي يولد المنطقة 1 في لقطة الشاشة أعلاه؛
  • السطر 6: يحدث الحدث [@submit] عندما ينقر المستخدم على زر [submit] في السطر 35. يضمن المعدل [prevent] عدم إعادة تحميل الصفحة عند [submit]. كان بإمكاننا أيضًا كتابة:
    • علامة <b-form> دون معالجة حدث [submit]؛
    • علامة <b-button> مع الحدث [@click='login'] وبدون السمة [type='submit']؛

هذا يعمل أيضًا. ميزة الحل المختار هي أن النموذج يتم إرساله ليس فقط بالنقر على زر [Submit]، بل أيضًا بالضغط على مفتاح [Enter] في حقول الإدخال. لذلك تم اختيار حل [<b-form @submit.prevent="login">] هنا لراحة المستخدم؛

  • الأسطر 33–37: تنبيه يظهر عندما يرفض الخادم بيانات الاعتماد التي أدخلها المستخدم:

Image

  • السطر 35: زر [Submit] ليس نشطًا دائمًا. تعتمد حالته على السمة المحسوبة [valid] في الأسطر 71–73. تكون السمة [valid] صحيحة إذا:
    • وجود شيء ما في حقول [user, password] في النموذج؛
    • بدأت جلسة JSON. في البداية، لم تبدأ هذه الجلسة (السطر 59) وبالتالي فإن زر [Validate] غير نشط.
  • الأسطر 49-60: حالة العرض؛
    • يمثل [user] إدخال المستخدم في حقل [user] (الأسطر 12-17) من النموذج. يحدد التوجيه [v-model] في السطر 15 ارتباطًا ثنائي الاتجاه بين إدخال المستخدم والسمة [user] للعرض؛
    • [password] يمثل إدخال المستخدم في حقل [password] (الأسطر 19–24) من النموذج. يحدد التوجيه [v-model] في السطر 22 ارتباطًا ثنائي الاتجاه بين إدخال المستخدم وسمة [password] للعرض؛
    • [showError] يتحكم (السطر 29) في عرض التنبيه في الأسطر 26-31؛
    • [message] هي رسالة الخطأ (السطر 31) التي سيتم عرضها في التنبيه في الأسطر 26-31؛
    • تشير [sessionStarted] إلى ما إذا كانت جلسة JSON مع الخادم قد بدأت أم لا. في البداية، تكون قيمة هذه السمة [false] (السطر 59). يتم تهيئة جلسة JSON مع الخادم في حدث [created] من دورة حياة العرض، الأسطر 126–156. إذا استجاب الخادم بشكل إيجابي، يتم تعيين السمة [sessionStarted] إلى [true] (السطر 149)؛
  • الأسطر 126–156: يتم تنفيذ الدالة [created] عند إنشاء عرض [Authentication] (على الرغم من أنه لم يتم عرضه بالضرورة بعد). في الخلفية، يتم بعد ذلك تهيئة جلسة JSON مع الخادم. نحن نعلم أن هذا هو الإجراء الأول الذي يجب تنفيذه مع خادم حساب الضرائب. للقيام بذلك، نستخدم طبقة [dao] للتطبيق (السطر 134). جميع الطرق في هذه الطبقة غير متزامنة. هنا، نستخدم Promise التي ترجعها طريقة [$dao.initSession]، والتي تهيئ جلسة JSON مع الخادم.
  • الأسطر 138-150: الكود الذي يتم تنفيذه عندما يعرض الخادم استجابته دون أخطاء؛
  • السطر 142: نتحقق من خاصية [status] للاستجابة. يجب أن تكون قيمتها [700] لكي تكون العملية ناجحة. وإلا، فقد حدث خطأ، ويشار إلى سبب هذا الخطأ في خاصية [response.response] (السطر 144). ثم نعرض رسالة الخطأ في العرض (السطر 145)؛
  • السطر 149: نلاحظ أن جلسة JSON قد بدأت؛
  • الأسطر 152–155: الكود الذي يتم تنفيذه في حالة حدوث خطأ. يتم نقل هذا الخطأ إلى العرض الأصلي [Mainوالذي
    • سيعرض الخطأ؛
    • يخفي رسالة الانتظار؛
    • يخفي العرض الموجه، عرض [Authentication]؛
  • الأسطر 79–124: تعالج طريقة [login] النقر على زر [Validate]؛
  • السطر 79: تم إضافة الكلمة الرئيسية [async] إلى بداية الأسلوب للسماح باستخدام الكلمة الرئيسية [await]، السطور 84 و 103؛
  • الأسطر 84-87: استدعاء حظر لطريقة [$dao.authenticateUser(user, password)]. كان بإمكاننا استخدام [Promise] كما تم في الدالة [created]. أردنا تنويع الأنماط. لا يوجد خطر من حجب المستخدم لأننا قمنا بتعيين [timeout] لمدة ثانيتين على جميع طلبات HTTP. لن يضطروا إلى الانتظار طويلاً. علاوة على ذلك، لا يمكنهم فعل أي شيء حتى يعود الخادم برده، حيث يظل زر [Validate] غير نشط حتى ذلك الحين؛
  • السطر 91: يرسل خادم حساب الضرائب استجابات بتنسيق JSON، وجميعها تتخذ الشكل التالي: [{‘action’:action, ‘status’:val, ‘response’:response}]. تعتبر عملية المصادقة ناجحة إذا كان [status==200]. وإلا، يتم عرض رسالة خطأ، في السطرين 93-94؛
  • السطر 98: يتم إخفاء أي رسائل خطأ من عملية سابقة؛
  • السطور 99-116: نطلب الآن من الخادم بيانات سلطة الضرائب اللازمة لحساب الضريبة. في [this.$métierلدينا مثيل لفئة [Métier] لا يمكنه فعل أي شيء في هذه المرحلة لأنه لا يمتلك هذه البيانات؛
  • السطر 103: يتم طلب بيانات سلطة الضرائب من الخادم عبر عملية حجب؛
  • الأسطر 107-112: يتم تحليل استجابة الخادم. يجب أن تكون قيمة الحالة 1000؛ وإلا، فقد حدث خطأ. في الحالة الأخيرة، يتم عرض رسالة خطأ (الأسطر 109-110)؛
  • الأسطر 113–118: إذا نجحت العملية، فإننا:
    • نخفي رسالة الخطأ (السطر 114)؛
    • نمرر بيانات سلطة الضرائب إلى طبقة [الأعمال] (السطر 116)؛
    • نعرض طريقة العرض [CalculImpot]، السطر 118. تذكر أن [this.$router] تشير إلى موجه التطبيق. تُستخدم طريقة [push] لتعيين طريقة العرض التالية التي سيتم توجيهها. هنا، نشير إليها بواسطة سمة [name] الخاصة بها. كان بإمكاننا أيضًا الإشارة إليها بواسطة سمة [path] الخاصة بها. توجد هذه المعلومات في ملف التوجيه:

// calcul de l'impôt
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot
  },
  • الأسطر 119–122: يتم تشغيل كتلة [catch] عند فشل أحد طلبات HTTP (عدم العثور على الخادم، تجاوز مهلة الانتظار، إلخ). ثم يتم الإبلاغ عن الخطأ إلى العرض الأصلي [Main]، الذي سيعرضه ويخفي رسالة التحميل ويخفي عرض [Authentication]؛

18.4.4. عرض [CalculImpot]

عرض [CalculImpot] كما يلي:

Image

  • [1]: تشغل قائمة التنقل العمود الأيسر من العرض الموجه؛
  • [2]: يشغل نموذج حساب الضريبة العمود الأيمن من العرض الموجه؛

فيما يلي كود عرض [TaxCalculation]:


<!-- definition HTML of the view -->
<template>
  <div>
    <Layout :left="true" :right="true">
      <!-- tax calculation form on the right -->
      <FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
      <!-- left-hand navigation menu -->
      <Menu slot="left" :options="options" />
    </Layout>
    <!-- display area for tax calculation results under the form -->
    <b-row v-if="résultatObtenu" class="mt-3">
      <!-- empty three-column zone -->
      <b-col cols="3" />
      <!-- nine-column zone -->
      <b-col cols="9">
        <b-alert show variant="success">
          <span v-html="résultat"></span>
        </b-alert>
      </b-col>
    </b-row>
  </div>
</template>
 
<script>
// imports
import FormCalculImpot from "./FormCalculImpot";
import Menu from "./Menu";
import Layout from "./Layout";
 
export default {
  // état interne
  data() {
    return {
      // options du menu
      options: [
        {
          text: "Liste des simulations",
          path: "/liste-des-simulations"
        },
        {
          text: "Fin de session",
          path: "/fin-session"
        }
      ],
      // résultat du calcul de l'impôt
      résultat: "",
      résultatObtenu: false
    };
  },
  // composants utilisés
  components: {
    Layout,
    FormCalculImpot,
    Menu
  },
  // méthodes de gestion des évts
  methods: {
    // résultat du calcul de l'impôt
    handleResultatObtenu(résultat) {
      // on construit le résultat en chaîne HTML
      const impôt = "Montant de l'impôt : " + résultat.impôt + " euro(s)";
      const décôte = "Décôte : " + résultat.décôte + " euro(s)";
      const réduction = "Réduction : " + résultat.réduction + " euro(s)";
      const surcôte = "Surcôte : " + résultat.surcôte + " euro(s)";
      const taux = "Taux d'imposition : " + résultat.taux;
      this.résultat =
        impôt +
        "<br/>" +
        décôte +
        "<br/>" +
        réduction +
        "<br/>" +
        surcôte +
        "<br/>" +
        taux;
      // affichage du résultat
      this.résultatObtenu = true;
      // ---- maj du store [Vuex]
      // une simulation de +
      this.$store.commit("addSimulation", résultat);
    }
  }
};
</script>

تعليقات

  • السطر 4: يظهر هنا العمودان الخاصان بـ [Layout]؛
  • السطر 6: نموذج حساب الضريبة يشغل العمود الأيمن. ويقوم بتشغيل حدث [resultatObtenu] عند الحصول على نتيجة حساب الضريبة. لاحظ أن أسماء الأحداث وأسماء الطرق التي تتعامل معها لا يمكن أن تحتوي على أحرف مشددة؛
  • السطر 8: تشغل قائمة التنقل العمود الأيسر؛
  • الأسطر 11-20: يتم عرض نتيجة حساب الضريبة أسفل النموذج:

Image

  • السطر 11: يتم عرض النتيجة فقط إذا كانت السمة [resultObtained] (السطر 47) هي [true]؛
  • الأسطر 34-48: حالة العرض:
    • [options]: قائمة خيارات قائمة التنقل. يتم تمرير هذا المصفوف كمعلمة إلى مكون [Menu] في السطر 8؛
    • [result]: نتيجة حساب الضريبة. هذه النتيجة عبارة عن سلسلة HTML. ولهذا السبب تم استخدام التوجيه [v-html] في السطر 17 لعرضها؛
    • [resultObtained]: القيمة المنطقية التي تتحكم في عرض النتيجة، السطر 11؛
  • الأسطر 59-81: تعرض طريقة [handleResultatObtenu] نتيجة حساب الضريبة التي أرسلتها إليها طريقة العرض الفرعية [FormCalculImpot]، السطر 6. هذه النتيجة عبارة عن كائن له الخصائص [tax, discount, reduction, surcharge, rate, married, children, salary]؛
  • الأسطر 61-75: يتم إدراج الكائن [tax, discount, reduction, surcharge, rate] في نص HTML الذي يتم عرضه بواسطة السطر 17 من القالب؛
  • السطر 77: يتم عرض هذه النتيجة؛
  • السطر 80: يستدعي طريقة [addSimulation] لمخزن Vuex، والتي تضيف [result] إلى المحاكاة الموجودة بالفعل في المخزن؛

18.4.5. قائمة التنقل [Menu]

يتم عرض قائمة التنقل في العمود الأيسر من طرق العرض الموجهة:

Image

فيما يلي كود عرض [Menu]:


<!-- definition HTML of the view -->
<template>
  <!-- bootstrap vertical menu -->
  <b-nav vertical>
    <!-- menu options -->
    <b-nav-item
      v-for="(option,index) of options"
      :key="index"
      :to="option.path"
      exact
      exact-active-class="active"
    >{{option.text}}</b-nav-item>
  </b-nav>
</template>
 
<script>
export default {
  // paramètres de la vue
  props: {
    options: {
      type: Array
    }
  }
};
</script>

تعليقات

  • يتم توفير خيارات القائمة بواسطة المعلمة [options] (الأسطر 7، 20–22)؛
  • يحتوي كل عنصر من عناصر المصفوفة [options] على خاصية [text] (السطر 12) وهي نص الرابط، وخاصية [path] (السطر 9) وهي المسار إلى عرض الهدف للرابط؛

18.4.6. عرض [FormCalculImpot]

توفر هذه العرض نموذج حساب الضريبة:

Image

وإليك كودها:


  <!-- définition HTML de la vue -->
  <template>
  <!-- formulaire HTML -->
  <b-form @submit.prevent="calculerImpot" class="mb-3">
    <!-- message sur 12 colonnes sur fond bleu -->
    <b-alert show variant="primary">
      <h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
    </b-alert>
    <!-- éléments du formulaire -->
    <!-- première ligne -->
    <b-form-group label="Etes-vous marié(e) ou pacsé(e) ?" label-cols="4">
      <!-- boutons radio sur 5 colonnes-->
      <b-col cols="5">
        <b-form-radio v-model="marié" value="oui">Oui</b-form-radio>
        <b-form-radio v-model="marié" value="non">Non</b-form-radio>
      </b-col>
    </b-form-group>
    <!-- deuxième ligne -->
    <b-form-group label="Nombre d'enfants à charge" label-cols="4" label-for="enfants">
      <b-input
        type="text"
        id="enfants"
        placeholder="Indiquez votre nombre d'enfants"
        v-model="enfants"
        :state="enfantsValide"
      />
      <!-- message d'erreur éventuel -->
      <b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- troisème ligne -->
    <b-form-group
      label="Salaire annuel"
      label-cols="4"
      label-for="salaire"
      description="Arrondissez à l'euro inférieur"
    >
      <b-input
        type="text"
        id="salaire"
        placeholder="Salaire annuel"
        v-model="salaire"
        :state="salaireValide"
      />
      <!-- message d'erreur éventuel -->
      <b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- quatrième ligne, bouton [submit] sur 5 colonnes -->
    <b-col cols="5">
      <b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
    </b-col>
  </b-form>
</template>
 
<!-- script -->
<script>
export default {
  // inner state
  data() {
    return {
      // married or not
      marié: "non",
      // number of children
      enfants: "",
      // annual salary
      salaire: ""
    };
  },
  // calculated internal state
  computed: {
    // form validation
    formInvalide() {
      return (
        // disabled salary
        !this.salaireValide ||
        // or disabled children
        !this.enfantsValide ||
        // or tax data not obtained
        !this.$métier.taxAdminData
      );
    },
    // salary validation
    salaireValide() {
      // must be numeric >=0
      return Boolean(this.salaire.match(/^\s*\d+\s*$/));
    },
    // child validation
    enfantsValide() {
      // must be numeric >=0
      return Boolean(this.enfants.match(/^\s*\d+\s*$/));
    }
  },
  // event manager
  methods: {
    calculerImpot() {
      // tax is calculated using the [business] layer
      const résultat = this.$métier.calculerImpot(
        this.marié,
        this.enfants,
        this.salaire
      );
      // eslint-disable-next-line
      console.log("résultat=", résultat);
      // complete the result
      résultat.marié = this.marié;
      résultat.enfants = this.enfants;
      résultat.salaire = this.salaire;
      // the [resultatObtenu] event is issued
      this.$emit("resultatObtenu", résultat);
    }
  }
};
</script>

تعليقات

  • السطور 4–51: نموذج Bootstrap؛
  • الأسطر 11–17: مجموعة من أزرار الاختيار مع تسمياتها؛
  • السطور 14–15: تعرض علامة <b-form-radio> زر اختيار:
    • السطر 14: تضمن توجيهات [v-model] أنه عند النقر على الزر، سيتم تعيين السمة [married] في السطر 61 إلى [yes] (السمة [value="yes"]);
    • السطر 15: يضمن التوجيه [v-model] أنه عند النقر على الزر، سيتم تعيين السمة [married] في السطر 61 إلى [no] (السمة [value="no"]);
  • الأسطر 19-29: القسم الخاص بإدخال عدد الأطفال:
    • السطر 24: يرتبط إدخال عدد الأطفال بالسمة [children] في السطر 63؛
    • السطر 25: يتم التحقق من صحة الإدخال بواسطة السمة المحسوبة [validChildren] في الأسطر 87-89؛
    • السطر 28: يضمن عرض رسالة خطأ إذا كان الإدخال غير صالح؛
  • الأسطر 31-45: قسم إدخال الراتب السنوي:
    • السطر 35: يعرض رسالة مساعدة أسفل حقل الإدخال مباشرةً؛
    • السطر 41: يرتبط إدخال الراتب بالسمة [salary] في السطر 65؛
    • السطر 42: يتم التحقق من صحة الإدخال بواسطة السمة المحسوبة [validSalary] في الأسطر 82-85؛
    • السطر 45: يعرض رسالة خطأ إذا كان الإدخال غير صالح؛
  • الأسطر 48-50: زر [submit]. عند النقر فوق هذا الزر أو عند التحقق من صحة الإدخال باستخدام مفتاح [Enter]، يتم تنفيذ الأسلوب [calculateTax] (السطر 94)؛
    • السطر 49: يتم التحكم في حالة تنشيط/إيقاف تنشيط الزر بواسطة السمة المحسوبة [formInvalid] في الأسطر 71-80؛
  • الأسطر 71-80: يكون النموذج صالحًا إذا:
    • كان عدد الأطفال صحيحًا؛
    • الراتب صالح؛
    • حصل التطبيق على بيانات إدارة الضرائب من الخادم لحساب الضريبة. لاحظ أن هذه البيانات مخزنة في الخاصية [$métier.taxAdminData]. يمكن عرض طريقة العرض [FormCalculImpot] قبل الحصول على هذه البيانات لأنها تُطلب بشكل غير متزامن في نفس الوقت الذي يتم فيه عرض طريقة العرض. هنا، نضمن عدم تمكن المستخدم من النقر على زر [التحقق من الصحة] حتى يتم استرداد البيانات؛
  • الأسطر 94-109: طريقة حساب الضريبة:
    • الأسطر 96–100: تقوم طبقة [business] بإجراء هذا الحساب. هذا حساب متزامن. بمجرد استرداد [taxAdminData]، لا يحتاج العميل [View] إلى التواصل مع الخادم. يتم كل شيء محليًا. نحصل على كائن [result] بالخصائص [tax, discount, surcharge, reduction, rate]؛
    • الأسطر 104–106: تُضاف الخصائص [married، children، salary] إلى النتيجة؛
    • السطر 108: يتم تمرير النتيجة إلى العرض الأصلي [CalculImpot] عبر حدث [resultatObtenu]. هذا العرض مسؤول عن عرض النتيجة؛

18.4.7. طريقة العرض [SimulationList]

تعرض طريقة العرض [SimulationList] قائمة المحاكاة التي أجراها المستخدم:

Image

فيما يلي كود العرض:


<!-- définition HTML de la vue -->
<template>
  <div>
    <!-- mise en page -->
    <Layout :left="true" :right="true">
      <!-- simulations dans colonne de droite -->
      <template slot="right">
        <template v-if="simulations.length==0">
          <!-- pas de simulations -->
          <b-alert show variant="primary">
            <h4>Votre liste de simulations est vide</h4>
          </b-alert>
        </template>
        <template v-if="simulations.length!=0">
          <!-- il y a des simulations -->
          <b-alert show variant="primary">
            <h4>Liste de vos simulations</h4>
          </b-alert>
          <!-- tableau des simulations -->
          <b-table striped hover responsive :items="simulations" :fields="fields">
            <template v-slot:cell(action)="data">
              <b-button variant="link" @click="supprimerSimulation(data.index)">Supprimer</b-button>
            </template>
          </b-table>
        </template>
      </template>
      <!-- menu de navigation dans colonne de gauche -->
      <Menu slot="left" :options="options" />
    </Layout>
  </div>
</template>
 
<script>
  // imports
  import Layout from "./Layout";
  import Menu from "./Menu";
  export default {
    // composants
    components: {
      Layout,
      Menu
    },
    // état interne
    data() {
      return {
        // options du menu de navigation
        options: [
          {
            text: "Calcul de l'impôt",
            path: "/calcul-impot"
          },
          {
            text: "Fin de session",
            path: "/fin-session"
          }
        ],
        // paramètres de la table HTML
        fields: [
          { label: "#", key: "id" },
          { label: "Marié", key: "marié" },
          { label: "Nombre d'enfants", key: "enfants" },
          { label: "Salaire", key: "salaire" },
          { label: "Impôt", key: "impôt" },
          { label: "Décôte", key: "décôte" },
          { label: "Réduction", key: "réduction" },
          { label: "Surcôte", key: "surcôte" },
          { label: "", key: "action" }
        ]
      };
    },
    // état interne calculé
    computed: {
      // liste des simulations prise dans le store Vuex
      simulations() {
        return this.$store.state.simulations;
      }
    },
    // méthodes
    methods: {
      supprimerSimulation(index) {
        // eslint-disable-next-line
        console.log("supprimerSimulation", index);
        // suppression de la simulation n° [index]
        this.$store.commit("deleteSimulation", index);
      }
    }
  };
</script>

تعليقات

  • السطر 5: تشغل طريقة العرض كلا العمودين في [التخطيط] لطرق العرض الموجهة؛
  • الأسطر 7–26: توضع المحاكاة في العمود الأيمن؛
  • السطر 28: تظهر قائمة التنقل في العمود الأيسر؛
  • الأسطر 8 و 14 و 20 و 75: تأتي المحاكاة من المخزن [Vuex] [$this.store]؛
  • الأسطر 8-13: يتم عرض تنبيه عندما تكون قائمة المحاكاة فارغة؛
  • الأسطر 14-25: يتم عرض جدول HTML عندما تكون قائمة المحاكاة غير فارغة؛
  • الأسطر 20–24: يتم إنشاء جدول HTML بواسطة علامة <b-table
    • السطر 20: يتم توفير جدول المحاكاة بواسطة السمة المحسوبة [simulations] من الأسطر 74–76؛
    • السطر 20: يتم تكوين جدول HTML بواسطة السمة المحسوبة [fields] في الأسطر 58–69. السطر 67: عمود المفتاح [action] هو العمود الأخير من جدول HTML؛
    • الأسطر 21-23: قالب للعمود الأخير من جدول HTML؛
    • السطر 22: يتم وضع زر ارتباط هنا. عند النقر عليه، يتم استدعاء الطريقة [deleteSimulation(data.index)]، حيث يمثل [data] الصف الحالي (السطر 21). يمثل [data.index] رقم هذا الصف في قائمة الصفوف المعروضة؛
  • السطر 28: إنشاء قائمة التنقل. يتم توفير خياراتها بواسطة السمة [options] في الأسطر 47-56؛
  • الأسطر 80-85: الطريقة التي تستجيب للنقر على رابط [Delete] في صفحة HTML؛
    • السطر 84: يتم استدعاء طريقة [deleteSimulation] لمخزن [Vuex] (انظر القسم |vuejs-15|

18.5. تشغيل المشروع

Image

يجب عليك أيضًا تشغيل خادم [Laragon] (انظر الوثيقة |https://tahe.developpez.com/tutoriels-cours/php7|) حتى يكون خادم حساب الضرائب متصلاً بالإنترنت.

18.6. نشر التطبيق على خادم محلي

حاليًا، يتم نشر عميل [Vue] الخاص بنا على خادم اختبار على عنوان URL [http://localhost:8080]. سنقوم بنشره على خادم [Laragon] على عنوان URL [http://localhost:80]. هناك عدة خطوات يجب اتباعها للوصول إلى ذلك.

الخطوة 1

أولاً، سنتأكد من أن عميل [Vue] قد تم نشره على خادم الاختبار على الرابط [http://localhost:8080/client-vuejs-impot/].

نقوم بإنشاء ملف [vue.config.js] في المجلد الجذر لمشروع [VSCode] الحالي:

Image

سيحتوي ملف [vue.config.js] [1] على المحتوى التالي:


// vue.config.js
module.exports = {
  // l'URL de service du client [vuejs] du serveur de calcul de l'impôt
  publicPath: '/client-vuejs-impot/'
}

نحتاج أيضًا إلى تعديل ملف التوجيه [router.js] [2]:


// imports
import Vue from 'vue'
import VueRouter from 'vue-router'
// les vues
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'
 
// plugin de routage
Vue.use(VueRouter)
 
// les routes de l'application
const routes = [
  // authentification
  {
    path: '/', name: 'authentification', component: Authentification
  },
  // calcul de l'impôt
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot
  },
  // liste des simulations
  {
    path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations
  },
  // fin de session
  {
    path: '/fin-session', name: 'finSession', component: Authentification
  }
]
 
// le routeur
const router = new VueRouter({
  // les routes
  routes,
  // le mode d'affichage des routes dans le navigateur
  mode: 'history',
  // l'URL de base de l'application
  base: '/client-vuejs-impot/'
})
 
// export du router
export default router
  • السطر 39: نخبر الموجه أن مسارات المسارات المحددة في الأسطر 13–30 هي مسارات نسبية بالنسبة للمسار المحدد في السطر 39. على سبيل المثال، سيصبح المسار الموجود في السطر 20 [/calcul-import] هو [/client-vuejs-import/calcul-import]؛

يمكننا بعد ذلك اختبار مشروع [vuejs-20] مرة أخرى للتحقق من التغيير في مسارات التطبيق:

Image

الخطوة 2

سنقوم الآن بإنشاء الإصدار النهائي لمشروع [vuejs-20]:

Image

  • في [1-2]، نقوم بتكوين مهمة [build] [2] في ملف [package.json] [1]؛
  • في [3-5]، نقوم بتشغيل هذه المهمة. ستقوم ببناء النسخة النهائية لمشروع [vuejs-20]؛

تُشغَّل مهمة [build] في محطة [VSCode]:

Image

Image

  • في [3-6]، تشير التحذيرات إلى أن الكود الذي تم إنشاؤه كبير جدًا ويجب تقسيمه [8]. يتعلق هذا بتحسين بنية الكود، وهو ما لن نتناوله هنا؛
  • في [7]، يُذكر أن المجلد [dist] يحتوي على إصدار الإنتاج الذي تم إنشاؤه:

Image

  • في [3]، ملف [index.html] هو الملف الذي سيتم استخدامه عند طلب عنوان URL [https://localhost:80/client-vue-js-impot/]؛

لدينا هنا موقع ثابت يمكن نشره على أي خادم. سنقوم بنشره على خادم Laragon المحلي (انظر الوثيقة |https://tahe.developpez.com/tutoriels-cours/php7|). يتم نسخ المجلد [dist] [2] إلى المجلد [<laragon>/www] [4]، حيث <laragon> هو مجلد تثبيت خادم Laragon. نقوم بإعادة تسمية هذا المجلد إلى [client-vuejs-impot] [5] نظرًا لأننا قمنا بتكوين إصدار الإنتاج ليتم تشغيله على عنوان URL [/client-vuejs-impot/].

الخطوة 3

نضيف ملف [.htaccess] التالي إلى المجلد [client-vuejs-impot] الذي تم إنشاؤه للتو:


<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /client-vuejs-impot/
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /client-vuejs-impot/index.html [L]
</IfModule>

Image

هذا الملف هو ملف تكوين لخادم الويب Apache. إذا لم نقم بتضمينه وطلبنا عنوان URL [https://localhost/client-vuejs-impot/calcul-impot] مباشرةً، دون المرور أولاً عبر عنوان URL [https://localhost/client-vuejs-impot/]، فسنحصل على خطأ 404. باستخدام هذا الملف، نحصل بنجاح على عرض [CalculImpot].

بمجرد الانتهاء من ذلك، قم بتشغيل خادم Laragon إذا لم تكن قد قمت بذلك بالفعل، وانتقل إلى عنوان URL [https://localhost/client-vuejs-impot/]:

Image

ندعو القراء إلى اختبار الإصدار النهائي لتطبيقنا.

يمكننا تعديل خادم حساب الضرائب في جانب واحد: رؤوس CORS التي يرسلها بشكل منهجي إلى عملائه. كان هذا ضروريًا لإصدار العميل الذي يعمل من نطاق [localhost:8080]. والآن بعد أن أصبح كل من العميل والخادم يعملان على نطاق [localhost:80]، لم تعد رؤوس CORS ضرورية.

نقوم بتعديل ملف [config.json] للإصدار 14 من الخادم:

Image

  • في [4]، نحدد أن طلبات CORS يتم رفضها الآن؛

لنحفظ هذا التغيير ونطلب عنوان URL [https://localhost/client-vuejs-impot/] مرة أخرى. من المفترض أن يستمر العمل.

18.7. التعامل مع عناوين URL اليدوية

بدلاً من استخدام روابط قائمة التنقل، قد يرغب المستخدم في كتابة عناوين URL الخاصة بالتطبيق يدويًا في شريط عنوان المتصفح. على سبيل المثال، دعونا نطلب عنوان URL [https://client-vuejs-impot/calcul-impot] دون المرور بصفحة المصادقة. سيحاول المخترق بالتأكيد القيام بذلك. نحصل على العرض التالي:

Image

نحصل بالفعل على عرض حساب الضريبة. الآن دعونا نحاول ملء حقول الإدخال وإرسالها:

Image

ثم نكتشف أن زر [1] [إرسال] يظل معطلاً حتى لو كانت الإدخالات صحيحة. دعونا نلقي نظرة على كود عرض [FormCalculImpot]:


<b-col cols="5">
      <b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
</b-col>

في السطر 2، نرى أن حالته النشطة/غير النشطة تعتمد على الخاصية [formInvalide]. هذه هي الخاصية المحسوبة التالية:


formInvalide() {
      return (
        // salaire invalide
        !this.salaireValide ||
        // ou enfants invalide
        !this.enfantsValide ||
        // ou données fiscales pas obtenues
        !this.$métier.taxAdminData
      );
},

يوضح السطر 8 أنه لكي يكون النموذج صالحًا، يجب الحصول على بيانات الضرائب. ومع ذلك، يتم الحصول على هذه البيانات أثناء التحقق من صحة عرض [Authentication]، الذي "تخطاه" المستخدم. لذلك، لن يتمكن من إرسال النموذج. لو كان بإمكانه القيام بذلك، لكان قد تلقى رسالة خطأ من الخادم تشير إلى أنه لم يتم توثيقه. يجب إجراء التحقق من الصحة دائمًا على جانب الخادم. يمكن دائمًا تجاوز التحقق من الصحة من جانب المتصفح. كل ما يتطلبه الأمر هو عميل مثل [Postman] الذي يرسل طلبات أولية إلى الخادم.

الآن دعونا نطلب عنوان URL [https://localhost/client-vuejs-impot/liste-des-simulations]. نحصل على العرض التالي:

Image

الآن عنوان URL [https://localhost/client-vuejs-impot/fin-session]. نحصل على العرض التالي:

Image

الآن عرض غير موجود [https://localhost/client-vuejs-impot/abcd]:

Image

يتعامل تطبيقنا مع عناوين URL المكتوبة يدويًا بشكل جيد جدًا. عند استدعاء هذه العناوين، يتعرف موجه التطبيق عليها. لذلك، من الممكن التدخل قبل عرض العرض النهائي. سنلقي نظرة على هذا في مشروع [vuejs-21].

هناك نقطة أخرى يجب أخذها في الاعتبار وهي التالية. لنفترض أن المستخدم قد أجرى بعض عمليات المحاكاة وفقًا للقواعد:

Image

الآن دعونا نجدد الصفحة بالضغط على F5:

Image

لقد قمنا بشيء غير موصى به: كتابة عنوان URL يدويًا (الضغط على F5 هو في الأساس نفس الشيء). ونتيجة لذلك، فقدنا محاكاتنا.

يهدف المشروع التالي [vuejs-21] إلى تقديم تحسينين:

  • التحقق من صحة عناوين URL التي يدخلها المستخدم؛
  • الحفاظ على حالة التطبيق حتى إذا قام المستخدم بكتابة عنوان URL. في الأعلى، نرى أننا فقدنا قائمة المحاكاة؛