19. بهبودهای سمت کلاینت Vue.js
19.1. Introduction
ما قصد داریم پروژه [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] به شرح زیر بهروزرسانی شده است:
// پلاگین Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
//فروشگاه Vuex
const store = new Vuex.Store({
state: {
// جدول شبیهسازی
simulations: [],
// شماره آخرین شبیهسازی
idSimulation: 0
},
mutations: {
// حذف سطر با شمارهٔ شاخص
deleteSimulation(state, index) {
...
},
// افزودن شبیهسازی
addSimulation(state, simulation) {
...
},
// حالت را پاک کنید
clear(state) {
// دیگر شبیهسازی وجود ندارد
state.simulations = [];
//شمارهگذاری شبیهسازی از 0 مجدداً آغاز میشود
state.idSimulation = 0;
}
}
});
// صادرات شیء [store]
export default store;
- خطوط ۲۴–۲۹: جهش [clear] فهرست شبیهسازیهای ذخیرهشده را حذف کرده و شمارهی آخرین شبیهسازی را به ۰ بازمیگرداند.
19.3. جلسه
نیاز به یک جلسه به این دلیل پیش میآید که وقتی کاربر URL را در نوار آدرس مرورگر تایپ میکند، اسکریپت [main.js] دوباره اجرا میشود. با این حال، این اسکریپت حاوی دستور زیر است:
//فروشگاه Vuex
import store from './store'
این دستور فایل زیر را وارد میکند، [./store]:
// پلاگین Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
// انبار Vuex
const store = new Vuex.Store({
state: {
// جدول شبیهسازی
simulations: [],
// شماره آخرین شبیهسازی
idSimulation: 0
},
mutations: {
...
}
});
// خروجی شیء [store]
export default store;
همانطور که در خطوط ۷ تا ۱۳ دیده میشود، یک آرایه خالی از شبیهسازیها وارد میشود. بنابراین، اگر قبل از اینکه کاربر URL را در نوار آدرس مرورگر وارد کند، شبیهسازیهایی وجود داشته باشد، پس از آن دیگر وجود نخواهند داشت. ایده این است:
- برای استفاده از یک جلسه (session) به منظور ذخیره اطلاعاتی که میخواهیم در صورت وارد کردن دستی URL توسط کاربر، حفظ شوند؛
- ذخیره کردن آن در نقاط کلیدی برنامه؛
- برای بازیابی آن در [main.js]، که همیشه هنگام وارد کردن دستی URL اجرا میشود؛
اسکریپت [./session] به شرح زیر است:
// وارد کردن استور Vuex
import store from './store'
// وارد کردن پیکربندی
import config from './config';
// ابژه [session]
const session = {
// جلسه آغاز شد
started: false,
// احراز هویت
authenticated: false,
// صرفهجویی در زمان
saveTime: "",
//لایه [métier]
métier: null,
//وضعیت Vuex
state: null,
//ذخیرهٔ جلسه در یک رشته jSON
save() {
// افزودن برخی ویژگیها به جلسه
this.saveTime = Date.now();
this.state = store.state;
//آن را به jSON تبدیل کنید
const json = JSON.stringify(this);
// ذخیره آن در مرورگر
localStorage.setItem("session", json);
// eslint-disable-next-line no-console
console.log("session save", json);
},
//بازیابی جلسه
restore() {
//جلسه را از مرورگر بازیابی میکند jSON
const json = localStorage.getItem("session")
// اگر چیزی بازیابی شده باشد
if (json) {
// تمام کلیدهای جلسه را بازیابی میکند
const restore = JSON.parse(json);
for (var key in restore) {
if (restore.hasOwnProperty(key)) {
this[key] = restore[key];
}
}
// اگر از شروع جلسه مدت معینی عدم فعالیت سپری شده باشد، از ابتدا شروع میکنیم
let durée = Date.now() - this.saveTime;
if (durée > config.duréeSession) {
// پاک کردن جلسه – جلسه نیز ذخیره خواهد شد
session.clear();
} else {
// بازتولید فروشگاه Vuex
store.replaceState(JSON.parse(JSON.stringify(this.state)));
}
}
//eslint-disable-next-line no-console
console.log("session restore", this);
},
//پاکسازی جلسه
clear() {
// eslint-disable-next-line no-console
console.log("session clear");
// بازنشانی برخی فیلدها در جلسه
this.authenticated = false;
this.saveTime = "";
this.started = false;
if (this.métier) {
// بازنشانی فیلد [taxAdminData]
this.métier.taxAdminData = null;
}
// استور Vuex نیز پاک میشود
store.commit("clear");
// جلسه جدید ذخیره میشود
this.save();
},
}
// ابژه [session] را صادر کنید
export default session;
توضیحات
- خط ۲: جلسه همچنین شامل store [Vuex] (فهرست شبیهسازیها، شماره آخرین شبیهسازی انجامشده) خواهد بود؛
- خطوط ۷–۱۷: اطلاعاتی که توسط جلسه نگهداری میشود:
- [started]: اینکه آیا جلسه jSON با سرور آغاز شده است یا خیر؛
- [authenticated]: اینکه آیا کاربر احراز هویت شده است یا خیر؛
- [saveTime]: تاریخ، به میلیثانیه، آخرین ذخیره؛
- [métier]: مرجعی به لایه [métier]. این شامل داده [taxAdminData] است که امکان محاسبه مالیات را فراهم میکند؛
- [state]: وضعیت فروشگاه [Vuex] (فهرست شبیهسازیها، شماره جدیدترین شبیهسازی انجامشده);
- خطوط ۲۰–۳۰: متد [save] جلسه را بهصورت محلی روی مرورگری که برنامه را اجرا میکند، ذخیره میکند؛
- خط ۲۲: زمان ذخیره ثبت میشود؛
- خط ۲۳: [state] از مخزن [Vuex] بازیابی میشود؛
- خط ۲۵: رشته جلسه jSON ایجاد میشود؛
- خط ۲۷: این به صورت محلی در مرورگر مرتبط با کلید [session] ذخیره میشود؛
- خطوط ۳۳–۵۷: متد [restore] امکان بازیابی یک جلسه از پشتیبان محلی آن در مرورگر را فراهم میکند؛
- خط ۳۵: پشتیبان محلی jSON بازیابی میشود؛
- خط ۳۷: اگر چیزی بازیابی شده باشد؛
- خطوط ۳۹–۴۴: شیء [session] بازسازی میشود؛
- خط ۴۶: زمان سپریشده از آخرین پشتیبانگیری محاسبه میشود؛
- خطوط ۴۷–۵۰: اگر این مدت از مقداری که در پیکربندی با [config.duréeSession] تنظیم شده است فراتر رود، جلسه ریست (خط ۴۹) و همزمان ذخیره میشود؛
- خط ۵۲: در غیر این صورت، ویژگی [state] از مخزن [Vuex] مجدداً تولید میشود؛
- خطوط ۶۰–۷۵: متد [clear] جلسه را ریست میکند؛
- خطوط 64–70: ویژگیهای جلسه به مقادیر اولیه خود بازنشانی میشوند؛
- خط ۷۲: همینطور برای مخزن [Vuex]؛
- خط ۷۴: جلسه جدید ذخیره میشود؛
19.4. فایل پیکربندی [config]
فایل [./config] به شرح زیر تغییر میکند:
// با استفاده از کتابخانه [axios]
const axios = require('axios');
// انقضای درخواست HTTP
axios.defaults.timeout = 2000;
...
// صادرات پیکربندی
export default {
// [axios] شیء
axios: axios,
// حداکثر زمان انقضای عدم فعالیت جلسه: ۵ دقیقه = ۳۰۰ ثانیه = ۳۰۰٬۰۰۰ میلیثانیه
duréeSession: 300000
}
- خط ۱۲: ما جلسهٔ برنامه را به همان شیوهای که جلسهٔ وب را مدیریت میکنیم، مدیریت خواهیم کرد. در اینجا، حداکثر دورهٔ عدم فعالیت را ۵ دقیقه تعیین میکنیم؛
19.5. پلاگین [pluginSession]
همانطور که بارها قبلاً انجام شده است، پلاگین [pluginSession] به ویوها اجازه میدهد از طریق خاصیت [this.$session] به جلسه دسترسی پیدا کنند:
export default {
install(Vue, session) {
//یک ویژگی [$session] را به کلاس ویو اضافه میکند
Object.defineProperty(Vue.prototype, '$session', {
// وقتی Vue.$session مرجع داده میشود، پارامتر دوم روی [session] تنظیم میشود
get: () => session,
})
}
}
19.6. اسکریپت اصلی [main]
اسکریپت اصلی [./main.js] به شرح زیر تغییر میکند:
// لاگ راهاندازی
// eslint-disable-next-line no-console
console.log("main started");
// وارداتها
import Vue from 'vue'
...
// نمونه سازی لایه [métier]
import Métier from './couches/Métier';
const métier = new Métier();
// پلاگین [métier]
import pluginMétier from './plugins/pluginMétier'
Vue.use(pluginMétier, métier)
// ذخیرهی Vuex
import store from './store'
// جلسه
import session from './session';
import pluginSession from './plugins/pluginSession'
Vue.use(pluginSession, session)
// جلسه قبل از راهاندازی مجدد بازیابی میشود
session.restore();
//بازیابی لایه [métier]
if (session.métier && session.métier.taxAdminData) {
métier.setTaxAdminData(session.métier.taxAdminData);
}
// راهاندازی UI
new Vue({
el: '#اپ',
// روتر
router: router,
//فروشگاه Vuex
store: store,
// نمای اصلی
render: h => h(Main),
})
//پایان لاگ
// eslint-disable-next-line no-console
console.log("main terminated, session=", session);
- خط ۱۹: جلسه وارد میشود؛
- خط ۲۰: افزونهٔ آن وارد میشود؛
- خط ۲۱: پلاگین [pluginSession] در [Vue] ادغام میشود. پس از این دستور، تمام ویوها دارای session در ویژگی [$session] خود هستند؛
- خط ۲۷: جلسه بازیابی میشود. جلسهای که در خط ۱۱ وارد شده است، سپس با محتویات آخرین ذخیره خود مقداردهی اولیه میشود؛
- پس از خط ۱۶، ویوها دارای خاصیت [$métier] هستند که در خط ۱۲ مقداردهی اولیه شده است. این خاصیت حاوی اطلاعات [taxAdminData] که برای محاسبه مالیات استفاده میشود، نیست؛
- خطوط ۳۰–۳۲: اگر بازیابی انجامشده، ویژگی [session.métier.taxAdminData] را بازیابی کرده باشد، آنگاه ویژگی [$métier] ویوها با این مقدار مقداردهی اولیه میشود؛
19.7. فایل مسیریابی [router]
فایل مسیریابی [./router] به شرح زیر تغییر میکند:
// وارداتها
import Vue from 'vue'
import VueRouter from 'vue-router'
// نماها
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'
import NotFound from './views/NotFound'
// جلسه
import session from './session'
// پلاگین مسیریابی
Vue.use(VueRouter)
//مسیرهای برنامه
const routes = [
//احراز هویت
{ path: '/', name: 'authentification', component: Authentification },
{ path: '/authentification', name: 'authentification', component: Authentification },
// محاسبه مالیات
{
path: '/calcul-impot', name: 'calculImpot', component: CalculImpot,
meta: { authenticated: true }
},
// فهرست شبیهسازیها
{
path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations,
meta: { authenticated: true }
},
//پایان جلسه
{
path: '/fin-session', name: 'finSession'
},
// صفحهٔ ناشناخته
{
path: '*', name: 'notFound', component: NotFound,
},
]
// روتر
const router = new VueRouter({
//مسیرها
routes,
// حالت نمایش URL
mode: 'history',
// پایهٔ برنامه URL
base: '/client-vuejs-impot/'
})
// تأیید مسیر
router.beforeEach((to, from, next) => {
//eslint-disable-next-line no-console
console.log("router to=", to, "from=", from);
//مسیر رزرو شده برای کاربران احراز هویت شده؟
if (to.meta.authenticated && !session.authenticated) {
next({
// حرکت به سمت احراز هویت
name: 'authentification',
})
//بازگشت به حلقه رویداد
return;
}
// مورد خاص: پایان جلسه
if (to.name === "finSession") {
//پاک کردن جلسه
session.clear();
// پیمایش به نما [authentification]
next({
name: 'authentification',
})
//بازگشت به حلقه رویداد
return;
}
// سایر موارد – نمای مسیریابی عادی بعدی
next();
})
// خروجی روتر
export default router
توضیحات
- خطوط ۱۶–۳۸: برخی مسیرها با اطلاعات اضافی بهبود یافتهاند؛
- خط ۱۹: یک مسیر جدید به نما [Authentification] ایجاد شده است؛
- ردههای ۲۱–۲۴: مسیری که به نمای [CalculImpot] منتهی میشود، اکنون دارای خصوصیتی به نام [meta] است (این نام اجباری است). محتوای این شی میتواند هر چیزی باشد و توسط توسعهدهنده تعیین میشود؛
- خط ۲۳: ما ویژگی [authenticated] (این نام میتواند هر چیزی باشد) را در [meta] تنظیم میکنیم. برای ما، این بدان معناست که برای دسترسی به نما [CalculImpot]، کاربر باید احراز هویت شود؛
- خطوط ۲۶–۲۹: همین کار را برای مسیری که به نمای [ListeSimulations] منتهی میشود، انجام میدهیم. در اینجا نیز، کاربر باید احراز هویت شود؛
- ویژگی [meta.authenticated] به ما امکان میدهد تا تأیید کنیم که کاربری که مقادیر URL را برای ویوهای [CalculImpot, ListeSimulations] بهصورت دستی وارد میکند، نمیتواند به آنها دسترسی پیدا کند مگر اینکه احراز هویت شده باشد؛
- خطوط ۵۱–۷۶: متد [beforeEach] قبل از مسیریابی یک نما اجرا میشود. این زمان مناسبی برای انجام بررسیها است؛
- [to]: مسیر بعدی در صورتی که هیچ اقدامی انجام نشود؛
- [from]: آخرین مسیر نمایشدادهشده؛
- [next]: تابع تغییر مسیر نمایشدادهشده بعدی؛
- خط ۵۵: بررسی میکنیم که آیا مسیر بعدی نیازمند احراز هویت کاربر است؛
- خطوط ۵۶–۵۹: اگر چنین باشد و کاربر احراز هویت نشده باشد، مسیر بعدی به نمای [Authentification] تغییر داده میشود؛
- خطوط ۶۴–۷۳: رسیدگی به مورد خاص مسیر [finSession] از خطوط ۳۰–۳۲. این مسیر هیچ نمای مرتبطی ندارد؛
- خط ۶۶: جلسه به مقدار اولیه خود بازنشانی میشود؛
- خطوط ۶۸–۷۰: نمای [Authentification] بهعنوان نمای بعدی تنظیم میشود؛
- خط ۷۵: اگر هیچیک از دو مورد قبلی صدق نکند، ما به سادگی به مسیری که در فایل مسیریابی مشخص شده است، ادامه میدهیم؛
- خطوط ۳۵–۳۷: یک نما با نام [NotFound] ارائه میشود اگر مسیری که کاربر وارد کرده با هیچیک از مسیرهای شناختهشده مطابقت نداشته باشد. این نما در خط ۸ وارد میشود. مسیرها به ترتیبی که در فایل مسیریابی مشخص شده بررسی میشوند. بنابراین، اگر به خط ۳۶ برسیم، به این معنی است که مسیر درخواستی هیچیک از مسیرهای فهرستشده در خطوط ۱۸–۳۳ نیست؛
19.8. ویوی [NotFound]
نما [NotFound] در صورتی نمایش داده میشود که مسیری که کاربر وارد کرده با هیچ یک از مسیرهای شناختهشده مطابقت نداشته باشد:

کد نما به شرح زیر است:
<!--HTML تعریف نما -->
<template>
<!-- طرحبندی صفحه -->
<Layout :left="true" :right="true">
<!-- هشدار در ستون سمت راست -->
<template slot="right">
<!-- پیام روی پسزمینه زرد -->
<b-alert show variant="danger" align="center">
<h4>Cette page n'existe pas</h4>
</b-alert>
</template>
<!-- منوی ناوبری در ستون سمت چپ -->
<Menu slot="left" :options="options" />
</Layout>
</template>
<script>
// وارداتها
import Layout from "./Layout";
import Menu from "./Menu";
export default {
// کامپوننتها
components: {
Layout,
Menu
},
//وضعیت مؤلفهٔ داخلی
data() {
return {
// گزینههای منوی ناوبری
options: [
{
text: "Authentification",
path: "/"
}
]
};
},
//چرخهٔ عمر
created() {
// eslint-disable-next-line
console.log("NotFound created");
// ما بررسی میکنیم که کدام گزینههای منو را ارائه دهیم
if (this.$session.authenticated && this.$métier.taxAdminData) {
// کاربر میتواند شبیهسازیها را اجرا کند
Array.prototype.push.apply(this.options, [
{
text: "Calcul de l'impôt",
path: "/calcul-impot"
},
{
text: "Liste des simulations",
path: "/liste-des-simulations"
}
]);
}
}
};
</script>
نظرات
- خط ۴: از هر دو ستون نماهای مسیریابیشده استفاده میکند؛
- خطوط ۶–۱۱: یک پیام خطا؛
- خط ۱۳: منوی ناوبری ستون سمت چپ را اشغال میکند؛
- خطوط ۳۱–۳۶: گزینههای پیشفرض منو؛
- خطوط ۴۰–۵۷: کدی که هنگام ایجاد نما اجرا میشود؛
- خط ۴۴: بررسی میکند که آیا کاربر میتواند شبیهسازیها را اجرا کند؛
- خطوط ۴۵–۵۵: اگر چنین باشد، دو گزینه به منوی ناوبری اضافه میشوند: مواردی که نیاز دارند کاربر احراز هویت شود و لایه [métier] عملیاتی باشد (خطوط ۴۶–۵۵);
19.9. نما [Authentification]
نما [Authentification] به شرح زیر تغییر میکند:
<!-- تعریف نما HTML -->
<template>
<Layout :left="false" :right="true">
...
</Layout>
</template>
<!-- نما دینامیک -->
<script>
import Layout from "./Layout";
export default {
//وضعیت مؤلفه
data() {
return {
// کاربر
user: "",
// رمز عبور آنها
password: "",
// نمایش پیام خطا را کنترل میکند
showError: false,
// پیام خطا
message: ""
};
},
// کامپوننتهای استفاده شده
components: {
Layout
},
// ویژگیهای محاسباتی
computed: {
// ورودیهای معتبر
valid() {
return this.user && this.password && this.$session.started;
}
},
// پردازشگرهای رویداد
methods: {
// ----------- احراز هویت
async login() {
try {
// شروع انتظار
this.$emit("loading", true);
// هنوز احراز هویت نشده
this.$session.authenticated = false;
// مصالحه مسدودکننده با سرور
const response = await this.$dao.authentifierUtilisateur(
this.user,
this.password
);
//پایان بارگذاری
this.$emit("loading", false);
// در حال تجزیه و تحلیل پاسخ سرور
if (response.état != 200) {
// نمایش خطا
this.message = response.réponse;
this.showError = true;
//بازگشت به حلقه رویداد
return;
}
// بدون خطا
this.showError = false;
// احراز هویت موفق
this.$session.authenticated = true;
// --------- اکنون در حال درخواست داده از مقامات مالیاتی
// در ابتدا، هیچ دادهای
this.$métier.setTaxAdminData(null);
// انتظار آغاز شد
this.$emit("loading", true);
// درخواست به سرور مسدود شده است
const response2 = await this.$dao.getAdminData();
//پایان بارگذاری
this.$emit("loading", false);
// در حال تجزیه و تحلیل پاسخ
if (response2.état != 1000) {
//خطا نمایش داده شد
this.message = response2.réponse;
this.showError = true;
//بازگشت به حلقه رویداد
return;
}
// بدون خطا
this.showError = false;
//دادههای دریافتی در لایه [métier] ذخیره میشوند
this.$métier.setTaxAdminData(response2.réponse);
// میتوانیم به محاسبه مالیات ادامه دهیم
this.$router.push({ name: "calculImpot" });
} catch (error) {
//خطا به مؤلفهٔ اصلی ارجاع داده میشود
this.$emit("error", error);
} finally {
//بهروزرسانی جلسه
this.$session.métier = this.$métier;
//ذخیرهٔ جلسه
this.$session.save();
}
}
},
// چرخه عمر: کامپوننت به تازگی ایجاد شده است
created() {
// eslint-disable-next-line
console.log("Authentification created");
//آیا کاربر میتواند شبیهسازیها را اجرا کند؟
if (
this.$session.started &&
this.$session.authenticated &&
this.$métier.taxAdminData
) {
// اگر چنین باشد، کاربر میتواند شبیهسازیها را اجرا کند
this.$router.push({ name: "calculImpot" });
//بازگشت به حلقه رویداد
return;
}
// اگر جلسه jSON قبلاً شروع شده باشد، مجدداً راهاندازی نمیشود
if (!this.$session.started) {
// شروع انتظار
this.$emit("loading", true);
// ابتدای جلسه با سرور – درخواست ناهمزمان
// از وعدهای که توسط متدهای لایه [dao] بازگردانده شده است استفاده میشود
this.$dao
// ابتدای یک جلسه jSON
.initSession()
//پاسخ دریافت شده است
.then(response => {
//پایان انتظار
this.$emit("loading", false);
// در حال تجزیه و تحلیل پاسخ
if (response.état != 700) {
// نمایش خطا
this.message = response.réponse;
this.showError = true;
//بازگشت به حلقه رویداد
return;
}
// جلسه آغاز شده است
this.$session.started = true;
})
// در صورت بروز خطا
.catch(error => {
//خطا به ویو [Main] ارجاع داده میشود
this.$emit("error", error);
})
// در همه موارد
.finally(() => {
// جلسه ذخیره شد
this.$session.save();
});
}
}
};
</script>
نظرات
- دستورالعملهایی که از جلسه معرفیشده در این نسخه از [Vue.js] کلاینت استفاده میکنند، به رنگ زرد برجسته شدهاند؛
- خطوط ۹۷، ۱۴۸: در انتهای متدهای [login, created]، جلسه صرفنظر از نتیجه پرسوجوهای HTTP که در این متدها انجام میشوند، ذخیره میشود (بند [finally] در هر دو مورد)؛
- روش [created] در خطوط 102–150 هر بار که نمای [Authentification] ایجاد میشود، اجرا میگردد. اگر کاربر وارد URL برای نما شده باشد، جلسه به ما خواهد گفت که چه کاری انجام دهیم؛
- خطوط ۱۰۶–۱۱۵: اگر جلسه jSON آغاز شود، کاربر احراز هویت شده و دادههای [this.$métier.taxAdminData] اولیه میشوند، سپس کاربر میتواند مستقیماً به فرم محاسبه مالیات (خط ۱۱۲) برود؛
- خط ۱۱۷: متد [created] در نسخه قبلی برای راهاندازی یک جلسه jSON با سرور استفاده میشد. اگر این مرحله قبلاً انجام شده باشد، این گام غیرضروری است؛
- خطوط ۴۲–۶۶: روش احراز هویت؛
- خط ۶۶: اگر احراز هویت موفقیتآمیز باشد، این موضوع در جلسه ثبت میشود؛
- خطوط ۶۷–۹۲: درخواست به سرور برای دادههای مرجع مالیاتی [taxAdminData];
- خط ۹۵: در پایان این فاز، ویژگی [métier] جلسه، صرفنظر از موفقیت یا عدم موفقیت عملیات، بهروزرسانی میشود؛
19.10. نما [CalculImpot]
کد ویو [CalculImpot] به شرح زیر تغییر میکند:
<!--HTML تعریف نما -->
<template>
...
</template>
<script>
// وارداتها
import FormCalculImpot from "./FormCalculImpot";
import Menu from "./Menu";
import Layout from "./Layout";
export default {
//وضعیت داخلی
data() {
return {
// گزینههای منو
options: [
{
text: "Liste des simulations",
path: "/liste-des-simulations"
},
{
text: "Fin de session",
path: "/fin-session"
}
],
// نتیجه محاسبه مالیات
résultat: "",
résultatObtenu: false
};
},
// کامپوننتهای استفاده شده
components: {
Layout,
FormCalculImpot,
Menu
},
// روشهای رسیدگی به رویدادها
methods: {
// نتیجه محاسبه مالیات
handleResultatObtenu(résultat) {
// نتیجه به صورت یک رشته ساخته میشود HTML
...
// یک شبیهسازی از +
this.$store.commit("addSimulation", résultat);
//جلسه ذخیره میشود
this.$session.save();
}
},
//چرخه زندگی
created() {
// eslint-disable-next-line
console.log("CalculImpot created");
}
};
</script>
توضیحات
- خط ۴۵: شبیهسازی محاسبهشده به مخزن [Vuex] اضافه میشود. این امر بر جلسه تأثیر میگذارد، که شامل ویژگی مخزن [state] است. بنابراین جلسه ذخیره میشود (خط ۴۷)؛
- خط ۵۱: متدی به نام [created] برای ردیابی ایجاد ویوها در لاگها ایجاد شده است؛
19.11. ویو [ListeSimulations]
ویوی [ListeSimulations] به شرح زیر تغییر میکند:
<!--HTML تعریف نما -->
<template>
...
</div>
</template>
<script>
// واردات
import Layout from "./Layout";
import Menu from "./Menu";
export default {
// اجزاء
components: {
Layout,
Menu
},
// وضعیت داخلی
data() {
...
},
// وضعیت داخلی محاسبه شده
computed: {
// فهرست شبیهسازیهای برداشتهشده از فروشگاه Vuex
simulations() {
return this.$store.state.simulations;
}
},
// روشها
methods: {
supprimerSimulation(index) {
// eslint-disable-next-line
console.log("supprimerSimulation", index);
// حذف شبیهسازی شماره [index]
this.$store.commit("deleteSimulation", index);
//ذخیرهٔ جلسه
this.$session.save();
}
},
// چرخه عمر
created() {
// eslint-disable-next-line
console.log("ListeSimulations created");
}
};
</script>
توضیحات
- خط ۳۶: پس از حذف یک شبیهسازی در خط ۳۴، جلسه ذخیره میشود تا این تغییر وضعیت را منعکس کند؛
- خطوط ۴۰–۴۳: ما به نظارت بر ایجاد ویوها ادامه میدهیم؛
19.12. اجرای پروژه

