Skip to content

19. Удосконалення клієнта Vue.js

19.1. Introduction

Ми будемо тестувати проект [vuejs-21] на сервері розробки. Тому нам знову знадобиться, щоб сервер надсилав заголовки CORS. Отже, файл [config.json] версії 14 сервера розрахунку податків повинен підтримувати такі заголовки:

Image

Проєкт [vuejs-21] спочатку створюється шляхом дублювання проєкту [vuejs-20]. Потім його змінюють на [3].

З’являються нові файли:

  • [session.js]: експортує об’єкт [session], який буде містити інформацію про поточну сесію;
  • [pluginSession]: робить попередній об’єкт [session] доступним у властивості [$session] переглядів;
  • [NotFound.vue]: новий вигляд, що відображається, коли користувач вручну запитує URL, якого не існує;

Будуть змінені такі файли:

  • [main.js]: ініціалізує поточну сесію, а потім, коли користувач вручну введе URL, відновить її;
  • [router.js]: додано перевірки для обробки випадків, коли користувач вводить URL;
  • [store.js]: додано нову зміну;
  • [config.js]: додано нову конфігурацію;
  • різні види, в основному для збереження поточної сесії в ключові моменти роботи додатка. Потім вона відновлюється щоразу, коли користувач вручну вводить URL;

19.2. Магазин [Vuex]

Скрипт [./store] змінюється наступним чином:


// плагін Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);

// Vuex-сторінка
const store = new Vuex.Store({
  state: {
    // таблиця моделювань
    simulations: [],
    // номер останнього моделювання
    idSimulation: 0
  },
  mutations: {
    // видалення рядка № індексу
    deleteSimulation(state, index) {
      ...
    },
    // додавання симуляції
    addSimulation(state, simulation) {
      ...
    },
    // очищення стану
    clear(state) {
      // більше симуляцій
      state.simulations = [];
      // нумерація симуляцій починається з 0
      state.idSimulation = 0;
    }
  }
});
// експорт об’єкта [store]
export default store;
  • рядки 24–29: зміна [clear] видаляє список збережених симуляцій і обнуляє номер останньої симуляції.

19.3. Сесія

Необхідність у сесії пояснюється тим, що коли користувач вводить URL у поле адреси браузера, скрипт [main.js] виконується знову. Однак він містить інструкцію:


// Vuex-сторінка
import store from './store'

Ця інструкція імпортує наступний файл [./store]:


// плагін Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);

// архів Vuex
const store = new Vuex.Store({
  state: {
    // таблиця симуляцій
    simulations: [],
    // номер останнього моделювання
    idSimulation: 0
  },
  mutations: {
    ...
  }
});
// експорт об’єкта [store]
export default store;

Як бачимо в рядках 7–13, імпортується порожній масив симуляцій. Отже, якщо симуляції існували до того, як користувач ввів URL у поле адреси браузера, то після цього їх більше немає. Ідея полягає в тому, щоб:

  • використовувати сесію, яка зберігатиме інформацію, яку ми хочемо зберегти, якщо користувач вручну вводить URL;
  • зберігати її в ключові моменти роботи додатка;
  • відновити її в [main.js], який завжди виконується, коли URL вводиться вручну;

Скрипт [./session] має такий вигляд:


// імпортуємо Vuex-сторі
import store from './store'
// імпортуємо конфігурацію
import config from './config';

