Skip to content

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]:

Image

  • у [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] тут є довільними;

Image

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 у вашому браузері та, якщо він існує, видаліть його, щоб сервер створив нову сесію:

Image

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

Image

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

Image

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

Тепер збільшимо лічильник тричі:

Image

  • у [3] лічильник було збільшено, але сесія у [2] — ні. У той час як [3] відображає активний сеанс, [2] відображає сеанс, який не є активним:

Тепер перезавантажимо сторінку (F5). Після цього перезавантаження журнали мають такий вигляд:

Image

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

Сторінка, надіслана сервером, має такий вигляд:

Image

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

Тепер натиснімо на посилання [Page 1] [4]. Нова сторінка, що відображається, виглядає так:

Image

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

Image

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

Image

  • у [1-3] — значення сесії;
  • у [4-6] — реактивні методи getter та setter для елементів сесії;

Тепер натиснімо на посилання [Home] [4]. Ми отримаємо таку сторінку:

Image

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

Image

Бачимо, що й тут сесія стала активною: [2].

Перевіримо значення, яке повертає функція [this.$session()]:

Image

  • у вкладці [Vue] вибираємо поточну сторінку [Home], щоб отримати її посилання [$vm0] [3];

Потім у вкладці [Console] [4] запитуємо значення функції [$vm0.$session()]:

Image

  • у [5] бачимо, що сесія стала активною, хоча спочатку вона такою не була;
  • у [6] запитуємо значення сесії;
  • у [7-8] виявляється, що це значення також стало реактивним;

Отже, маємо несподіваний результат: якщо елемент стає реактивним на сторінці, оскільки його додано до властивостей сторінки, то він також стає реактивним на сторінках, де він не входить до властивостей.

9.9. Conclusion

Приклад [nuxt-05] показав, що можна зберігати state під час запитів до сервера. Приклад [nuxt-06] робить те саме з об’єктом, який ми назвали [session] за аналогією з веб-сесією. Ми побачили, що ця сесія може мати ті самі властивості, що й сховище [Vuex], і також стати реактивною, хоча за своєю природою вона такою не була.

То в чому ж полягає користь сховища [Vuex]? Мушу зізнатися, що наразі я її не бачу. Ймовірно, я щось пропустив. Тому, якщо є сумніви, я б порадив використовувати:

  • сховище [Vuex] для розміщення у ньому всього, що має бути спільним для сторінок клієнта, а також того, що, можливо, має бути спільним для клієнта та сервера;
  • сесійний файл cookie, якщо сховище має зберігатися під час звернення клієнта до сервера, причому сесія міститиме лише це сховище;

Приклади [nuxt-05] та [nuxt-06] мали на меті продемонструвати, як можна забезпечити безперервність роботи додатка, коли користувач примусово ініціює виклик сервера, вручну вводячи URL. Нагадаємо, що за замовчуванням у цьому випадку відбувається перезапуск додатка, причому його поточний стан при цьому втрачається.