9. مثال [nuxt-06]: حقن في سياق مدير الجلسة
9.1. Présentation
أظهر المثال [nuxt-05] أنه يمكن الحفاظ على المخزن حتى عندما يقوم المستخدم بإجبار الخادم على الاستجابة. تكون عناصر المخزن تفاعلية بحيث إذا تم دمجها في العروض، فإن هذه العروض تكون تفاعلية مع التغييرات في المخزن. قد نرغب أيضًا في الحفاظ على العناصر خلال التبادلات بين العميل والخادم دون أن نرغب في أن تكون تفاعلية، وذلك ببساطة لأنها لا يتم عرضها من خلال العروض. يمكننا إذن تخزينها في الجلسة دون أن تكون موجودة في المخزن.
يمكن الوصول إلى المخزن بسهولة من خلال خصائص مثل [context.app.$store] خارج العروض أو [this.$store] داخل العروض. نرغب في شيء مشابه للجلسة، شيء مثل [context.app.$session] أو [this.$session]. سنرى أن هذا ممكن بفضل مفهوم الحقن. لكن لا يمكن حقن كائنات في السياق، بل وظائف فقط. وستكون هذه متاحة إذن من خلال التعبيرات [context.app.$session()] أو [this.$session()].
أخيرًا، سنقدم مفهوم [nuxt] من [plugin].
يتم الحصول على المثال [nuxt-06] في البداية عن طريق نسخ المشروع [nuxt-05]:

- في [1]، سنضيف مجلد [plugins]؛
9.2. مفهوم المكون الإضافي [nuxt]
[nuxt] يسمي [plugin] أي كود يتم تنفيذه عند بدء تشغيل التطبيق، حتى قبل تنفيذ الوظيفة [nuxtServerInit] بواسطة الخادم والتي كانت حتى الآن أول وظيفة مستخدم يتم تنفيذها. يجب الإعلان عن مكونات التطبيق الإضافية في المفتاح [plugins] في ملف التكوين [nuxt.config.js]:
/*
** Plugins to load before mounting the App
*/
plugins: [
{ src: '~/plugins/client/session', mode: 'client' },
{ src: '~/plugins/server/session', mode: 'server' }
],
- السطران 5-6: يتم تعيين المكون الإضافي من خلال مساره [src] ووضع التنفيذ [mode]. يمكن أن يكون لـ [mode] ثلاث قيم:
- [client]: يجب تشغيل المكون الإضافي على جانب العميل فقط؛
- [server]: يجب تشغيل المكون الإضافي على جانب الخادم فقط؛
- عدم وجود المفتاح [mode]: في هذه الحالة، يجب تشغيل المكون الإضافي على جانب العميل والخادم معًا؛
- السطران 5-6: وضعنا المكونين الإضافيين في مجلد [plugins]. لا يوجد أي إلزام بذلك. يمكن وضع المكونات الإضافية في أي مكان في شجرة المشروع. وبالمثل، فإن أسماء المجلدات الفرعية [client, server] هنا عشوائية؛

