Skip to content

19. Ulepszenia klienta Vue.js

19.1. Introduction

Przetestujemy projekt [vuejs-21] na serwerze deweloperskim. Będziemy więc ponownie potrzebować, aby serwer wysyłał nagłówki CORS. W związku z tym plik [config.json] z wersji 14 serwera obliczającego podatek musi zezwalać na te nagłówki:

Image

Projekt [vuejs-21] został początkowo utworzony poprzez skopiowanie projektu [vuejs-20]. Następnie zmieniono jego nazwę na [3].

Pojawiają się nowe pliki:

  • [session.js]: eksportuje obiekt [session], który będzie zawierał informacje o bieżącej sesji;
  • [pluginSession]: udostępnia poprzedni obiekt [session] we właściwości [$session] widoków;
  • [NotFound.vue]: nowy widok wyświetlany, gdy użytkownik ręcznie zażąda nieistniejącego obiektu URL;

Zostaną zmodyfikowane następujące pliki:

  • [main.js]: zainicjuje bieżącą sesję, a następnie, gdy użytkownik ręcznie wprowadzi URL, przywróci ją;
  • [router.js]: dodano mechanizmy sprawdzające w celu obsługi przypadków, gdy użytkownik wpisze URL;
  • [store.js]: dodano nową mutację;
  • [config.js]: dodano nową konfigurację;
  • różne widoki służące przede wszystkim do zapisywania bieżącej sesji w kluczowych momentach działania aplikacji. Sesja ta jest następnie przywracana za każdym razem, gdy użytkownik ręcznie wpisuje URL;

19.2. Sklep [Vuex]

Skrypt [./store] zmienia się w następujący sposób:


// wtyczka Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);

// magazyn Vuex
const store = new Vuex.Store({
  state: {
    // tabela symulacji
    simulations: [],
    // numer ostatniej symulacji
    idSimulation: 0
  },
  mutations: {
    // usunięcie wiersza o numerze indeksu
    deleteSimulation(state, index) {
      ...
    },
    // dodanie symulacji
    addSimulation(state, simulation) {
      ...
    },
    // oczyszczenie stanu
    clear(state) {
      // brak symulacji
      state.simulations = [];
      // numeracja symulacji zaczyna się od 0
      state.idSimulation = 0;
    }
  }
});
// eksport obiektu [store]
export default store;
  • wiersze 24–29: zmiana [clear] usuwa listę zapisanych symulacji i resetuje numer ostatniej symulacji do 0.

19.3. Sesja

Konieczność istnienia sesji wynika z faktu, że gdy użytkownik wpisze adres URL w polu adresu przeglądarki, skrypt [main.js] zostanie ponownie uruchomiony. Zawiera on jednak instrukcję:


// sklep Vuex
import store from './store'

Ta instrukcja importuje następujący plik [./store]:


// wtyczka Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);

// sklep Vuex
const store = new Vuex.Store({
  state: {
    // tabela symulacji
    simulations: [],
    // numer ostatniej symulacji
    idSimulation: 0
  },
  mutations: {
    ...
  }
});
// eksport obiektu [store]
export default store;

W wierszach 7–13 widać, że importowana jest pusta tablica symulacji. Jeśli więc przed wpisaniem przez użytkownika kodu URL w polu adresu przeglądarki istniały jakieś symulacje, to po tej operacji już ich nie ma. Pomysł polega na tym, aby:

  • wykorzystać sesję, która przechowywałaby informacje, które chcemy zachować, jeśli użytkownik ręcznie wpisze URL;
  • zapisywanie jej w kluczowych momentach działania aplikacji;
  • przywrócić ją w [main.js], który jest zawsze uruchamiany, gdy URL zostanie wpisany ręcznie;

Skrypt [./session] wygląda następująco:


// importujemy roletę Vuex
import store from './store'
// importujemy konfigurację
import config from './config';

