Skip to content

19. تحسينات عميل Vue.js

19.1. مقدمة

سنقوم باختبار مشروع [vuejs-21] باستخدام خادم التطوير. لذلك، سنحتاج مرة أخرى إلى أن يرسل الخادم رؤوس CORS. لذا يجب أن يسمح ملف [config.json] الخاص بالإصدار 14 من خادم حساب الضرائب بهذه الرؤوس:

Image

يتم إنشاء مشروع [vuejs-21] مبدئيًا عن طريق نسخ مشروع [vuejs-20]. ثم يتم تعديله [3].

تظهر ملفات جديدة:

  • [session.js]: يصدر كائن [session] الذي سيحتوي على معلومات حول الجلسة الحالية؛
  • [pluginSession]: يجعل كائن [session] السابق متاحًا في خاصية [$session] في العروض؛
  • [NotFound.vue]: عرض جديد يظهر عندما يطلب المستخدم يدويًا عنوان URL غير موجود؛

سيتم تعديل الملفات التالية:

  • [main.js]: سيقوم بتهيئة الجلسة الحالية ثم استعادتها عندما يقوم المستخدم بإدخال عنوان URL يدويًا؛
  • [router.js]: تمت إضافة عناصر تحكم للتعامل مع عناوين URL التي يدخلها المستخدم؛
  • [store.js]: تتم إضافة تعديل جديد؛
  • [config.js]: تتم إضافة تكوين جديد؛
  • طرق عرض متنوعة، تهدف في المقام الأول إلى حفظ الجلسة الحالية في نقاط رئيسية من دورة حياة التطبيق. ثم يتم استعادة الجلسة كلما أدخل المستخدم عناوين URL يدويًا؛

19.2. مخزن [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) {
      ...
    },
    // ajout d'une simulation
    addSimulation(state, simulation) {
      ...
    },
    // nettoyage state
    clear(state) {
      // plus de simulations
      state.simulations = [];
      // la numérotation des simulations repart de 0
      state.idSimulation = 0;
    }
  }
});
// export de l'objet [store]
export default store;
  • الأسطر 24-29: تعيين [clear] يحذف قائمة المحاكاة المحفوظة ويعيد تعيين رقم المحاكاة الأخيرة إلى 0.

19.3. الجلسة

تنشأ الحاجة إلى جلسة لأن البرنامج النصي [main.js] يُنفَّذ مرة أخرى عندما يكتب المستخدم عنوان URL في شريط عنوان المتصفح. ومع ذلك، يحتوي هذا البرنامج النصي على التعليمات التالية:


// store Vuex
import store from './store'

تستورد هذه التعليمات الملف [./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: {
    ...
  }
});
// export de l'objet [store]
export default store;

