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,
// збереження сесії у файлі cookie
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], у сесійному файлі cookie;
- рядок 6: коли виконується плагін [server / session], він спочатку перевіряє, чи отримав сервер файл cookie сеансу;
- якщо так, то об’єкт [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] використовується для позначення того, що store ще не було додано до сесії. Це буде зроблено за допомогою функції [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: ініціалізується store;
- рядок 50: стан сховища записується в сесію;
- рядок 52: до сесії додається ще один об’єкт [somethingImportant]. Він не буде частиною сховища;
- рядок 54: фіксується той факт, що сесія тепер ініціалізована;
- рядки 55–59: якщо сесія вже була ініціалізована:
- рядок 58: новий магазин ініціалізується з вмістом сесії;
- рядок 61: сесія зберігається у сесійному файлі cookie. Нагадаємо, що це полягає у розміщенні файлу cookie у відповіді HTTP, яку сервер надішле браузеру клієнта;
9.5. Плагін [client / session] на стороні клієнта
Після того як сервер виконає скрипти [plugins / server / session] та [store / index], він надішле одну зі сторінок [index, page1] до браузера клієнта. У відповіді HTTP від сервера буде міститися сесійний файл cookie. Після отримання сторінки клієнтським браузером вбудовані в неї клієнтські скрипти почнуть виконуватися. Тоді запуститься плагін [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'),
// збереження сеансу в файлі cookie
save(context) {
context.app.$cookies.set('session', this.value, { path: context.base, maxAge: context.env.maxAge })
}
}
// в [context, Vue] вставляється функція, яка зробить сесію активною
inject('session', () => session)
}
- на момент запуску клієнтського плагіна сесійний файл cookie вже отримано клієнтським браузером;
- мета плагіна [client] полягає в тому, щоб також ввести функцію [$session] у контекст клієнта. Ця функція відтворює сесію, надіслану сервером;
- рядок 19: вставлена функція [$session] поверне сесію з рядків 9–16;
- рядки 9–16: об’єкт [session], що керується клієнтом. Це буде копія сесії, надісланої сервером;
- рядок 11: значення сесії клієнта береться з файлу cookie сесії, надісланого сервером [nuxt];
- рядки 13–15: як і у випадку з сесією сервера, сесія клієнта має функцію [save], яка дозволяє зберегти значення сесії ([this.value], рядок 14) у файлі cookie сесії, що зберігається в браузері;
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++
// збереження сесії у сесійному файлі cookie
session.save(this.$nuxt.context)
}
}
}
</script>
Слід пам’ятати, що ця сторінка виконується як на стороні сервера, так і на стороні клієнта.
- рядок 8: тепер відображаються як сесія, так і store;
- рядок 30: [jsonSession] — це обчислювана властивість, яка формує рядок jSON на основі значення сесії;
- рядок 41: значення сесії виводиться за допомогою вбудованої функції [this.$session]. Ця функція існує як у контексті сервера, так і в контексті клієнта;
- рядок 53: метод [incrementCounter] виконується лише на стороні клієнта;
- рядок 56: лічильник store інкрементується та відображається, як і раніше;
- рядок 58: сесія отримується за допомогою вбудованої функції [this.$session];
- рядок 59: оновлюється сховище сесії;
- рядки 60–61: збільшуються атрибути сесії [somethingImportant.x, somethingImportant.y]. Це зроблено лише для того, щоб показати, що сесія може використовуватися для передачі не лише даних сховища;
- рядок 63: сесія зберігається у файлі cookie сесії, що зберігається у браузері. У клієнтському представленні контекст цього файлу доступний у [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++
// збереження сесії у сесійному файлі cookie
this.session.save(this.$nuxt.context)
}
}
}
</script>
- рядок 47: головна відмінність полягає в тому, що поточна сесія додається до властивостей сторінки (рядки 29–33). Це призведе до того, що відтепер сесія стане інтерактивною. Коли функція [incrementCounter] збільшуватиме елементи сесії, представлення [page1] оновлюватиметься;
9.8. Виконання проекту
Перед виконанням проекту перевірте наявність сесійного файлу cookie у вашому браузері та, якщо він існує, видаліть його, щоб сервер створив нову сесію:

Тепер звернемося до URL та [http://localhost:81/nuxt-06/]:

Журнали в браузері виглядають наступним чином:

- у [2] сервер запускає нову сесію у плагіні [session] сервера;
- у [3] ця нова сесія ініціалізується в [nuxtServerInit];
- у [4] — нова сесія, як вона відома на сервері;
- у [5] клієнт правильно отримав цю сесію;
Тепер збільшимо лічильник тричі:

- у [3] лічильник було збільшено, але сесія у [2] — ні. У той час як [3] відображає активний сеанс, [2] відображає сеанс, який не є активним:
Тепер перезавантажимо сторінку (F5). Після цього перезавантаження журнали мають такий вигляд:

- у записі [2] видно, що сервер отримав сесійний файл cookie, надісланий клієнтським браузером;
- у [4] видно, що значення store не скинуто, а перенесено з отриманої сесії;
- у [4-5]: бачимо, що всі атрибути сесії дійсно були збільшені на три;
Сторінка, надіслана сервером, має такий вигляд:

Висновок, який можна зробити на основі цієї сторінки, полягає в тому, що сесія може містити інші елементи, крім store, але вони не реагують на дії користувача.
Тепер натиснімо на посилання [Page 1] [4]. Нова сторінка, що відображається, виглядає так:

Потім тричі натиснімо кнопку [Incrémenter]. Сторінка виглядатиме так:

Цього разу сесія відображається правильно як [2]. Вона тут реагує на дії користувача. Це видно в логах:

- у [1-3] — значення сесії;
- у [4-6] — реактивні методи getter та setter для елементів сесії;
Тепер натиснімо на посилання [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] показав, що можна зберігати state під час запитів до сервера. Приклад [nuxt-06] робить те саме з об’єктом, який ми назвали [session] за аналогією з веб-сесією. Ми побачили, що ця сесія може мати ті самі властивості, що й сховище [Vuex], і також стати реактивною, хоча за своєю природою вона такою не була.
То в чому ж полягає користь сховища [Vuex]? Мушу зізнатися, що наразі я її не бачу. Ймовірно, я щось пропустив. Тому, якщо є сумніви, я б порадив використовувати:
- сховище [Vuex] для розміщення у ньому всього, що має бути спільним для сторінок клієнта, а також того, що, можливо, має бути спільним для клієнта та сервера;
- сесійний файл cookie, якщо сховище має зберігатися під час звернення клієнта до сервера, причому сесія міститиме лише це сховище;
Приклади [nuxt-05] та [nuxt-06] мали на меті продемонструвати, як можна забезпечити безперервність роботи додатка, коли користувач примусово ініціює виклик сервера, вручну вводячи URL. Нагадаємо, що за замовчуванням у цьому випадку відбувається перезапуск додатка, причому його поточний стан при цьому втрачається.