// об’єкт [session]
const session = {
  // сесія запущена
  started: false,
  // аутентифікація
  authenticated: false,
  // час збереження
  saveTime: "",
  // шар [métier]
  métier: null,
  // стан Vuex
  state: null,

  // збереження сеансу в рядку jSON
  save() {
    // до сеансу додаються деякі властивості
    this.saveTime = Date.now();
    this.state = store.state;
    // перетворюємо її на jSON
    const json = JSON.stringify(this);
    // зберігаємо її в браузері
    localStorage.setItem("session", json);
    // eslint-disable-next-line no-console
    console.log("session save", json);
  },

  // відновлення сесії
  restore() {
    // отримуємо сесію jSON з браузера
    const json = localStorage.getItem("session")
    // якщо щось було отримано
    if (json) {
      // відновлюються всі ключі сеансу
      const restore = JSON.parse(json);
      for (var key in restore) {
        if (restore.hasOwnProperty(key)) {
          this[key] = restore[key];
        }
      }
      // якщо з моменту початку сеансу минув певний час бездіяльності, починаємо з нуля
      let durée = Date.now() - this.saveTime;
      if (durée > config.duréeSession) {
        // очищаємо сесію — вона також буде збережена
        session.clear();
      } else {
        // регенерується сховище Vuex
        store.replaceState(JSON.parse(JSON.stringify(this.state)));
      }
    }
    // eslint-disable-next-line no-console
    console.log("session restore", this);
  },

    // очищаємо сесію
  clear() {
    // eslint-disable-next-line no-console
    console.log("session clear");
    // очищення деяких полів сесії
    this.authenticated = false;
    this.saveTime = "";
    this.started = false;
    if (this.métier) {
      // поле [taxAdminData] скидається
      this.métier.taxAdminData = null;
    }
    // також очищається Vuex-сторінг
    store.commit("clear");
    // зберігається нова сесія
    this.save();
  },
}

// експорт об’єкта [session]
export default session;

Коментарі

  • рядок 2: сесія також інкапсулює сховище [Vuex] (список симуляцій, номер останньої виконаної симуляції);
  • рядки 7–17: інформація, що зберігається сесією:
    • [started]: чи розпочалася сесія jSON із сервером;
    • [authenticated]: чи пройшов користувач аутентифікацію;
    • [saveTime]: дата останнього збереження у мілісекундах;
    • [métier]: посилання на рівень [métier]. Воно містить дані [taxAdminData], які дозволяють розрахувати податок;
    • [state]: стан сховища [Vuex] (список симуляцій, номер останньої виконаної симуляції);
  • рядки 20–30: метод [save] зберігає сесію локально в браузері, що виконує додаток;
    • рядок 22: фіксується час збереження;
    • рядок 23: отримується [state] із сховища [Vuex];
    • рядок 25: створюється рядок сеансу jSON;
    • рядок 27: зберігається локально в браузері, пов’язаному з ключем [session];
  • рядки 33–57: метод [restore] дозволяє відновити сесію з її локальної резервної копії в браузері;
    • рядок 35: завантажується локальна резервна копія jSON;
    • рядок 37: якщо щось було відновлено;
    • рядки 39–44: об’єкт [session] відновлюється;
    • рядок 46: обчислюється час, що минув з моменту останньої резервної копії;
    • рядки 47–50: якщо цей проміжок часу перевищує значення [config.duréeSession], встановлене в налаштуваннях, сесія перезапускається (рядок 49) і при цьому зберігається;
    • рядок 52: в іншому випадку регенерується атрибут [state] сховища [Vuex];
  • рядки 60–75: метод [clear] скидає сесію;
    • рядки 64–70: властивості сеансу скидаються до початкових значень;
    • рядок 72: так само як і сховище [Vuex];
    • рядок 74: нова сесія зберігається;

19.4. Конфігураційний файл [config]

Файл [./config] змінюється наступним чином:


// використання бібліотеки [axios]
const axios = require('axios');
// тайм-аут запитів HTTP
axios.defaults.timeout = 2000;
...

// експорт конфігурації
export default {
  // об’єкт [axios]
  axios: axios,
  // максимальний час бездіяльності сеансу: 5 хв = 300 с = 300000 мс
  duréeSession: 300000
}
  • рядок 12: сесію додатка будемо керувати приблизно так само, як і веб-сесію. Тут встановлюємо максимальний час бездіяльності — 5 хвилин;

19.5. Плагін [pluginSession]

Як це вже робилося багато разів, плагін [pluginSession] дозволить подам отримувати доступ до сесії через властивість [this.$session]:


export default {
  install(Vue, session) {
    // додає властивість [$session] до класу представлення
    Object.defineProperty(Vue.prototype, '$session', {
      // коли відбувається звернення до Vue.$session, повертається другий параметр [session]
      get: () => session,
    })
  }
}

19.6. Головний скрипт [main]

Головний скрипт [./main.js] змінюється наступним чином:


// журнал запуску
// eslint-disable-next-line no-console
console.log("main started");

// імпорти
import Vue from 'vue'

...