كما نرى في الأسطر 7–13، نقوم باستيراد مصفوفة فارغة من عمليات المحاكاة. لذا، إذا كانت لدينا عمليات محاكاة قبل أن يكتب المستخدم عنوان URL في شريط عنوان المتصفح، فلن يكون لدينا أي منها بعد ذلك. الفكرة هي:

  • استخدام جلسة عمل تخزن المعلومات التي تريد الاحتفاظ بها إذا أدخل المستخدم عناوين URL يدويًا؛
  • حفظها في نقاط رئيسية في التطبيق؛
  • استعادتها في [main.jsالذي يتم تنفيذه دائمًا عند إدخال عنوان URL يدويًا؛

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


// on importe le store Vuex
import store from './store'
// on importe la configuration
import config from './config';
 
// l'objet [session]
const session = {
  // session démarrée
  started: false,
  // authentification
  authenticated: false,
  // heure de sauvegarde
  saveTime: "",
  // couche [métier]
  métier: null,
  // état Vuex
  state: null,
 
  // sauvegarde de la session dans une chaîne jSON
  save() {
    // on ajoute à la session quelques proprités
    this.saveTime = Date.now();
    this.state = store.state;
    // on la transforme en jSON
    const json = JSON.stringify(this);
    // on la stocke sur le navigateur
    localStorage.setItem("session", json);
    // eslint-disable-next-line no-console
    console.log("session save", json);
  },
 
  // restauration de la session
  restore() {
    // on récupère la session jSON à partir du navigateur
    const json = localStorage.getItem("session")
    // si on a récupéré qq chose
    if (json) {
      // on restaure toutes les clés de la session
      const restore = JSON.parse(json);
      for (var key in restore) {
        if (restore.hasOwnProperty(key)) {
          this[key] = restore[key];
        }
      }
      // si on a dépassé une certaine durée d'inactivité depuis le début de la session, on repart de zéro
      let durée = Date.now() - this.saveTime;
      if (durée > config.duréeSession) {
        // on vide la session - elle sera également sauvegardée
        session.clear();
      } else {
        // on régénère le store Vuex
        store.replaceState(JSON.parse(JSON.stringify(this.state)));
      }
    }
    // eslint-disable-next-line no-console
    console.log("session restore", this);
  },
 
    // on nettoie la session
  clear() {
    // eslint-disable-next-line no-console
    console.log("session clear");
    // raz de certains champs de la session
    this.authenticated = false;
    this.saveTime = "";
    this.started = false;
    if (this.métier) {
      // on réinitialise le champ [taxAdminData]
      this.métier.taxAdminData = null;
    }
    // le store Vuex est nettoyé également
    store.commit("clear");
    // on sauvegarde la nouvelle session
    this.save();
  },
}
 
// export de l'objet [session]
export default session;

تعليقات

  • السطر 2: ستقوم الجلسة أيضًا بتغليف مخزن [Vuex] (قائمة المحاكاة، معرف آخر محاكاة تم إجراؤها)؛
  • الأسطر 7-17: المعلومات المخزنة بواسطة الجلسة:
    • [started]: ما إذا كانت جلسة JSON مع الخادم قد بدأت أم لا؛
    • [authenticated]: ما إذا كان المستخدم قد قام بالمصادقة أم لا؛
    • [saveTime]: تاريخ آخر عملية حفظ بالمللي ثانية؛
    • [business]: مرجع إلى طبقة [business]. يحتوي هذا على بيانات [taxAdminData] المستخدمة لحساب الضريبة؛
    • [state]: حالة مخزن [Vuex] (قائمة المحاكاة، رقم آخر محاكاة تم إجراؤها)؛
  • الأسطر 20-30: تقوم طريقة [save] بحفظ الجلسة محليًا على المتصفح الذي يشغل التطبيق؛
    • السطر 22: يتم تسجيل وقت الحفظ؛
    • السطر 23: يتم استرداد [state] لمخزن [Vuex]؛
    • السطر 25: يتم إنشاء سلسلة JSON للجلسة؛
    • السطر 27: يتم تخزينها محليًا على المتصفح المرتبط بمفتاح [session]؛
  • الأسطر 33-57: تعيد طريقة [restore] الجلسة من حفظها المحلي في المتصفح؛
    • السطر 35: استرداد النسخة الاحتياطية المحلية من JSON؛
    • السطر 37: إذا تم استرداد شيء ما؛
    • الأسطر 39-44: يتم إعادة بناء كائن [session]؛
    • السطر 46: يتم حساب الوقت المنقضي منذ آخر نسخة احتياطية؛
    • الأسطر 47-50: إذا تجاوزت هذه المدة القيمة [config.sessionDuration] المحددة في التكوين، تتم إعادة تعيين الجلسة (السطر 49) وحفظها في ذلك الوقت؛
    • السطر 52: خلاف ذلك، يتم إعادة إنشاء السمة [state] لمخزن [Vuex]؛
  • الأسطر 60-75: تعيد طريقة [clear] تعيين الجلسة؛
    • الأسطر 64-70: يتم إعادة تعيين خصائص الجلسة إلى قيمها الأولية؛
    • السطر 72: وكذلك مخزن [Vuex]؛
    • السطر 74: يتم حفظ الجلسة الجديدة؛

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

يتطور ملف [./config] على النحو التالي:


// utilisation de la bibliothèque [axios]
const axios = require('axios');
// timeout des requêtes HTTP
axios.defaults.timeout = 2000;
...
 
// export de la configuration
export default {
  // objet [axios]
  axios: axios,
  // délai maximal d'inactivité de la session : 5 mn = 300 s = 300000 ms
  duréeSession: 300000
}
  • السطر 12: سنقوم بإدارة جلسة التطبيق بشكل مشابه لإدارة جلسة الويب. هنا، نحدد مدة قصوى لعدم النشاط تبلغ 5 دقائق؛

19.5. المكوّن الإضافي [pluginSession]

كما تم فعله مرات عديدة من قبل، سيسمح المكون الإضافي [pluginSession] للعروض بالوصول إلى الجلسة عبر الخاصية [this.$session]:


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

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

يتطور البرنامج النصي الرئيسي [./main.js] على النحو التالي:


// log de démarrage
// eslint-disable-next-line no-console
console.log("main started");
 
// imports
import Vue from 'vue'
 
...

// 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'
 
// session
import session from './session';
import pluginSession from './plugins/pluginSession'
Vue.use(pluginSession, session)
 
// on restore la session avant de redémarrer
session.restore();
 
// on restaure la couche [métier]
if (session.métier && session.métier.taxAdminData) {
  métier.setTaxAdminData(session.métier.taxAdminData);
}
 
// 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),
})
 