در حین آزمایش، موارد زیر را بررسی کنید:
- اگر کاربر از طریق پیوندهای منوی ناوبری و دکمهها/پیوندهای عملیاتی از برنامه «استفاده» کند، برنامه کار میکند؛
- اگر کاربر بهصورت دستی URL را وارد کند، برنامه همچنان به کار خود ادامه میدهد. بهویژه، تست زیر را انجام دهید:
- شبیهسازیها را انجام دهید؛
- هنگامی که در نمای [ListeSimulations] قرار گرفتید، نما را دوباره بارگذاری (F5) کنید. در برنامه قبلی ([vuejs-20])، شبیهسازیها در این صورت از بین میرفتند. اما در اینجا اینطور نیست: شبیهسازیهای قبلاً انجامشده همچنان موجود هستند؛
- لاگها را بررسی کنید تا متوجه شوید:
- زمانی که اسکریپت [main] اجرا میشود. باید ببینید که هر بار کاربر بهصورت دستی URL را وارد میکند، اجرا میشود؛
- زمانی که ویوها ایجاد میشوند. باید ببینید که هر بار که قرار است نمایش داده شوند، ایجاد میشوند؛
- نحوه کار مسیریابی. قبل از هر عملیات مسیریابی، یک ورودی لاگ ایجاد میشود که نشان میدهد:
- مسیر ورودی شما؛
- مسیر مقصد شما؛
19.13. راهاندازی برنامه روی یک سرور محلی
بهعنوان یک تمرین، بخش «استقرار روی سرور محلی» را دنبال کنید تا پروژه [vuejs-21] را روی سرور محلی Laragon مستقر کنید. سپس آن را آزمایش کنید.
19.14. بهینهسازی نسخه موبایل
از نظر تئوری، استفاده از بوتاسترپ باید به ما امکان دهد تا برنامهای داشته باشیم که در دستگاههای مختلف کار کند: گوشیهای هوشمند، تبلتها، لپتاپها و رایانههای رومیزی. چیزی که این دستگاهها را از هم متمایز میکند، اندازه صفحهنمایش آنهاست.
اگر نسخه [vuejs-21] را روی یک دستگاه موبایل آزمایش کنیم، متوجه میشویم که چیدمان ویوها بههمریخته است. نسخه [vuejs-22] این مشکل را برطرف میکند. تمام تغییرات بر روی قالبهای نما اعمال شده است. این تغییرات عمدتاً شامل بهینهسازی نمایش برای صفحه نمایش گوشی هوشمند بود. پس از حل این مشکل، نمایش بر روی صفحههای بزرگتر به لطف بوتاسترپ به طور روان کار میکند.