9.3. المكوّن الإضافي [session] الخاص بالخادم
المكوّن الإضافي [server / session.js] هو التالي:
/* eslint-disable no-console */
export default (context, inject) => {
// إدارة جلسة عمل الخادم
// هل توجد جلسة عمل حالية؟
let value = context.app.$cookies.get('session')
if (!value) {
// جلسة جديدة
console.log("[plugin session server], démarrage d'une nouvelle session")
value = initValue
} else {
// جلسة عمل موجودة
console.log("[plugin session server], reprise d'une session existante")
}
// تحديد الجلسة
const session = {
// محتوى الجلسة
value,
// حفظ الجلسة في ملف تعريف ارتباط
save(context) {
context.app.$cookies.set('session', this.value, { path: context.base, maxAge: context.env.maxAge })
}
}
// يتم إدخال دالة في [context, Vue] تجعل الجلسة الحالية
inject('session', () => session)
}
// القيمة الأولية للجلسة
const initValue = {
initSessionDone: false
}
- السطر 2: يتم تنفيذ المكونات الإضافية في كل مرة يتم فيها استدعاء الخادم: عند بدء التشغيل وفي كل مرة يقوم فيها المستخدم بإجبار الخادم على الاستجابة عن طريق كتابة URL يدويًا:
- يتم أولاً تنفيذ المكون الإضافي (أو المكونات الإضافية) للخادم؛
- عندما يتلقى متصفح العميل الرد من الخادم، يحين دور المكون الإضافي (أو المكونات الإضافية) للعميل للتنفيذ؛
- السطر 2: يتلقى كل مكون إضافي، سواء كان للعميل أو للخادم، معلمتين:
- [context]: سياق الخادم أو العميل حسب من يقوم بتنفيذ المكون الإضافي؛
- [inject]: وظيفة تسمح بإدخال وظيفة في سياق الخادم أو العميل؛
- الغرض من المكون الإضافي [server / session] مزدوج:
- تحديد جلسة (الأسطر 16-23)؛
- تحديد وظيفة [$session] داخل السياق والتي ستُرجع نتيجةً الجلسة الموجودة في السطر 16. السطر 25 هو الذي يقوم بذلك؛
- الأسطر 16-23: ستقوم الجلسة بتغليف بياناتها في الكائن [value] في السطر 18؛
- الأسطر 20-22: تحتوي على دالة [save] التي تتلقى كمعلمة كائن [context]. ويزودها الرمز المستدعي بهذا السياق. وباستخدام هذا السياق، تقوم الدالة [save] بحفظ قيمة الجلسة، الكائن [value]، في ملف تعريف ارتباط الجلسة؛
- السطر 6: عند تشغيل المكون الإضافي [server / session]، يبدأ بالتحقق مما إذا كان الخادم قد تلقى ملف تعريف ارتباط للجلسة؛
- إذا كان الجواب نعم، فإن الكائن [value] في السطر 6 يمثل قيمة الجلسة، أي مجموعة البيانات المُغلفة فيها؛
- إذا لم يكن الأمر كذلك، في الأسطر 7-11، يتم تحديد القيمة الأولية للجلسة. وستكون هذه هي الكائن [initValue] في الأسطر 29-31. وسيتم تعريف عناصر الجلسة في الدالة [nuxtServerInit] التي يتم تنفيذها بعد المكون الإضافي للخادم؛
- السطر 18: الترميز [value] هو اختصار للترميز [value:value]. [value] على اليسار هو اسم مفتاح كائن، و[value] على اليمين هو الكائن [value] المعلن في السطر 6؛
- السطر 25: عند الوصول إلى هذا السطر، تكون الجلسة قد تم إنشاؤها لأنها لم تكن موجودة، أو تم استردادها من طلب HTTP من متصفح العميل؛
- السطر 25: يتم إدخال وظيفة جديدة في سياق الخادم:
- المعلمة الأولى لـ [inject] هي اسم الوظيفة التي يتم إنشاؤها، وهي هنا "session". ستعطيها [nuxt] في الواقع الاسم "$session"؛
- المعلمة الثانية هي تعريف الدالة. هنا، لن تقبل الدالة [$session]
- لن تقبل أي معلمة؛
- ستُرجع الكائن [session] من السطر 16؛
- بمجرد تشغيل المكون الإضافي:
- تصبح الدالة [$session] متاحة في [context.app.$session] حيث يكون الكائن [context] متاحًا، أو [this.$session] في عرض أو في مخزن [vuex]؛
- تُرجع الدالة [$session] كائن [session] بمفتاح فريد [value]؛
- عند الإنشاء الأولي للجلسة، لا يحتوي الكائن [value] إلا على مفتاح واحد [initStoreDone] (الأسطر 29-31). يُستخدم المفتاح [initStoreDone:false] للإشارة إلى أن المخزن لم يتم وضعه بعد في الجلسة. سيتم ذلك بواسطة الدالة [nuxtServerInit]؛
9.4. تهيئة الجلسة
بمجرد تنفيذ المكون الإضافي [session / server] بواسطة الخادم، سيقوم الخادم بتنفيذ البرنامج النصي [store / index.js] التالي:
/* eslint-disable no-console */
export const state = () => ({
// العداد
counter: 0
})
export const mutations = {
// زيادة العداد بقيمة واحدة [inc]
increment(state, inc) {
state.counter += inc
},
// استبدال الحالة
replace(state, newState) {
for (const attr in newState) {
state[attr] = newState[attr]
}
}
}
export const actions = {
async nuxtServerInit(store, context) {
// من الذي ينفذ هذا الكود؟
console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
// ننتظر انتهاء وعد
await new Promise(function(resolve, reject) {
// عادة ما يكون لدينا هنا دالة غير متزامنة
// نحاكيها بانتظار لمدة ثانية
setTimeout(() => {
// بدء الجلسة
initSession(store, context)
// نجاح
resolve()
}, 1000)
})
}
}
function initSession(store, context) {
// store هو المخزن المراد تهيئته
// يتم استرداد الجلسة
const session = context.app.$session()
// هل تم تهيئة الجلسة بالفعل؟
if (!session.value.initSessionDone) {
// يتم بدء تشغيل مخزن جديد
console.log("nuxtServerInit, initialisation d'une nouvelle session")
// يتم تهيئة المخزن
store.commit('increment', 77)
// وضع المخزن في الجلسة
session.value.store = store.state
// يتم تهيئة جلسة جديدة
session.value.somethingImportant = { x: 2, y: 4 }
// تم تهيئة الجلسة الآن
session.value.initSessionDone = true
} else {
console.log("nuxtServerInit, reprise d'un store existant")
// يتم تحديث المخزن بمخزن الجلسة
store.commit('replace', session.value.store)
}
// يتم حفظ الجلسة
session.save(context)
// سجل
console.log('initSession terminé, store=', store.state, 'session=', session.value)
}
بالنسبة إلى ملف التخزين المؤقت الخاص بالمشروع [nuxt-05]، لم تتغير سوى الدالة [initSession] (المعروفة سابقًا باسم initStore) في الأسطر 38-60:
- السطر 42: يتم استرداد الجلسة بفضل الوظيفة [$session] التي تم إدخالها في سياق الخادم؛
- السطر 44: يتم التحقق مما إذا كانت الجلسة قد تم تهيئتها بالفعل؛
- الأسطر 45-54: إذا لم يكن الأمر كذلك:
- السطر 48: يتم تهيئة المخزن؛
- السطر 50: يتم وضع حالة المخزن في الجلسة؛
- السطر 52: نضيف كائنًا آخر [somethingImportant] إلى الجلسة. لن يكون هذا الكائن جزءًا من المخزن؛
- السطر 54: يتم تسجيل أن الجلسة قد تم تهيئتها الآن؛
- الأسطر 55-59: إذا كانت الجلسة قد تم تهيئتها بالفعل:
- السطر 58: يتم تهيئة المخزن الجديد بمحتوى الجلسة؛
- السطر 61: يتم حفظ الجلسة في ملف تعريف الارتباط الخاص بالجلسة. نذكر أن ذلك يتمثل في وضع ملف تعريف الارتباط في الاستجابة HTTP التي سيقوم الخادم بإرسالها إلى متصفح العميل؛
9.5. المكوّن الإضافي [client / session] الخاص بالعميل
بمجرد أن يقوم الخادم بتنفيذ البرامج النصية [plugins / server / session] و [store / index]، سيرسل إحدى الصفحات [index, page1] إلى متصفح العميل. في استجابة الخادم HTTP، سيكون هناك ملف تعريف ارتباط الجلسة. بمجرد استلام المتصفح العميل للصفحة، سيتم تنفيذ البرامج النصية للعميل المضمنة في الصفحة. سيتم بعد ذلك تنفيذ المكون الإضافي [client / session]:
/* eslint-disable no-console */
export default (context, inject) => {
// إدارة جلسة عمل العميل
// الجلسة موجودة بالضرورة، يتم تهيئتها بواسطة الخادم
console.log('[plugin session client], reprise de la session du serveur')
// تعريف الجلسة
const session = {
// محتوى الجلسة
value: context.app.$cookies.get('session'),
// حفظ الجلسة في ملف تعريف ارتباط
save(context) {
context.app.$cookies.set('session', this.value, { path: context.base, maxAge: context.env.maxAge })
}
}
// يتم إدخال دالة في [context, Vue] تجعل الجلسة الحالية
inject('session', () => session)
}
- عندما يتم تشغيل المكون الإضافي للعميل، يكون متصفح العميل قد استقبل ملف تعريف الارتباط الخاص بالجلسة بالفعل؛
- الهدف من المكون الإضافي [client] هو إدخال وظيفة [$session] أيضًا في سياق العميل. ستؤدي هذه الوظيفة إلى إرجاع الجلسة المرسلة من الخادم؛
- السطر 19: ستقوم الدالة المُدرجة [$session] بإرجاع الجلسة الموجودة في الأسطر 9-16؛
- الأسطر 9-16: الكائن [session] الذي يديره العميل. سيكون هذا نسخة من الجلسة المرسلة من الخادم؛
- السطر 11: يتم أخذ قيمة جلسة عمل العميل من ملف تعريف الارتباط الخاص بجلسة العمل الذي أرسله الخادم [nuxt]؛
- السطور 13-15: كما هو الحال بالنسبة لجلسة عمل الخادم، تحتوي جلسة عمل العميل على وظيفة [save] التي تسمح بحفظ قيمة الجلسة، [this.value] السطر 14، في ملف تعريف الارتباط الخاص بالجلسة المخزن على المتصفح؛
9.6. الصفحة [index]
تتطور الصفحة [index] على النحو التالي:
<!-- صفحة [index] -->
<template>
<Layout :left="true" :right="true">
<!-- التنقل -->
<Navigation slot="left" />
<!-- رسالة-->
<template slot="right">
<b-alert show variant="warning"> Home - session= {{ jsonSession }}, counter= {{ $store.state.counter }} </b-alert>
<!-- زر -->
<b-button @click="incrementCounter" class="ml-3" variant="primary">Incrémenter</b-button>
</template>
</Layout>
</template>
<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */
import Layout from '@/components/layout'
import Navigation from '@/components/navigation'
export default {
name: 'Home',
// المكونات المستخدمة
components: {
Layout,
Navigation
},
computed: {
jsonSession() {
return JSON.stringify(this.$session().value)
}
},
// دورة الحياة
beforeCreate() {
// العميل والخادم
console.log('[home beforeCreate]')
},
created() {
// العميل والخادم
console.log('[home created], session=', this.$session().value)
},
beforeMount() {
// العميل فقط
console.log('[home beforeMount]')
},
mounted() {
// العميل فقط
console.log('[home mounted]')
},
// إدارة الأحداث
methods: {
incrementCounter() {
console.log('incrementCounter')
// زيادة العداد بمقدار 1
this.$store.commit('increment', 1)
// تعديل الجلسة
const session = this.$session()
session.value.store = this.$store.state
session.value.somethingImportant.x++
session.value.somethingImportant.y++
// حفظ الجلسة في ملف تعريف الارتباط الخاص بالجلسة
session.save(this.$nuxt.context)
}
}
}
</script>
يجب أن نتذكر أن هذه الصفحة يتم تنفيذها على جانب الخادم وكذلك على جانب العميل.
- السطر 8: يتم الآن عرض كل من الجلسة والمخزن؛
- السطر 30: [jsonSession] هي خاصية محسوبة تجعل السلسلة jSON تساوي قيمة الجلسة؛
- السطر 41: يتم عرض قيمة الجلسة باستخدام الدالة المُدرجة [this.$session]. هذه الدالة موجودة في سياق الخادم وكذلك في سياق العميل؛
- السطر 53: لا يتم تنفيذ الطريقة [incrementCounter] إلا على جانب العميل؛
- السطر 56: يتم زيادة عداد المخزن وعرضه كما في السابق؛
- السطر 58: يتم استرداد الجلسة بفضل الدالة المُدرجة [this.$session]؛
- السطر 59: يتم تحديث مخزن الجلسة؛
- السطران 60-61: يتم زيادة سمات [somethingImportant.x, somethingImportant.y] للجلسة. هذا فقط لإظهار أن الجلسة يمكن أن تستخدم لنقل شيء آخر غير المخزن؛
- السطر 63: يتم حفظ الجلسة في ملف تعريف الارتباط الخاص بالجلسة المخزن على المتصفح. من منظور العميل، يتوفر سياق هذا الملف في [this.$nuxt.context]؛
الغرض من الصفحة [index] هو إظهار أن الجلسة غير تفاعلية في حين أن المخزن تفاعلي. عند زيادة عناصر الجلسة، سنكتشف أن العرض لم يتم تحديثه. يقدم العرض [page1] حلاً لهذه المشكلة.
9.7. الصفحة [page1]
يتم الحصول على الصفحة [page1] عن طريق نسخ الصفحة [index] ثم تعديلها قليلاً:
<!-- الصفحة [index] -->
<template>
<Layout :left="true" :right="true">
<!-- التنقل -->
<Navigation slot="left" />
<!-- رسالة-->
<template slot="right">
<b-alert show variant="warning"> Page1 - session= {{ jsonSession }}, counter= {{ $store.state.counter }} </b-alert>
<!-- زر -->
<b-button @click="incrementCounter" class="ml-3" variant="primary">Incrémenter</b-button>
</template>
</Layout>
</template>
<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */
import Layout from '@/components/layout'
import Navigation from '@/components/navigation'
export default {
name: 'Page1',
// المكونات المستخدمة
components: {
Layout,
Navigation
},
data() {
return {
session: {}
}
},
computed: {
jsonSession() {
return JSON.stringify(this.session.value)
}
},
// دورة الحياة
beforeCreate() {
// العميل والخادم
console.log('[page1 beforeCreate]')
},
created() {
// العميل والخادم
// نضع الجلسة في الخصائص التفاعلية للصفحة
this.session = this.$session()
// السجل
console.log('[page1 created], session=', this.session.value)
},
beforeMount() {
// العميل فقط
console.log('[page1 beforeMount]')
},
mounted() {
// العميل فقط
console.log('[page1 mounted]')
},
// إدارة الأحداث
methods: {
incrementCounter() {
console.log('incrementCounter')
// زيادة العداد بمقدار 1
this.$store.commit('increment', 1)
// تعديل الجلسة
this.session.value.store = this.$store.state
this.session.value.somethingImportant.x++
this.session.value.somethingImportant.y++
// حفظ الجلسة في ملف تعريف الارتباط الخاص بالجلسة
this.session.save(this.$nuxt.context)
}
}
}
</script>
- السطر 47: يتمثل الاختلاف الرئيسي في وضع الجلسة الحالية في خصائص الصفحة (الأسطر 29-33). سيؤدي ذلك إلى أن تصبح الجلسة تفاعلية من الآن فصاعدًا. عندما تقوم الدالة [incrementCounter] بزيادة عناصر الجلسة، سيتم تحديث العرض [page1]؛
9.8. تنفيذ المشروع
قبل تنفيذ المشروع، تحقق من ملف تعريف ارتباط الجلسة في متصفحك، وإذا كان موجودًا، فاحذفه حتى يقوم الخادم بإنشاء جلسة جديدة:

الآن نطلب URL [http://localhost:81/nuxt-06/]:

تكون السجلات في المتصفح عندئذٍ كما يلي:

- في [2]، يبدأ الخادم جلسة جديدة في المكون الإضافي [session] الخاص بالخادم؛
- في [3]، يتم تهيئة هذه الجلسة الجديدة في [nuxtServerInit]؛
- في [4]، الجلسة الجديدة كما هي معروفة على الخادم؛
- في [5]، استرد العميل هذه الجلسة بشكل صحيح؛
الآن لنقم بزيادة العداد ثلاث مرات:

- في [3]، تمت زيادة العداد بشكل صحيح ولكن لم يتم زيادة الجلسة في [2]. بينما يعرض [3] المخزن الذي يستجيب، يعرض [2] الجلسة التي لا تستجيب:
الآن، لنقم بإعادة تحميل الصفحة (F5). فيما يلي السجلات بعد إعادة التحميل:

- في [2]، نرى أن الخادم تلقى ملف تعريف ارتباط للجلسة أرسله متصفح العميل؛
- في [4]، نرى أن المخزن لم يتم إعادة تعيينه بل تم استعادته في الجلسة المستلمة؛
- في [4-5]: نرى أن سمات الجلسة قد تمت زيادتها ثلاث مرات؛
الصفحة المرسلة من الخادم هي التالية؛

الاستنتاج المستخلص من هذه الصفحة هو أن الجلسة يمكن أن تنقل عناصر أخرى غير المخزن، ولكن هذه العناصر غير تفاعلية.
الآن، لنضغط على الرابط [Page 1] [4]. الصفحة الجديدة المعروضة هي:

ثم نستخدم الزر [Incrémenter] ثلاث مرات. تصبح الصفحة كما يلي:

هذه المرة، يتم عرض الجلسة بشكل صحيح في [2]. وهي هنا تفاعلية. ويمكن ملاحظة ذلك في السجلات:

- في [1-3]، قيم الجلسة؛
- في [4-6]، دالات القراءة والكتابة التفاعلية لعناصر الجلسة؛
الآن، لنضغط على الرابط [Home] [4]. نحصل على الصفحة التالية:

ثم نضغط مرتين على الزر [Incrémenter] [4]. تصبح الصفحة كما يلي:

نلاحظ أن الجلسة أصبحت تفاعلية هنا أيضًا [2].
لنطلب القيمة التي تعطيها الدالة [this.$session()]:

- في علامة التبويب [Vue]، نختار الصفحة الحالية [Home] للحصول على مرجعها [$vm0] [3]؛
ثم في علامة التبويب [Console] [4]، نطلب قيمة الدالة [$vm0.$session()]:

- في [5]، نرى أن الجلسة أصبحت تفاعلية بينما لم تكن كذلك في البداية؛
- في [6]، نطلب عرض قيمة الجلسة؛
- في [7-8]، نكتشف أن هذه القيمة أصبحت تفاعلية أيضًا؛
لذلك لدينا هنا نتيجة غير متوقعة: إذا أصبح عنصر ما تفاعليًا في صفحة ما لأنه تم وضعه في خصائص الصفحة، فإنه يصبح تفاعليًا أيضًا في الصفحات التي لا يشكل جزءًا من خصائصها.
9.9. Conclusion
أظهر المثال [nuxt-05] أنه يمكن الحفاظ على المخزن عبر الطلبات الموجهة إلى الخادم. يقوم المثال [nuxt-06] بنفس الشيء مع كائن أطلقنا عليه اسم [session] على غرار جلسة الويب. لقد رأينا أن هذه الجلسة يمكن أن تتمتع بنفس خصائص المخزن [Vuex] وتصبح تفاعلية هي الأخرى، في حين أنها لم تكن كذلك في الأصل.
إذن ما فائدة المخزن [Vuex]؟ يجب أن أعترف أنه لم يتضح لي ذلك حتى الآن. من المحتمل أن يكون هناك شيء قد فاتني. لذا في حالة الشك، أنصح باستخدام:
- مخزن [Vuex] لوضع فيه كل ما يجب مشاركته بين صفحات العميل، وما قد يجب مشاركته بين العميل والخادم؛
- ملف تعريف ارتباط للجلسة إذا كان يجب أن يستمر المخزن أثناء اتصال العميل بالخادم، بحيث تحتوي الجلسة على المخزن فقط؛
كان الهدف من الأمثلة [nuxt-05] و [nuxt-06] هو إظهار كيفية ضمان استمرارية التطبيق عندما يفرض المستخدم الاتصال بالخادم عن طريق كتابة URL يدويًا. تجدر الإشارة إلى أن السلوك الافتراضي في هذه الحالة هو إعادة تشغيل التطبيق، مما يؤدي إلى فقدان حالته في ذلك الوقت.