// log de fin
// eslint-disable-next-line no-console
console.log("main terminated, session=", session);
  • السطر 19: استيراد الجلسة؛
  • السطر 20: استيراد المكون الإضافي؛
  • السطر 21: يتم دمج المكون الإضافي [pluginSession] في [Vue]. بعد هذه التعليمات، تصبح الجلسة متاحة في سمة [$session] لجميع العروض؛
  • السطر 27: يتم استعادة الجلسة. ثم يتم تهيئة الجلسة المستوردة في السطر 11 بمحتويات آخر حفظ لها؛
  • بعد السطر 16، تحتوي طرق العرض على خاصية [$métier] تم تهيئتها في السطر 12. لا تحتوي هذه الخاصية على معلومات [taxAdminData] المستخدمة لحساب الضريبة؛
  • الأسطر 30-32: إذا كانت عملية الاستعادة التي تم إجراؤها للتو قد أعادت خاصية [session.métier.taxAdminData]، يتم تهيئة خاصية [$métier] للعروض بهذه القيمة؛

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

يتطور ملف التوجيه [./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'
import NotFound from './views/NotFound'
// la session
import session from './session'
 
// plugin de routage
Vue.use(VueRouter)
 
// les routes de l'application
const routes = [
  // authentification
  { path: '/', name: 'authentification', component: Authentification },
  { path: '/authentification', name: 'authentification', component: Authentification },
  // calcul de l'impôt
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot,
    meta: { authenticated: true }
  },
  // liste des simulations
  {
    path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations,
    meta: { authenticated: true }
  },
  // fin de session
  {
    path: '/fin-session', name: 'finSession'
  },
  // page inconnue
  {
    path: '*', name: 'notFound', component: NotFound,
  },
]
 
// le routeur
const router = new VueRouter({
  // les routes
  routes,
  // le mode d'affichage des URL
  mode: 'history',
  // l'URL de base de l'application
  base: '/client-vuejs-impot/'
})
 
// vérification des routes
router.beforeEach((to, from, next) => {
  // eslint-disable-next-line no-console
  console.log("router to=", to, "from=", from);
  // route réservée aux utilisateurs authentifiés ?
  if (to.meta.authenticated && !session.authenticated) {
    next({
      // on passe à l'authentification
      name: 'authentification',
    })
    // retour à la boucle événementielle
    return;
  }
  // cas particulier de la fin de session
  if (to.name === "finSession") {
    // on nettoie la session
    session.clear();
    // on va sur la vue [authentification]
    next({
      name: 'authentification',
    })
    // retour à la boucle événementielle
    return;
  }
  // autres cas - vue suivante normale du routage
  next();
})
 
// export du router
export default router