19.14.1. نما [Main]
ویوی [Main] به شرح زیر تغییر میکند:
<!-- HTML تعریف نما -->
<template>
<div class="container">
<b-card>
<!-- جَمبوتِرون -->
<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>
نظرات
- خط ۸: جایی که قبلاً [cols=’4’] نوشته شده بود، اکنون [sm=’4’] نوشته میشود. [sm] به معنای [small] است. صفحههای نمایش گوشیهای هوشمند در این دسته قرار میگیرند. سایر دستهها عبارتند از [xs=extra small, md=medium, lg=large, xl=extra large]؛
- خط ۱۱: همانند بالا؛
19.14.2. نما [Layout]
نما [Layout] به شرح زیر تغییر میکند:
<!-- HTML تعریف چیدمان نمای مسیریابیشده -->
<template>
<!-- ردیف -->
<div>
<b-row>
<!-- منطقهٔ سهستونی در سمت چپ -->
<b-col sm="3" v-if="left">
<slot name="left" />
</b-col>
<!-- منطقهٔ نهستونی در سمت راست -->
<b-col sm="9" v-if="right">
<slot name="right" />
</b-col>
</b-row>
</div>
</template>
19.14.3. نما [Authentification]
نمایش [Authentification] به شرح زیر تغییر میکند:
<!-- تعریف نما HTML -->
<template>
<Layout :left="false" :right="true">
<template slot="right">
<!--فرم HTML – مقادیر با استفاده از action QZXW2HTMLBW2F1dGhlbnRpZmllci-11dGlsaXNhdGV1cl0ZQX ارسال میشوند -->
<b-form @submit.prevent="login">
<!-- عنوان -->
<b-alert show variant="primary">
<h4>Bienvenue. Veuillez vous authentifier pour vous connecter</h4>
</b-alert>
<!-- خط اول -->
<b-form-group label="Nom d'utilisateur" label-for="user" description="Tapez admin">
<!-- میدان ورودی کاربر -->
<b-col sm="6">
<b-form-input type="text" id="user" placeholder="Nom d'utilisateur" v-model="user" />
</b-col>
</b-form-group>
<!-- خط دوم -->
<b-form-group label="Mot de passe" label-for="password" description="Tapez admin">
<!-- میدان وارد کردن رمز عبور -->
<b-col sm="6">
<b-input type="password" id="password" placeholder="Mot de passe" v-model="password" />
</b-col>
</b-form-group>
<!-- خط سوم -->
<b-alert
show
variant="danger"
v-if="showError"
class="mt-3"
>L'erreur suivante s'est produite : {{message}}</b-alert>
<!-- دکمه از نوع [submit] در خط سوم -->
<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>
نظرات
- ردههای ۱۱ و ۱۹: ویژگی [label-cols] که تعداد ستونهای برچسب ورودی را تعیین میکرد، حذف شده است. بدون این ویژگی، برچسب بالای فیلد ورودی نمایش داده میشود. این حالت برای صفحههای نمایش گوشیهای هوشمند مناسبتر است؛
19.14.4. نما [CalculImpot]
نما [CalculImpot] به شرح زیر بهروزرسانی شده است:
<!-- HTML تعریف نما -->
<template>
<div>
<Layout :left="true" :right="true">
<!-- فرم محاسبه مالیات در سمت راست -->
<FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
<!-- منوی ناوبری در سمت چپ -->
<Menu slot="left" :options="options" />
</Layout>
<!-- منطقه نمایش نتایج محاسبه مالیات در زیر فرم -->
<b-row v-if="résultatObtenu" class="mt-3">
<!-- فضای خالی سهستونی -->
<b-col sm="3" />
<!-- منطقهٔ نهستونی -->
<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] به شرح زیر تغییر میکند:
<!-- HTML تعریف نما -->
<template>
<!-- فرم HTML -->
<b-form @submit.prevent="calculerImpot" class="mb-3">
<!-- پیام دوازدهستونی روی پسزمینه آبی -->
<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>
<!-- عناصر فرم -->
<!-- اولین سطر -->
<b-form-group label="Etes-vous marié(e) ou pacsé(e) ?">
<!-- دکمههای رادیویی در ۵ ستون-->
<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>
<!-- ردیف دوم -->
<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>
<!-- پیام خطای احتمالی -->
<b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
</b-form-group>
<!-- ردیف سوم -->
<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>
<!-- پیام خطای احتمالی -->
<b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
</b-form-group>
<!-- خط چهارم، دکمه [submit] -->
<b-col sm="3">
<b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
</b-col>
</b-form>
</template>
توضیحات
- خطوط ۱۵، ۲۳، ۳۵: ویژگی [label-cols] حذف شده است؛
علاوه بر این، بررسیهای اعتبارسنجی بهروزرسانی شدهاند:
...
//وضعیت داخلی محاسبهشده
computed: {
//اعتبارسنجی فرم
formInvalide() {
return (
// حقوق نامعتبر
!this.salaire.match(/^\s*\d+\s*$/) ||
// یا فرزندان نامعتبر
!this.enfants.match(/^\s*\d+\s*$/) ||
// یا دریافت نشدن اطلاعات مالیاتی
!this.$métier.taxAdminData
);
},
//اعتبارسنجی حقوق
salaireValide() {
//باید یک عدد ≥ 0 باشد
return Boolean(
this.salaire.match(/^\s*\d+\s*$/) || this.salaire.match(/^\s*$/)
);
},
//اعتبارسنجی فرزندان
enfantsValide() {
//باید یک مقدار عددی ≥ 0 باشد
return Boolean(
this.enfants.match(/^\s*\d+\s*$/) || this.enfants.match(/^\s*$/)
);
}
},
...
توضیحات
- خط ۱۹: هنگامی که هیچ چیزی وارد نشده باشد، ورودی معتبر در نظر گرفته میشود. این امر تضمین میکند که ورودی هنگام اولین نمایش نما معتبر باشد. در نسخه قبلی، ورودی در ابتدا نادرست به نظر میرسید؛
- خط ۲۶: مشابه مورد بالا؛
- خطوط ۵–۱۴: دکمه اعتبارسنجی تنها در صورتی فعال است که هر دو فیلد حاوی داده باشند و معتبر باشند؛
19.14.6. نما [Menu]
نما [Menu] به شرح زیر بهروزرسانی شده است:
<!-- HTML تعریف نما -->
<template>
<b-card class="mb-3">
<!-- منوی عمودی بوتاسترپ -->
<b-nav vertical>
<!-- گزینههای منو -->
<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>
توضیحات
- خط ۳: تگ <b-card> اضافه شده تا منو را با یک حاشیه نازک احاطه کند. این کار به قرارگیری واضحتر منو روی گوشی هوشمند کمک میکند؛
19.14.7. نما [ListeSimulations]
نما [ListeSimulations] بدون تغییر باقی میماند:
<!-- تعریف نما HTML -->
<template>
<div>
<!-- قالببندی -->
<Layout :left="true" :right="true">
<!-- شبیهسازیها در ستون سمت راست -->
<template slot="right">
<template v-if="simulations.length==0">
<!-- بدون شبیهسازی -->
<b-alert show variant="primary">
<h4>Votre liste de simulations est vide</h4>
</b-alert>
</template>
<template v-if="simulations.length!=0">
<!-- شبیهسازیها وجود دارند -->
<b-alert show variant="primary">
<h4>Liste de vos simulations</h4>
</b-alert>
<!-- جدول شبیهسازیها -->
<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 slot="left" :options="options" />
</Layout>
</div>
</template>
توضیحات
- خط ۲۰: به ویژگی [responsive] توجه کنید که تضمین میکند نمایش جدول با اندازه صفحه سازگار شود:

- در [2]، در صفحههای نمایش کوچک، از یک نوار پیمایش افقی برای نمایش جدول استفاده میشود؛
19.14.8. نما [NotFound]
بدون تغییر باقی میماند.
19.14.9. نمایهای موبایل


توجه: قطعاً میتوان ویوهایی ایجاد کرد که برای دستگاههای موبایل حتی مناسبتر باشند. من به خصوص به منوی ناوبری فکر میکنم که میتوان آن را بهبود بخشید، اما حوزههای دیگری نیز وجود دارد. هدف اصلی این سند ایجاد یک اپلیکیشن موبایل نبوده است. در این صورت، میتوانستیم به فریمورکی مانند Ionic |https://ionicframework.com/| روی بیاوریم.