9. Przykład [nuxt-06]: wstrzyknięcie w kontekście menedżera sesji
9.1. Présentation
Przykład [nuxt-05] pokazał, że można zachować stan magazynu danych nawet wtedy, gdy użytkownik wymusza wywołania do serwera. Elementy magazynu danych są reaktywne, dzięki czemu po włączeniu do widoków widoki te reagują na zmiany w magazynie danych. Można również chcieć zachować elementy w trakcie wymiany danych między klientem a serwerem, nie chcąc jednak, aby były one reaktywne, po prostu dlatego, że nie są wyświetlane przez widoki. Można je wówczas przechowywać w sesji, bez konieczności umieszczania ich w magazynie danych.
Dostęp do magazynu danych jest łatwy poprzez właściwości takie jak [context.app.$store] poza widokami lub [this.$store] w widokach. Chcielibyśmy mieć coś analogicznego dla sesji, coś w rodzaju [context.app.$session] lub [this.$session]. Zobaczymy, że jest to możliwe dzięki koncepcji wstrzykiwania. Nie można jednak wstrzykiwać obiektów do kontekstu, a jedynie funkcje. Będzie ona wówczas dostępna poprzez wyrażenia [context.app.$session()] lub [this.$session()].
Na koniec przedstawimy koncepcję [nuxt] opartą na [plugin].
Przykład [nuxt-06] uzyskano początkowo poprzez skopiowanie projektu [nuxt-05]:

- w [1] dodamy folder [plugins];
9.2. pojęcie wtyczki [nuxt]
[nuxt] określa jako [plugin] każdy kod wykonywany podczas uruchamiania aplikacji, jeszcze przed wykonaniem funkcji [nuxtServerInit] przez serwer, która do tej pory była pierwszą funkcją użytkownika podlegającą wykonaniu. Wtyczki aplikacji należy zadeklarować w kluczu [plugins] pliku konfiguracyjnego [nuxt.config.js]:
/*
** Plugins to load before mounting the App
*/
plugins: [
{ src: '~/plugins/client/session', mode: 'client' },
{ src: '~/plugins/server/session', mode: 'server' }
],
- wiersze 5–6: wtyczka jest określona przez ścieżkę [src] oraz tryb wykonania [mode]. [mode] może przyjmować trzy wartości:
- [client]: wtyczka musi być uruchamiana wyłącznie po stronie klienta;
- [server]: wtyczka musi być uruchamiana wyłącznie po stronie serwera;
- brak klucza [mode]: w tym przypadku wtyczka musi być uruchamiana zarówno po stronie klienta, jak i serwera;
- wiersze 5–6: umieściliśmy nasze dwie wtyczki w folderze [plugins]. Nie ma jednak takiego wymogu. Wtyczki można umieścić w dowolnym miejscu w drzewie katalogów projektu. Podobnie nazwy podfolderów [client, server] są tutaj dowolne;