// інстанціювання шару [métier]
import Métier from './couches/Métier';
const métier = new Métier();

// плагін [métier]
import pluginMétier from './plugins/pluginMétier'
Vue.use(pluginMétier, métier)

// Vuex-сховище
import store from './store'

// сесія
import session from './session';
import pluginSession from './plugins/pluginSession'
Vue.use(pluginSession, session)

// перед перезапуском сесія відновлюється
session.restore();

// відновлюється рівень [métier]
if (session.métier && session.métier.taxAdminData) {
  métier.setTaxAdminData(session.métier.taxAdminData);
}

// запуск програми UI
new Vue({
  el: '#додаток,
  // маршрутизатор
  router: router,
  // жалюзі Vuex
  store: store,
  // головний екран
  render: h => h(Main),
})

// завершальний журнал
// eslint-disable-next-line no-console
console.log("main terminated, session=", session);
  • рядок 19: імпортується сесія;
  • рядок 20: імпортується його плагін;
  • рядок 21: плагін [pluginSession] інтегрується в [Vue]. Після цієї інструкції всі подання мають сесію у своєму атрибуті [$session];
  • рядок 27: сесія відновлюється. Сесія, імпортована в рядку 11, ініціалізується вмістом її останнього збереження;
  • після рядка 16 перегляди мають властивість [$métier], ініціалізовану в рядку 12. Ця властивість не містить інформації [taxAdminData], яка дозволяє розрахувати податок;
  • рядки 30–32: якщо під час щойно виконаного відновлення було відновлено властивість [session.métier.taxAdminData], то властивість [$métier] переглядів ініціалізується цим значенням;

19.7. Файл маршрутизації [router]

Файл маршрутизації [./router] змінюється наступним чином:


// імпорти
import Vue from 'vue'
import VueRouter from 'vue-router'
// види
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'
import NotFound from './views/NotFound'
// сесія
import session from './session'

// плагін маршрутизації
Vue.use(VueRouter)

// маршрути додатка
const routes = [
  // аутентифікація
  { path: '/', name: 'authentification', component: Authentification },
  { path: '/authentification', name: 'authentification', component: Authentification },
  // розрахунок податку
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot,
    meta: { authenticated: true }
  },
  // список моделювань
  {
    path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations,
    meta: { authenticated: true }
  },
  // завершення сеансу
  {
    path: '/fin-session', name: 'finSession'
  },
  // невідома сторінка
  {
    path: '*', name: 'notFound', component: NotFound,
  },
]

// маршрутизатор
const router = new VueRouter({
  // маршрути
  routes,
  // режим відображення URL
  mode: 'history',
  // основний URL додатка
  base: '/client-vuejs-impot/'
})

// перевірка маршрутів
router.beforeEach((to, from, next) => {
  // eslint-disable-next-line no-console
  console.log("router to=", to, "from=", from);
  // шлях, доступний лише для автентифікованих користувачів?
  if (to.meta.authenticated && !session.authenticated) {
    next({
      // переходимо до автентифікації
      name: 'authentification',
    })
    // повернення до циклу обробки подій
    return;
  }
  // особливий випадок завершення сеансу
  if (to.name === "finSession") {
    // очищення сесії
    session.clear();
    // перехід до подання [authentification]
    next({
      name: 'authentification',
    })
    // повернення до циклу обробки подій
    return;
  }
  // інші випадки — наступний звичайний вигляд маршрутизації
  next();
})

// експорт маршрутизатора
export default router