تعليقات

  • الأسطر 16–38: تم تحسين بعض المسارات بمعلومات إضافية؛
  • السطر 19: تم إنشاء مسار جديد للانتقال إلى عرض [Authentication]؛
  • الأسطر 21–24: المسار المؤدي إلى عرض [TaxCalculation] يحتوي الآن على خاصية [meta] (هذا الاسم مطلوب). يمكن أن يكون محتوى هذا الكائن أي شيء ويتم تعيينه بواسطة المطور؛
  • السطر 23: نضيف الخاصية [authenticated] إلى [meta] (يمكن أن يكون هذا الاسم أي شيء). وهذا يعني أنه للوصول إلى عرض [TaxCalculation]، يجب أن يكون المستخدم قد تمت مصادقته؛
  • الأسطر 26-29: نفعل الشيء نفسه بالنسبة للمسار المؤدي إلى عرض [ListeSimulations]. هنا أيضًا، يجب أن يكون المستخدم مصدقًا؛
  • ستسمح لنا الخاصية [meta.authenticated] بالتحقق من أن المستخدم الذي يكتب عناوين URL لعرضي [CalculImpot] و[ListeSimulations] يدويًا لا يمكنه الوصول إليهما إذا لم يتم توثيقه؛
  • الأسطر 51-76: يتم تنفيذ طريقة [beforeEach] قبل توجيه العرض. هذا هو الوقت المناسب لإجراء الفحوصات؛
    • [to]: المسار التالي إذا لم يتم القيام بأي شيء؛
    • [from]: المسار الأخير المعروض؛
    • [next]: وظيفة لتغيير المسار التالي المعروض؛
  • السطر 55: نتحقق مما إذا كان المسار التالي يتطلب مصادقة المستخدم؛
  • الأسطر 56-59: إذا كان الأمر كذلك، ولم يتم مصادقة المستخدم، فإننا نغير المسار التالي إلى عرض [Authentication]؛
  • الأسطر 64-73: التعامل مع الحالة الخاصة للمسار [endSession] من الأسطر 30-32. لا يوجد عرض مرتبط بهذا المسار؛
    • السطر 66: يتم إعادة تعيين الجلسة إلى قيمتها الأولية؛
    • الأسطر 68-70: تعيين عرض [Authentication] كالعرض التالي؛
  • السطر 75: إذا لم تنطبق أي من الحالتين السابقتين، فإننا ننتقل ببساطة إلى المسار المحدد بواسطة ملف التوجيه؛
  • الأسطر 35–37: يتم توفير عرض [NotFound] إذا كان المسار الذي أدخله المستخدم لا يتطابق مع أي مسار معروف. يتم استيراد هذا العرض في السطر 8. يتم فحص المسارات حسب ترتيب ملف التوجيه. لذلك، إذا وصلنا إلى السطر 36، فهذا يعني أن المسار المطلوب ليس أيًا من المسارات الموجودة في الأسطر 18–33؛

19.8. عرض [NotFound]

يتم عرض عرض [NotFound] إذا كان المسار الذي أدخله المستخدم لا يتطابق مع أي مسار معروف:

Image

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


<!-- définition HTML de la vue -->
<template>
  <!-- mise en page -->
  <Layout :left="true" :right="true">
    <!-- alerte dans la colonne de droite -->
    <template slot="right">
      <!-- message sur fond jaune -->
      <b-alert show variant="danger" align="center">
        <h4>Cette page n'existe pas</h4>
      </b-alert>
    </template>
    <!-- menu de navigation dans la colonne de gauche -->
    <Menu slot="left" :options="options" />
  </Layout>
</template>
 
<script>
// imports
import Layout from "./Layout";
import Menu from "./Menu";
export default {
  // composants
  components: {
    Layout,
    Menu
  },
  // état interne du composant
  data() {
    return {
      // options du menu de navigation
      options: [
        {
          text: "Authentification",
          path: "/"
        }
      ]
    };
  },
  // cycle de vie
  created() {
    // eslint-disable-next-line
    console.log("NotFound created");
    // on regarde quelles options de menu offrir
    if (this.$session.authenticated && this.$métier.taxAdminData) {
      // l'utilisateur peut faire des simulations
      Array.prototype.push.apply(this.options, [
        {
          text: "Calcul de l'impôt",
          path: "/calcul-impot"
        },
        {
          text: "Liste des simulations",
          path: "/liste-des-simulations"
        }
      ]);
    }
  }
};
</script>

تعليقات

  • السطر 4: يستخدم كلا العمودين في طرق العرض الموجهة؛
  • الأسطر 6–11: رسالة خطأ؛
  • السطر 13: تشغل قائمة التنقل العمود الأيسر؛
  • الأسطر 31–36: الخيارات الافتراضية للقائمة؛
  • الأسطر 40-57: كود يتم تنفيذه عند إنشاء العرض؛
  • السطر 44: يتحقق مما إذا كان بإمكان المستخدم تشغيل عمليات المحاكاة؛
  • الأسطر 45-55: إذا كان الأمر كذلك، تتم إضافة خيارين إلى قائمة التنقل — الخيارات التي تتطلب المصادقة وطبقة [الأعمال] التشغيلية (الأسطر 46-55)؛

