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

يتم إنشاء مشروع [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] إذا كان المسار الذي أدخله المستخدم لا يتطابق مع أي مسار معروف:

رمز العرض هو كما يلي:
<!-- 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. تنفيذ المشروع

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

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]، التي تضمن تكيُّف عرض الجدول مع حجم الشاشة:

- في [2]، على الشاشات الصغيرة، يسمح شريط التمرير الأفقي بعرض الجدول؛
19.14.8. طريقة العرض [NotFound]
تظل دون تغيير.
19.14.9. طرق العرض على الأجهزة المحمولة


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