Коментарі

  • рядки 16–38: деякі маршрути було доповнено додатковою інформацією;
  • рядок 19: створено новий маршрут до виду [Authentification];
  • рядки 21–24: маршрут, що веде до подання [CalculImpot], тепер має властивість [meta] (ця назва є обов’язковою). Вміст цього об’єкта може бути будь-яким і визначається розробником;
  • рядок 23: у [meta] задаємо властивість [authenticated] (ця назва може бути будь-якою). Для нас це означатиме, що для переходу до подання [CalculImpot] користувач повинен пройти автентифікацію;
  • рядки 26–29: те саме робимо для маршруту, що веде до подання [ListeSimulations]. Тут також користувач повинен пройти автентифікацію;
  • властивість [meta.authenticated] дозволить нам перевірити, що користувач, який вручну вводить URL з переглядів [CalculImpot, ListeSimulations], не зможе отримати їх, якщо він не пройшов аутентифікацію;
  • рядки 51–76: метод [beforeEach] виконується до того, як погляд буде маршрутизовано. Це вдалий момент для проведення перевірок;
    • [to]: наступний маршрут, якщо нічого не робити;
    • [from]: останній відображений маршрут;
    • [next]: функція, що дозволяє змінити наступний маршрут, який відображається;
  • рядок 55: перевіряємо, чи наступний маршрут вимагає автентифікації користувача;
  • рядки 56–59: якщо так і користувач не пройшов автентифікацію, наступний маршрут змінюється на перегляд [Authentification];
  • рядки 64–73: обробляється особливий випадок маршруту [finSession] із рядків 30–32. Цей маршрут не має пов’язаного з ним виду;
    • рядок 66: сесія скидається до початкового стану;
    • рядки 68–70: призначаємо вигляд [Authentification] як наступний вигляд;
  • рядок 75: якщо ситуація не відповідає жодному з двох попередніх випадків, просто переходимо до маршруту, передбаченого файлом маршрутизації;
  • рядки 35–37: передбачається вигляд [NotFound], якщо маршрут, введений користувачем, не відповідає жодному відомому маршруту. Цей вигляд імпортується у рядку 8. Маршрути перевіряються у порядку, заданому файлом маршрутизації. Отже, якщо програма дійшла до рядка 36, це означає, що запитуваний маршрут не є жодним із маршрутів, зазначених у рядках 18–33;

19.8. Вигляд [NotFound]

Вигляд [NotFound] відображається, якщо маршрут, введений користувачем, не відповідає жодному відомому маршруту:

Image

Код цього виду такий:


<!-- визначення HTML подання -->
<template>
  <!-- верстка -->
  <Layout :left="true" :right="true">
    <!-- сповіщення у правій колонці -->
    <template slot="right">
      <!-- повідомлення на жовтому тлі -->
      <b-alert show variant="danger" align="center">
        <h4>Cette page n'existe pas</h4>
      </b-alert>
    </template>
    <!-- меню навігації у лівій колонці -->
    <Menu slot="left" :options="options" />
  </Layout>
</template>

<script>
// імпорт
import Layout from "./Layout";
import Menu from "./Menu";
export default {
  // компоненти
  components: {
    Layout,
    Menu
  },
  // внутрішній стан компонента
  data() {
    return {
      // параметри меню навігації
      options: [
        {
          text: "Authentification",
          path: "/"
        }
      ]
    };
  },
  // життєвий цикл
  created() {
    // eslint-disable-next-line
    console.log("NotFound created");
    // визначаємо, які опції меню пропонувати
    if (this.$session.authenticated && this.$métier.taxAdminData) {
      // користувач може проводити симуляції
      Array.prototype.push.apply(this.options, [
        {
          text: "Calcul de l'impôt",
          path: "/calcul-impot"
        },
        {
          text: "Liste des simulations",
          path: "/liste-des-simulations"
        }
      ]);
    }
  }
};
</script>

Коментарі

  • рядок 4: використовуються обидва стовпці маршрутизованих поглядів;
  • рядки 6–11: повідомлення про помилку;
  • рядок 13: меню навігації займає ліву колонку;
  • рядки 31–36: опції меню за замовчуванням;
  • рядки 40–57: код, що виконується під час створення подання;
  • рядок 44: перевіряється, чи може користувач виконувати симуляції;
  • рядки 45–55: якщо так, до меню навігації додаються два пункти, для яких необхідна авторизація та наявність робочого шару [métier] (рядки 46–55);

19.9. Вигляд [Authentification]

Вигляд [Authentification] змінюється наступним чином:


<!-- визначення HTML подання -->
<template>
  <Layout :left="false" :right="true">
    ...
  </Layout>
</template>