19.9. طريقة عرض [المصادقة]

تتطور طريقة عرض [المصادقة] على النحو التالي:


<!-- définition HTML de la vue -->
<template>
  <Layout :left="false" :right="true">
    ...
  </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: ""
    };
  },
 
  // components used
  components: {
    Layout
  },
 
  // calculated properties
  computed: {
    // valid entries
    valid() {
      return this.user && this.password && this.$session.started;
    }
  },
 
  // event managers
  methods: {
    // ----------- authentication
    async login() {
      try {
        // start waiting
        this.$emit("loading", true);
        // you are not yet authenticated
        this.$session.authenticated = false;
        // blocking server authentication
        const response = await this.$dao.authentifierUtilisateur(
          this.user,
          this.password
        );
        // end of loading
        this.$emit("loading", false);
        // server response analysis
        if (response.état != 200) {
          // error is displayed
          this.message = response.réponse;
          this.showError = true;
          // return to event loop
          return;
        }
        // no error
        this.showError = false;
        // you are authenticated
        this.$session.authenticated = true;
        // --------- we now request data from the tax authorities
        // initially, no data
        this.$métier.setTaxAdminData(null);
        // 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 to event loop
          return;
        }
        // no error
        this.showError = false;
        // the received data is stored in the [business] layer
        this.$métier.setTaxAdminData(response2.réponse);
        // we can move on to tax calculation
        this.$router.push({ name: "calculImpot" });
      } catch (error) {
        // the error is traced back to the main component
        this.$emit("error", error);
      } finally {
        // maj session
        this.$session.métier = this.$métier;
        // save the session
        this.$session.save();
      }
    }
  },
  // life cycle: the component has just been created
  created() {
    // eslint-disable-next-line
    console.log("Authentification created");
    // can the user run simulations?
    if (
      this.$session.started &&
      this.$session.authenticated &&
      this.$métier.taxAdminData
    ) {
      // then the user can run simulations
      this.$router.push({ name: "calculImpot" });
      // return to event loop
      return;
    }
    // if the jSON session has already been started, it will not be restarted
    if (!this.$session.started) {
      // 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 to event loop
            return;
          }
          // the session has started
          this.$session.started = true;
        })
        // in case of error
        .catch(error => {
          // the error is traced back to the [Main] view
          this.$emit("error", error);
        })
        // in all cases
        .finally(() => {
          // save the session
          this.$session.save();
        });
    }
  }
};
</script>

تعليقات

  • تم تمييز العبارات التي تستخدم الجلسة التي تم تقديمها في هذا الإصدار من عميل [Vue.js] باللون الأصفر؛
  • السطران 97 و 148: في نهاية طريقتي [login و created]، يتم حفظ الجلسة بغض النظر عن نتيجة طلبات HTTP التي تحدث داخل هاتين الطريقتين (جملة [finally] في كلتا الحالتين)؛
  • يتم تنفيذ الأسلوب [created] في الأسطر 102–150 في كل مرة يتم فيها إنشاء عرض [Authentication]. إذا قام المستخدم بكتابة عنوان URL للعرض، فستخبرنا الجلسة بما يجب القيام به؛
  • الأسطر 106–115: إذا تم بدء جلسة JSON، وتم مصادقة المستخدم، وتم تهيئة البيانات [this.$métier.taxAdminData]، فيمكن للمستخدم الانتقال مباشرةً إلى نموذج حساب الضريبة (السطر 112)؛
  • السطر 117: تم استخدام طريقة [created] في الإصدار السابق لتهيئة جلسة JSON مع الخادم. هذه الخطوة غير ضرورية إذا تم تنفيذها بالفعل؛
  • الأسطر 42–66: طريقة المصادقة؛
  • السطر 66: إذا نجحت المصادقة، يتم تسجيلها في الجلسة؛
  • الأسطر 67-92: طلب بيانات إدارة الضرائب [taxAdminData] من الخادم؛
  • السطر 95: في نهاية هذه المرحلة، يتم تحديث خاصية [business] للجلسة بغض النظر عما إذا نجحت العملية أم لا؛

19.10. طريقة العرض [CalculImpot]

يتطور كود عرض [CalculImpot] على النحو التالي:


<!-- définition HTML de la vue -->
<template>
  ...