// obiekt [session]
const session = {
  // sesja uruchomiona
  started: false,
  // uwierzytelnianie
  authenticated: false,
  // czas zapisania
  saveTime: "",
  // warstwa [métier]
  métier: null,
  // stan Vuex
  state: null,

  // zapis sesji w łańcuchu jSON
  save() {
    // do sesji dodaje się kilka właściwości
    this.saveTime = Date.now();
    this.state = store.state;
    // przekształcamy ją w jSON
    const json = JSON.stringify(this);
    // zapisywanie w przeglądarce
    localStorage.setItem("session", json);
    // eslint-disable-next-line no-console
    console.log("session save", json);
  },

  // przywracamy sesję
  restore() {
    // pobieramy sesję jSON z przeglądarki
    const json = localStorage.getItem("session")
    // jeśli coś udało się pobrać
    if (json) {
      // przywracamy wszystkie klucze sesji
      const restore = JSON.parse(json);
      for (var key in restore) {
        if (restore.hasOwnProperty(key)) {
          this[key] = restore[key];
        }
      }
      // jeśli od początku sesji upłynął określony czas bezczynności, zaczynamy od nowa
      let durée = Date.now() - this.saveTime;
      if (durée > config.duréeSession) {
        // sesja zostaje wyczyszczona – zostanie również zapisana
        session.clear();
      } else {
        // regeneruje się magazyn Vuex
        store.replaceState(JSON.parse(JSON.stringify(this.state)));
      }
    }
    // eslint-disable-next-line no-console
    console.log("session restore", this);
  },

    // czyścimy sesję
  clear() {
    // eslint-disable-next-line no-console
    console.log("session clear");
    // resetowanie niektórych pól sesji
    this.authenticated = false;
    this.saveTime = "";
    this.started = false;
    if (this.métier) {
      // resetujemy pole [taxAdminData]
      this.métier.taxAdminData = null;
    }
    // magazyn Vuex jest również czyszczony
    store.commit("clear");
    // zapisywana jest nowa sesja
    this.save();
  },
}

// eksport obiektu [session]
export default session;

Komentarze

  • wiersz 2: sesja będzie również zawierać magazyn [Vuex] (lista symulacji, numer ostatniej przeprowadzonej symulacji);
  • wiersze 7–17: informacje przechowywane przez sesję:
    • [started]: czy sesja jSON z serwerem została uruchomiona, czy nie;
    • [authenticated]: czy użytkownik się uwierzytelnił, czy nie;
    • [saveTime]: data ostatniego zapisu w milisekundach;
    • [métier]: odwołanie do warstwy [métier]. Zawiera ona dane [taxAdminData], które umożliwiają obliczenie podatku;
    • [state]: stan magazynu [Vuex] (lista symulacji, numer ostatniej przeprowadzonej symulacji);
  • wiersze 20–30: metoda [save] zapisuje sesję lokalnie w przeglądarce, w której uruchomiona jest aplikacja;
    • wiersz 22: odnotowuje się godzinę zapisania;
    • wiersz 23: pobierany jest identyfikator [state] ze sklepu [Vuex];
    • wiersz 25: tworzy się ciąg jSON dla sesji;
    • wiersz 27: zapisuje się go lokalnie w przeglądarce w powiązaniu z kluczem [session];
  • wiersze 33–57: metoda [restore] umożliwia przywrócenie sesji z jej lokalnej kopii zapasowej w przeglądarce;
    • wiersz 35: odczytujemy lokalną kopię zapasową jSON;
    • wiersz 37: jeśli coś zostało pobrane;
    • wiersze 39–44: odtwarzany jest obiekt [session];
    • wiersz 46: obliczamy czas, jaki upłynął od ostatniej kopii zapasowej;
    • wiersze 47–50: jeśli czas ten jest dłuższy niż wartość [config.duréeSession] ustalona w konfiguracji, sesja jest resetowana (wiersz 49) i przy tej okazji zapisywana;
    • wiersz 52: w przeciwnym razie odświeża się atrybut [state] magazynu [Vuex];
  • wiersze 60–75: metoda [clear] resetuje sesję;
    • wiersze 64–70: właściwości sesji są resetowane do wartości początkowych;
    • wiersz 72: podobnie jak magazyn [Vuex];
    • wiersz 74: nowa sesja zostaje zapisana;