<!-- динаміка подання -->
<script>
import Layout from "./Layout";
export default {
  // стан компонента
  data() {
    return {
      // користувач
      user: "",
      // його пароль
      password: "",
      // контролює відображення повідомлення про помилку
      showError: false,
      // повідомлення про помилку
      message: ""
    };
  },

  // використані компоненти
  components: {
    Layout
  },

  // розраховані властивості
  computed: {
    // дійсні введені дані
    valid() {
      return this.user && this.password && this.$session.started;
    }
  },

  // обробники подій
  methods: {
    // ----------- автентифікація
    async login() {
      try {
        // початок очікування
        this.$emit("loading", true);
        // аутентифікація ще не виконана
        this.$session.authenticated = false;
        // автентифікація, що блокує роботу сервера
        const response = await this.$dao.authentifierUtilisateur(
          this.user,
          this.password
        );
        // завершення завантаження
        this.$emit("loading", false);
        // аналіз відповіді сервера
        if (response.état != 200) {
          // відображення помилки
          this.message = response.réponse;
          this.showError = true;
          // повернення до циклу обробки подій
          return;
        }
        // помилки немає
        this.showError = false;
        // автентифікація пройшла успішно
        this.$session.authenticated = true;
        // --------- тепер запитуються дані податкової адміністрації
        // спочатку даних немає
        this.$métier.setTaxAdminData(null);
        // початок очікування
        this.$emit("loading", true);
        // запит, що блокує роботу сервера
        const response2 = await this.$dao.getAdminData();
        // завершення завантаження
        this.$emit("loading", false);
        // аналіз відповіді
        if (response2.état != 1000) {
          // виводиться повідомлення про помилку
          this.message = response2.réponse;
          this.showError = true;
          // повернення до циклу обробки подій
          return;
        }
        // помилки немає
        this.showError = false;
        // збереження отриманих даних у шарі [métier]
        this.$métier.setTaxAdminData(response2.réponse);
        // можна перейти до розрахунку податку
        this.$router.push({ name: "calculImpot" });
      } catch (error) {
        // помилку передаємо до головного компонента
        this.$emit("error", error);
      } finally {
        // Оновлення сесії
        this.$session.métier = this.$métier;
        // зберігаємо сесію
        this.$session.save();
      }
    }
  },
  // життєвий цикл: компонент щойно створено
  created() {
    // eslint-disable-next-line
    console.log("Authentification created");
    // чи може користувач проводити симуляції?
    if (
      this.$session.started &&
      this.$session.authenticated &&
      this.$métier.taxAdminData
    ) {
      // тоді користувач може проводити симуляції
      this.$router.push({ name: "calculImpot" });
      // повернення до циклу подій
      return;
    }
    // якщо сесія jSON вже запущена, її не запускають знову
    if (!this.$session.started) {
      // початок очікування
      this.$emit("loading", true);
      // ініціалізуємо сесію з сервером — асинхронний запит
      // використовується обіцянка, повернута методами шару [dao]
      this.$dao
        // ініціалізується сесія jSON
        .initSession()
        // отримано відповідь
        .then(response => {
          // завершено очікування
          this.$emit("loading", false);
          // аналіз відповіді
          if (response.état != 700) {
            // виводиться повідомлення про помилку
            this.message = response.réponse;
            this.showError = true;
            // повернення до циклу обробки подій
            return;
          }
          // сесія розпочалася
          this.$session.started = true;
        })
        // у разі помилки
        .catch(error => {
          // помилка передається до подання [Main]
          this.$emit("error", error);
        })
        // у будь-якому випадку
        .finally(() => {
          // сесія зберігається
          this.$session.save();
        });
    }
  }
};
</script>

Коментарі

  • жовтим кольором виділено інструкції, які використовують сесію, введену в цій версії клієнта [Vue.js];
  • рядки 97, 148: у кінці методів [login, created] сесія зберігається незалежно від результату запитів HTTP, що виконуються в цих методах (у обох випадках використовується оператор [finally]);
  • метод [created] у рядках 102–150 виконується щоразу, коли створюється представлення [Authentification]. Якщо користувач ввів URL для цього подання, сесія дозволить нам визначити, що робити;
  • рядки 106–115: якщо сесія jSON запущена, користувач автентифікований, а дані [this.$métier.taxAdminData] ініціалізовані, то користувач може безпосередньо перейти до форми розрахунку податку (рядок 112);
  • рядок 117: метод [created] використовувався в попередній версії для ініціалізації сесії jSON із сервером. Цей етап є зайвим, якщо він уже відбувся;
  • рядки 42–66: метод автентифікації;
  • рядок 66: якщо автентифікація пройшла успішно, це фіксується в сесії;
  • рядки 67–92: запит до сервера щодо даних податкової адміністрації [taxAdminData];
  • рядок 95: наприкінці цього етапу оновлюється властивість [métier] сесії незалежно від того, чи операція була успішною;