</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
      ...
      // une simulation de +
      this.$store.commit("addSimulation", résultat);
      // on sauvegarde la session
      this.$session.save();
    }
  },
  // cycle de vie
  created() {
    // eslint-disable-next-line
    console.log("CalculImpot created");
  }
};
</script>

تعليقات

  • السطر 45: تتم إضافة المحاكاة المحسوبة إلى مخزن [Vuex]. يؤثر هذا على الجلسة، التي تشمل خاصية [state] الخاصة بالمخزن. لذلك، نقوم بحفظ الجلسة (السطر 47)؛
  • السطر 51: نقوم بإنشاء طريقة [created] لتتبع عمليات إنشاء العروض في السجلات؛

19.11. عرض [SimulationList]

تتطور طريقة عرض [ListeSimulations] على النحو التالي:


<!-- définition HTML de la vue -->
<template>
  ...
  </div>
</template>
 
<script>
// imports
import Layout from "./Layout";
import Menu from "./Menu";
export default {
  // composants
  components: {
    Layout,
    Menu
  },
  // état interne
  data() {
    ...
  },
  // é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);
      // on sauvegarde la session
      this.$session.save();
    }
  },
  // cycle de vie
  created() {
    // eslint-disable-next-line
    console.log("ListeSimulations created");
  }
};
</script>

تعليقات

  • السطر 36: بعد حذف محاكاة في السطر 34، نقوم بحفظ الجلسة لتعكس هذا التغيير في الحالة؛
  • الأسطر 40–43: نواصل تتبع إنشاء طرق العرض؛

19.12. تنفيذ المشروع

Image

أثناء الاختبار، تحقق من النقاط التالية:

  • إذا «استخدم» المستخدم التطبيق عبر روابط قائمة التنقل وأزرار/روابط الإجراءات، فإنه يعمل؛
  • إذا أدخل المستخدم عناوين URL يدويًا، يستمر التطبيق في العمل. على وجه الخصوص، قم بإجراء الاختبار التالي:
    • قم بتشغيل عمليات المحاكاة؛
    • بمجرد الوصول إلى عرض [SimulationList]، أعد تحميل (F5) العرض. في التطبيق السابق [vuejs-20]، كانت المحاكاة تُفقد عند هذه النقطة. هذا ليس هو الحال هنا: المحاكاة التي تم إجراؤها بالفعل لا تزال موجودة؛
  • تحقق من السجلات لفهم:
    • متى يتم تنفيذ البرنامج النصي [main]. يجب أن ترى أنه يعمل في كل مرة يدخل فيها المستخدم عنوان URL يدويًا؛
    • متى يتم إنشاء العروض. يجب أن ترى أنها يتم إنشاؤها في كل مرة توشك على العرض؛
    • كيف يعمل التوجيه. قبل كل توجيه، يتم إنشاء سجل يخبرك بما يلي:
      • المسار الذي أتيت منه؛
      • المسار الذي ستنتقل إليه؛

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

كممارسة، اتبع القسم |النشر على خادم محلي| لنشر مشروع [vuejs-21] على خادم Laragon المحلي. ثم اختبره.

19.14. تطوير النسخة المحمولة

من الناحية النظرية، من المفترض أن يتيح لنا استخدام Bootstrap الحصول على تطبيق يعمل على أجهزة مختلفة: الهواتف الذكية، والأجهزة اللوحية، وأجهزة الكمبيوتر المحمولة، وأجهزة الكمبيوتر المكتبية. ما يميز هذه الأجهزة هو حجم شاشاتها.

إذا اختبرنا إصدار [vuejs-21] على جهاز محمول، نلاحظ أن عرض الواجهة غير منظم. يعمل إصدار [vuejs-22] على حل هذه المشكلة. تم إجراء جميع التغييرات في قوالب العرض. وتضمنت بشكل أساسي تحسين العرض لشاشة الهاتف الذكي. وبمجرد تحسين ذلك، يعمل العرض على الشاشات الأكبر حجمًا بسلاسة بفضل Bootstrap.

Image

19.14.1. عرض [Main]

تتطور طريقة العرض [Main] على النحو التالي:


<!-- definition HTML of the view -->
<template>
  <div class="container">
    <b-card>
      <!-- jumbotron -->
      <b-jumbotron>
        <b-row>
          <b-col sm="4">
            <img src="../assets/logo.jpg" alt="Cerisier en fleurs" />
          </b-col>
          <b-col sm="8">
            <h1>Calculez votre impôt</h1>
          </b-col>
        </b-row>
      </b-jumbotron>
      ....
    </b-card>
  </div>