19.4. Plik konfiguracyjny [config]

Plik [./config] zmienia się w następujący sposób:


// korzystanie z biblioteki [axios]
const axios = require('axios');
// limit czasu dla żądań HTTP
axios.defaults.timeout = 2000;
...

// eksport konfiguracji
export default {
  // obiekt [axios]
  axios: axios,
  // maksymalny czas bezczynności sesji: 5 min = 300 s = 300 000 ms
  duréeSession: 300000
}
  • wiersz 12: sesja aplikacji będzie zarządzana podobnie jak sesja internetowa. Ustawiamy tutaj maksymalny czas bezczynności wynoszący 5 minut;

19.5. Wtyczka [pluginSession]

Tak jak to już wielokrotnie robiono, wtyczka [pluginSession] umożliwi widokom dostęp do sesji poprzez właściwość [this.$session]:


export default {
  install(Vue, session) {
    // dodaje właściwość [$session] do klasy widoku
    Object.defineProperty(Vue.prototype, '$session', {
      // gdy odwołuje się do Vue.$session, zwracany jest drugi parametr [session]
      get: () => session,
    })
  }
}

19.6. Główny skrypt [main]

Główny skrypt [./main.js] zmienia się w następujący sposób:


// log uruchomienia
// eslint-disable-next-line no-console
console.log("main started");

// importy
import Vue from 'vue'

...

// instancjonowanie warstwy [métier]
import Métier from './couches/Métier';
const métier = new Métier();

// wtyczka [métier]
import pluginMétier from './plugins/pluginMétier'
Vue.use(pluginMétier, métier)

// magazyn Vuex
import store from './store'

// sesja
import session from './session';
import pluginSession from './plugins/pluginSession'
Vue.use(pluginSession, session)

// przed ponownym uruchomieniem przywracamy sesję
session.restore();

// przywracamy warstwę [métier]
if (session.métier && session.métier.taxAdminData) {
  métier.setTaxAdminData(session.métier.taxAdminData);
}

// uruchomienie aplikacji UI
new Vue({
  el: '#aplikacji,
  // router
  router: router,
  // żaluzja Vuex
  store: store,
  // ekran główny
  render: h => h(Main),
})

// log końcowy
// eslint-disable-next-line no-console
console.log("main terminated, session=", session);
  • wiersz 19: importujemy sesję;
  • wiersz 20: importowana jest wtyczka;
  • wiersz 21: wtyczka [pluginSession] jest zintegrowana z [Vue]. Po tej instrukcji wszystkie widoki mają sesję w swoim atrybucie [$session];
  • wiersz 27: sesja zostaje przywrócona. Sesja zaimportowana w wierszu 11 jest wówczas inicjowana zawartością jej ostatniej kopii zapasowej;
  • po wierszu 16 widoki posiadają właściwość [$métier] zainicjowaną w wierszu 12. Właściwość ta nie zawiera informacji [taxAdminData], która umożliwia obliczenie podatku;
  • wiersze 30–32: jeśli właśnie przeprowadzona operacja przywrócenia przywróciła właściwość [session.métier.taxAdminData], wówczas właściwość [$métier] widoków jest inicjowana tą wartością;

19.7. Plik routingu [router]

Plik routingu [./router] zmienia się w następujący sposób:


// 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: 'authentification', 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") {
    // czyszczenie sesji
    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