19.10. Вигляд [CalculImpot]

Код подання [CalculImpot] змінюється наступним чином:


<!-- визначення HTML для подання -->
<template>
  ...
</template>

<script>
// імпорт
import FormCalculImpot from "./FormCalculImpot";
import Menu from "./Menu";
import Layout from "./Layout";

export default {
  // внутрішній стан
  data() {
    return {
      // параметри меню
      options: [
        {
          text: "Liste des simulations",
          path: "/liste-des-simulations"
        },
        {
          text: "Fin de session",
          path: "/fin-session"
        }
      ],
      // результат розрахунку податку
      résultat: "",
      résultatObtenu: false
    };
  },
  // використані компоненти
  components: {
    Layout,
    FormCalculImpot,
    Menu
  },
  // методи управління подіями
  methods: {
    // результат розрахунку податку
    handleResultatObtenu(résultat) {
      // результат формується у вигляді ланцюжка HTML
      ...
      // моделювання +
      this.$store.commit("addSimulation", résultat);
      // збереження сеансу
      this.$session.save();
    }
  },
  // життєвий цикл
  created() {
    // eslint-disable-next-line
    console.log("CalculImpot created");
  }
};
</script>

Коментарі

  • рядок 45: розрахована симуляція додається до сховища [Vuex]. Це впливає на сесію, яка охоплює властивість [state] цього сховища. Тому сесію зберігають (рядок 47);
  • рядок 51: створюється метод [created] для відстеження у журналах створення переглядів;

19.11. Вигляд [ListeSimulations]

Вигляд [ListeSimulations] змінюється наступним чином:


<!-- визначення HTML подання -->
<template>
  ...
  </div>
</template>

<script>
// імпорти
import Layout from "./Layout";
import Menu from "./Menu";
export default {
  // компоненти
  components: {
    Layout,
    Menu
  },
  // внутрішній стан
  data() {
    ...
  },
  // розрахований внутрішній стан
  computed: {
    // список симуляцій, взятих із сховища Vuex
    simulations() {
      return this.$store.state.simulations;
    }
  },
  // методи
  methods: {
    supprimerSimulation(index) {
      // eslint-disable-next-line
      console.log("supprimerSimulation", index);
      // видалення симуляції № [index]
      this.$store.commit("deleteSimulation", index);
      // збереження сесії
      this.$session.save();
    }
  },
  // життєвий цикл
  created() {
    // eslint-disable-next-line
    console.log("ListeSimulations created");
  }
};
</script>

Коментарі

  • рядок 36: після видалення симуляції з рядка 34 сесія зберігається, щоб врахувати цю зміну стану;
  • рядки 40–43: продовжується відстеження створення поглядів;

19.12. Виконання проєкту

Image

Під час тестування перевірте наступні моменти:

  • чи працює додаток, коли користувач «користується» ним через посилання в меню навігації та кнопки/посилання дій;
  • якщо користувач вручну вводить URL, додаток продовжує працювати. Зокрема, проведіть таке тестування:
    • проведіть симуляції;
    • перейшовши на сторінку [ListeSimulations], перезавантажте (F5) цю сторінку. У попередньому додатку [vuejs-20] у цьому випадку втрачалися симуляції. Тут цього не відбувається: вже виконані симуляції зберігаються;
  • перегляньте журнали, щоб зрозуміти:
    • у який момент виконується скрипт [main]. Ви повинні побачити, що він виконується щоразу, коли користувач вручну вводить URL;
    • у які моменти створюються перегляди. Ви повинні побачити, що вони створюються щоразу, коли їх мають відобразити;
    • як працює маршрутизація. Перед кожною маршрутизацією створюється запис у журналі, який вказує:
      • маршрут, з якого ви прийшли;
      • маршрут, куди ви прямуєте;

19.13. Розгортання додатка на локальному сервері

