17. مثال [nuxt-20]: انتقال مثال [vuejs-22]
17.1. Présentation
در اینجا، ما پیشنهاد میکنیم که مثال [vuejs-22] – که یک برنامه [vue.js] از نوع SPA بود – را به یک زمینه [nuxt] SSR منتقل کنیم. [vuejs-22] یک برنامهٔ کلاینت برای سرور محاسبهٔ مالیات بود که نماهای زیر را ارائه میداد:
نمای اول نمای احراز هویت است:

نمای دوم نمای محاسبه مالیات است:

نمای سوم فهرست شبیهسازیهای انجامشده توسط کاربر را نمایش میدهد:

صفحه بالا نشان میدهد که شبیهسازی شماره ۱ قابل حذف است. این منجر به نمای زیر میشود:

اگر اکنون آخرین شبیهسازی را حذف کنیم، نمای جدید زیر به ما نمایش داده میشود:

ما به تدریج برنامه [vuejs-22] را به برنامه [nuxt-20] منتقل خواهیم کرد. ما دیگر کد [vuejs-22] را توضیح نخواهیم داد. از خوانندگان دعوت میشود تا بار دیگر سند «مقدمهای بر چارچوب VUE.JS از طریق مثالها» را مرور کنند. مراحل مختلف باید تفاوتهای بین یک برنامه [vuejs] و یک برنامه [nuxt] را نشان دهد.
17.2. مرحله ۱
پروژه [nuxt-20] در ابتدا با کلون کردن پروژه [nuxt-12] ایجاد میشود. این در واقع نقطه شروع خوبی است:
- میتواند با سرور محاسبه مالیات ارتباط برقرار کند؛
- خطاهای بازگشتی از سرور را بهدرستی مدیریت میکند؛
- کلاینت و سرور [nuxt] میتوانند از طریق یک جلسه [nuxt] با یکدیگر ارتباط برقرار کنند؛
بنابراین ما یک زیرساخت اولیه محکم داریم. وظیفه اصلی ما باید اصلاح موارد زیر باشد:
- صفحات. ما از صفحات پروژه [vuejs-22] استفاده خواهیم کرد که باید با محیط جدید سازگار شوند؛
- مدیریت انبار. اطلاعات اضافی باید نمایش داده شود (فهرست شبیهسازیها) در حالی که جزئیات دیگر ممکن است زائد شوند؛
- مدیریت مسیریابی کلاینت و سرور برای [nuxt]؛
بنابراین، در وهله اول، پروژه [nuxt-20] را با کپی کردن پروژه [nuxt-12] ایجاد میکنیم:

سپس ما صفحات و کامپوننتهایی را که دیگر مورد نیاز نیستند، [2]:
- کامپوننت [components/navigation] ناپدید میشود؛
- قالب [layout/default] حذف میشود؛
- صفحات [index, authentification, get-admindata, fin-session] حذف میشوند؛
سپس، عناصر از [vuejs-22] و [3] در [nuxt-20] ادغام میشوند:
- سه صفحه [Authentification, CalculImpot, ListeSimulations] از برنامه [vuejs-22] به پوشه [pages] منتقل میشوند؛
- کامپوننتهای [FormCalculImpot, Menu, Layout] از برنامه [vuejs-22] به پوشه [components] منتقل میشوند؛
- صفحه [Main] از [vuejs-22]، که بهعنوان [layout] برای برنامه [vuejs-22] عمل میکرد، به پوشه [layouts] منتقل میشود؛
عناصر جاسازیشده [4] تغییر نام یافتهاند:

- در [layouts]، [Main] به [default] تبدیل شده است، زیرا این نام پیشفرض برای طرحبندی یک برنامه [nuxt] است؛
- در [pages]، صفحه [Authentification] به [index] تبدیل شده است، زیرا [Authentification] این نقش را در برنامه [vuejs-22] ایفا کرده است؛
در این مرحله میتوانید پروژه را کامپایل کنید تا اولین خطاها را مشاهده کنید. فایل [nuxt.config] را از مثال [nuxt-12] اصلاح کنید تا اکنون [nuxt-20] را اجرا کند:
export default {
mode: 'universal',
/*
** Headers of the page
*/
head: {
title: 'Introduction à [nuxt.js]',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{
hid: 'description',
name: 'description',
content: 'ssr routing loading asyncdata middleware plugins store'
}
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
},
/*
** Customize the progress-bar color
*/
loading: false,
/*
** Global CSS
*/
css: [],
/*
** Plugins to load before mounting the App
*/
plugins: [
{ src: '@/plugins/client/plgSession', mode: 'client' },
{ src: '@/plugins/server/plgSession', mode: 'server' },
{ src: '@/plugins/client/plgDao', mode: 'client' },
{ src: '@/plugins/server/plgDao', mode: 'server' },
{ src: '@/plugins/client/plgEventBus', mode: 'client' }
],
/*
** Nuxt.js dev-modules
*/
buildModules: [
// سند: https://github.com/nuxt-community/eslint-module
'@nuxtjs/eslint-module'
],
/*
** Nuxt.js modules
*/
modules: [
// Doc: https://bootstrap-vue.js.org
'bootstrap-vue/nuxt',
// Doc: https://axios.nuxtjs.org/usage
'@nuxtjs/axios',
// https://www.npmjs.com/package/cookie-universal-nuxt
'cookie-universal-nuxt'
],
/*
** Axios module configuration
** See https://axios.nuxtjs.org/options
*/
axios: {},
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
extend(config, ctx) {}
},
// دایرکتوری کد منبع
srcDir: 'nuxt-20',
// روتر
router: {
//ریشهٔ برنامه
base: '/nuxt-20/',
// میانافزار مسیریابی
middleware: ['routing']
},
// سرور
server: {
// پورت سرویس، بهطور پیشفرض 3000
port: 81,
// آدرسهای شبکهای که گوش داده میشوند، پیشفرض localhost: 127.0.0.1
// 0.0.0.0 = تمام آدرسهای شبکه روی دستگاه
host: 'localhost'
},
// محیط
env: {
//پیکربندی axios
timeout: 2000,
withCredentials: true,
baseURL: 'http://'localhost/php7/scripts-web/impots/version-14',
// تنظیمات کوکی جلسه [nuxt]
maxAge: 60 * 5
}
}
سپس یک [build] را از پروژه تولید میکنیم:

خطاهای زیر گزارش شدهاند:
- خطای خط ۱ نشان میدهد که به یک تصویر ناموجود ارجاع داده شده است. ما آن را در [vuejs-22] بازیابی خواهیم کرد؛
- خطای خط ۲ نشان میدهد که کامپوننت [./FormCalculImpot] وجود ندارد. در واقع، این کامپوننت اکنون در [@/components/form-calcul-impot] قرار دارد؛
- خطاها در خطوط [3-5] نشان میدهند که کامپوننت [./Layout] وجود ندارد. در واقع، این کامپوننت اکنون در [@/components/layout] قرار دارد؛
- خطاها در خطوط [6-7] نشان میدهند که کامپوننت [./Menu] وجود ندارد. در واقع، اکنون با نام [@/components/menu] نامیده میشود؛
ما تصویر [assets/logo.jpg] را به پروژه [nuxt-20] اضافه میکنیم:

علاوه بر این، ما مسیرهای کامپوننتها را در تمام صفحات اصلاح خواهیم کرد. بیایید صفحه [calcul-impot] را بهعنوان مثال در نظر بگیریم:
<!-- 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>
<script>
//واردات
import FormCalculImpot from './FormCalculImpot'
import Menu from './Menu'
import Layout from './Layout'
export default {
// کامپوننتهای استفاده شده
components: {
Layout,
FormCalculImpot,
Menu
},
سه نمونه از '[import]' در خطوط 26–28 به صورت زیر تغییر میکنند:
//وارداتی
import FormCalculImpot from '@/components/form-calcul-impot'
import Menu from '@/components/menu'
import Layout from '@/components/layout'
ما به همین ترتیب مقادیر [import] را برای همه اجزا، طرحبندیها و صفحات بررسی و در صورت لزوم اصلاح میکنیم. پس از انجام این اصلاحات، میتوانیم یک اجرای جدید [build] را امتحان کنیم. معمولاً نباید خطای دیگری وجود داشته باشد.
سپس میتوانید دستور زیر را اجرا کنید:

خطاها ظاهر میشوند:
میتوانیم ببینیم که دستور [dev]، هنگامی که در کنار ماژول [eslint] استفاده میشود، از نظر نحوی سختگیرانهتر از دستور [build] است. در اینجا، نیاز است که عملگر مقایسهای [!=] به صورت [!==] نوشته شود، که یک عملگر سختگیرتر است (این عملگر همچنین نوع آرگومانها را بررسی میکند). این خطاها در صفحه [index.vue] رخ میدهند.
ما خطاهای بالا را اصلاح کرده و پروژه را مجدداً راهاندازی میکنیم. سپس هشداری از ماژول [eslint] دریافت میکنیم:

ما این خطا را با استفاده از ماژول [Quick fix] از ماژولهای [eslint] و [2] اصلاح میکنیم.
ما پروژه را دوباره اجرا میکنیم. دیگر هیچ خطای کامپایل وجود ندارد. سپس از طریق یک مرورگر وب به URL و [http://localhost:81/nuxt-20/] دسترسی پیدا میکنیم. یک خطای زمان اجرا دریافت میکنیم:

این خطا در [index.vue] [2] یافت میشود. خطای [1] به این دلیل رخ میدهد که، در [vuejs-22]، لایه [dao] در [this.$dao] در دسترس بود، در حالی که در [nuxt-12] – که ما زیرساخت آن را پذیرفتهایم – در تابع [this.$dao()] در دسترس است.
خطا در تابع [created] از چرخه عمر صفحه [index] رخ میدهد:

فعلاً، ما به سادگی [created] را به [created2] تغییر نام میدهیم تا تابع چرخه عمر [created] اجرا نشود ([3]).
ما تغییر را ذخیره کرده و صفحه [index] را در مرورگر دوباره بارگذاری میکنیم. این بار کار میکند:

17.3. مرحله ۲
صفحات پروژه [vuejs-22] از عناصر تزریقی زیر استفاده کردند:
- $dao: برای لایه [dao] از کلاینت [vue.js]؛
- $session: برای یک جلسه ذخیره شده در [localStorage] مرورگر؛
این عناصر دیگر در زیرساخت پروژه [nuxt-12] که ما آن را تکثیر کردهایم، وجود ندارند:
- اکنون دو لایه [dao] وجود دارد، یکی برای کلاینت [nuxt] و دیگری برای سرور [nuxt]. هر دو از طریق یک تابع تزریقی به نام [$dao] در دسترس هستند. این بدان معناست که در صفحات برنامه، [this.$dao] باید با [this.$dao()] جایگزین شود؛
- سشن [nuxt] که توسط برنامه [nuxt-20] مدیریت میشود، دیگر به شیء [$session] دربرنامه [vuejs-22]، جایی که مفهومی به نام کوکی جلسه وجود نداشت. با این حال، آنها هدف مشابهی را دنبال میکنند: ذخیره اطلاعات پایدار در حین تعامل کاربر با سیستم. جلسه [nuxt] اطلاعات را به جای ذخیره مستقیم در جلسه، در مخزن ذخیره میکند. در صفحات برنامه، هنگام ذخیره اطلاعات در جلسه، باید [this.$session] با [this.$store] و هنگام دستکاری خود جلسه، با [this.$session()] جایگزین شود؛
- برای بررسی وضعیت ویژگی P فروشگاه، باید [this.$store.state.P] را بنویسید؛
- برای تغییر ویژگی P فروشگاه، باید [this.$store.commit(‘replace’, {P:value}] را بنویسید؛
ما این تغییرات را در صفحه [index] انجام میدهیم:
<!-- تعریف نما HTML -->
<template>
<Layout :left="false" :right="true">
<template slot="right">
<!-- فرم HTML – مقادیر با استفاده از اقدام 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" label-cols="3">
<!-- میدان ورودی کاربر -->
<b-col cols="6">
<b-form-input id="user" v-model="user" type="text" placeholder="Nom d'utilisateur" />
</b-col>
</b-form-group>
<!-- خط دوم -->
<b-form-group label="Mot de passe" label-for="password" label-cols="3">
<!-- میدان وارد کردن رمز عبور -->
<b-col cols="6">
<b-input id="password" v-model="password" type="password" placeholder="Mot de passe" />
</b-col>
</b-form-group>
<!-- خط سوم -->
<b-alert v-if="showError" show variant="danger" class="mt-3">L'erreur suivante s'est produite : {{ message }}</b-alert>
<!--دکمه از نوع [submit] در خط سوم -->
<b-row>
<b-col cols="2">
<b-button :disabled="!valid" variant="primary" type="submit">Valider</b-button>
</b-col>
</b-row>
</b-form>
</template>
</Layout>
</template>
<!-- پویایی نما -->
<script>
/*eslint-disable no-console */
import Layout from '@/components/layout'
export default {
// کامپوننتهای استفاده شده
components: {
Layout
},
//وضعیت کامپوننت
data() {
return {
// کاربر
user: '',
// رمز عبور آنها
password: '',
// کنترل میکند که آیا پیام خطا نمایش داده شود یا خیر
showError: false,
// پیام خطا
message: ''
}
},
// ویژگیهای محاسباتی
computed: {
// ورودیهای معتبر
valid() {
return this.user && this.password && this.$store.state.started
}
},
// چرخهٔ عمر: کامپوننت بهتازگی ایجاد شده است
mounted() {
// eslint-disable-next-line
console.log("Authentification mounted");
//آیا کاربر میتواند شبیهسازیها را اجرا کند؟
if (this.$store.state.started && this.$store.state.authenticated && this.$métier.taxAdminData) {
// اگر چنین باشد، کاربر میتواند شبیهسازیها را اجرا کند
this.$router.push({ name: 'calculImpot' })
//بازگشت به حلقه رویداد
return
}
//اگر جلسه jSON قبلاً شروع شده باشد، مجدداً راهاندازی نمیشود
if (!this.$store.state.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.$store.commit('replace', { started: true })
console.log('[authentification], session=', this.$session())
})
//در صورت بروز خطا
.catch((error) => {
//خطا به مشاهدهگر ارجاع داده میشود [Main]
this.$emit('error', error)
})
// در همه موارد
.finally(() => {
// جلسه ذخیره میشود
this.$session().save()
})
}
},
// پردازشگرهای رویداد
methods: {
// ----------- احراز هویت
async login() {
try {
// شروع انتظار
this.$emit('loading', true)
// هنوز احراز هویت نشده است
this.$store.commit('replace', { 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.$store.commit('replace', { 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.$store.commit('replace', { métier: this.$métier })
//ذخیرهٔ جلسه
this.$session().save()
}
}
}
}
</script>
لطفاً به نکات زیر توجه کنید:
- خط ۶۹: تابع [created2] به [mounted] تغییر نام داده شده است، تا سرور [nuxt] آن را اجرا نکند (نه [beforeMount] و نه [mounted] را اجرا نمیکند). فقط کلاینت [nuxt] آن را اجرا خواهد کرد، همانطور که در مثال [vuejs-22] نیز چنین بود؛
- خط ۷۳: به [this.$métier] ارجاع داده شده است که در حال حاضر وجود ندارد؛
- خط ۷۵: ما هرگز از این روش در یک برنامه [nuxt] استفاده نکردهایم. باید بررسی کنیم که آیا در یک زمینه [nuxt] کار میکند یا خیر؛
- خطوط ۱۱۲ و ۱۷۲: در [vuejs-22]، جلسه پروژه به این صورت ذخیره شد. با پروژه [nuxt-20]، باید زمینهٔ جاری به روش [save] ارسال شود. ما میدانیم که در یک صفحه [nuxt]، شیء [context] در [this.$nuxt.context] در دسترس است؛
بنابراین خطوط ۱۱۲ و ۱۷۲ به شرح زیر بازنویسی میشوند:
this.$session().save(this.$nuxt.context)
شایان ذکر است که این کد بهینهسازی نشده است. به جای استفاده چندباره از تابع [this.$session()]، ترجیح دارد که به صورت زیر نوشته شود:
const session=this.$session()
و سپس از متغیر [session] استفاده کنید. همین استدلال برای تابع [this.$dao()] نیز صدق میکند.
با اعمال این اصلاحات، میتوانیم URL و [http://localhost:81/nuxt-20/] را در یک مرورگر دوباره بارگذاری کنیم. ما همچنان همان صفحه قبلی را دریافت میکنیم:

بیایید نگاهی به لاگهای مرورگر بیندازیم:

ورودی لاگ [1] جدیدترین ورودی لاگ تولید شده توسط کلاینت [nuxt] است. در [2]، میتوانیم ببینیم که ویژگی [started] روی [vrai] تنظیم شده است، که به این معنی است که تابع [mounted] با موفقیت یک جلسه jSON را با سرور محاسبه مالیات آغاز کرده است. همچنین میتوانیم ببینیم که این استور دارای ویژگیهایی است که باید یا حذف شوند یا نامگذاری مجدد شوند. به یاد داشته باشید که ما از استور مثال، [nuxt-12]، استفاده میکنیم.
اکنون بیایید دوباره URL و [http://localhost:81/nuxt-20/] را زمانی که سرور محاسبه مالیات در حال اجرا نیست درخواست کنیم. ابتدا، باید اطمینان حاصل کنیم که کوکی مربوط به جلسه [nuxt] را حذف میکنیم:

اسکرینشات بالا مربوط به کروم است. پس از انجام این کار، کوکیهای URL و [http://localhost:81/nuxt-20/] نتیجه زیر را تولید میکنند:

این خطا توسط پروژه [vuejs-22] به درستی مدیریت شد. این خطا همچنان توسط پروژه [nuxt-20] به درستی مدیریت میشود.
17.4. مرحله ۳
اکنون که صفحه احراز هویت را داریم، باید به کدی که هنگام کلیک کاربر روی دکمه [Valider] اجرا میشود، نگاه کنیم:
// پردازشگرهای رویداد
methods: {
// ----------- احراز هویت
async login() {
try {
// شروع انتظار
this.$emit('loading', true)
// هنوز احراز هویت نشده است
this.$store.commit('replace', { 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.$store.commit('replace', { 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.$store.commit('replace', { métier: this.$métier })
// ذخیرهٔ جلسه
this.$session().save(this.$nuxt.context)
}
}
}
مشکل اصلی در اینجا به نظر میرسد عدم وجود داده [this.$métier] باشد. برای حل این مشکل، ما:
- کلاس [Métier] را از مثال [vuejs-22] وارد کنیم. آن را در پوشه [api] قرار خواهیم داد؛
- یک تابع [$métier] را در زمینهٔ کلاینت [nuxt] تزریق میکنیم که دسترسی به این کلاس را فراهم خواهد کرد؛
ابتدا کلاس [Métier] را به پوشه [api] کپی کنید:

پس از اینکه کلاس [Métier] در پروژه موجود باشد، یک افزونه جدید برای مشتری [nuxt] ایجاد کنید. این افزونه با نام [pluginMétier]، تابع [$métier] را تزریق میکند که دسترسی به کلاس [Métier] را فراهم میآورد:
/* eslint-disable no-console */
// ایجاد یک نقطه دسترسی به لایه [métier]
import Métier from '@/api/client/Métier'
export default (context, inject) => {
// نمونه سازی لایه [métier]
const métier = new Métier()
//تزریق یک تابع [$métier] به زمینه
inject('métier', () => métier)
// لاگ
console.log('[fonction client $métier créée]')
}
پس از انجام این کار، میتوانیم صفحه [index] را اصلاح کنیم:
//چرخهٔ عمر: کامپوننت بهتازگی ایجاد شده است
mounted() {
// eslint-disable-next-line
console.log("Authentification mounted");
//آیا کاربر میتواند شبیهسازیها را اجرا کند؟
if (this.$store.state.started && this.$store.state.authenticated && this.$métier().taxAdminData) {
// اگر چنین باشد، کاربر میتواند شبیهسازیها را اجرا کند
this.$router.push({ name: 'calcul-impot' })
//بازگشت به حلقه رویداد
return
}
// اگر جلسه jSON قبلاً شروع شده باشد، مجدداً راهاندازی نمیشود
...
},
// پردازشگرهای رویداد
methods: {
// ----------- احراز هویت
async login() {
try {
// شروع انتظار
this.$emit('loading', true)
// هنوز احراز هویت نشده است
this.$store.commit('replace', { 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.$store.commit('replace', { 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: 'calcul-impot' })
} catch (error) {
//خطا به مؤلفهٔ اصلی ارجاع داده میشود
this.$emit('error', error)
} finally {
//بهروزرسانی فروشگاه
this.$store.commit('replace', { métier: this.$métier() })
//جلسه ذخیره میشود
this.$session().save(this.$nuxt.context)
}
}
}
- خطوط ۴۳، ۶۱، ۶۹: [this.$métier] با [this.$métier()] جایگزین شده است؛
- ردههای ۸ و ۶۳: نام صفحه [CalculImpot] از پروژه [vuejs-22] به صفحه [calcul-impot] در پروژه [nuxt-20] تغییر یافته است؛
با اعمال این اصلاحات، اکنون میتوانیم صفحه احراز هویت را اعتبارسنجی کنیم:

صفحه حاصل به شرح زیر است:

ما در واقع صفحه محاسبه مالیات را دریافت کردهایم. اکنون بیایید به لاگها نگاه کنیم:

در [2]، میتوانیم ببینیم که تأیید هویت ما به درستی ثبت شده است. در [3-4] میبینیم که دادهی [taxAdminData] بازیابی شده است، که محاسبهی مالیات را از طریق کلاس [Métier] امکانپذیر میسازد.
17.5. مرحله ۴
بیایید صفحه [calcul-impot] را که به دست آوردهایم بررسی کنیم:
<!-- 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>
<script>
// وارداتها
import FormCalculImpot from '@/components/form-calcul-impot'
import Menu from '@/components/menu'
import Layout from '@/components/layout'
export default {
// کامپوننتهای استفاده شده
components: {
Layout,
FormCalculImpot,
Menu
},
//گزارش داخلی
data() {
return {
// گزینههای منو
options: [
{
text: 'Liste des simulations',
path: '/liste-des-simulations'
},
{
text: 'Fin de session',
path: '/fin-session'
}
],
// نتیجه محاسبه مالیات
résultat: '',
résultatObtenu: false
}
},
//چرخه عمر
created() {
// eslint-disable-next-line
console.log("CalculImpot created");
},
// روشهای رسیدگی به رویدادها
methods: {
// نتیجه محاسبه مالیات
handleResultatObtenu(résultat) {
//ساخت نتیجه به صورت رشته HTML
const impôt = "Montant de l'impôt : " + résultat.impôt + ' euro(s)'
const décôte = 'Décôte : ' + résultat.décôte + ' euro(s)'
const réduction = 'Réduction : ' + résultat.réduction + ' euro(s)'
const surcôte = 'Surcôte : ' + résultat.surcôte + ' euro(s)'
const taux = "Taux d'imposition : " + résultat.taux
this.résultat = impôt + '<br/>' + décôte + '<br/>' + réduction + '<br/>' + surcôte + '<br/>' + taux
// نمایش نتیجه
this.résultatObtenu = true
// ---- فروشگاه را بهروزرسانی کنید [Vuex]
// یک شبیهسازی از +
this.$store.commit('addSimulation', résultat)
// ذخیرهٔ جلسه
this.$session.save()
}
}
}
</script>
- خطوط ۴۴ و ۴۸: لینکهای منوی ناوبری صحیح هستند. صفحه [/fin-session] وجود ندارد. پروژه [vuejs-22] این مشکل را با استفاده از مسیریابی (routing) حل کرده است. ما نیز همین کار را با پروژه [nuxt-20] انجام خواهیم داد؛
- خط ۷۶: به تغییری به نام [addSimulation] ارجاع داده شده است که در حال حاضر وجود ندارد. ما آن را ایجاد خواهیم کرد؛
- خط ۷۸: همانند صفحه [index]، باید [this.$session().save(this.$nuxt.context)] را بنویسیم؛
بیایید فروشگاه [store/index] را اصلاح کنیم. این فروشگاه از پروژه [nuxt-12] به ارث رسیده و در حال حاضر به شرح زیر است:
/* eslint-disable no-console */
//وضعیت فروشگاه
export const state = () => ({
// جلسه jSON آغاز شد
jsonSessionStarted: false,
// کاربر احراز هویت شد
userAuthenticated: false,
// کوکی جلسه PHP
phpSessionCookie: '',
// adminData
adminData: ''
})
// بهروزرسانیهای فروشگاه
export const mutations = {
// تعویض وضعیت
replace(state, newState) {
for (const attr in newState) {
state[attr] = newState[attr]
}
},
// ریست فروشگاه
reset() {
this.commit('replace', { jsonSessionStarted: false, userAuthenticated: false, phpSessionCookie: '', adminData: '' })
}
}
// اقدامات فروشگاه
export const actions = {
nuxtServerInit(store, context) {
// چه کسی این کد را اجرا میکند؟
console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
// راهاندازی جلسه
initStore(store, context)
}
}
function initStore(store, context) {
// store همان فروشگاهی است که باید راهاندازی شود
//بازیابی جلسه
const session = context.app.$session()
//آیا جلسه قبلاً راهاندازی شده است؟
if (!session.value.initStoreDone) {
// شروع یک فروشگاه جدید
console.log("nuxtServerInit, initialisation d'un nouveau store")
//فروشگاه به جلسه اضافه میشود
session.value.store = store.state
//فروشگاه اکنون راهاندازی شده است
session.value.initStoreDone = true
} else {
console.log("nuxtServerInit, reprise d'un store existant")
// بهروزرسانی فروشگاه با فروشگاه جلسه
store.commit('replace', session.value.store)
}
//جلسه ذخیره شد
session.save(context)
// لاگ
console.log('initStore terminé, store=', store.state)
}
- خطوط ۳–۲۷: ما وضعیت و تغییرات را از برنامه [vuejs-22] اتخاذ خواهیم کرد (به سند [3] مراجعه کنید):
//وضعیت کور
export const state = () => ({
//jSON جلسه آغاز شد
started: false,
// کاربر احراز هویت شد
authenticated: false,
// کوکی جلسه PHP
phpSessionCookie: '',
// فهرست شبیهسازیها
simulations: [],
// شماره آخرین شبیهسازی
idSimulation: 0,
// لایه [métier]
métier: null
})
// حرکات کور
export const mutations = {
// تعویض وضعیت
replace(state, newState) {
for (const attr in newState) {
state[attr] = newState[attr]
}
},
// بازنشانی فروشگاه
reset() {
this.commit('replace', { started: false, authenticated: false, phpSessionCookie: '', idSimulation: 0, simulations: [], métier: null })
},
// حذف خط در موقعیت اندیس
deleteSimulation(state, index) {
// eslint-disable-next-line no-console
console.log('mutation deleteSimulation')
// خط شماره [index] حذف شد
state.simulations.splice(index, 1)
console.log('store simulations', state.simulations)
},
// افزودن یک شبیهسازی
addSimulation(state, simulation) {
// eslint-disable-next-line no-console
console.log('mutation addSimulation')
// شماره شبیهسازی
state.idSimulation++
simulation.id = state.idSimulation
//شبیهسازی به جدول شبیهسازی اضافه میشود
state.simulations.push(simulation)
}
}
- خطوط ۴ و ۶: ما ویژگیهایی را که از قبل در حال استفاده هستند معرفی میکنیم؛
- خط ۸: ما کوکی جلسه PHP را حفظ میکنیم. این امر برای اطمینان از اینکه کلاینت و سرور [nuxt] همان جلسه PHP را با سرور محاسبه مالیات به اشتراک میگذارند، ضروری است؛
- خط ۱۰: فهرست شبیهسازیهای انجامشده توسط کاربر؛
- خط ۱۲: شماره آخرین شبیهسازی انجامشده توسط کاربر؛
- خط ۱۴: لایه [métier]؛
- خطوط ۳۰–۴۷: جهشهای موجود در پروژه [vuejs-22] که توسط صفحات برنامه ارجاع شدهاند. پروژه [vuejs-22] دارای جهشی به نام [clear] بود که لیست شبیهسازیها را پاک میکرد. ما آن را شامل نمیکنیم زیرا جهش [reset] که از قبل موجود است، باید کافی باشد؛
- خطوط ۲۶–۲۸: جهش [reset] برای در نظر گرفتن محتوای وضعیت جدید اصلاح شده است؛
صفحه [calcul-impot] از مؤلفه زیر [form-calcul-impot] استفاده میکند:
<!-- تعریف نما 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 id="enfants" v-model="enfants" :state="enfantsValide" type="text" placeholder="Indiquez votre nombre d'enfants"></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 id="salaire" v-model="salaire" :state="salaireValide" type="text" placeholder="Salaire annuel"></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 :disabled="formInvalide" type="submit" variant="primary">Valider</b-button>
</b-col>
</b-form>
</template>
<!-- اسکریپت -->
<script>
export default {
//وضعیت داخلی
data() {
return {
// متأهل یا مجرد
marié: 'non',
// تعداد فرزندان
enfants: '',
// حقوق سالانه
salaire: ''
}
},
//وضعیت داخلی محاسبه شده
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*$/))
}
},
// چرخه عمر
created() {
// لاگ
// eslint-disable-next-line
console.log("FormCalculImpot created");
},
// پردازشگر رویداد
methods: {
calculerImpot() {
// مالیات با استفاده از لایه [métier] محاسبه میشود
const résultat = this.$métier.calculerImpot(this.marié, Number(this.enfants), Number(this.salaire))
//eslint-disable-next-line
console.log("résultat=", résultat);
// نتیجه نهایی میشود
résultat.marié = this.marié
résultat.enfants = this.enfants
résultat.salaire = this.salaire
// رویداد فعال میشود [resultatObtenu]
this.$emit('resultatObtenu', résultat)
}
}
}
</script>
- خطوط ۶۵ و ۸۹: مرجع [this.$métier] باید به [this.$métier()] تغییر یابد؛
پس از اعمال این اصلاحات، میتوان یک شبیهسازی را انجام داد:

پاسخ زیر بازگردانده میشود:

اگر به لاگها نگاه کنیم:

- در [9-10]، میتوانیم ببینیم که شبیهسازی اول واقعاً در داخل [store] قرار دارد؛
- در [5]، شماره آخرین شبیهسازی واقعاً افزایش یافته است؛
17.6. مرحله ۵
حالا که یک شبیهسازی را اجرا کردهایم، بیایید روی لینک [Liste des simulations] کلیک کنیم. به صفحه زیر هدایت میشویم:

کلاینت [nuxt] بهدرستی مسیریابی شده است. بیایید کد صفحه [liste-des-simulations] را بررسی کنیم:
<!-- 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 :items="simulations" :fields="fields" striped hover responsive>
<template v-slot:cell(action)="data">
<b-button @click="supprimerSimulation(data.index)" variant="link">Supprimer</b-button>
</template>
</b-table>
</template>
</template>
<!-- منوی ناوبری در ستون سمت چپ -->
<Menu slot="left" :options="options" />
</Layout>
</div>
</template>
<script>
// وارداتها
import Layout from '@/components/layout'
import Menu from '@/components/menu'
export default {
// کامپوننتها
components: {
Layout,
Menu
},
//وضعیت داخلی
data() {
return {
// گزینههای منوی ناوبری
options: [
{
text: "Calcul de l'impôt",
path: '/calcul-impot'
},
{
text: 'Fin de session',
path: '/fin-session'
}
],
//پارامترهای جدول HTML
fields: [
{ label: '#', key: 'id' },
{ label: 'Marié', key: 'marié' },
{ label: "Nombre d'enfants", key: 'enfants' },
{ label: 'Salaire', key: 'salaire' },
{ label: 'Impôt', key: 'impôt' },
{ label: 'Décôte', key: 'décôte' },
{ label: 'Réduction', key: 'réduction' },
{ label: 'Surcôte', key: 'surcôte' },
{ label: '', key: 'action' }
]
}
},
//وضعیت داخلی محاسبه شده
computed: {
// فهرست شبیهسازیهای برداشتهشده از فروشگاه Vuex
simulations() {
return this.$store.state.simulations
}
},
//چرخه زندگی
created() {
// eslint-disable-next-line
console.log("ListeSimulations created");
},
// روشها
methods: {
supprimerSimulation(index) {
// eslint-disable-next-line
console.log("supprimerSimulation", index);
// حذف شبیهسازی شماره [index]
this.$store.commit('deleteSimulation', index)
// ذخیرهٔ جلسه
this.$session.save()
}
}
}
</script>
- خطوط ۴۷–۵۶: اهداف منوی ناوبری صحیح هستند؛
- خط ۷۵: منوی کشویی به درستی ارجاع داده شده است؛
- خط ۸۹: از تبدیل [deleteSimulation] استفاده میشود که در مرحله قبلی آن را یکپارچه کردیم؛
- خط ۹۱: این خط باید به صورت [this.$session().save(this.$nuxt.context)] بازنویسی شود؛
ما تغییرات لازم را اعمال میکنیم، سپس سعی میکنیم شبیهسازی نمایشدادهشده را حذف کنیم:

سپس صفحه زیر را دریافت میکنیم:

بیایید نگاهی به لاگها بیندازیم:

- در [6]، میتوانیم ببینیم که جدول شبیهسازی خالی است؛
حالا بیایید به فرم محاسبه مالیات بازگردیم:

صفحه زیر را مشاهده میکنیم:

پس مسیریابی کار کرده است.
17.7. مرحله ۶
اکنون باید گزینه ناوبری [Fin de session] را در منوی ناوبری پیکربندی کنیم:
// گزینههای منو
options: [
{
text: 'Liste des simulations',
path: '/liste-des-simulations'
},
{
text: 'Fin de session',
path: '/fin-session'
}
]
- خط ۹: صفحه [/fin-session] وجود ندارد. پروژه [vuejs-22] این مورد را با استفاده از قوانین مسیریابی در فایلی به نام [router.js] مدیریت کرد:
// وارداتها
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: 'authentification2', 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
- خطوط ۶۴–۷۶ مورد خاص مسیر به پات [/fin-session] را مدیریت کردند؛
- خط ۶۶: جلسهٔ جاری پاک میشود؛
- خطوط ۶۸–۷۰: نمای [authentification] نمایش داده میشود؛
ما سعی خواهیم کرد کاری مشابه را در فایل مسیریابی مشتری [nuxt] انجام دهیم:

اسکریپت [client/routing.js] به صورت زیر درمیآید:
/* eslint-disable no-console */
export default function(context) {
// چه کسی این کد را اجرا میکند؟
console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
// پردازش کوکی جلسه PHP در مرورگر
//کوکی جلسه مرورگر PHP باید دقیقاً مشابه کوکی یافتشده در جلسه Nuxt باشد
// عمل [fin-session] یک کوکی جدید PHP دریافت میکند (هم سرور و هم کلاینت Nuxt)
// اگر سرور آن را دریافت کند، کلاینت باید آن را به مرورگر منتقل کند
//برای ارتباطات خود با سرور PHP
//این یک سناریوی مسیریابی سمت کلاینت است
//کوکی جلسه بازیابی میشود: PHP
const phpSessionCookie = context.store.state.phpSessionCookie
if (phpSessionCookie) {
// اگر وجود داشته باشد، کوکی جلسه PHP به مرورگر اختصاص داده میشود
document.cookie = phpSessionCookie
}
// کجا میرویم؟
const to = context.route.path
if (to === '/fin-session') {
//پاک کردن جلسه
const session = context.app.$session()
session.reset(context)
// ارسال مجدد به صفحهٔ شاخص
context.redirect({ name: 'index' })
}
}
- ما خطوط [19-27] را به کد موجود اضافه کردهایم؛
- خط ۲۰: [path] را از هدف مسیر فعلی بازیابی کنید؛
- خط ۲۱: بررسی میکنیم که آیا آن [/fin-session] است. اگر چنین باشد:
- خطوط ۲۳–۲۴: جلسه ریست میشود؛
- خط ۲۶: کلاینت [nuxt] به صفحه اصلی هدایت میشود؛
روش [session.reset(context)] (خط 24) برای جلسه به شرح زیر است:
// ریست جلسه
reset(context) {
console.log('nuxt-session reset')
// بازنشانی فروشگاه
context.store.commit('reset')
//ذخیره فروشگاه جدید در جلسه و ذخیره جلسه
this.save(context)
}
روش [context.store.commit('reset')] (خط ۵) به شرح زیر است:
// بازنشانی فروشگاه
reset() {
this.commit('replace', { started: false, authenticated: false, phpSessionCookie: '', idSimulation: 0, simulations: [], métier: null })
}
وقتی لینک [Fin de session] اکنون استفاده میشود، صفحه اصلی با لاگهای زیر نمایش داده میشود:

- در [3]، میبینیم که دیگر احراز هویت نشدهایم؛
- در [4]، میتوانیم ببینیم که جلسه jSON آغاز شده است؛
- در [6]، لایه [métier] دیگر در فروشگاه وجود ندارد (این لایه همچنان در صفحاتی که [this.$métier()] دارند، موجود است)؛
- در [5, 7]، دیگر هیچ شبیهسازیای وجود ندارد؛
مهم است که بدانیم در پایان یک جلسه چه اتفاقی میافتد:
- جلسه [nuxt] ریست میشود: ویژگی [started] فروشگاه به [false] تغییر میکند؛
- کاربر به صفحه [index] هدایت میشود؛
- متد [mounted] در صفحه [index] اجرا میشود. این یک جلسه جدید، jSON، با سرور محاسبه مالیات را آغاز میکند. اگر عملیات موفقیتآمیز باشد، ویژگی [started] فروشگاه به [true] تغییر میکند؛
17.8. مرحله ۷
در این مرحله، برنامه [nuxt-20] تمام قابلیتهای برنامه [vuejs-22] را دارد. به نظر میرسد پورتکردن کامل شده است.
ما قصد داریم در راستای [nuxt]، یک قدم فراتر برویم. متد [mounted] در صفحه [index] یک مشکل ایجاد میکند. این متد یک عملیات غیرهمزمان را آغاز میکند که موتور جستجو برای تکمیل آن صبر نخواهد کرد. ما میدانیم که در این مورد، عملیات غیرهمزمان باید در داخل تابع [asyncData] قرار گیرد، زیرا سرور [nuxt] که آن را اجرا میکند، سپس قبل از تحویل صفحه به موتور جستجو، منتظر اتمام آن خواهد ماند.
در اینجا، ما از تابع [asyncData] استفاده میکنیم که در برنامه کاربردی [nuxt-12] برای صفحه [index] نوشته شده است:
export default {
name: 'InitSession',
// کامپوننتهای استفاده شده
components: {
Layout,
Navigation
},
// دادههای ناهمزمان
async asyncData(context) {
// لاگ
console.log('[index asyncData started]')
// ما کارها را دو بار انجام نمیدهیم اگر صفحه قبلاً درخواست شده باشد
if (process.server && context.store.state.jsonSessionStarted) {
console.log('[index asyncData canceled]')
return { result: '[succès]' }
}
try {
//یک جلسه آغاز میشود jSON
const dao = context.app.$dao()
const response = await dao.initSession()
// لاگ
console.log('[index asyncData response=]', response)
// بازیابی کوکی جلسه PHP برای درخواستهای آینده
const phpSessionCookie = dao.getPhpSessionCookie()
//کوکی جلسه PHP در جلسه [nuxt] ذخیره میشود
context.store.commit('replace', { phpSessionCookie })
//آیا خطایی رخ داده است؟
if (response.état !== 700) {
//خطا در response.réponse است
throw new Error(response.réponse)
}
//توجه داشته باشید که جلسه jSON آغاز شده است
context.store.commit('replace', { jsonSessionStarted: true })
// نتیجه بازگردانده میشود
return { result: '[succès]' }
} catch (e) {
// لاگ
console.log('[index asyncData error=]', e)
// مشخص شد که جلسه jSON هنوز شروع نشده است
context.store.commit('replace', { jsonSessionStarted: false })
//خطا گزارش میشود
return { result: '[échec]', showErrorLoading: true, errorLoadingMessage: e.message }
} finally {
//فروشگاه ذخیره شد
const session = context.app.$session()
session.save(context)
// لاگ
console.log('[index asyncData finished]')
}
},
//چرخه عمر
beforeCreate() {
console.log('[index beforeCreate]')
},
created() {
console.log('[index created]')
},
beforeMount() {
console.log('[index beforeMount]')
},
mounted() {
console.log('[index mounted]')
//فقط برای کلاینت
if (this.showErrorLoading) {
console.log('[index mounted, showErrorLoading=true]')
this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
}
}
- خطوط ۱۳، ۳۳، ۴۰: ویژگی [jsonSessionStarted] باید به [started] تغییر یابد؛
- خط ۱۳: در برنامه [nuxt-12]، تنها سرور [nuxt] صفحه [index] و تابع آن [asyncData] را اجرا میکرد. کلاینت [nuxt] تنها پس از دریافت صفحه [index] از سرور [nuxt] آن را اجرا کرد و در آن لحظه تابع [asyncData] را اجرا نکرد. در [nuxt-20]، وضعیت متفاوت است: لینک [Fin de session] صفحه [index] را در محیط کلاینت [nuxt] نمایش خواهد داد. تابع [asyncData] سپس اجرا خواهد شد. با این حال، هنگامی که صفحه [index] به این روش دسترسی پیدا میکند، در این فاصله، جلسه [nuxt] ریست شده و ویژگی store [started] روی [false] تنظیم خواهد شد، به این معنی که شرط در خط ۱۳ ناگزیر نادرست خواهد بود. بنابراین میتوانیم [process.server] را همانطور که هست رها کنیم، و در نتیجه کلاینت [nuxt] این بررسی را انجام نخواهد داد؛
- خطوط ۱۵، ۳۵، ۴۲: یک ویژگی [result] به ویژگیهای [data] صفحه [index] اضافه میشود. در [nuxt-20]، از این ویژگی استفاده نخواهد شد، بنابراین آن را از نتیجه بازگرداندهشده توسط تابع حذف خواهیم کرد؛
- خطوط ۶۱–۶۷: این متد [mounted] باید حفظ شود زیرا به کلاینت [nuxt] امکان نمایش پیام خطا را میدهد. با این حال، نحوهٔ رسیدگی به خطا تغییر خواهد کرد؛
در صفحهٔ فعلی [index]، ما تابع [asyncData] را جایگزین تابع قدیمی [mounted] میکنیم و یک تابع جدید [mounted] اضافه میکنیم. کد صفحه [index] در مثال [nuxt-20] سپس به صورت زیر در میآید:
...
<!-- پویایی نما -->
<script>
/*eslint-disable no-console */
import Layout from '@/components/layout'
export default {
// کامپوننتهای استفاده شده
components: {
Layout
},
//وضعیت مؤلفه
data() {
return {
// کاربر
user: '',
// رمز عبور آنها
password: '',
// نمایش خطا
showError: false
}
},
// ویژگیهای محاسباتی
computed: {
// ورودیهای معتبر
valid() {
return this.user && this.password && this.$store.state.started
}
},
// دادههای ناهمزمان
async asyncData(context) {
// لاگ
console.log('[index asyncData started]')
// ما کارها را دو بار انجام نمیدهیم اگر صفحه قبلاً درخواست شده باشد
if (process.server && context.store.state.started) {
console.log('[index asyncData canceled]')
return
}
try {
// یک جلسه آغاز میشود jSON
const dao = context.app.$dao()
const response = await dao.initSession()
// لاگ
console.log('[index asyncData response=]', response)
//بازیابی کوکی جلسه PHP برای درخواستهای آینده
const phpSessionCookie = dao.getPhpSessionCookie()
//کوکی جلسه PHP در جلسه [nuxt] ذخیره میشود
context.store.commit('replace', { phpSessionCookie })
//آیا خطایی رخ داد؟
if (response.état !== 700) {
//خطا در response.réponse است
throw new Error(response.réponse)
}
//توجه داشته باشید که جلسه jSON آغاز شده است
context.store.commit('replace', { started: true })
// نتیجهای وجود ندارد
return
} catch (e) {
// لاگ
console.log('[index asyncData error=]', e.message)
// توجه شود که جلسه jSON هنوز شروع نشده است
context.store.commit('replace', { started: false })
//خطا گزارش شد
return { showErrorLoading: true, errorLoadingMessage: e.message }
} finally {
//فروشگاه ذخیره شده است
const session = context.app.$session()
session.save(context)
// لاگ
console.log('[index asyncData finished]')
}
},
//چرخه عمر
beforeCreate() {
console.log('[index beforeCreate]')
},
created() {
console.log('[index created]')
},
beforeMount() {
//فقط برای کلاینت
console.log('[index beforeMount]')
// رسیدگی به هرگونه خطا
if (this.showErrorLoading) {
// لاگ
console.log('[index beforeMount, showErrorLoading=true]')
//خطا به مؤلفه اصلی [default] ارجاع داده میشود
this.$emit('error', new Error(this.errorLoadingMessage))
}
},
mounted() {
console.log('[index mounted]')
},
// پردازشکنندههای رویداد
methods: {
// ----------- احراز هویت
async login() {
...
}
</script>
- خطوط ۵۸ و ۶۵: تابع [asyncData] دیگر ویژگی [result] را که در اینجا بلااستفاده است، رندر نمیکند؛
- خط ۸۱: متد [beforeMount] از کلاینت [nuxt]. این متد به جای متد [mounted] انتخاب شد تا هرگونه خطایی را که ممکن است در [asyncData] رخ دهد، مدیریت کند؛
- خط ۸۵: بررسی میشود که آیا ویژگی [errorLoading] مقداردهی شده است. این ویژگی تنها توسط تابع [asyncData] مقداردهی میشود؛
- خطوط ۸۵–۹۰: اگر تابع [asyncData] خطایی گزارش کرده باشد، این خطا از طریق رویداد [error] به صفحه [default] ارسال میشود. این روشی است که تابع قدیمی [created]، که ما به تازگی آن را جایگزین کردهایم، با هرگونه خطا برخورد میکرد؛
بیایید چند تست اجرا کنیم.
ابتدا کوکی جلسه [nuxt] و کوکی جلسه PHP را، در صورت وجود، حذف میکنیم. سپس در حالی که سرور محاسبه مالیات در حال اجرا نیست، صفحه [http://localhost:81/nuxt-20/] را درخواست میکنیم. صفحه زیر به ما نمایش داده میشود:

پس از راهاندازی سرور محاسبه مالیات، همان صفحه را دوباره بارگذاری میکنیم:

بیایید نگاهی به لاگها بیندازیم:

- در [2-3]، میتوانیم ببینیم که جلسه jSON آغاز شده است؛
- در [4] میتوانیم کوکی جلسه PHP را ببینیم که سرور [nuxt] در طول تبادل با سرور محاسبه مالیات بازیابی کرده است. کلاینت [nuxt] اکنون از آن استفاده خواهد کرد؛
حالا بیایید احراز هویت کنیم:

صفحه زیر به ما نمایش داده میشود:

در [1]، ما یک پیام خطا دریافت کردیم. این بدان معناست که مرورگر کوکی جلسه صحیح (PHP) را که در مرحله قبلی توسط سرور [nuxt] آغاز شده بود، ارسال نکرده است. در [nuxt-12]، کوکی جلسه PHP از سرور [nuxt] به کلاینت [nuxt] از طریق کلاینت [nuxt] در اسکریپت [middleware/client/routing] ارسال شد. :
/*eslint-disable no-console */
export default function(context) { //چه کسی این کد را اجرا میکند؟
console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
// پردازش کوکی جلسه PHP در مرورگر
//کوکی جلسه مرورگر PHP باید دقیقاً مشابه کوکی یافتشده در جلسه Nuxt باشد
// عمل [fin-session] یک کوکی جدید PHP دریافت میکند (هم سرور و هم کلاینت Nuxt)
// اگر سرور آن را دریافت کند، کلاینت باید آن را به مرورگر منتقل کند
//برای ارتباطات خود با سرور PHP
//این یک سناریوی مسیریابی سمت کلاینت است
// کوکی جلسه بازیابی میشود: PHP
const phpSessionCookie = context.store.state.phpSessionCookie
if (phpSessionCookie) {
// اگر وجود داشته باشد، کوکی جلسه PHP به مرورگر اختصاص داده میشود
document.cookie = phpSessionCookie
}
// کجا میرویم؟
const to = context.route.path
if (to === '/fin-session') {
//پاک کردن جلسه
const session = context.app.$session()
session.reset(context)
// ارسال مجدد به صفحهٔ شاخص
context.redirect({ name: 'index' })
}
}
خطوط ۱۳–۱۷ به کلاینت [nuxt] امکان میدهند کوکی جلسه PHP را از سرور [nuxt] بازیابی کند.
مشکل اینجا این است که وقتی دکمه [Valider] کلیک میشود، کلاینت [nuxt] مسیریابی نمیشود. بنابراین تابع مسیریابی آن فراخوانی نمیشود. این مشکل با کپی کردن خطوط ۱۲ تا ۱۷ در ابتدای روش احراز هویت در صفحه [index] حل میشود:
// پردازشگرهای رویداد
methods: {
// ----------- احراز هویت
async login() {
// بازیابی کوکی جلسه PHP از فروشگاه
const phpSessionCookie = this.$store.state.phpSessionCookie
if (phpSessionCookie) {
//اگر وجود داشته باشد، کوکی جلسه PHP به مرورگر اختصاص داده میشود
document.cookie = phpSessionCookie
}
try {
// شروع انتظار
this.$emit('loading', true)
// کاربر هنوز احراز هویت نشده است
خطوط ۵–۱۰: کوکی جلسه برای PHP که توسط سرور [nuxt] ایجاد شده است، از مخزن بازیابی میشود. پس از اعمال این تغییر، صفحه محاسبه مالیات با موفقیت بازیابی میشود که نشان میدهد احراز هویت با موفقیت انجام شده است.
17.9. مرحله ۸
اکنون ما یک برنامه کاملاً کاربردی داریم که در چارچوب [nuxt] در حال اجرا است. همانطور که برای برنامه [nuxt-13] انجام دادیم، اکنون به بررسی ناوبری سرور [nuxt] میپردازیم. همانطور که قبلاً ذکر شد، از کاربر انتظار نمیرود که URL برنامه را به صورت دستی وارد کند. انتظار میرود از لینکهای ارائهشده استفاده کنند که توسط کلاینت [nuxt] اجرا میشوند و سپس در حالت SPA عمل میکنند. با این حال، ما اطمینان حاصل خواهیم کرد که ناوبری در سرور [nuxt] همیشه اپلیکیشن را در وضعیت پایدار رها میکند.
از تحلیل انجامشده برای [nuxt-13] (به بخش «لینک» مراجعه کنید)، میدانیم که باید:
- ویرایش اسکریپت [midleware/routing]؛
- افزودن یک اسکریپت [middleware/server/routing];

اسکریپت [middleware/routing] به شرح زیر اصلاح میشود:
/* eslint-disable no-console */
// ما میانافزار سرور و کلاینت را وارد میکنیم
import serverRouting from './server/routing'
import clientRouting from './client/routing'
export default function(context) {
// چه کسی این کد را اجرا میکند؟
console.log('[middleware], process.server', process.server, ', process.client=', process.client)
if (process.server) {
//مسیریابی سرور
serverRouting(context)
} else {
// راستهسازی سمت کلاینت
clientRouting(context)
}
}
- خط ۴: اسکریپت مسیریابی از سرور [nuxt] وارد میشود؛
- خطوط ۱۰–۱۲: اگر اسکریپت توسط سرور [nuxt] اجرا شود، از تابع مسیریابی آن استفاده میشود؛
اسکریپت [middleware/server/routing] به شرح زیر است:
/* eslint-disable no-console */
export default function(context) {
// چه کسی این کد را اجرا میکند؟
console.log('[middleware server], process.server', process.server, ', process.client=', process.client)
// ما اینجا و آنجا کمی اطلاعات جمعآوری میکنیم
const store = context.store
// از کجا میآییم؟
const from = store.state.from || 'nowhere'
// به کجا میرویم؟
let to = context.route.name
//یک مورد ویژه از /fin-session است که هیچ صفتی ندارد [name]
if (context.route.path === '/fin-session') {
to = 'fin-session'
}
// انحراف احتمالی
let redirection = ''
//مدیریت مسیریابی کامل شد
let done = false
//آیا هدایت سرور در حال انجام است ([nuxt])؟
if (store.state.serverRedirection) {
// نیازی به اقدام نیست
done = true
}
//آیا این بارگذاری مجدد صفحه است؟
if (!done && from === to) {
// کاری برای انجام دادن وجود ندارد
done = true
}
//بررسی ناوبری سرور [nuxt]
// ما منوی ناوبری مشتری را دنبال میکنیم
//ابتدا مورد پایان جلسه را رسیدگی کنید
if (!done && store.state.started && store.state.authenticated && to === 'fin-session') {
// حذف جلسه
const session = context.app.$session()
session.reset(context)
// ارسال مجدد به صفحهٔ شاخص
redirection = 'index'
// وظیفه انجام شد
done = true
}
// موردی که جلسه PHP هنوز شروع نشده است
if (!done && !store.state.started && to !== 'index') {
//هدایت به [index]
redirection = 'index'
// وظیفه تکمیل شد
done = true
}
//در مواردی که کاربر احراز هویت نشده است
if (!done && store.state.started && !store.state.authenticated && to !== 'index') {
redirection = 'index'
// وظیفه تکمیل شد
done = true
}
// موردی که [adminData] به دست نیامده است
if (!done && store.state.started && store.state.authenticated && !store.state.métier.taxAdminData && to !== 'index') {
// ارسال مجدد به [index]
redirection = 'index'
// کار به پایان رسید
done = true
}
// اگر [adminData] بهدست آمده باشد
if (
!done &&
store.state.started &&
store.state.authenticated &&
store.state.métier.taxAdminData &&
to !== 'calcul-impot' &&
to !== 'liste-des-simulations'
) {
// در همان صفحه باقی بمانید
redirection = from
// وظیفه انجام شد
done = true
}
// تمام بررسیها بهطور معمول انجام شدهاند ---------------------
// بازراستگذاری؟
if (redirection) {
// redirect در فروشگاه ثبت شده است
store.commit('replace', { serverRedirection: true })
} else {
// بدون هدایت
store.commit('replace', { serverRedirection: false, from: to })
}
//فروشگاه در جلسه ذخیره میشود [nuxt]
const session = context.app.$session()
session.value.store = store.state
session.save(context)
//هرگونه هدایت لازم از سمت سرور انجام میشود [nuxt]
if (redirection) {
context.redirect({ name: redirection })
}
}
- در این اسکریپت، ما مفاهیم از پیش توسعه یافته و مورد استفاده در مسیریابی سرور [nuxt] برای اپلیکیشن [nuxt-13] را ادغام میکنیم؛
- ما دو ویژگی به فروشگاه برنامه اضافه میکنیم:
- [from]: نام آخرین صفحه نمایشدادهشده. ما میدانیم که کلاینت [nuxt] این اطلاعات را دارد، اما سرور [nuxt] آن را ندارد. ما این اطلاعات را با ذخیرهسازی نام صفحهای که باید نمایش داده شود در فروشگاه، هر بار که سرور [nuxt] یک درخواست را مسیریابی میکند، اضافه خواهیم کرد. ما همین کار را هر بار که کلاینت [nuxt] یک عملیات مسیریابی را انجام میدهد، انجام خواهیم داد. بنابراین، دفعه بعد که سرور [nuxt] یک عملیات مسیریابی را انجام میدهد، نام آخرین صفحه نمایش داده شده توسط برنامه را در فروشگاه پیدا خواهد کرد؛
- [serverRedirection]: هنگامی که یک مقصد مسیریابی توسط سرور [nuxt] رد میشود، یک تغییر مسیر انجام میدهد. سپس در مخزن مشخص میکند که مقصد بعدی برای سرور [nuxt] یک صفحه تغییر مسیر است. این هدایت، اجرای جدیدی از روتر را روی سرور [nuxt] آغاز میکند. اگر روتر تشخیص دهد که مقصد فعلی نتیجه یک هدایت است، اجازه میدهد که ادامه یابد؛
- خطوط ۶–۱۱: اطلاعات مورد نیاز برای مسیریابی بازیابی میشود؛
- خطوط ۱۳–۱۶: هدف [/fin-session] با صفحهای به نام [fin-session] مرتبط نیست. بنابراین نامی ندارد. ما برای آن یک نام تعیین میکنیم؛
- خط ۱۹: هدف هرگونه تغییر مسیر احتمالی؛
- خط ۲۱: [done=true] پس از اتمام آزمایشهای مسیریابی؛
- خطوط ۲۳–۲۷: همانطور که ذکر شد، اگر مسیریابی فعلی نتیجه یک تغییر مسیر باشد، کاری برای انجام دادن وجود ندارد. در واقع، در طول مرحله مسیریابی قبلی، مسیریاب تصمیم گرفته بود که مرورگر مشتری باید به صفحه دیگری هدایت شود. نیازی به بازنگری در این تصمیم نیست؛
- خطوط ۲۹–۳۳: اگر این بارگذاری مجدد صفحه باشد، اجازه میدهیم ادامه یابد. این یک قانون کلی برای همه برنامههای [nuxt] نیست: اثرات بارگذاری مجدد باید به صورت صفحه به صفحه ارزیابی شود. در این مورد، مشخص میشود که بارگذاری مجدد صفحات [index, calcul-impot, liste-des-simulations] هیچگونه اثر نامطلوبی به همراه ندارد؛
- خطوط ۳۵–۸۵: مسیریابی در سرور [nuxt] آیینهای از مسیریابی کلاینت [nuxt] است. هنگامی که در یک صفحه هستیم، مسیریابی برای سرور [nuxt] باید منوی ناوبری ارائهشده توسط کلاینت [nuxt] را در آن صفحه منعکس کند؛
- خطوط ۳۸–۴۷: ابتدا موردی را بررسی میکنیم که هدف [fin-session] با هیچ صفحهٔ موجودی مطابقت ندارد. اگر شرایط برقرار باشد (جلسه شروع شده، کاربر احراز هویت شده باشد)، جلسه را پاک میکنیم و کاربر را به صفحهٔ [index] هدایت میکنیم؛
- خطوط ۴۹–۵۵: اگر جلسه jSON با سرور محاسبه مالیات آغاز نشده باشد، تنها مقصد ممکن صفحه [index] است؛
- خطوط ۵۷–۶۲: اگر جلسه jSON آغاز شده باشد و کاربر احراز هویت نشده و صفحه احراز هویت را درخواست نکرده باشد، آنگاه کاربر به صفحه احراز هویت، یعنی [index]، هدایت میشود؛
- خطوط ۶۴–۷۰: اگر کاربر احراز هویت شده باشد اما داده [adminData] دریافت نشده باشد، کاربر به صفحه احراز هویت هدایت میشود. فرآیند احراز هویت دو کار انجام میدهد: کاربر را احراز هویت میکند و در صورت موفقیت، دادههای [adminData] را نیز درخواست میکند. اگر این دادهها به دست نیامده باشند، فرآیند احراز هویت باید تکرار شود؛
- خطوط ۷۲–۸۵: اگر داده [adminData] به دست آمده باشد، آنگاه تنها اهداف ممکن [calcul-impot] و [liste-des-simulations] هستند. اگر اینطور نباشد، مسیریابی رد میشود؛
- خطوط ۸۸–۹۵: فروشگاه بسته به اینکه آیا تغییر مسیر وجود داشته باشد یا خیر، بهروزرسانی میشود؛
- خط ۹۴: هیچ مسیریابی مجدد وجود ندارد. بنابراین، [to] فعلی برای مسیریابی بعدی به [from] تبدیل خواهد شد؛
- خطوط ۹۶–۹۹: اطلاعات ذخیرهسازی در کوکی جلسه [nuxt] ذخیره میشود؛
- خطوط ۱۰۰–۱۰۳: اگر هدایت لازم است، آن را انجام دهید؛
برای اجرای آزمایشها، اطمینان حاصل کنید که با پاک کردن کوکی جلسه [nuxt] و کوکی جلسه PHP از سرور محاسبه مالیات، از یک صفحه سفید شروع میکنید:

برای آزمایش مسیریابی سرور [nuxt]، در هر صفحه تمام مقادیر ممکن URL و [/, /calcul-impot, /liste-des-simulations] را امتحان کنید. هر بار، برنامه باید در وضعیت ثابتی باقی بماند.
17.10. مرحله ۹
مرحله ۹ مستلزم استقرار برنامه [nuxt-20] است. این امر نیازمند میزبانیای است که یک محیط [node.js] را برای اجرای سرور [nuxt] فراهم کند. من این را ندارم. خواننده میتواند رویههای توصیفشده در بخش پیوندی را برای استقرار برنامه [nuxt-20] روی ماشین توسعه خود دنبال کند و آن را با استفاده از پروتکل HTTPS ایمن سازد.
17.11. Conclusion
مهاجرت برنامه [vuejs-22] به برنامه [nuxt-20] اکنون تکمیل شده است. بیایید چند نکته در مورد این مهاجرت ذکر کنیم:
- صفحات مربوط به [vuejs-22] حفظ شدهاند؛
- عملیات غیرهمزمان موجود در صفحات [vuejs-22] به تابع [asyncData] منتقل شدهاند؛
- در [nuxt-20]، دو موجودیت باید مدیریت میشدند: کلاینت [nuxt] و سرور [nuxt]. موجودیت دوم در [vuejs-22] وجود نداشت. برای حفظ سازگاری بین این دو موجودیت، به یک جلسه [nuxt] نیاز داشتیم؛
- ما باید مسیریابی را برای سرور [nuxt] مدیریت میکردیم؛
در عمل، بدون شک ترجیح داده میشود که مستقیماً با معماری [nuxt] شروع کرد تا اینکه یک معماری [vue.js] ایجاد کرده و سپس آن را به یک محیط [nuxt] منتقل کرد.