Uwagi

  • wiersze 16–38: niektóre trasy zostały wzbogacone o dodatkowe informacje;
  • wiersz 19: utworzono nową trasę prowadzącą do widoku [Authentification];
  • wiersze 21–24: ścieżka prowadząca do widoku [CalculImpot] posiada teraz właściwość [meta] (nazwa ta jest obowiązkowa). Zawartość tego obiektu może być dowolna i jest ustalana przez programistę;
  • wiersz 23: w [meta] umieszczamy właściwość [authenticated] (nazwa ta może być dowolna). Będzie to dla nas oznaczało, że aby przejść do widoku [CalculImpot], użytkownik musi być uwierzytelniony;
  • wiersze 26–29: wykonujemy tę samą czynność dla ścieżki prowadzącej do widoku [ListeSimulations]. Również w tym przypadku użytkownik musi być uwierzytelniony;
  • właściwość [meta.authenticated] pozwoli nam sprawdzić, czy użytkownik, który ręcznie wpisuje adresy URL widoków [CalculImpot, ListeSimulations], nie może uzyskać do nich dostępu, jeśli nie jest uwierzytelniony;
  • wiersze 51–76: metoda [beforeEach] jest wykonywana przed przekierowaniem widoku. Jest to odpowiedni moment na przeprowadzenie weryfikacji;
    • [to]: następna trasa, jeśli nie zostaną podjęte żadne działania;
    • [from]: ostatnia wyświetlona trasa;
    • [next]: funkcja umożliwiająca zmianę kolejnej wyświetlanej trasy;
  • wiersz 55: sprawdzamy, czy następna trasa wymaga uwierzytelnienia użytkownika;
  • wiersze 56–59: jeśli tak, a użytkownik nie jest uwierzytelniony, następną trasę zmieniamy na widok [Authentification];
  • wiersze 64–73: obsługujemy szczególny przypadek trasy [finSession] z wierszy 30–32. Nie ma ona powiązanego widoku;
    • wiersz 66: resetujemy sesję do wartości początkowej;
    • wiersze 68–70: ustalamy widok [Authentification] jako następny widok;
  • wiersz 75: jeśli nie ma miejsca żaden z dwóch poprzednich przypadków, po prostu przechodzi się do trasy przewidzianej w pliku trasowania;
  • wiersze 35–37: przewiduje się widok [NotFound], jeśli trasa wpisana przez użytkownika nie odpowiada żadnej znanej trasie. Widok ten jest importowany w wierszu 8. Trasy są sprawdzane w kolejności określonej w pliku tras. Jeśli więc dotrzemy do wiersza 36, oznacza to, że żądana trasa nie jest żadną z tras z wierszy 18–33;

19.8. Widok [NotFound]

Widok [NotFound] jest wyświetlany, jeśli trasa wpisana przez użytkownika nie odpowiada żadnej znanej trasie:

Image

Kod widoku jest następujący:


<!-- definicja widoku HTML -->
<template>
  <!-- układ strony -->
  <Layout :left="true" :right="true">
    <!-- alert w prawej kolumnie -->
    <template slot="right">
      <!-- komunikat na żółtym tle -->
      <b-alert show variant="danger" align="center">
        <h4>Cette page n'existe pas</h4>
      </b-alert>
    </template>
    <!-- menu nawigacyjne w lewej kolumnie -->
    <Menu slot="left" :options="options" />
  </Layout>
</template>

<script>
// importy
import Layout from "./Layout";
import Menu from "./Menu";
export default {
  // komponenty
  components: {
    Layout,
    Menu
  },
  // stan wewnętrzny komponentu
  data() {
    return {
      // opcje menu nawigacyjnego
      options: [
        {
          text: "Authentification",
          path: "/"
        }
      ]
    };
  },
  // cykl życia
  created() {
    // eslint-disable-next-line
    console.log("NotFound created");
    // sprawdzamy, jakie opcje menu należy udostępnić
    if (this.$session.authenticated && this.$métier.taxAdminData) {
      // użytkownik może przeprowadzać symulacje
      Array.prototype.push.apply(this.options, [
        {
          text: "Calcul de l'impôt",
          path: "/calcul-impot"
        },
        {
          text: "Liste des simulations",
          path: "/liste-des-simulations"
        }
      ]);
    }
  }
};
</script>

