17. Przykład [nuxt-20]: przeniesienie przykładu [vuejs-22]
17.1. Présentation
W niniejszym przykładzie zamierzamy przenieść przykład [vuejs-22], który był aplikacją [vue.js] typu SPA, do kontekstu [nuxt] SSR. [vuejs-22] była aplikacją kliencką serwera obliczającego podatek, która zawierała następujące widoki:
Pierwszy widok to widok uwierzytelniania:

Drugi ekran to ekran obliczania podatku:

Trzeci ekran to ekran wyświetlający listę symulacji wykonanych przez użytkownika:

Powyższy ekran pokazuje, że można usunąć symulację nr 1. Otrzymujemy wówczas następujący widok:

Jeśli teraz usuniemy ostatnią symulację, otrzymamy następujący widok:

Będziemy stopniowo przenosić aplikację [vuejs-22] do aplikacji [nuxt-20]. Nie będziemy ponownie wyjaśniać kodu aplikacji [vuejs-22]. Zachęcamy czytelnika do ponownego zapoznania się z dokumentem |Introduction au framework VUE.JS par l’exemple|. Poszczególne etapy powinny pokazać różnice między aplikacją [vuejs] a aplikacją [nuxt].
17.2. Krok 1
Projekt [nuxt-20] powstaje początkowo poprzez skopiowanie projektu [nuxt-12]. Jest to bowiem dobry punkt wyjścia:
- komunikuje się z serwerem obliczającym podatek;
- prawidłowo obsługuje błędy, które serwer ten wysyła;
- klient i serwer [nuxt] potrafią komunikować się za pośrednictwem sesji [nuxt];
Mamy więc dobrą infrastrukturę wyjściową. Naszym głównym zadaniem powinno być zmodyfikowanie:
- stron. Wykorzystamy te z projektu [vuejs-22], które trzeba będzie dostosować do nowego środowiska;
- zarządzanie sklepem. Powinny pojawić się dodatkowe informacje (lista symulacji), a inne mogą stać się zbędne;
- zarządzanie routingiem klienta i serwera [nuxt];
W związku z tym najpierw tworzymy projekt [nuxt-20], kopiując projekt [nuxt-12]:

Następnie usuwamy zbędne strony i komponenty [2]:
- komponent [components/navigation] znika;
- układ [layout/default] znika;
- znikają strony [index, authentification, get-admindata, fin-session];
Następnie do [nuxt-20] włączamy elementy z [vuejs-22] i [3]:
- trzy strony [Authentification, CalculImpot, ListeSimulations] z aplikacji [vuejs-22] trafiają do folderu [pages];
- komponenty [FormCalculImpot, Menu, Layout] z aplikacji [vuejs-22] trafiają do folderu [components];
- strona [Main] z [vuejs-22], która służyła jako [layout] dla aplikacji [vuejs-22], trafia do folderu [layouts];
Zmieniamy nazwy elementów zintegrowanych [4]:

- w [layouts] element [Main] stał się [default], ponieważ jest to domyślna nazwa układu aplikacji [nuxt];
- w pliku [pages] strona [Authentification] zmieniła nazwę na [index], ponieważ w aplikacji [vuejs-22] tę rolę pełniła strona [Authentification];
W tym momencie można skompilować projekt, aby sprawdzić pierwsze błędy. Modyfikujemy plik [nuxt.config] z przykładu [nuxt-12], aby odtąd uruchamiał [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: [
// Dokumentacja: https://github.com/nuxt-community/eslint-module
'@nuxtjs/eslint-module'
],
/*
** Nuxt.js modules
*/
modules: [
// Dokumentacja: https://bootstrap-vue.js.org
'bootstrap-vue/nuxt',
// Dokumentacja: 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) {}
},
// katalog kodu źródłowego
srcDir: 'nuxt-20',
// router
router: {
// katalog główny aplikacji URL
base: '/nuxt-20/',
// oprogramowanie pośredniczące routingu
middleware: ['routing']
},
// serwer
server: {
// port usługi, domyślnie 3000
port: 81,
// adresy sieciowe, na których nasłuchuje aplikacja, domyślnie localhost: 127.0.0.1
// 0.0.0.0 = wszystkie adresy sieciowe komputera
host: 'localhost'
},
// środowisko
env: {
// konfiguracja axios
timeout: 2000,
withCredentials: true,
baseURL: 'http://„localhost/php7/scripts-web/impots/version-14”,
// konfiguracja pliku cookie sesji [nuxt]
maxAge: 60 * 5
}
}
Następnie kompilujemy projekt jako [build]:

Zgłoszone błędy są następujące:
- błąd w wierszu 1 wskazuje, że odwołujemy się do nieistniejącego obrazu. Pobierzemy go w [vuejs-22];
- błąd w wierszu 2 wskazuje, że komponent [./FormCalculImpot] nie istnieje. Rzeczywiście, komponent ten znajduje się teraz w [@/components/form-calcul-impot];
- błędy w wierszach dotyczących pliku [3-5] wskazują, że komponent [./Layout] nie istnieje. Rzeczywiście, komponent ten znajduje się teraz w pliku [@/components/layout];
- błędy w wierszach [6-7] wskazują, że komponent [./Menu] nie istnieje. Rzeczywiście, nazywa się on teraz [@/components/menu];
Dodajemy obraz [assets/logo.jpg] do projektu [nuxt-20]:

Ponadto na wszystkich stronach poprawimy ścieżki dostępu do komponentów. Weźmy na przykład stronę [calcul-impot]:
<!-- definicja widoku HTML -->
<template>
<div>
<Layout :left="true" :right="true">
<!-- formularz obliczania podatku po prawej stronie -->
<FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
<!-- menu nawigacyjne po lewej stronie -->
<Menu slot="left" :options="options" />
</Layout>
<!-- obszar wyświetlania wyników obliczeń podatku pod formularzem -->
<b-row v-if="résultatObtenu" class="mt-3">
<!-- pusty obszar z trzema kolumnami -->
<b-col sm="3" />
<!-- obszar z dziewięcioma kolumnami -->
<b-col sm="9">
<b-alert show variant="success">
<span v-html="résultat"></span>
</b-alert>
</b-col>
</b-row>
</div>
</template>
<script>
// importy
import FormCalculImpot from './FormCalculImpot'
import Menu from './Menu'
import Layout from './Layout'
export default {
// wykorzystane komponenty
components: {
Layout,
FormCalculImpot,
Menu
},
Trzy elementy [import] w wierszach 26–28 zmienimy na:
// importy
import FormCalculImpot from '@/components/form-calcul-impot'
import Menu from '@/components/menu'
import Layout from '@/components/layout'
W ten sposób sprawdzamy i ewentualnie poprawiamy wartości [import] we wszystkich komponentach, układach i stronach. Po wprowadzeniu tych poprawek można spróbować ponownie wygenerować [build]. Zazwyczaj nie ma już żadnych błędów.
Można wówczas spróbować uruchomić:

Pojawiają się błędy:
Widać, że polecenie [dev] w połączeniu z modułem [eslint] jest bardziej rygorystyczne pod względem składniowym niż polecenie [build]. W tym przypadku wymaga ona, aby operator porównania [!=] został zapisany jako [!==], który jest operatorem bardziej rygorystycznym (sprawdza on również typ operandów). Błędy te występują na stronie [index.vue].
Poprawiamy powyższe błędy i ponownie uruchamiamy projekt. Pojawia się wówczas ostrzeżenie dotyczące modułu [eslint]:

Poprawiamy ten błąd za pomocą modułu [Quick fix] z modułu [eslint] [2].
Ponownie uruchamiamy projekt. Nie ma już błędu kompilacji. Następnie wywołujemy pliki URL i [http://localhost:81/nuxt-20/] w przeglądarce. Pojawia się błąd wykonania:

Błąd występuje w plikach [index.vue] i [2]. Błąd [1] wynika z faktu, że w pliku [vuejs-22] warstwa [dao] była dostępna w [this.$dao], podczas gdy w [nuxt-12], którego infrastrukturę przyjęliśmy, jest ona dostępna w funkcji [this.$dao()].
Błąd występuje w funkcji [created] w cyklu życia strony [index]:

Na razie wystarczy zmienić nazwę funkcji [created] na [created2], aby funkcja cyklu życia [created] nie była wykonywana jako [3].
Zapisujemy zmianę i ponownie ładujemy stronę [index] w przeglądarce. Tym razem wszystko działa poprawnie:

17.3. krok 2
Strony projektu [vuejs-22] wykorzystywały następujące wstawione elementy:
- $dao: dla warstwy [dao] klienta [vue.js];
- $session: dla sesji zapisanej w przeglądarce [localStorage];
Elementy te nie istnieją już w infrastrukturze projektu [nuxt-12], którą skopiowaliśmy:
- obecnie istnieją dwie warstwy [dao], jedna dla klienta [nuxt], a druga dla serwera [nuxt]. Obie są dostępne za pośrednictwem wstrzykniętej funkcji o nazwie [$dao]. Oznacza to, że na stronach aplikacji [this.$dao] należy zastąpić na [this.$dao()];
- sesja [nuxt] zarządzana przez aplikację [nuxt-20] nie ma już nic wspólnego z obiektem [$session] waplikacji [vuejs-22], w której nie istniała koncepcja pliku cookie sesji. Niemniej jednak pełnią one podobną funkcję: przechowują trwałe informacje w miarę wykonywania czynności przez użytkownika. Sesja [nuxt] przechowuje informacje w magazynie danych, a nie bezpośrednio w sesji. Na stronach aplikacji [this.$session] należy zastąpić na [this.$store], gdy chodzi o zapisywanie informacji w sesji, oraz na [this.$session()], gdy chodzi o manipulowanie samą sesją;
- aby sprawdzić stan właściwości P magazynu, należy wpisać [this.$store.state.P];
- aby zmienić właściwość P magazynu, należy wpisać [this.$store.commit(‘replace’, {P:value}];
Zmiany te wprowadzamy na stronie [index]:
<!-- definicja widoku HTML -->
<template>
<Layout :left="false" :right="true">
<template slot="right">
<!-- formularz HTML – wartości są przesyłane za pomocą akcji [authentifier-utilisateur] -->
<b-form @submit.prevent="login">
<!-- tytuł -->
<b-alert show variant="primary">
<h4>Bienvenue. Veuillez vous authentifier pour vous connecter</h4>
</b-alert>
<!-- pierwszy wiersz -->
<b-form-group label="Nom d'utilisateur" label-for="user" label-cols="3">
<!-- pole wprowadzania danych użytkownika -->
<b-col cols="6">
<b-form-input id="user" v-model="user" type="text" placeholder="Nom d'utilisateur" />
</b-col>
</b-form-group>
<!-- drugi wiersz -->
<b-form-group label="Mot de passe" label-for="password" label-cols="3">
<!-- pole wprowadzania hasła -->
<b-col cols="6">
<b-input id="password" v-model="password" type="password" placeholder="Mot de passe" />
</b-col>
</b-form-group>
<!-- trzeci wiersz -->
<b-alert v-if="showError" show variant="danger" class="mt-3">L'erreur suivante s'est produite : {{ message }}</b-alert>
<!-- przycisk typu [submit] w trzecim wierszu -->
<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>
<!-- dynamika widoku -->
<script>
/* eslint-disable no-console */
import Layout from '@/components/layout'
export default {
// używane komponenty
components: {
Layout
},
// stan komponentu
data() {
return {
// użytkownik
user: '',
// jego hasło
password: '',
// kontroluje wyświetlanie komunikatu o błędzie
showError: false,
// komunikat o błędzie
message: ''
}
},
// właściwości obliczone
computed: {
// prawidłowe dane wejściowe
valid() {
return this.user && this.password && this.$store.state.started
}
},
// cykl życia: komponent został właśnie utworzony
mounted() {
// eslint-disable-next-line
console.log("Authentification mounted");
// czy użytkownik może przeprowadzać symulacje?
if (this.$store.state.started && this.$store.state.authenticated && this.$métier.taxAdminData) {
// w takim razie użytkownik może przeprowadzać symulacje
this.$router.push({ name: 'calculImpot' })
// powrót do pętli zdarzeń
return
}
// jeśli sesja jSON została już uruchomiona, nie uruchamia się jej ponownie
if (!this.$store.state.started) {
// rozpoczęcie oczekiwania
this.$emit('loading', true)
// inicjowanie sesji z serwerem – żądanie asynchroniczne
// wykorzystuje się obietnicę zwróconą przez metody warstwy [dao]
this.$dao()
// inicjowanie sesji jSON
.initSession()
// otrzymano odpowiedź
.then((response) => {
// koniec oczekiwania
this.$emit('loading', false)
// analiza odpowiedzi
if (response.état !== 700) {
// wyświetlono błąd
this.message = response.réponse
this.showError = true
// powrót do pętli zdarzeń
return
}
// rozpoczęto sesję
this.$store.commit('replace', { started: true })
console.log('[authentification], session=', this.$session())
})
// w przypadku błędu
.catch((error) => {
// błąd jest przekazywany do widoku [Main]
this.$emit('error', error)
})
// we wszystkich przypadkach
.finally(() => {
// zapisuje się sesję
this.$session().save()
})
}
},
// menedżerzy zdarzeń
methods: {
// ----------- uwierzytelnianie
async login() {
try {
// początek oczekiwania
this.$emit('loading', true)
// nie jesteśmy jeszcze uwierzytelnieni
this.$store.commit('replace', { authenticated: false })
// blokujące uwierzytelnianie na serwerze
const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
// koniec ładowania
this.$emit('loading', false)
// analiza odpowiedzi serwera
if (response.état !== 200) {
// wyświetlanie błędu
this.message = response.réponse
this.showError = true
// powrót do pętli zdarzeń
return
}
// brak błędu
this.showError = false
// autoryzacja zakończona sukcesem
this.$store.commit('replace', { authenticated: true })
// --------- teraz wysyłamy zapytanie o dane do urzędu skarbowego
// na początku brak danych
this.$métier.setTaxAdminData(null)
// początek oczekiwania
this.$emit('loading', true)
// żądanie blokujące wysłane do serwera
const response2 = await this.$dao().getAdminData()
// koniec ładowania
this.$emit('loading', false)
// analiza odpowiedzi
if (response2.état !== 1000) {
// wyświetlanie błędu
this.message = response2.réponse
this.showError = true
// powrót do pętli zdarzeń
return
}
// brak błędu
this.showError = false
// zapisano otrzymane dane w warstwie [métier]
this.$métier.setTaxAdminData(response2.réponse)
// można przejść do obliczania podatku
this.$router.push({ name: 'calculImpot' })
} catch (error) {
// błąd jest przekazywany do głównego komponentu
this.$emit('error', error)
} finally {
// aktualizacja pamięci
this.$store.commit('replace', { métier: this.$métier })
// zapisano sesję
this.$session().save()
}
}
}
}
</script>
Zwróćmy uwagę na następujące kwestie:
- wiersz 69: funkcja [created2] została przemianowana na [mounted], aby serwer [nuxt] jej nie uruchamiał (nie uruchamia on ani [beforeMount], ani [mounted]). Tylko klient [nuxt] uruchomi ten plik, tak jak miało to miejsce w przypadku przykładu [vuejs-22];
- wiersz 73: odwołujemy się do [this.$métier], który na razie nie istnieje;
- wiersz 75: nigdy nie używaliśmy tej metody w aplikacji [nuxt]. Trzeba sprawdzić, czy działa ona w kontekście [nuxt];
- wiersz 112, 172: w [vuejs-22] sesja projektu była zapisywana w ten sposób. W przypadku projektu [nuxt-20] metoda [save] musi otrzymać bieżący kontekst. Wiadomo, że na stronie [nuxt] obiekt [context] jest dostępny w [this.$nuxt.context];
W związku z tym wiersze 112 i 172 zostają przepisane w następujący sposób:
this.$session().save(this.$nuxt.context)
Należy zauważyć, że kod ten nie jest zoptymalizowany. Zamiast wielokrotnego wywoływania funkcji [this.$session()], lepiej byłoby zapisać:
const session=this.$session()
a następnie użyć zmiennej [session]. To samo rozumowanie można zastosować w przypadku funkcji [this.$dao()].
Po wprowadzeniu tych poprawek możemy ponownie załadować URL i [http://localhost:81/nuxt-20/] w przeglądarce. Nadal otrzymujemy tę samą stronę, co poprzednio:

Przyjrzyjmy się logom przeglądarki:

Log [1] jest ostatnim logiem wygenerowanym przez klienta [nuxt]. W [2] widać, że właściwość [started] ma wartość [vrai], co oznacza, że funkcja [mounted] pomyślnie uruchomiła sesję jSON z serwerem obliczającym podatek. Widać również, że magazyn danych posiada właściwości, które należy albo usunąć, albo przemianować. Przypomnijmy, że korzystamy z magazynu danych z przykładu [nuxt-12].
Teraz ponownie wywołajmy sesję URL [http://localhost:81/nuxt-20/], gdy serwer obliczający podatek nie jest uruchomiony. Najpierw należy usunąć plik cookie sesji [nuxt]:

Powyższy zrzut ekranu pochodzi z przeglądarki Chrome. Po wykonaniu tej czynności pliki URL i [http://localhost:81/nuxt-20/] dają następujący wynik:

Błąd został poprawnie obsłużony przez projekt [vuejs-22]. Nadal jest on poprawnie obsługiwany przez projekt [nuxt-20].
17.4. Krok 3
Teraz, gdy mamy już stronę uwierzytelniającą, należy przyjrzeć się kodowi wykonywanemu po kliknięciu przez użytkownika przycisku [Valider]:
// menedżery zdarzeń
methods: {
// ----------- uwierzytelnianie
async login() {
try {
// początek oczekiwania
this.$emit('loading', true)
// nie jesteśmy jeszcze uwierzytelnieni
this.$store.commit('replace', { authenticated: false })
// blokujące uwierzytelnianie na serwerze
const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
// koniec ładowania
this.$emit('loading', false)
// analiza odpowiedzi serwera
if (response.état !== 200) {
// wyświetlanie błędu
this.message = response.réponse
this.showError = true
// powrót do pętli zdarzeń
return
}
// brak błędu
this.showError = false
// autoryzacja zakończona sukcesem
this.$store.commit('replace', { authenticated: true })
// --------- teraz wysyłamy zapytanie o dane do urzędu skarbowego
// na początku brak danych
this.$métier.setTaxAdminData(null)
// początek oczekiwania
this.$emit('loading', true)
// żądanie blokujące wysłane do serwera
const response2 = await this.$dao().getAdminData()
// koniec ładowania
this.$emit('loading', false)
// analiza odpowiedzi
if (response2.état !== 1000) {
// wyświetlanie błędu
this.message = response2.réponse
this.showError = true
// powrót do pętli zdarzeń
return
}
// brak błędu
this.showError = false
// zapisano otrzymane dane w warstwie [métier]
this.$métier.setTaxAdminData(response2.réponse)
// można przejść do obliczania podatku
this.$router.push({ name: 'calculImpot' })
} catch (error) {
// błąd jest przekazywany do głównego komponentu
this.$emit('error', error)
} finally {
// aktualizacja pamięci
this.$store.commit('replace', { métier: this.$métier })
// zapisujemy sesję
this.$session().save(this.$nuxt.context)
}
}
}
Głównym problemem wydaje się tutaj brak danych o nazwie [this.$métier]. Aby to naprawić, wykonamy następujące czynności:
- dołączymy klasę [Métier] z przykładu [vuejs-22]. Umieścimy ją w folderze [api];
- wstrzykniemy funkcję [$métier] do kontekstu klienta [nuxt], która zapewni dostęp do tej klasy;
Najpierw skopiujemy klasę [Métier] do folderu [api]:

Gdy klasa [Métier] znajdzie się już w projekcie, należy utworzyć nową wtyczkę dla klienta [nuxt]. Ta wtyczka o nazwie [pluginMétier] wprowadzi funkcję [$métier], która zapewni dostęp do klasy [Métier]:
/* eslint-disable no-console */
// tworzy się punkt dostępu do warstwy [métier]
import Métier from '@/api/client/Métier'
export default (context, inject) => {
// instancjonowanie warstwy [métier]
const métier = new Métier()
// wstrzyknięcie funkcji [$métier] do kontekstu
inject('métier', () => métier)
// log
console.log('[fonction client $métier créée]')
}
Po wykonaniu tej czynności możemy poprawić stronę [index]:
// cykl życia: komponent został właśnie utworzony
mounted() {
// eslint-disable-next-line
console.log("Authentification mounted");
// czy użytkownik może przeprowadzać symulacje?
if (this.$store.state.started && this.$store.state.authenticated && this.$métier().taxAdminData) {
// w takim razie użytkownik może przeprowadzać symulacje
this.$router.push({ name: 'calcul-impot' })
// powrót do pętli zdarzeń
return
}
// jeśli sesja jSON została już uruchomiona, nie uruchamia się jej ponownie
...
},
// obsługa zdarzeń
methods: {
// ----------- uwierzytelnianie
async login() {
try {
// początek oczekiwania
this.$emit('loading', true)
// nie jesteśmy jeszcze uwierzytelnieni
this.$store.commit('replace', { authenticated: false })
// blokujące uwierzytelnianie na serwerze
const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
// koniec ładowania
this.$emit('loading', false)
// analiza odpowiedzi serwera
if (response.état !== 200) {
// wyświetlanie błędu
this.message = response.réponse
this.showError = true
// powrót do pętli zdarzeń
return
}
// brak błędu
this.showError = false
// autoryzacja zakończona sukcesem
this.$store.commit('replace', { authenticated: true })
// --------- teraz wysyłamy zapytanie o dane do urzędu skarbowego
// na początku brak danych
this.$métier().setTaxAdminData(null)
// początek oczekiwania
this.$emit('loading', true)
// żądanie blokujące wysłane do serwera
const response2 = await this.$dao().getAdminData()
// koniec ładowania
this.$emit('loading', false)
// analiza odpowiedzi
if (response2.état !== 1000) {
// wyświetlanie błędu
this.message = response2.réponse
this.showError = true
// powrót do pętli zdarzeń
return
}
// brak błędu
this.showError = false
// zapisano otrzymane dane w warstwie [métier]
this.$métier().setTaxAdminData(response2.réponse)
// można przejść do obliczania podatku
this.$router.push({ name: 'calcul-impot' })
} catch (error) {
// błąd jest przekazywany do głównego komponentu
this.$emit('error', error)
} finally {
// aktualizacja pamięci
this.$store.commit('replace', { métier: this.$métier() })
// zapisano sesję
this.$session().save(this.$nuxt.context)
}
}
}
- w wierszach 43, 61, 69 nazwa [this.$métier] została zastąpiona przez [this.$métier()];
- wiersze 8, 63: nazwa strony [CalculImpot] z projektu [vuejs-22] została zmieniona na stronę [calcul-impot] w projekcie [nuxt-20];
Po wprowadzeniu tych poprawek można spróbować zatwierdzić stronę uwierzytelniającą:

Otrzymana strona wygląda następująco:

Udało nam się wyświetlić stronę obliczania podatku. Teraz przyjrzyjmy się logom:

W [2] widać, że fakt uwierzytelnienia został poprawnie zapisany. W pliku [3-4] widać, że pobrano dane [taxAdminData], które umożliwiają obliczenie podatku za pomocą klasy [Métier].
17.5. Krok 4
Przyjrzyjmy się otrzymanej stronie [calcul-impot]:
<!-- definicja widoku HTML -->
<template>
<div>
<Layout :left="true" :right="true">
<!-- formularz obliczania podatku po prawej stronie -->
<FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
<!-- menu nawigacyjne po lewej stronie -->
<Menu slot="left" :options="options" />
</Layout>
<!-- obszar wyświetlania wyników obliczeń podatku pod formularzem -->
<b-row v-if="résultatObtenu" class="mt-3">
<!-- pusty obszar z trzema kolumnami -->
<b-col sm="3" />
<!-- obszar z dziewięcioma kolumnami -->
<b-col sm="9">
<b-alert show variant="success">
<span v-html="résultat"></span>
</b-alert>
</b-col>
</b-row>
</div>
</template>
<script>
// importy
import FormCalculImpot from '@/components/form-calcul-impot'
import Menu from '@/components/menu'
import Layout from '@/components/layout'
export default {
// wykorzystane komponenty
components: {
Layout,
FormCalculImpot,
Menu
},
// raport wewnętrzny
data() {
return {
// opcje menu
options: [
{
text: 'Liste des simulations',
path: '/liste-des-simulations'
},
{
text: 'Fin de session',
path: '/fin-session'
}
],
// wynik obliczenia podatku
résultat: '',
résultatObtenu: false
}
},
// cykl życia
created() {
// eslint-disable-next-line
console.log("CalculImpot created");
},
// metody obsługi zdarzeń
methods: {
// wynik obliczenia podatku
handleResultatObtenu(résultat) {
// tworzymy wynik jako ciąg znaków 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
// wyświetlanie wyniku
this.résultatObtenu = true
// ---- aktualizacja sklepu [Vuex]
// symulacja dodania
this.$store.commit('addSimulation', résultat)
// zapisujemy sesję
this.$session.save()
}
}
}
</script>
- wiersze 44 i 48: linki w menu nawigacyjnym są poprawne. Strona [/fin-session] nie istnieje. Projekt [vuejs-22] rozwiązywał ten problem za pomocą przekierowań. Postąpimy tak samo w przypadku projektu [nuxt-20];
- wiersz 76: odwołujemy się do mutacji [addSimulation], która na razie nie istnieje. Utworzymy ją;
- wiersz 78: podobnie jak na stronie [index], należy wpisać [this.$session().save(this.$nuxt.context)];
Zmodyfikujmy plik [store/index]. Pochodzi on z projektu [nuxt-12] i obecnie ma następującą postać:
/* eslint-disable no-console */
// stan sklepu
export const state = () => ({
// rozpoczęto sesję jSON
jsonSessionStarted: false,
// użytkownik uwierzytelniony
userAuthenticated: false,
// plik cookie sesji PHP
phpSessionCookie: '',
// adminData
adminData: ''
})
// zmiany w sklepie
export const mutations = {
// aktualizacja stanu
replace(state, newState) {
for (const attr in newState) {
state[attr] = newState[attr]
}
},
// reset pamięci
reset() {
this.commit('replace', { jsonSessionStarted: false, userAuthenticated: false, phpSessionCookie: '', adminData: '' })
}
}
// operacje na pamięci
export const actions = {
nuxtServerInit(store, context) {
// kto wykonuje ten kod?
console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
// inicjowanie sesji
initStore(store, context)
}
}
function initStore(store, context) {
// pamięć to pamięć, którą należy zainicjować
// pobieramy sesję
const session = context.app.$session()
// czy sesja została już zainicjowana?
if (!session.value.initStoreDone) {
// uruchamiamy nowy obiekt przechowujący
console.log("nuxtServerInit, initialisation d'un nouveau store")
// umieszczamy magazyn w sesji
session.value.store = store.state
// magazyn jest teraz zainicjowany
session.value.initStoreDone = 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('initStore terminé, store=', store.state)
}
- wiersze 3–27: przejmujemy stan i zmiany z aplikacji [vuejs-22] (patrz dokument [3]):
// stan magazynu
export const state = () => ({
// rozpoczęto sesję jSON
started: false,
// użytkownik uwierzytelniony
authenticated: false,
// plik cookie sesji PHP
phpSessionCookie: '',
// lista symulacji
simulations: [],
// numer ostatniej symulacji
idSimulation: 0,
// warstwa [métier]
métier: null
})
// zmiany w roletach
export const mutations = {
// zmiana stanu
replace(state, newState) {
for (const attr in newState) {
state[attr] = newState[attr]
}
},
// reset pamięci
reset() {
this.commit('replace', { started: false, authenticated: false, phpSessionCookie: '', idSimulation: 0, simulations: [], métier: null })
},
// usunięcie wiersza o numerze indeksu
deleteSimulation(state, index) {
// eslint-disable-next-line no-console
console.log('mutation deleteSimulation')
// usuwamy wiersz nr [index]
state.simulations.splice(index, 1)
console.log('store simulations', state.simulations)
},
// dodano symulację
addSimulation(state, simulation) {
// eslint-disable-next-line no-console
console.log('mutation addSimulation')
// numer symulacji
state.idSimulation++
simulation.id = state.idSimulation
// dodajemy symulację do tabeli symulacji
state.simulations.push(simulation)
}
}
- wiersze 4 i 6: wprowadzamy właściwości, które były już używane;
- wiersz 8: zachowujemy plik cookie sesji PHP. Jest to niezbędne, aby klient i serwer [nuxt] miały tę samą sesję PHP z serwerem obliczającym podatek;
- wiersz 10: lista symulacji wykonanych przez użytkownika;
- wiersz 12: numer ostatniej symulacji przeprowadzonej przez użytkownika;
- wiersz 14: warstwa [métier];
- wiersze 30–47: zmiany obecne w magazynie projektu [vuejs-22] i odwołujące się do stron aplikacji. Projekt [vuejs-22] zawierał zmianę o nazwie [clear], która usuwała listę symulacji. Nie dodajemy jej, ponieważ już istniejąca mutacja [reset] powinna wystarczyć;
- wiersze 26–28: mutacja [reset] została zmodyfikowana w celu uwzględnienia nowej zawartości stanu;
Strona [calcul-impot] wykorzystuje następujący komponent [form-calcul-impot]:
<!-- definicja widoku HTML -->
<template>
<!-- formularz HTML -->
<b-form @submit.prevent="calculerImpot" class="mb-3">
<!-- komunikat w 12 kolumnach na niebieskim tle -->
<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>
<!-- elementy formularza -->
<!-- pierwszy wiersz -->
<b-form-group label="Etes-vous marié(e) ou pacsé(e) ?">
<!-- przyciski opcji w 5 kolumnach-->
<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>
<!-- drugi wiersz -->
<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>
<!-- ewentualny komunikat o błędzie -->
<b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
</b-form-group>
<!-- trzeci wiersz -->
<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>
<!-- ewentualny komunikat o błędzie -->
<b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
</b-form-group>
<!-- czwarty wiersz, przycisk [submit] -->
<b-col sm="3">
<b-button :disabled="formInvalide" type="submit" variant="primary">Valider</b-button>
</b-col>
</b-form>
</template>
<!-- skrypt -->
<script>
export default {
// stan wewnętrzny
data() {
return {
// stan cywilny
marié: 'non',
// liczba dzieci
enfants: '',
// roczne wynagrodzenie
salaire: ''
}
},
// obliczony stan wewnętrzny
computed: {
// zatwierdzenie formularza
formInvalide() {
return (
// wynagrodzenie nieprawidłowe
!this.salaire.match(/^\s*\d+\s*$/) ||
// lub nieprawidłowa liczba dzieci
!this.enfants.match(/^\s*\d+\s*$/) ||
// lub brak danych podatkowych
!this.$métier.taxAdminData
)
},
// weryfikacja wynagrodzenia
salaireValide() {
// musi być liczbą >=0
return Boolean(this.salaire.match(/^\s*\d+\s*$/) || this.salaire.match(/^\s*$/))
},
// weryfikacja danych dotyczących dzieci
enfantsValide() {
// musi być liczbą >=0
return Boolean(this.enfants.match(/^\s*\d+\s*$/) || this.enfants.match(/^\s*$/))
}
},
// cykl życia
created() {
// log
// eslint-disable-next-line
console.log("FormCalculImpot created");
},
// menedżer zdarzeń
methods: {
calculerImpot() {
// podatek oblicza się przy użyciu warstwy [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);
// uzupełnia się wynik
résultat.marié = this.marié
résultat.enfants = this.enfants
résultat.salaire = this.salaire
// wysyłamy zdarzenie [resultatObtenu]
this.$emit('resultatObtenu', résultat)
}
}
}
</script>
- wiersze 65, 89: odwołanie [this.$métier] należy zmienić na [this.$métier()];
Po wprowadzeniu tych poprawek można przeprowadzić symulację:

Otrzymujemy następującą odpowiedź:

Jeśli przyjrzymy się logom:

- w [9-10] widać, że pierwsza symulacja rzeczywiście znajduje się w [store];
- w pliku [5] numer ostatniej symulacji został prawidłowo zwiększony;
17.6. krok 5
Teraz, gdy przeprowadziliśmy symulację, kliknijmy na link [Liste des simulations]. Otrzymujemy następującą stronę:

Przekierowanie klienta [nuxt] przebiegło poprawnie. Przyjrzyjmy się kodowi strony [liste-des-simulations]:
<!-- definicja widoku HTML -->
<template>
<div>
<!-- układ strony -->
<Layout :left="true" :right="true">
<!-- symulacje w prawej kolumnie -->
<template slot="right">
<template v-if="simulations.length == 0">
<!-- brak symulacji -->
<b-alert show variant="primary">
<h4>Votre liste de simulations est vide</h4>
</b-alert>
</template>
<template v-if="simulations.length != 0">
<!-- są symulacje -->
<b-alert show variant="primary">
<h4>Liste de vos simulations</h4>
</b-alert>
<!-- tabela symulacji -->
<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 nawigacyjne w lewej kolumnie -->
<Menu slot="left" :options="options" />
</Layout>
</div>
</template>
<script>
// importy
import Layout from '@/components/layout'
import Menu from '@/components/menu'
export default {
// komponenty
components: {
Layout,
Menu
},
// stan wewnętrzny
data() {
return {
// opcje menu nawigacyjnego
options: [
{
text: "Calcul de l'impôt",
path: '/calcul-impot'
},
{
text: 'Fin de session',
path: '/fin-session'
}
],
// parametry tabeli 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' }
]
}
},
// obliczony stan wewnętrzny
computed: {
// lista symulacji pobranych z magazynu Vuex
simulations() {
return this.$store.state.simulations
}
},
// cykl życia
created() {
// eslint-disable-next-line
console.log("ListeSimulations created");
},
// metody
methods: {
supprimerSimulation(index) {
// eslint-disable-next-line
console.log("supprimerSimulation", index);
// usunięcie symulacji nr [index]
this.$store.commit('deleteSimulation', index)
// zapisujemy sesję
this.$session.save()
}
}
}
</script>
- wiersze 47–56: pozycje w menu nawigacyjnym są poprawne;
- wiersz 75: listwa nawigacyjna jest poprawnie odwołana;
- wiersz 89: użyto mutacji [deleteSimulation], którą dodaliśmy w poprzednim etapie;
- wiersz 91: ten wiersz należy przepisać jako [this.$session().save(this.$nuxt.context)];
Wprowadzamy niezbędne zmiany, a następnie próbujemy usunąć wyświetlaną symulację:

Otrzymujemy wówczas następującą stronę:

Przyjrzyjmy się logom:

- w [6] widać, że tabela symulacji jest pusta;
Teraz wróćmy do formularza obliczania podatku:

Otrzymujemy następującą stronę:

Oznacza to, że przekierowanie zadziałało.
17.7. Krok 6
Pozostaje nam jeszcze skonfigurować opcję nawigacji [Fin de session] w menu nawigacyjnym:
// opcje menu
options: [
{
text: 'Liste des simulations',
path: '/liste-des-simulations'
},
{
text: 'Fin de session',
path: '/fin-session'
}
]
- w wierszu 9 strona [/fin-session] nie istnieje. Projekt [vuejs-22] obsługiwał ten przypadek za pomocą reguł routingu zawartych w pliku [router.js]:
// importy
import Vue from 'vue'
import VueRouter from 'vue-router'
// widoki
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'
import NotFound from './views/NotFound'
// sesja
import session from './session'
// wtyczka routingu
Vue.use(VueRouter)
// trasy aplikacji
const routes = [
// uwierzytelnianie
{ path: '/', name: 'authentification', component: Authentification },
{ path: '/authentification', name: 'authentification2', component: Authentification },
// obliczanie podatku
{
path: '/calcul-impot', name: 'calculImpot', component: CalculImpot,
meta: { authenticated: true }
},
// lista symulacji
{
path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations,
meta: { authenticated: true }
},
// zakończenie sesji
{
path: '/fin-session', name: 'finSession'
},
// nieznana strona
{
path: '*', name: 'notFound', component: NotFound,
},
]
// router
const router = new VueRouter({
// trasy
routes,
// tryb wyświetlania URL
mode: 'history',
// podstawowy URL aplikacji
base: '/client-vuejs-impot/'
})
// weryfikacja tras
router.beforeEach((to, from, next) => {
// eslint-disable-next-line no-console
console.log("router to=", to, "from=", from);
// trasa zarezerwowana dla uwierzytelnionych użytkowników?
if (to.meta.authenticated && !session.authenticated) {
next({
// przechodzimy do uwierzytelniania
name: 'authentification',
})
// powrót do pętli zdarzeń
return;
}
// szczególny przypadek zakończenia sesji
if (to.name === "finSession") {
// czyścimy sesję
session.clear();
// przechodzimy do widoku [authentification]
next({
name: 'authentification',
})
// powrót do pętli zdarzeń
return;
}
// inne przypadki – następny normalny widok routingu
next();
})
// eksport routera
export default router
- wiersze 64–76 obsługiwały szczególny przypadek trasy prowadzącej do ścieżki [/fin-session];
- wiersz 66: czyszczenie bieżącej sesji;
- wiersze 68–70: wyświetla się widok [authentification];
Spróbujemy zrobić coś podobnego w pliku routingu klienta [nuxt]:

Skrypt [client/routing.js] wygląda następująco:
/* eslint-disable no-console */
export default function(context) {
// kto wykonuje ten kod?
console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
// obsługa pliku cookie sesji PHP w przeglądarce
// plik cookie sesji PHP w przeglądarce musi być identyczny z tym znalezionym w sesji Nuxt
// akcja [fin-session] otrzymuje nowy plik cookie PHP (zarówno serwer, jak i klient Nuxt)
// jeśli otrzymuje go serwer, klient musi przekazać go przeglądarce
// w celu własnej komunikacji z serwerem PHP
// mamy tu do czynienia z routingiem po stronie klienta
// pobieramy plik cookie sesji PHP
const phpSessionCookie = context.store.state.phpSessionCookie
if (phpSessionCookie) {
// jeśli istnieje, przypisujemy plik cookie sesji PHP do przeglądarki
document.cookie = phpSessionCookie
}
// dokąd zmierzamy?
const to = context.route.path
if (to === '/fin-session') {
// czyścimy sesję
const session = context.app.$session()
session.reset(context)
// przekierowujemy na stronę główną
context.redirect({ name: 'index' })
}
}
- do istniejącego kodu dodaliśmy wiersze [19-27];
- wiersz 20: pobieramy [path] z celu bieżącej trasy;
- wiersz 21: sprawdzamy, czy jest to [/fin-session]. Jeśli tak:
- wiersze 23–24: sesja zostaje zresetowana;
- wiersz 26: przekierowuje się klienta o identyfikatorze [nuxt] na stronę główną;
Metoda [session.reset(context)] (wiersz 24) sesji wygląda następująco:
// reset sesji
reset(context) {
console.log('nuxt-session reset')
// resetowanie bufora
context.store.commit('reset')
// zapis nowego magazynu w sesji i zapis sesji
this.save(context)
}
Metoda [context.store.commit('reset')] (wiersz 5) ma następującą postać:
// resetowanie pamięci
reset() {
this.commit('replace', { started: false, authenticated: false, phpSessionCookie: '', idSimulation: 0, simulations: [], métier: null })
}
Po kliknięciu linku [Fin de session] wyświetla się strona główna wraz z następującymi logami:

- w [3] widać, że nie jesteśmy już uwierzytelnieni;
- w [4] widać, że sesja jSON została uruchomiona;
- w [6] warstwa [métier] nie występuje już w pamięci (nadal występuje na stronach z [this.$métier()]);
- w [5, 7] nie ma już żadnych symulacji;
Należy dobrze zrozumieć, co dzieje się pod koniec sesji:
- sesja [nuxt] zostaje zresetowana: właściwość [started] w sklepie zmienia się na [false];
- następuje przekierowanie na stronę [index];
- wykonana zostaje metoda [mounted] strony [index]. Metoda ta uruchamia nową sesję jSON z serwerem obliczającym podatek. Jeśli operacja zakończy się powodzeniem, właściwość [started] w pamięci tymczasowej zmienia się na [true];
17.8. krok 7
Na tym etapie aplikacja [nuxt-20] posiada wszystkie funkcje aplikacji [vuejs-22]. Przeniesienie wydaje się zakończone.
Pójdziemy nieco dalej w duchu [nuxt]. Metoda [mounted] na stronie [index] stanowi problem. Uruchamia ona operację asynchroniczną, na zakończenie której wyszukiwarka nie będzie czekać. Wiadomo, że w takim przypadku operację asynchroniczną należy umieścić w funkcji [asyncData], ponieważ wtedy serwer [nuxt], który ją wykonuje, poczeka na jej zakończenie przed dostarczeniem strony do wyszukiwarki.
Korzystamy tutaj z funkcji [asyncData] napisanej w aplikacji [nuxt-12] dla strony [index]:
export default {
name: 'InitSession',
// używane komponenty
components: {
Layout,
Navigation
},
// dane asynchroniczne
async asyncData(context) {
// log
console.log('[index asyncData started]')
// nie wykonuje się czynności dwukrotnie, jeśli strona została już wywołana
if (process.server && context.store.state.jsonSessionStarted) {
console.log('[index asyncData canceled]')
return { result: '[succès]' }
}
try {
// rozpoczynamy sesję jSON
const dao = context.app.$dao()
const response = await dao.initSession()
// log
console.log('[index asyncData response=]', response)
// pobieramy plik cookie sesji PHP na potrzeby kolejnych żądań
const phpSessionCookie = dao.getPhpSessionCookie()
// zapisujemy plik cookie sesji PHP w sesji [nuxt]
context.store.commit('replace', { phpSessionCookie })
// czy wystąpił błąd?
if (response.état !== 700) {
// błąd znajduje się w response.réponse
throw new Error(response.réponse)
}
// odnotowuje się, że sesja jSON została uruchomiona
context.store.commit('replace', { jsonSessionStarted: true })
// zwracamy wynik
return { result: '[succès]' }
} catch (e) {
// log
console.log('[index asyncData error=]', e)
// odnotowano, że sesja jSON nie została uruchomiona
context.store.commit('replace', { jsonSessionStarted: false })
// zgłaszamy błąd
return { result: '[échec]', showErrorLoading: true, errorLoadingMessage: e.message }
} finally {
// zapisujemy zawartość pamięci
const session = context.app.$session()
session.save(context)
// log
console.log('[index asyncData finished]')
}
},
// cykl życia
beforeCreate() {
console.log('[index beforeCreate]')
},
created() {
console.log('[index created]')
},
beforeMount() {
console.log('[index beforeMount]')
},
mounted() {
console.log('[index mounted]')
// tylko klient
if (this.showErrorLoading) {
console.log('[index mounted, showErrorLoading=true]')
this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
}
}
- wiersze 13, 33, 40: należy zmienić właściwość [jsonSessionStarted] na [started];
- wiersz 13: w aplikacji [nuxt-12] tylko serwer [nuxt] uruchamiał stronę [index] i jej funkcję [asyncData]. Klient [nuxt] uruchamiał stronę [index] dopiero po jej otrzymaniu od serwera [nuxt], a zatem nie uruchamiał funkcji [asyncData]. W przypadku [nuxt-20] sytuacja wygląda inaczej: link [Fin de session] spowoduje wyświetlenie strony [index] w środowisku klienta [nuxt]. Następnie zostanie wykonana funkcja [asyncData]. Jednak gdy w ten sposób przejdzie się do strony [index], sesja [nuxt] została w międzyczasie zresetowana, a właściwość [started] w magazynie ma wartość [false], przez co warunek w wierszu 13 będzie z konieczności fałszywy. Można zatem pozostawić [process.server], dzięki czemu klient [nuxt] nie przeprowadzi tego testu;
- wiersze 15, 35, 42: właściwość [result] jest umieszczana we właściwościach [data] strony [index]. W [nuxt-20] ta właściwość nie będzie wykorzystywana, dlatego usuniemy ją z wyniku zwracanego przez funkcję;
- wiersze 61–67: ta metoda [mounted] musi zostać zachowana, ponieważ to właśnie ona umożliwia klientowi [nuxt] wyświetlenie komunikatu o błędzie. Niemniej jednak sposób obsługi błędu zostanie zmodyfikowany;
Na obecnej stronie [index] wstawiamy powyższą funkcję [asyncData] w miejsce starej funkcji [mounted] oraz dodajemy nową funkcję [mounted]. Kod strony [index] z przykładu [nuxt-20] przyjmuje wówczas następującą postać:
...
<!-- dynamika widoku -->
<script>
/* eslint-disable no-console */
import Layout from '@/components/layout'
export default {
// używane komponenty
components: {
Layout
},
// stan komponentu
data() {
return {
// użytkownik
user: '',
// jego hasło
password: '',
// wyświetlanie błędu
showError: false
}
},
// właściwości obliczone
computed: {
// prawidłowe wpisy
valid() {
return this.user && this.password && this.$store.state.started
}
},
// dane asynchroniczne
async asyncData(context) {
// dziennik
console.log('[index asyncData started]')
// nie wykonuje się tej samej czynności dwukrotnie, jeśli strona została już wywołana
if (process.server && context.store.state.started) {
console.log('[index asyncData canceled]')
return
}
try {
// rozpoczynamy sesję jSON
const dao = context.app.$dao()
const response = await dao.initSession()
// log
console.log('[index asyncData response=]', response)
// pobieramy plik cookie sesji PHP na potrzeby kolejnych żądań
const phpSessionCookie = dao.getPhpSessionCookie()
// zapisujemy plik cookie sesji PHP w sesji [nuxt]
context.store.commit('replace', { phpSessionCookie })
// czy wystąpił błąd?
if (response.état !== 700) {
// błąd znajduje się w response.réponse
throw new Error(response.réponse)
}
// odnotowano, że sesja jSON została uruchomiona
context.store.commit('replace', { started: true })
// brak wyniku
return
} catch (e) {
// log
console.log('[index asyncData error=]', e.message)
// odnotowano, że sesja jSON nie została uruchomiona
context.store.commit('replace', { started: false })
// zgłaszany jest błąd
return { showErrorLoading: true, errorLoadingMessage: e.message }
} finally {
// zapisano pamięć
const session = context.app.$session()
session.save(context)
// log
console.log('[index asyncData finished]')
}
},
// cykl życia
beforeCreate() {
console.log('[index beforeCreate]')
},
created() {
console.log('[index created]')
},
beforeMount() {
// tylko klient
console.log('[index beforeMount]')
// obsługa ewentualnego błędu
if (this.showErrorLoading) {
// log
console.log('[index beforeMount, showErrorLoading=true]')
// błąd jest zgłaszany do głównego komponentu [default]
this.$emit('error', new Error(this.errorLoadingMessage))
}
},
mounted() {
console.log('[index mounted]')
},
// obsługa zdarzeń
methods: {
// ----------- uwierzytelnianie
async login() {
...
}
</script>
- wiersze 58, 65: funkcja [asyncData] nie powoduje już, że właściwość [result] jest tutaj nieużywana;
- wiersz 81: metoda [beforeMount] klienta [nuxt]. Została ona wybrana zamiast metody [mounted] w celu obsługi ewentualnego błędu funkcji [asyncData];
- wiersz 85: sprawdzamy, czy właściwość [errorLoading] została ustawiona. Może to zrobić wyłącznie funkcja [asyncData];
- wiersze 85–90: jeśli funkcja [asyncData] zgłosiła błąd, przekazujemy go do strony [default] za pośrednictwem zdarzenia [error]. W ten sposób poprzednia funkcja [created], którą właśnie zastąpiliśmy, obsługiwała ewentualny błąd;
Przeprowadźmy kilka testów.
Najpierw usuwamy plik cookie sesji [nuxt] oraz plik cookie sesji PHP, jeśli istnieją. Następnie wywołujemy stronę [http://localhost:81/nuxt-20/], gdy serwer obliczający podatek nie jest uruchomiony. Otrzymujemy następującą stronę:

Po uruchomieniu serwera obliczającego podatek ponownie ładujemy tę samą stronę:

Przyjrzyjmy się logom:

- w [2-3] widać, że uruchomiono sesję jSON;
- w [4] widoczny jest plik cookie sesji PHP, który serwer [nuxt] pobrał podczas wymiany danych z serwerem obliczającym podatek. Klient [nuxt] będzie go teraz wykorzystywał;
Teraz zalogujmy się:

Otrzymujemy następującą stronę:

W przypadku [1] otrzymaliśmy komunikat o błędzie. Oznacza to, że przeglądarka nie wysłała właściwego pliku cookie sesji PHP, uruchomionej przez serwer [nuxt] w poprzednim kroku. W [nuxt-12] przekazanie pliku cookie sesji PHP z serwera [nuxt] do klienta [nuxt] odbywało się w ramach routingu klienta [nuxt] ze skryptu [middleware/client/routing] :
/* eslint-disable no-console */
export default function(context) { // kto wykonuje ten kod?
console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
// zarządzanie plikiem cookie sesji PHP w przeglądarce
// plik cookie sesji PHP w przeglądarce musi być identyczny z tym znalezionym w sesji Nuxt
// akcja [fin-session] otrzymuje nowy plik cookie PHP (zarówno serwer, jak i klient Nuxt)
// jeśli to serwer je odbiera, klient musi przekazać je przeglądarce
// w celu własnej komunikacji z serwerem PHP
// mamy tu do czynienia z routingiem po stronie klienta
// pobieramy plik cookie sesji PHP
const phpSessionCookie = context.store.state.phpSessionCookie
if (phpSessionCookie) {
// jeśli istnieje, przypisujemy plik cookie sesji PHP do przeglądarki
document.cookie = phpSessionCookie
}
// dokąd idziemy?
const to = context.route.path
if (to === '/fin-session') {
// czyścimy sesję
const session = context.app.$session()
session.reset(context)
// przekierowujemy na stronę główną
context.redirect({ name: 'index' })
}
}
To właśnie linie 13–17 umożliwiają klientowi [nuxt] pobranie pliku cookie sesji PHP z serwera [nuxt].
Problem polega na tym, że po kliknięciu przycisku [Valider] nie następuje przekierowanie klienta [nuxt]. W związku z tym jego funkcja przekierowująca nie jest wywoływana. Problem rozwiązuje się poprzez skopiowanie wierszy 12–17 na początek metody uwierzytelniania strony [index]:
// obsługa zdarzeń
methods: {
// ----------- uwierzytelnianie
async login() {
// pobieramy plik cookie sesji PHP z magazynu
const phpSessionCookie = this.$store.state.phpSessionCookie
if (phpSessionCookie) {
// jeśli istnieje, przypisuje się plik cookie sesji PHP do przeglądarki
document.cookie = phpSessionCookie
}
try {
// początek oczekiwania
this.$emit('loading', true)
// użytkownik nie jest jeszcze uwierzytelniony
W wierszach 5–10 pobieramy z pamięci podręcznej plik cookie sesji PHP zainicjowanej przez serwer [nuxt]. Po wprowadzeniu tej zmiany strona obliczania podatku wyświetla się poprawnie, co oznacza, że uwierzytelnienie przebiegło pomyślnie.
17.9. Krok 8
Mamy działającą aplikację, która funkcjonuje zgodnie z logiką [nuxt]. Podobnie jak w przypadku aplikacji [nuxt-13], przyjrzymy się teraz nawigacji serwera [nuxt]. Jak już wspomniano, użytkownik nie powinien ręcznie wpisywać adresów URL aplikacji. Powinien on korzystać z wyświetlanych linków, które są uruchamiane przez klienta [nuxt], działającego wówczas w trybie SPA. Niemniej jednak zadbamy o to, aby nawigacja na serwerze [nuxt] zawsze pozostawiała aplikację w stabilnym stanie.
Z analizy przeprowadzonej dla [nuxt-13] (patrz akapit dotyczący linków) wiemy, że należy:
- zmodyfikować skrypt [midleware/routing];
- dodać skrypt [middleware/server/routing];

Skrypt [middleware/routing] należy zmodyfikować w następujący sposób:
/* eslint-disable no-console */
// importujemy middleware serwera i klienta
import serverRouting from './server/routing'
import clientRouting from './client/routing'
export default function(context) {
// kto wykonuje ten kod?
console.log('[middleware], process.server', process.server, ', process.client=', process.client)
if (process.server) {
// przekierowanie na serwer
serverRouting(context)
} else {
// trasowanie klienta
clientRouting(context)
}
}
- wiersz 4: importujemy skrypt routingu z serwera [nuxt];
- wiersze 10–12: jeśli kod jest wykonywany przez serwer [nuxt], używana jest jego funkcja routingu;
Skrypt [middleware/server/routing] ma następującą postać:
/* eslint-disable no-console */
export default function(context) {
// kto wykonuje ten kod?
console.log('[middleware server], process.server', process.server, ', process.client=', process.client)
// zbieramy tu i ówdzie pewne informacje
const store = context.store
// skąd pochodzimy?
const from = store.state.from || 'nowhere'
// dokąd zmierzamy?
let to = context.route.name
// szczególny przypadek /fin-session, który nie ma atrybutu [name]
if (context.route.path === '/fin-session') {
to = 'fin-session'
}
// ewentualne przekierowanie
let redirection = ''
// zakończono obsługę routingu
let done = false
// czy jesteśmy już w trakcie przekierowania z serwera [nuxt]?
if (store.state.serverRedirection) {
// nie ma potrzeby podejmowania żadnych działań
done = true
}
// czy chodzi o odświeżenie strony?
if (!done && from === to) {
// nie ma co robić
done = true
}
// kontrola nawigacji serwera [nuxt]
// dostosowujemy się do menu nawigacyjnego klienta
// najpierw obsługujemy przypadek zakończenia sesji
if (!done && store.state.started && store.state.authenticated && to === 'fin-session') {
// czyścimy sesję
const session = context.app.$session()
session.reset(context)
// przekierowujemy na stronę główną
redirection = 'index'
// praca zakończona
done = true
}
// przypadek, w którym sesja PHP nie została uruchomiona
if (!done && !store.state.started && to !== 'index') {
// przekierowanie do [index]
redirection = 'index'
// zadanie zakończone
done = true
}
// w przypadku, gdy użytkownik nie jest uwierzytelniony
if (!done && store.state.started && !store.state.authenticated && to !== 'index') {
redirection = 'index'
// zadanie zakończone
done = true
}
// przypadek, w którym nie uzyskano [adminData]
if (!done && store.state.started && store.state.authenticated && !store.state.métier.taxAdminData && to !== 'index') {
// przekierowanie do [index]
redirection = 'index'
// zadanie zakończone
done = true
}
// w przypadku, gdy uzyskano [adminData]
if (
!done &&
store.state.started &&
store.state.authenticated &&
store.state.métier.taxAdminData &&
to !== 'calcul-impot' &&
to !== 'liste-des-simulations'
) {
// pozostajemy na tej samej stronie
redirection = from
// praca zakończona
done = true
}
// zazwyczaj przeprowadzono wszystkie kontrole ---------------------
// przekierowanie?
if (redirection) {
// odnotowujemy przekierowanie w sklepie
store.commit('replace', { serverRedirection: true })
} else {
// brak przekierowania
store.commit('replace', { serverRedirection: false, from: to })
}
// zapisujemy stan pamięci w sesji [nuxt]
const session = context.app.$session()
session.value.store = store.state
session.save(context)
// w razie potrzeby przeprowadzane jest przekierowanie z serwera [nuxt]
if (redirection) {
context.redirect({ name: redirection })
}
}
- w tym skrypcie wykorzystujemy pomysły już opracowane i zastosowane w routingu serwera [nuxt] aplikacji [nuxt-13];
- dodajemy dwie właściwości do magazynu aplikacji:
- [from]: nazwa ostatnio wyświetlonej strony. Wiemy, że klient [nuxt] posiada tę informację, ale serwer [nuxt] jej nie ma. Dodamy tę informację, zapisując w pamięci przy każdym przekierowaniu serwera [nuxt] nazwę strony, która ma zostać wyświetlona. To samo zrobimy przy każdym przekierowaniu klienta [nuxt]. W ten sposób przy kolejnym przekierowaniu serwera [nuxt] znajdzie on w pamięci nazwę ostatniej strony wyświetlonej przez aplikację;
- [serverRedirection]: gdy miejsce docelowe przekierowania zostanie odrzucone przez serwer [nuxt], serwer ten przeprowadzi przekierowanie. Następnie zaznaczy w pamięci, że kolejnym miejscem docelowym serwera [nuxt] jest strona przekierowująca. To przekierowanie spowoduje ponowne uruchomienie routera serwera [nuxt]. Jeśli router ten wykryje, że bieżący cel pochodzi z przekierowania, nie będzie podejmował żadnych działań;
- wiersze 6–11: pobierane są informacje przydatne do routingu;
- wiersze 13–16: cel [/fin-session] nie jest powiązany ze stroną o nazwie [fin-session]. Nie ma więc nazwy. Nadajemy mu nazwę;
- wiersz 19: cel ewentualnego przekierowania;
- wiersz 21: [done=true] po zakończeniu testów routingu;
- wiersze 23–27: jak już wspomniano, jeśli bieżący routing wynika z przekierowania, nie ma nic do zrobienia. W rzeczywistości podczas poprzedniego routingu router zdecydował, że należy przekierować przeglądarkę klienta. Nie ma powodu, by ponownie rozważać tę decyzję;
- wiersze 29–33: jeśli chodzi o ponowne załadowanie strony, należy pozostawić proces bez zmian. Nie jest to zasada obowiązująca dla wszystkich aplikacji [nuxt]: dla każdej strony należy sprawdzić skutki ponownego załadowania. W tym przypadku okazuje się, że odświeżenie stron [index, calcul-impot, liste-des-simulations] nie powoduje niepożądanych skutków;
- wiersze 35–85: routing serwera [nuxt] przejmuje routing klienta [nuxt]. Gdy użytkownik znajduje się na stronie, routing serwera [nuxt] musi odzwierciedlać menu nawigacyjne oferowane przez klienta [nuxt], gdy użytkownik znajduje się na tej stronie;
- wiersze 38–47: najpierw rozpatrujemy przypadek adresu docelowego [fin-session], który nie odpowiada żadnej istniejącej stronie. Jeśli warunki są spełnione (sesja rozpoczęta, użytkownik uwierzytelniony), czyszczymy sesję i przekierowujemy użytkownika na stronę [index];
- wiersze 49–55: jeśli sesja jSON z serwerem obliczającym podatek nie została rozpoczęta, wówczas jedynym możliwym miejscem docelowym jest strona [index];
- wiersze 57–62: jeśli sesja jSON została uruchomiona, a użytkownik nie jest uwierzytelniony i nie zażądał strony uwierzytelniającej, następuje przekierowanie na stronę uwierzytelniającą, którą jest strona [index];
- wiersze 64–70: jeśli użytkownik jest uwierzytelniony, ale nie uzyskano danych [adminData], następuje przekierowanie na stronę uwierzytelniania. Uwierzytelnianie pełni dwie funkcje: po pierwsze uwierzytelnia użytkownika, a po drugie – w przypadku pomyślnego uwierzytelnienia – żąda dodatkowo danych o wartości [adminData]. Jeśli dane te nie zostały uzyskane, należy ponowić proces uwierzytelniania;
- wiersze 72–85: jeśli uzyskano dane [adminData], wówczas jedynymi możliwymi celami są [calcul-impot] i [liste-des-simulations]. W przeciwnym razie routing zostaje odrzucony;
- wiersze 88–95: aktualizuje się pamięć w zależności od tego, czy nastąpi przekierowanie, czy nie;
- wiersz 94: nie ma przekierowania. W związku z tym obecny identyfikator [to] stanie się identyfikatorem [from] dla następnego przekierowania;
- wiersze 96–99: informacje z pamięci są zapisywane w pliku cookie sesji o nazwie [nuxt];
- wiersze 100–103: jeśli konieczne jest przekierowanie, jest ono wykonywane;
Aby przeprowadzić testy, należy zadbać o to, by rozpocząć od czystej sytuacji, usuwając plik cookie sesji [nuxt] oraz plik cookie sesji PHP z serwera obliczającego podatek:

Aby przetestować routing serwera [nuxt], na każdej stronie należy wypróbować wszystkie możliwe wartości URL i [/, /calcul-impot, /liste-des-simulations]. Za każdym razem aplikacja musi pozostać w spójnym stanie.
17.10. Krok 9
Krok 9 polega na wdrożeniu aplikacji [nuxt-20]. Wymaga to hostingu zapewniającego środowisko [node.js] do uruchomienia serwera [nuxt]. Nie dysponuję takim środowiskiem. Czytelnik może postępować zgodnie z procedurami opisanymi w akapicie „link”, aby wdrożyć aplikację [nuxt-20] na swoim komputerze deweloperskim i zabezpieczyć ją za pomocą protokołu HTTPS.
17.11. Conclusion
Portowanie aplikacji [vuejs-22] do aplikacji [nuxt-20] zostało zakończone. Zwróćmy uwagę na kilka kwestii związanych z tym portowaniem:
- strony z aplikacji [vuejs-22] zostały zachowane;
- operacje asynchroniczne, które występowały na stronach aplikacji [vuejs-22], zostały przeniesione do funkcji [asyncData];
- w [nuxt-20] konieczne było zarządzanie dwoma podmiotami: klientem [nuxt] oraz serwerem [nuxt]. Ten ostatni podmiot nie istniał w [vuejs-22]. Aby zachować spójność między tymi dwoma podmiotami, potrzebowaliśmy sesji [nuxt];
- musieliśmy zająć się routingiem serwera [nuxt];
W praktyce niewątpliwie lepiej jest rozpocząć bezpośrednio od architektury [nuxt] niż tworzyć architekturę [vue.js], którą następnie przenosi się do środowiska [nuxt].