Skip to content

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:

Image

Drugi ekran to ekran obliczania podatku:

Image

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

Image

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

Image

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

Image

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

Image

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

Image

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

Image

Zgłoszone błędy są następujące:

1
2
3
4
5
6
7
Module not found: Error: Can't resolve '../assets/logo.jpg' @ ./nuxt-20/layouts/default.vue?...
Module not found: Error: Can't resolve './FormCalculImpot' @ ./nuxt-20/pages/calcul-impot.vue?...
Module not found: Error: Can't resolve './Layout' @ ./nuxt-20/pages/_.vue?...
Module not found: Error: Can't resolve './Layout' @ ./nuxt-20/pages/liste-des-simulations...
Module not found: Error: Can't resolve './Layout' @ ./nuxt-20/pages/calcul-impot.vue?...
Module not found: Error: Can't resolve './Menu' @ ./nuxt-20/pages/_.vue?...
Module not found: Error: Can't resolve './Menu' @ ./nuxt-20/pages/calcul-impot.vue
  • 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]:

Image

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

Image

Pojawiają się błędy:

1
2
3
4
5
6
Module Error (from ./node_modules/eslint-loader/dist/cjs.js):

c:\Data\st-2019\dev\nuxtjs\dvp\nuxt-20\pages\index.vue
   92:29  error  Expected '!==' and instead saw '!='  eqeqeq
  129:27  error  Expected '!==' and instead saw '!='  eqeqeq
150:28  error  Expected '!==' and instead saw '!='  eqeqeq

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

c:\Data\st-2019\dev\nuxtjs\dvp\nuxt-20\components\menu.vue
14:5  warning  Prop 'options' requires default value to be set  vue/require-default-prop

Image

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:

Image

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

Image

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:

Image

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:

Image

Przyjrzyjmy się logom przeglądarki:

Image

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

Image

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:

Image

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

Image

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

Image

Otrzymana strona wygląda następująco:

Image

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

Image

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

Image

Otrzymujemy następującą odpowiedź:

Image

Jeśli przyjrzymy się logom:

Image

  • 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ę:

Image

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

Image

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

Image

Przyjrzyjmy się logom:

Image

  • w [6] widać, że tabela symulacji jest pusta;

Teraz wróćmy do formularza obliczania podatku:

Image

Otrzymujemy następującą stronę:

Image

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

Image

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:

Image

  • 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ę:

Image

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

Image

Przyjrzyjmy się logom:

Image

  • 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ę:

Image

Otrzymujemy następującą stronę:

Image

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

Image

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:

Image

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