Uwagi

  • wiersz 4: wykorzystuje obie kolumny widoków trasowanych;
  • wiersze 6–11: komunikat o błędzie;
  • wiersz 13: menu nawigacyjne zajmuje lewą kolumnę;
  • wiersze 31–36: domyślne opcje menu;
  • wiersze 40–57: kod wykonywany podczas tworzenia widoku;
  • wiersz 44: sprawdzamy, czy użytkownik może przeprowadzać symulacje;
  • wiersze 45–55: jeśli tak, dodaje się dwie opcje do menu nawigacyjnego – te, które wymagają uwierzytelnienia i posiadania działającej warstwy [métier] (wiersze 46–55);

19.9. Widok [Authentification]

Widok [Authentification] zmienia się w następujący sposób:


<!-- definicja widoku HTML -->
<template>
  <Layout :left="false" :right="true">
    ...
  </Layout>
</template>

<!-- dynamika widoku -->
<script>
import Layout from "./Layout";
export default {
  // 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: ""
    };
  },

  // używane komponenty
  components: {
    Layout
  },

  // właściwości obliczone
  computed: {
    // prawidłowe dane wprowadzone
    valid() {
      return this.user && this.password && this.$session.started;
    }
  },

  // procedury obsługi zdarzeń
  methods: {
    // ----------- uwierzytelnianie
    async login() {
      try {
        // początek oczekiwania
        this.$emit("loading", true);
        // nie jesteśmy jeszcze uwierzytelnieni
        this.$session.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.$session.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 sesji
        this.$session.métier = this.$métier;
        // zapisano sesję
        this.$session.save();
      }
    }
  },
  // cykl życia: komponent został właśnie utworzony
  created() {
    // eslint-disable-next-line
    console.log("Authentification created");
    // czy użytkownik może przeprowadzać symulacje?
    if (
      this.$session.started &&
      this.$session.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.$session.started) {
      // początek 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.$session.started = true;
        })
        // 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();
        });
    }
  }
};
</script>

Komentarze

  • na żółto zaznaczono instrukcje, które wykorzystują sesję wprowadzoną w tej wersji klienta [Vue.js];
  • wiersze 97, 148: na końcu metod [login, created] sesja jest zapisywana niezależnie od wyniku zapytań HTTP wykonywanych w tych metodach (klauzula [finally] w obu przypadkach);
  • metoda [created] z wierszy 102–150 jest wykonywana za każdym razem, gdy tworzony jest widok [Authentification]. Jeśli to użytkownik wprowadził kod URL widoku, sesja wskaże nam, co należy zrobić;
  • wiersze 106–115: jeśli sesja jSON została uruchomiona, użytkownik uwierzytelniony, a dane [this.$métier.taxAdminData] zainicjowane, wówczas użytkownik może przejść bezpośrednio do formularza obliczania podatku (wiersz 112);
  • wiersz 117: metoda [created] była używana w poprzedniej wersji do zainicjowania sesji jSON z serwerem. Ta faza jest zbędna, jeśli już miała miejsce;
  • wiersze 42–66: metoda uwierzytelniania;
  • wiersz 66: jeśli uwierzytelnienie się powiedzie, odnotowuje się to w sesji;
  • wiersze 67–92: żądanie danych z urzędu skarbowego [taxAdminData] skierowane do serwera;
  • wiersz 95: na zakończenie tej fazy aktualizowana jest właściwość [métier] sesji, niezależnie od tego, czy operacja zakończyła się powodzeniem, czy nie;

19.10. Widok [CalculImpot]