9.3. Wtyczka serwera [session]
Wtyczka [server / session.js] wygląda następująco:
/* eslint-disable no-console */
export default (context, inject) => {
// zarządzanie sesją serwera
// czy istnieje już sesja?
let value = context.app.$cookies.get('session')
if (!value) {
// nowa sesja
console.log("[plugin session server], démarrage d'une nouvelle session")
value = initValue
} else {
// istniejąca sesja
console.log("[plugin session server], reprise d'une session existante")
}
// definicja sesji
const session = {
// zawartość sesji
value,
// zapis sesji w pliku cookie
save(context) {
context.app.$cookies.set('session', this.value, { path: context.base, maxAge: context.env.maxAge })
}
}
// wstrzykujemy funkcję do [context, Vue], która ustawi bieżącą sesję
inject('session', () => session)
}
// wartość początkowa sesji
const initValue = {
initSessionDone: false
}
- wiersz 2: wtyczki są uruchamiane za każdym razem, gdy następuje wywołanie serwera: podczas uruchamiania oraz za każdym razem, gdy użytkownik wymusza wywołanie serwera, wpisując ręcznie URL:
- najpierw uruchamiana jest wtyczka (lub wtyczki) serwera;
- gdy przeglądarka klienta otrzyma odpowiedź z serwera, uruchamiana jest (są) wtyczka (wtyczki) klienta;
- wiersz 2: każda wtyczka, zarówno po stronie klienta, jak i serwera, otrzymuje dwa parametry:
- [context]: kontekst serwera lub klienta, w zależności od tego, kto uruchamia wtyczkę;
- [inject]: funkcja umożliwiająca wstrzyknięcie funkcji do kontekstu serwera lub klienta;
- cel wtyczki [server / session] jest dwojaki:
- zdefiniowanie sesji (wiersze 16–23);
- zdefiniowanie w ramach tego kontekstu funkcji [$session], która zwróci jako wynik sesję z linii 16. Odbywa się to w linii 25;
- wiersze 16–23: sesja zamieści swoje dane w obiekcie [value] z wiersza 18;
- wiersze 20–22: sesja dysponuje funkcją [save], która jako parametr otrzymuje obiekt [context]. Kod wywołujący dostarcza jej ten kontekst. Korzystając z niego, funkcja [save] zapisuje wartość sesji, czyli obiekt [value], w pliku cookie sesji;
- wiersz 6: gdy wtyczka [server / session] jest uruchamiana, najpierw sprawdza, czy serwer otrzymał plik cookie sesji;
- jeśli tak, to obiekt [value] z wiersza 6 reprezentuje wartość sesji, czyli zbiór danych w niej zawartych;
- jeśli nie, w wierszach 7–11 ustala się początkową wartość sesji. Będzie to obiekt [initValue] z wierszy 29–31. Elementy sesji zostaną zdefiniowane w funkcji [nuxtServerInit], która jest wykonywana po wtyczce serwera;
- wiersz 18: zapis [value] jest skrótem dla zapisu [value:value]. [value] po lewej stronie to nazwa klucza obiektu, natomiast [value] po prawej stronie to obiekt [value] zadeklarowany w wierszu 6;
- wiersz 25: po dotarciu do tego wiersza sesja została albo utworzona, ponieważ wcześniej nie istniała, albo pobrana z żądania HTTP przeglądarki klienta;
- wiersz 25: do kontekstu serwera wstrzykuje się nową funkcję:
- pierwszym parametrem funkcji [inject] jest nazwa tworzonej funkcji, w tym przypadku „session”. Funkcja [nuxt] nada jej w rzeczywistości nazwę „$session”;
- drugim parametrem jest definicja funkcji. W tym przypadku funkcja [$session]
- nie przyjmuje żadnych parametrów;
- zwróci obiekt [session] z linii 16;
- po uruchomieniu wtyczki:
- funkcja [$session] jest dostępna w [context.app.$session], gdzie dostępny jest obiekt [context], lub [this.$session] w widoku lub w magazynie [vuex];
- funkcja [$session] zwraca obiekt [session] z unikalnym kluczem [value];
- podczas początkowego utworzenia sesji obiekt [value] posiada tylko jeden klucz [initStoreDone] (wiersze 29–31). Klucz [initStoreDone:false] służy do wskazania, że magazyn nie został jeszcze umieszczony w sesji. Zostanie to wykonane przez funkcję [nuxtServerInit];
9.4. Inicjalizacja sesji
Po uruchomieniu przez serwer wtyczki [session / server] serwer wykona następujący skrypt [store / index.js]:
/* eslint-disable no-console */
export const state = () => ({
// licznik
counter: 0
})
export const mutations = {
// zwiększenie licznika o jedną wartość [inc]
increment(state, inc) {
state.counter += inc
},
// zmiana stanu
replace(state, newState) {
for (const attr in newState) {
state[attr] = newState[attr]
}
}
}
export const actions = {
async nuxtServerInit(store, context) {
// kto wykonuje ten kod?
console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
// czekamy na zakończenie obietnicy
await new Promise(function(resolve, reject) {
// zwykle mamy tu do czynienia z funkcją asynchroniczną
// symulujemy ją, czekając jedną sekundę
setTimeout(() => {
// inicjowanie sesji
initSession(store, context)
// powodzenie
resolve()
}, 1000)
})
}
}
function initSession(store, context) {
// store to zmienna, którą należy zainicjować
// pobieramy sesję
const session = context.app.$session()
// czy sesja została już zainicjowana?
if (!session.value.initSessionDone) {
// uruchamiamy nowy magazyn
console.log("nuxtServerInit, initialisation d'une nouvelle session")
// inicjujemy magazyn
store.commit('increment', 77)
// umieszczamy magazyn w sesji
session.value.store = store.state
// inicjowana jest nowa sesja
session.value.somethingImportant = { x: 2, y: 4 }
// sesja została zainicjowana
session.value.initSessionDone = true
} else {
console.log("nuxtServerInit, reprise d'un store existant")
// aktualizujemy magazyn danymi z magazynu sesji
store.commit('replace', session.value.store)
}
// zapisujemy sesję
session.save(context)
// log
console.log('initSession terminé, store=', store.state, 'session=', session.value)
}
W porównaniu z kodem źródłowym projektu [nuxt-05] zmienia się jedynie funkcja [initSession] (wcześniej initStore) w wierszach 38–60:
- wiersz 42: pobieramy sesję za pomocą funkcji [$session], która została wstrzyknięta do kontekstu serwera;
- wiersz 44: sprawdzamy, czy sesja została już zainicjowana;
- wiersze 45–54: jeśli nie:
- wiersz 48: inicjowana jest pamięć;
- wiersz 50: stan magazynu jest zapisywany w sesji;
- wiersz 52: do sesji dodawany jest kolejny obiekt [somethingImportant]. Nie będzie on częścią magazynu;
- wiersz 54: odnotowuje się, że sesja jest teraz zainicjowana;
- wiersze 55–59: jeśli sesja była już zainicjowana:
- wiersz 58: nowa pamięć jest inicjowana zawartością sesji;
- wiersz 61: sesja jest zapisywana w pliku cookie sesji. Przypominamy, że polega to na umieszczeniu pliku cookie w odpowiedzi HTTP, którą serwer wyśle do przeglądarki klienta;
9.5. Wtyczka kliencka [client / session]
Gdy serwer wykona skrypty [plugins / server / session] i [store / index], wyśle jedną ze stron [index, page1] do przeglądarki klienta. W odpowiedzi serwera o numerze HTTP znajdzie się plik cookie sesji. Po odebraniu strony przez przeglądarkę klienta uruchomią się skrypty klienckie osadzone na stronie. Następnie uruchomi się wtyczka [client / session]:
/* eslint-disable no-console */
export default (context, inject) => {
// zarządzanie sesją klienta
// sesja na pewno istnieje, zainicjowana przez serwer
console.log('[plugin session client], reprise de la session du serveur')
// definicja sesji
const session = {
// zawartość sesji
value: context.app.$cookies.get('session'),
// zapis sesji w pliku cookie
save(context) {
context.app.$cookies.set('session', this.value, { path: context.base, maxAge: context.env.maxAge })
}
}
// do [context, Vue] wstrzykuje się funkcję, która ustanowi bieżącą sesję
inject('session', () => session)
}
- gdy wtyczka kliencka się uruchamia, plik cookie sesji został już odebrany przez przeglądarkę klienta;
- celem wtyczki [client] jest również wstrzyknięcie funkcji [$session] do kontekstu klienta. Funkcja ta przywróci sesję wysłaną przez serwer;
- wiersz 19: wstrzyknięta funkcja [$session] zwróci sesję z wierszy 9–16;
- wiersze 9–16: obiekt [session] zarządzany przez klienta. Będzie to kopia sesji wysłanej przez serwer;
- wiersz 11: wartość sesji klienta jest pobierana z pliku cookie sesji wysłanego przez serwer [nuxt];
- wiersze 13–15: podobnie jak w przypadku sesji serwera, sesja klienta posiada funkcję [save], która umożliwia zapisanie wartości sesji, [this.value] (wiersz 14), w pliku cookie sesji przechowywanym w przeglądarce;
9.6. Strona [index]
Strona [index] zmienia się w następujący sposób:
<!-- stroną [index] -->
<template>
<Layout :left="true" :right="true">
<!-- nawigacja -->
<Navigation slot="left" />
<!-- komunikat-->
<template slot="right">
<b-alert show variant="warning"> Home - session= {{ jsonSession }}, counter= {{ $store.state.counter }} </b-alert>
<!-- przycisk -->
<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',
// używane komponenty
components: {
Layout,
Navigation
},
computed: {
jsonSession() {
return JSON.stringify(this.$session().value)
}
},
// cykl życia
beforeCreate() {
// klient i serwer
console.log('[home beforeCreate]')
},
created() {
// klient i serwer
console.log('[home created], session=', this.$session().value)
},
beforeMount() {
// tylko klient
console.log('[home beforeMount]')
},
mounted() {
// tylko klient
console.log('[home mounted]')
},
// zarządzanie zdarzeniami
methods: {
incrementCounter() {
console.log('incrementCounter')
// zwiększenie licznika o 1
this.$store.commit('increment', 1)
// modyfikacja sesji
const session = this.$session()
session.value.store = this.$store.state
session.value.somethingImportant.x++
session.value.somethingImportant.y++
// zapisanie sesji w pliku cookie sesji
session.save(this.$nuxt.context)
}
}
}
</script>
Należy pamiętać, że strona ta jest wykonywana zarówno po stronie serwera, jak i po stronie klienta.
- wiersz 8: wyświetlane są teraz zarówno sesja, jak i store;
- wiersz 30: [jsonSession] jest właściwością obliczoną, która ustawia ciąg jSON na wartość sesji;
- wiersz 41: wyświetlana jest wartość sesji za pomocą wstrzykniętej funkcji [this.$session]. Funkcja ta istnieje zarówno w kontekście serwera, jak i klienta;
- wiersz 53: metoda [incrementCounter] jest wykonywana wyłącznie po stronie klienta;
- wiersz 56: licznik magazynu jest zwiększany i wyświetlany tak jak poprzednio;
- wiersz 58: sesja jest pobierana za pomocą wstrzykniętej funkcji [this.$session];
- wiersz 59: aktualizowana jest pamięć sesji;
- wiersze 60–61: zwiększane są atrybuty [somethingImportant.x, somethingImportant.y] sesji. Ma to na celu jedynie pokazanie, że sesja może służyć do przenoszenia innych danych niż magazyn;
- wiersz 63: sesja jest zapisywana w pliku cookie sesji przechowywanym w przeglądarce. W widoku klienta kontekst tego pliku jest dostępny w [this.$nuxt.context];
Celem strony [index] jest pokazanie, że sesja nie jest reaktywna, podczas gdy magazyn danych jest. Gdy zwiększymy wartości elementów sesji, zauważymy, że widok nie zostanie zaktualizowany. Widok [page1] przedstawia rozwiązanie tego problemu.
9.7. Strona [page1]
Strona [page1] powstała poprzez skopiowanie strony [index], a następnie jej nieznaczne zmodyfikowanie:
<!-- strona [index] -->
<template>
<Layout :left="true" :right="true">
<!-- nawigacja -->
<Navigation slot="left" />
<!-- komunikat-->
<template slot="right">
<b-alert show variant="warning"> Page1 - session= {{ jsonSession }}, counter= {{ $store.state.counter }} </b-alert>
<!-- przycisk -->
<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',
// używane komponenty
components: {
Layout,
Navigation
},
data() {
return {
session: {}
}
},
computed: {
jsonSession() {
return JSON.stringify(this.session.value)
}
},
// cykl życia
beforeCreate() {
// klient i serwer
console.log('[page1 beforeCreate]')
},
created() {
// klient i serwer
// umieszczamy sesję w właściwościach reaktywnych strony
this.session = this.$session()
// log
console.log('[page1 created], session=', this.session.value)
},
beforeMount() {
// tylko klient
console.log('[page1 beforeMount]')
},
mounted() {
// tylko klient
console.log('[page1 mounted]')
},
// zarządzanie zdarzeniami
methods: {
incrementCounter() {
console.log('incrementCounter')
// zwiększenie licznika o 1
this.$store.commit('increment', 1)
// modyfikacja sesji
this.session.value.store = this.$store.state
this.session.value.somethingImportant.x++
this.session.value.somethingImportant.y++
// zapisanie sesji w pliku cookie sesji
this.session.save(this.$nuxt.context)
}
}
}
</script>
- wiersz 47: główna różnica polega na tym, że bieżąca sesja jest umieszczana we właściwościach strony (wiersze 29–33). Spowoduje to, że sesja stanie się odtąd reaktywna. Gdy funkcja [incrementCounter] zwiększy liczbę elementów sesji, widok [page1] zostanie zaktualizowany;
9.8. Uruchomienie projektu
Przed uruchomieniem projektu sprawdź plik cookie sesji w swojej przeglądarce i, jeśli istnieje, usuń go, aby serwer utworzył nową sesję:

Teraz wywołajmy URL i [http://localhost:81/nuxt-06/]:

Wpisów w dzienniku przeglądarki są wówczas następujące:

- w [2] serwer rozpoczyna nową sesję we wtyczce [session] serwera;
- w [3] ta nowa sesja jest inicjowana w [nuxtServerInit];
- w [4] nowa sesja jest znana na serwerze;
- w pliku [5] klient poprawnie pobrał tę sesję;
Teraz zwiększmy licznik trzykrotnie:

- w [3] licznik został prawidłowo zwiększony, ale nie sesja w [2]. Podczas gdy [3] pokazuje aktywny sklep, [2] pokazuje sesję, która nie jest aktywna:
Teraz odświeżmy stronę (F5). Po odświeżeniu logi wyglądają następująco:

- w [2] widać, że serwer otrzymał plik cookie sesji wysłany przez przeglądarkę klienta;
- w [4] widać, że pamięć nie została zresetowana, lecz przejęta z otrzymanej sesji;
- w [4-5]: widać, że wszystkie atrybuty sesji zostały rzeczywiście zwiększone trzykrotnie;
Strona wysłana przez serwer wygląda zatem następująco:

Wniosek wynikający z tej strony jest taki, że sesja może przenosić inne elementy poza zmienną „store”, ale nie są one reaktywne.
Teraz kliknijmy link [Page 1] [4]. Wyświetli się wówczas następująca strona:

Następnie trzykrotnie kliknijmy przycisk [Incrémenter]. Strona zmienia się w następującą:

Tym razem sesja wyświetla się poprawnie jako [2]. Jest ona tutaj aktywna. Widać to w logach:

- w [1-3] – wartości sesji;
- w [4-6] – reaktywne metody getter i setter elementów sesji;
Teraz kliknijmy na link [Home] [4]. Otrzymujemy następującą stronę:

Następnie kliknijmy dwukrotnie przycisk [Incrémenter] [4]. Strona zmienia się w następującą:

Widzimy, że również w tym przypadku sesja stała się aktywna: [2].
Sprawdźmy wartość zwracaną przez funkcję [this.$session()]:

- w zakładce [Vue] wybieramy bieżącą stronę [Home], aby uzyskać jej odwołanie [$vm0] [3];
Następnie w zakładce [Console] [4] należy sprawdzić wartość funkcji [$vm0.$session()]:

- w [5] widać, że sesja stała się aktywna, podczas gdy początkowo nie była;
- w [6] sprawdzamy wartość sesji;
- w [7-8] okazuje się, że ta wartość również stała się reaktywna;
Mamy więc do czynienia z nieoczekiwanym rezultatem: jeśli element staje się reaktywny na stronie, ponieważ został umieszczony we właściwościach tej strony, to staje się on również reaktywny na stronach, na których nie jest częścią właściwości.
9.9. Conclusion
Przykład [nuxt-05] pokazał, że można zachować stan magazynu danych (store) na przestrzeni kolejnych żądań wysyłanych do serwera. Przykład [nuxt-06] robi to samo z obiektem, który nazwaliśmy [session] przez analogię do sesji internetowej. Zauważyliśmy, że ta sesja może posiadać te same właściwości co magazyn [Vuex] i również stać się reaktywna, mimo że pierwotnie nie była.
Jaki jest zatem sens istnienia magazynu [Vuex]? Muszę przyznać, że na razie nie dostrzegłem go. Prawdopodobnie coś mi umknęło. Dlatego w razie wątpliwości radziłbym używać:
- zmienną [Vuex], w której przechowywane są wszystkie dane, które muszą być współdzielone między stronami klienta, a także te, które ewentualnie muszą być współdzielone między klientem a serwerem;
- plik cookie sesji, jeśli magazyn danych ma być zachowany podczas połączenia klienta z serwerem, przy czym sesja zawiera wówczas wyłącznie ten magazyn danych;
Przykłady [nuxt-05] i [nuxt-06] miały na celu pokazanie, w jaki sposób można zapewnić ciągłość działania aplikacji, gdy użytkownik wymusza wywołanie serwera, ręcznie wpisując URL. Przypominamy, że domyślnym zachowaniem w tym przypadku jest ponowne uruchomienie aplikacji, co powoduje utratę jej aktualnego stanu.