Як вправу, виконайте вказівки з розділу |Розгортання на локальному сервері|, щоб розгорнути проєкт [vuejs-21] на локальному сервері Laragon. Потім протестуйте його.

19.14. Налагодження мобільної версії

Теоретично використання Bootstrap має дозволити нам отримати додаток, придатний для використання на різних пристроях: смартфонах, планшетах, ноутбуках та настільних комп’ютерах. Ці пристрої відрізняються розміром екрану.

Якщо протестувати версію [vuejs-21] на мобільному пристрої, можна помітити, що відображення сторінок є хаотичним. Версія [vuejs-22] виправляє цю проблему. Усі зміни внесені в шаблони сторінок. Вони полягали, головним чином, у налагодженні відображення для екрану смартфона. Коли це налагоджено, відображення на екранах більшого розміру відбувається плавно завдяки Bootstrap.

Image

19.14.1. Вигляд [Main]

Вигляд [Main] змінюється наступним чином:


<!-- визначення 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>

Коментарі

  • рядок 8: там, де було [cols=’4’], записуємо [sm=’4’]. [sm] означає [small]. Екрани смартфонів належать до цієї категорії. Інші категорії — [xs=extra small, md=medium, lg=large, xl=extra large];
  • рядок 11: те саме;

19.14.2. Вигляд [Layout]

Вигляд [Layout] змінюється наступним чином:


<!-- визначення HTML макета маршрутизованого виду -->
<template>
  <!-- рядок -->
  <div>
    <b-row>
      <!-- зона з трьома стовпцями ліворуч -->
      <b-col sm="3" v-if="left">
        <slot name="left" />
      </b-col>
      <!-- зона з дев'ятьма стовпцями праворуч -->
      <b-col sm="9" v-if="right">
        <slot name="right" />
      </b-col>
    </b-row>
  </div>
</template>

19.14.3. Вигляд [Authentification]

Вигляд [Authentification] змінюється наступним чином:


<!-- визначення HTML для подання -->
<template>
  <Layout :left="false" :right="true">
    <template slot="right">
      <!-- форма HTML — значення відправляються за допомогою дії [authentifier-utilisateur] -->
      <b-form @submit.prevent="login">
        <!-- заголовок -->
        <b-alert show variant="primary">
          <h4>Bienvenue. Veuillez vous authentifier pour vous connecter</h4>
        </b-alert>
        <!-- 1-й рядок -->
        <b-form-group label="Nom d'utilisateur" label-for="user" description="Tapez admin">
          <!-- поле введення користувача -->
          <b-col sm="6">
            <b-form-input type="text" id="user" placeholder="Nom d'utilisateur" v-model="user" />
          </b-col>
        </b-form-group>
        <!-- 2-й рядок -->
        <b-form-group label="Mot de passe" label-for="password" description="Tapez admin">
          <!-- поле введення пароля -->
          <b-col sm="6">
            <b-input type="password" id="password" placeholder="Mot de passe" v-model="password" />
          </b-col>
        </b-form-group>
        <!-- третій рядок -->
        <b-alert
          show
          variant="danger"
          v-if="showError"
          class="mt-3"
        >L'erreur suivante s'est produite : {{message}}</b-alert>
        <!-- кнопка типу [submit] у третьому рядку -->
        <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>

Коментарі

  • рядки 11 і 19: було видалено атрибут [label-cols], який встановлював кількість стовпців для мітки поля введення. За відсутності цього атрибута мітка розташовується над полем введення. Це краще підходить для екранів смартфонів;

19.14.4. Вигляд [CalculImpot]

Вигляд [CalculImpot] змінюється наступним чином:


<!-- визначення HTML для подання -->
<template>
  <div>
    <Layout :left="true" :right="true">
      <!-- форма для розрахунку податку праворуч -->
      <FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
      <!-- меню навігації ліворуч -->
      <Menu slot="left" :options="options" />
    </Layout>
    <!-- область відображення результатів розрахунку податку під формою -->
    <b-row v-if="résultatObtenu" class="mt-3">
      <!-- порожня область із трьома стовпцями -->
      <b-col sm="3" />
      <!-- область з дев'ятьма стовпцями -->
      <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. Вигляд [FormCalculImpot]