Kod widoku [CalculImpot] zmienia się w następujący sposób:


<!-- definicja widoku HTML -->
<template>
  ...
</template>

<script>
// importy
import FormCalculImpot from "./FormCalculImpot";
import Menu from "./Menu";
import Layout from "./Layout";

export default {
  // stan 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
    };
  },
  // wykorzystane komponenty
  components: {
    Layout,
    FormCalculImpot,
    Menu
  },
  // metody zarządzania zdarzeniami
  methods: {
    // wynik obliczenia podatku
    handleResultatObtenu(résultat) {
      // tworzymy wynik w łańcuchu HTML
      ...
      // symulacja +
      this.$store.commit("addSimulation", résultat);
      // zapisywanie sesji
      this.$session.save();
    }
  },
  // cykl życia
  created() {
    // eslint-disable-next-line
    console.log("CalculImpot created");
  }
};
</script>

Komentarze

  • wiersz 45: obliczona symulacja jest dodawana do magazynu [Vuex]. Ma to wpływ na sesję, która obejmuje właściwość [state] magazynu. Dlatego zapisujemy sesję (wiersz 47);
  • wiersz 51: tworzona jest metoda [created] w celu śledzenia w logach tworzenia widoków;

19.11. Widok [ListeSimulations]

Widok [ListeSimulations] zmienia się w następujący sposób:


<!-- definicja widoku HTML -->
<template>
  ...
  </div>
</template>

<script>
// importy
import Layout from "./Layout";
import Menu from "./Menu";
export default {
  // komponenty
  components: {
    Layout,
    Menu
  },
  // stan wewnętrzny
  data() {
    ...
  },
  // obliczony stan wewnętrzny
  computed: {
    // lista symulacji pobranych z magazynu Vuex
    simulations() {
      return this.$store.state.simulations;
    }
  },
  // metody
  methods: {
    supprimerSimulation(index) {
      // eslint-disable-next-line
      console.log("supprimerSimulation", index);
      // usunięcie symulacji nr [index]
      this.$store.commit("deleteSimulation", index);
      // zapisywanie sesji
      this.$session.save();
    }
  },
  // cykl życia
  created() {
    // eslint-disable-next-line
    console.log("ListeSimulations created");
  }
};
</script>

Komentarze

  • wiersz 36: po usunięciu symulacji z wiersza 34 zapisujemy sesję, aby uwzględnić tę zmianę stanu;
  • wiersze 40–43: nadal śledzimy tworzenie widoków;

19.12. Kompilacja projektu

Image

Podczas testów należy sprawdzić następujące kwestie:

  • czy aplikacja działa, gdy użytkownik „korzysta” z niej za pomocą linków w menu nawigacyjnym oraz przycisków / linków akcji;
  • jeśli użytkownik ręcznie wpisuje URL, aplikacja nadal działa. W szczególności należy przeprowadzić następujący test:
    • przeprowadź symulacje;
    • po przejściu do widoku [ListeSimulations] odśwież (F5) widok. W poprzedniej aplikacji [vuejs-20] powodowało to utratę symulacji. Tutaj tak nie jest: zachowują się wszystkie wcześniej wykonane symulacje;
  • przejrzyjcie logi, aby zrozumieć:
    • w którym momencie wykonywany jest skrypt [main]. Powinieneś zauważyć, że jest on uruchamiany za każdym razem, gdy użytkownik ręcznie wpisuje URL;
    • w jakich momentach tworzone są widoki. Powinniście zauważyć, że są one tworzone za każdym razem, gdy mają zostać wyświetlone;
    • jak działa routing. Przed każdym routingiem tworzony jest wpis w logu, który wskazuje:
      • trasę, z której pochodzisz;
      • trasę, do której zmierzasz;

19.13. Wdrożenie aplikacji na serwerze lokalnym