</template>

تعليقات

  • السطر 8: حيث كان مكتوبًا [cols=’4’]، نكتب الآن [sm=’4’]. [sm] تعني [صغير]. تندرج شاشات الهواتف الذكية ضمن هذه الفئة. الفئات الأخرى هي [xs=صغير جدًا، md=متوسط، lg=كبير، xl=كبير جدًا]؛
  • السطر 11: كما هو مذكور أعلاه؛

19.14.2. طريقة عرض [Layout]

تتطور طريقة عرض [Layout] على النحو التالي:


<!-- definition HTML of the routed view layout -->
<template>
  <!-- line -->
  <div>
    <b-row>
      <!-- three-column zone on the left -->
      <b-col sm="3" v-if="left">
        <slot name="left" />
      </b-col>
      <!-- nine-column zone on the right -->
      <b-col sm="9" v-if="right">
        <slot name="right" />
      </b-col>
    </b-row>
  </div>
</template>

19.14.3. عرض [المصادقة]

تتطور طريقة عرض [المصادقة] على النحو التالي:


<!-- definition HTML of the view -->
<template>
  <Layout :left="false" :right="true">
    <template slot="right">
      <!-- form HTML - post its values with the [authenticate-user] action -->
      <b-form @submit.prevent="login">
        <!-- title -->
        <b-alert show variant="primary">
          <h4>Bienvenue. Veuillez vous authentifier pour vous connecter</h4>
        </b-alert>
        <!-- 1st line -->
        <b-form-group label="Nom d'utilisateur" label-for="user" description="Tapez admin">
          <!-- user input field -->
          <b-col sm="6">
            <b-form-input type="text" id="user" placeholder="Nom d'utilisateur" v-model="user" />
          </b-col>
        </b-form-group>
        <!-- 2nd line -->
        <b-form-group label="Mot de passe" label-for="password" description="Tapez admin">
          <!-- password input field -->
          <b-col sm="6">
            <b-input type="password" id="password" placeholder="Mot de passe" v-model="password" />
          </b-col>
        </b-form-group>
        <!-- 3rd line -->
        <b-alert
          show
          variant="danger"
          v-if="showError"
          class="mt-3"
        >L'erreur suivante s'est produite : {{message}}</b-alert>
        <!-- submit] button on a 3rd line -->
        <b-row>
          <b-col sm="2">
            <b-button variant="primary" type="submit" :disabled="!valid">Valider</b-button>
          </b-col>
        </b-row>
      </b-form>
    </template>
  </Layout>
</template>

تعليقات

  • السطران 11 و 19: قمنا بإزالة السمة [label-cols]، التي تحدد عدد الأعمدة لتسمية حقل الإدخال. بدون هذه السمة، تظهر التسمية فوق حقل الإدخال. وهذا أكثر ملاءمة لشاشات الهواتف الذكية؛

19.14.4. طريقة العرض [CalculImpot]

تم تحديث عرض [CalculImpot] على النحو التالي:


<!-- 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 sm="3" />
      <!-- nine-column zone -->
      <b-col sm="9">
        <b-alert show variant="success">
          <span v-html="résultat"></span>
        </b-alert>
      </b-col>
    </b-row>
  </div>
</template>

19.14.5. طريقة العرض [FormCalculImpot]

تتطور طريقة عرض [FormCalculImpot] على النحو التالي:


<!-- definition HTML of the view -->
  <template>
  <!-- form HTML -->
  <b-form @submit.prevent="calculerImpot" class="mb-3">
    <!-- 12-column message on blue background -->
    <b-row>
      <b-col sm="12">
        <b-alert show variant="primary">
          <h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
        </b-alert>
      </b-col>
    </b-row>
    <!-- form elements -->
    <!-- first line -->
    <b-form-group label="Etes-vous marié(e) ou pacsé(e) ?">
      <!-- 5-column radio buttons-->
      <b-col sm="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>
    <!-- second line -->
    <b-form-group label="Nombre d'enfants à charge" label-for="enfants">
      <b-form-input
        type="text"
        id="enfants"
        placeholder="Indiquez votre nombre d'enfants"
        v-model="enfants"
        :state="enfantsValide"
      ></b-form-input>
      <!-- possible error message -->
      <b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- third line -->
    <b-form-group
      label="Salaire annuel net imposable"
      label-for="salaire"
      description="Arrondissez à l'euro inférieur"
    >
      <b-form-input
        type="text"
        id="salaire"
        placeholder="Salaire annuel"
        v-model="salaire"
        :state="salaireValide"
      ></b-form-input>
      <!-- possible error message -->
      <b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- fourth line, [submit] button -->
    <b-col sm="3">
      <b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
    </b-col>
  </b-form>