Вигляд [FormCalculImpot] змінюється наступним чином:


<!-- визначення HTML для подання -->
  <template>
  <!-- форма HTML -->
  <b-form @submit.prevent="calculerImpot" class="mb-3">
    <!-- повідомлення на 12 стовпців на синьому тлі -->
    <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>
    <!-- елементи форми -->
    <!-- перший рядок -->
    <b-form-group label="Etes-vous marié(e) ou pacsé(e) ?">
      <!-- перемикачі у 5 стовпців-->
      <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>
    <!-- другий рядок -->
    <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>
      <!-- можливе повідомлення про помилку -->
      <b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- третій рядок -->
    <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>
      <!-- можливе повідомлення про помилку -->
      <b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- четвертий рядок, кнопка [submit] -->
    <b-col sm="3">
      <b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
    </b-col>
  </b-form>
</template>

Коментарі

  • рядки 15, 23, 35: видалено атрибут [label-cols];

Крім того, вдосконалюються тести на валідність:


...
// розрахований внутрішній стан
  computed: {
    // підтвердження форми
    formInvalide() {
      return (
        // неправильна заробітна плата
        !this.salaire.match(/^\s*\d+\s*$/) ||
        // або недійсні дані про дітей
        !this.enfants.match(/^\s*\d+\s*$/) ||
        // або податкові дані не отримано
        !this.$métier.taxAdminData
      );
    },
    // перевірка заробітної плати
    salaireValide() {
      // повинно бути числом >=0
      return Boolean(
        this.salaire.match(/^\s*\d+\s*$/) || this.salaire.match(/^\s*$/)
      );
    },
    // перевірка даних про дітей
    enfantsValide() {
      // повинно бути числом >=0
      return Boolean(
        this.enfants.match(/^\s*\d+\s*$/) || this.enfants.match(/^\s*$/)
      );
    }
},
...

Коментарі

  • рядок 19: якщо нічого не введено, введення вважається правильним. Це дозволяє мати правильне введення при першому відображенні вікна. У попередній версії введення спочатку відображалося як помилкове;
  • рядок 26: те саме;
  • рядки 5–14: кнопка підтвердження активна лише тоді, коли обидва поля містять дані та є правильними;

19.14.6. Вікно [Menu]

Вигляд [Menu] змінюється наступним чином:


<!-- визначення HTML для подання -->
<template>
  <b-card class="mb-3">
    <!-- вертикальне меню Bootstrap -->
    <b-nav vertical>
      <!-- параметри меню -->
      <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>

Коментарі

  • рядок 3: додається тег <b-card>, щоб оточити меню тонкою рамкою. Це дозволяє краще розмістити меню на смартфоні;

19.14.7. Вигляд [ListeSimulations]

Вигляд [ListeSimulations] залишається без змін:


<!-- визначення HTML для подання -->
<template>
  <div>
    <!-- верстка -->
    <Layout :left="true" :right="true">
      <!-- симуляції у правій колонці -->
      <template slot="right">
        <template v-if="simulations.length==0">
          <!-- без моделювань -->
          <b-alert show variant="primary">
            <h4>Votre liste de simulations est vide</h4>
          </b-alert>
        </template>
        <template v-if="simulations.length!=0">
          <!-- є симуляції -->
          <b-alert show variant="primary">
            <h4>Liste de vos simulations</h4>
          </b-alert>
          <!-- таблиця симуляцій -->
          <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 slot="left" :options="options" />
    </Layout>
  </div>
</template>

Коментарі

  • рядок 20: зверніть увагу на атрибут [responsive], завдяки якому відображення таблиці адаптується до розміру екрану:

Image

  • у [2] на невеликих екранах горизонтальна смуга прокрутки дозволяє переглядати таблицю;

19.14.8. Вигляд [NotFound]

Залишається без змін.

19.14.9. Види на мобільних пристроях

Image

Image

Примітка: напевно, є можливість отримати види, ще краще адаптовані до мобільних пристроїв. Я маю на увазі, зокрема, меню навігації, яке можна було б вдосконалити, але є й інші моменти. Головною метою цього документа не було створення мобільного додатка. У такому випадку, можливо, ми б звернулися до фреймворку, такого як Ionic |https://ionicframework.com/|.