Jako ćwiczenie postępuj zgodnie z instrukcją zawartą w akapicie |Déploiement sur un serveur local|, aby wdrożyć projekt [vuejs-21] na lokalnym serwerze Laragon. Następnie przetestuj go.

19.14. Dopracowanie wersji mobilnej

Teoretycznie wykorzystanie Bootstrap powinno pozwolić nam na stworzenie aplikacji działającej na różnych urządzeniach: smartfonach, tabletach, laptopach i komputerach stacjonarnych. Tym, co odróżnia te urządzenia, jest rozmiar ich ekranów.

Jeśli przetestujemy wersję [vuejs-21] na urządzeniu mobilnym, zauważymy, że wyświetlanie widoków jest chaotyczne. Wersja [vuejs-22] koryguje ten problem. Wszystkie zmiany zostały wprowadzone w szablonach widoków. Polegały one głównie na dostosowaniu wyświetlania do ekranu smartfona. Gdy wyświetlanie na ekranie smartfona jest już dopracowane, wyświetlanie na ekranach o większych rozmiarach przebiega płynnie dzięki bibliotece Bootstrap.

Image

19.14.1. Widok [Main]

Widok [Main] zmienia się w następujący sposób:


<!-- definicja widoku HTML -->
<template>
  <div class="container">
    <b-card>
      <!-- jumbotron -->
      <b-jumbotron>
        <b-row>
          <b-col sm="4">
            <img src="../assets/logo.jpg" alt="Cerisier en fleurs" />
          </b-col>
          <b-col sm="8">
            <h1>Calculez votre impôt</h1>
          </b-col>
        </b-row>
      </b-jumbotron>
      ....
    </b-card>
  </div>
</template>

Uwagi

  • wiersz 8: w miejscu, gdzie było [cols=’4’], wpisuje się [sm=’4’]. [sm] oznacza [small]. Ekrany smartfonów należą do tej kategorii. Pozostałe kategorie to [xs=extra small, md=medium, lg=large, xl=extra large];
  • wiersz 11: to samo;

19.14.2. Widok [Layout]

Widok [Layout] zmienia się w następujący sposób:


<!-- definicja HTML układu widoku przekierowanego -->
<template>
  <!-- wiersz -->
  <div>
    <b-row>
      <!-- obszar z trzema kolumnami po lewej stronie -->
      <b-col sm="3" v-if="left">
        <slot name="left" />
      </b-col>
      <!-- obszar z dziewięcioma kolumnami po prawej stronie -->
      <b-col sm="9" v-if="right">
        <slot name="right" />
      </b-col>
    </b-row>
  </div>
</template>

19.14.3. Widok [Authentification]

Widok [Authentification] zmienia się w następujący sposób:


<!-- 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" description="Tapez admin">
          <!-- pole wprowadzania danych użytkownika -->
          <b-col sm="6">
            <b-form-input type="text" id="user" placeholder="Nom d'utilisateur" v-model="user" />
          </b-col>
        </b-form-group>
        <!-- drugi wiersz -->
        <b-form-group label="Mot de passe" label-for="password" description="Tapez admin">
          <!-- pole wprowadzania hasła -->
          <b-col sm="6">
            <b-input type="password" id="password" placeholder="Mot de passe" v-model="password" />
          </b-col>
        </b-form-group>
        <!-- trzeci wiersz -->
        <b-alert
          show
          variant="danger"
          v-if="showError"
          class="mt-3"
        >L'erreur suivante s'est produite : {{message}}</b-alert>
        <!-- przycisk typu [submit] w trzecim wierszu -->
        <b-row>
          <b-col sm="2">
            <b-button variant="primary" type="submit" :disabled="!valid">Valider</b-button>
          </b-col>
        </b-row>
      </b-form>
    </template>
  </Layout>
</template>