</template>

تعليقات

  • الأسطر 15 و23 و35: تمت إزالة السمة [label-cols]؛

بالإضافة إلى ذلك، نقوم بتحديث اختبارات التحقق من الصحة:


...
// état interne calculé
  computed: {
    // validation du formulaire
    formInvalide() {
      return (
        // salaire invalide
        !this.salaire.match(/^\s*\d+\s*$/) ||
        // ou enfants invalide
        !this.enfants.match(/^\s*\d+\s*$/) ||
        // ou données fiscales pas obtenues
        !this.$métier.taxAdminData
      );
    },
    // validation du salaire
    salaireValide() {
      // doit être numérique >=0
      return Boolean(
        this.salaire.match(/^\s*\d+\s*$/) || this.salaire.match(/^\s*$/)
      );
    },
    // validation des enfants
    enfantsValide() {
      // doit être numérique >=0
      return Boolean(
        this.enfants.match(/^\s*\d+\s*$/) || this.enfants.match(/^\s*$/)
      );
    }
},
...

تعليقات

  • السطر 19: عندما لا يتم إدخال أي شيء، يُعتبر الإدخال صحيحًا. وهذا يضمن صحة الإدخال عند عرض الصفحة لأول مرة. في الإصدار السابق، كان الإدخال يظهر في البداية على أنه غير صحيح؛
  • السطر 26: كما هو مذكور أعلاه؛
  • الأسطر 5-14: لا يكون زر الإرسال نشطًا إلا إذا كان كلا الحقلين يحتويان على بيانات وصالحين؛

19.14.6. عرض [القائمة]

تتطور طريقة عرض [القائمة] على النحو التالي:


<!-- definition HTML of the view -->
<template>
  <b-card class="mb-3">
    <!-- 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>
  </b-card>
</template>

التعليقات

  • السطر 3: نضيف علامة <b-card> لإحاطة القائمة بحد رفيع. وهذا يساعد على تسهيل تحديد موقع القائمة على الهاتف الذكي؛

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

تظل طريقة العرض [ListeSimulations] دون تغيير:


<!-- definition HTML of the view -->
<template>
  <div>
    <!-- layout -->
    <Layout :left="true" :right="true">
      <!-- simulations in right-hand column -->
      <template slot="right">
        <template v-if="simulations.length==0">
          <!-- no simulations -->
          <b-alert show variant="primary">
            <h4>Votre liste de simulations est vide</h4>
          </b-alert>
        </template>
        <template v-if="simulations.length!=0">
          <!-- there are simulations -->
          <b-alert show variant="primary">
            <h4>Liste de vos simulations</h4>
          </b-alert>
          <!-- simulation table -->
          <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>
      <!-- navigation menu in left-hand column -->
      <Menu slot="left" :options="options" />
    </Layout>
  </div>
</template>

تعليقات

  • السطر 20: لاحظ السمة [responsive]، التي تضمن تكيُّف عرض الجدول مع حجم الشاشة:

Image

  • في [2]، على الشاشات الصغيرة، يسمح شريط التمرير الأفقي بعرض الجدول؛

19.14.8. طريقة العرض [NotFound]

تظل دون تغيير.

19.14.9. طرق العرض على الأجهزة المحمولة

Image

Image

ملاحظة: من الممكن بالتأكيد إنشاء طرق عرض أكثر ملاءمة للأجهزة المحمولة. أفكر بشكل خاص في قائمة التنقل، التي يمكن تحسينها، ولكن هناك مجالات أخرى أيضًا. لم يكن الهدف الأساسي من هذا المستند هو إنشاء تطبيق للأجهزة المحمولة. في تلك الحالة، كنا قد لجأنا إلى إطار عمل مثل Ionic |https://ionicframework.com/|.