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:

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:

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

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.

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:

- 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


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