Uwagi

  • wiersze 11 i 19: usunięto atrybut [label-cols], który określał liczbę kolumn etykiety pola wprowadzania danych. W przypadku braku tego atrybutu etykieta znajduje się nad polem wprowadzania danych. Rozwiązanie to lepiej sprawdza się na ekranach smartfonów;

19.14.4. Widok [CalculImpot]

Widok [CalculImpot] ulega następującym zmianom:


<!-- 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>

19.14.5. Widok [FormCalculImpot]

Widok [FormCalculImpot] zmienia się w następujący sposób:


<!-- 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
        type="text"
        id="enfants"
        placeholder="Indiquez votre nombre d'enfants"
        v-model="enfants"
        :state="enfantsValide"
      ></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
        type="text"
        id="salaire"
        placeholder="Salaire annuel"
        v-model="salaire"
        :state="salaireValide"
      ></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 type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
    </b-col>
  </b-form>
</template>

Uwagi

  • wiersze 15, 23, 35: usunięto atrybut [label-cols];

Ponadto wprowadzono zmiany w testach poprawności:


...
// obliczony stan wewnętrzny
  computed: {
    // zatwierdzenie formularza
    formInvalide() {
      return (
        // nieprawidłowe wynagrodzenie
        !this.salaire.match(/^\s*\d+\s*$/) ||
        // lub nieprawidłowe dane dotyczące 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*$/)
      );
    }
},
...

Uwagi

  • wiersz 19: gdy nie wprowadzono żadnych danych, wpis jest uznawany za poprawny. Dzięki temu wpis jest poprawny już przy pierwszym wyświetleniu widoku. W poprzedniej wersji wpis był początkowo wyświetlany jako błędny;
  • wiersz 26: to samo;
  • wiersze 5–14: przycisk zatwierdzenia jest aktywny tylko wtedy, gdy oba pola zawierają dane i są prawidłowe;

19.14.6. Widok [Menu]

Widok [Menu] zmienia się w następujący sposób:


<!-- definicja widoku HTML -->
<template>
  <b-card class="mb-3">
    <!-- pionowe menu Bootstrap -->
    <b-nav vertical>
      <!-- opcje menu -->
      <b-nav-item
        v-for="(option,index) of options"
        :key="index"
        :to="option.path"
        exact
        exact-active-class="active"
      >{{option.text}}</b-nav-item>
    </b-nav>
  </b-card>
</template>

Uwagi

  • wiersz 3: dodano tag <b-card>, aby otoczyć menu cienką ramką. Pozwala to na lepszą lokalizację menu na smartfonie;

19.14.7. Widok [ListeSimulations]

Widok [ListeSimulations] pozostaje bez zmian:


<!-- 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 striped hover responsive :items="simulations" :fields="fields">
            <template v-slot:cell(action)="data">
              <b-button variant="link" @click="supprimerSimulation(data.index)">Supprimer</b-button>
            </template>
          </b-table>
        </template>
      </template>
      <!-- menu nawigacyjne w lewej kolumnie -->
      <Menu slot="left" :options="options" />
    </Layout>
  </div>
</template>

Komentarze

  • wiersz 20: warto zwrócić uwagę na atrybut [responsive], który sprawia, że wyświetlanie tabeli dostosowuje się do rozmiaru ekranu:

Image

  • w [2] na małych ekranach poziomy pasek przewijania umożliwia wyświetlenie tabeli;

19.14.8. Widok [NotFound]

Pozostaje bez zmian.

19.14.9. Widoki na urządzeniach mobilnych

Image

Image

Uwaga: z pewnością istnieje możliwość uzyskania widoków jeszcze lepiej dostosowanych do urządzeń mobilnych. Mam tu na myśli zwłaszcza menu nawigacyjne, które można by ulepszyć, ale są też inne kwestie. Głównym celem tego dokumentu nie było stworzenie aplikacji mobilnej. W takim przypadku być może skorzystalibyśmy z frameworka takiego jak Ionic |https://ionicframework.com/|.