Skip to content

17. Приклад [nuxt-20]: адаптація прикладу [vuejs-22]

17.1. Présentation

Тут ми пропонуємо перенести приклад [vuejs-22], який був додатком [vue.js] типу SPA, у контекст [nuxt] SSR. [vuejs-22] був клієнтським додатком сервера розрахунку податків, який містив такі вікна:

Перший екран — екран автентифікації:

Image

Другий екран — екран розрахунку податку:

Image

Третя сторінка — це сторінка, на якій відображається список моделювань, виконаних користувачем:

Image

На екрані вище показано, що можна видалити моделювання № 1. Після цього з’являється таке вікно:

Image

Якщо тепер видалити останню симуляцію, з’явиться таке нове вікно:

Image

Ми будемо поступово переносити додаток [vuejs-22] у додаток [nuxt-20]. Ми не будемо повторно пояснювати код [vuejs-22]. Читачеві пропонується ще раз ознайомитися з документом |Вступ до фреймворку VUE.JS на прикладі|. Окремі етапи повинні продемонструвати відмінності між додатком [vuejs] та додатком [nuxt].

17.2. Етап 1

Проєкт [nuxt-20] спочатку створюється шляхом копіювання проєкту [nuxt-12]. Останній є дійсно гарною відправною точкою:

  • він взаємодіє з сервером розрахунку податку;
  • він коректно обробляє помилки, які надсилає сервер;
  • клієнт і сервер [nuxt] можуть взаємодіяти через сесію [nuxt];

Отже, ми маємо хорошу базову інфраструктуру. Наша головна робота полягатиме у зміні:

  • сторінки. Ми візьмемо сторінки з проєкту [vuejs-22], які потрібно буде адаптувати до нового середовища;
  • управління сховищем. Повинна з’явитися додаткова інформація (список симуляцій), а інша може стати непотрібною;
  • управління маршрутизацією клієнта та сервера [nuxt];

Отже, насамперед створюємо проєкт [nuxt-20], скопіювавши проєкт [nuxt-12]:

Image

Потім видаляються сторінки та компоненти, які стали непотрібними [2]:

  • компонент [components/navigation] зникає;
  • макет [layout/default] зникає;
  • сторінки [index, authentification, get-admindata, fin-session] зникають;

Потім у [nuxt-20] інтегруються елементи з [vuejs-22] та [3]:

  • три сторінки [Authentification, CalculImpot, ListeSimulations] з додатка [vuejs-22] переміщуються до папки [pages];
  • компоненти [FormCalculImpot, Menu, Layout] додатка [vuejs-22] переміщуються до папки [components];
  • сторінка [Main] з [vuejs-22], яка використовувалася як [layout] у додатку [vuejs-22], переміщується до папки [layouts];

Перейменовуємо вбудовані елементи [4]:

Image

  • у [layouts] [Main] стало [default], оскільки це ім’я за замовчуванням макета додатка [nuxt];
  • у [pages] сторінка [Authentification] стала [index], оскільки [Authentification] виконувала цю роль у додатку [vuejs-22];

На цьому етапі можна виконати компіляцію проєкту, щоб побачити перші помилки. Змінюємо файл [nuxt.config] з прикладу [nuxt-12], щоб відтепер виконувати [nuxt-20]:


export default {
  mode: 'universal',
  /*
   ** Headers of the page
   */
  head: {
    title: 'Introduction à [nuxt.js]',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      {
        hid: 'description',
        name: 'description',
        content: 'ssr routing loading asyncdata middleware plugins store'
      }
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
  },
  /*
   ** Customize the progress-bar color
   */
  loading: false,

  /*
   ** Global CSS
   */
  css: [],
  /*
   ** Plugins to load before mounting the App
   */
  plugins: [
    { src: '@/plugins/client/plgSession', mode: 'client' },
    { src: '@/plugins/server/plgSession', mode: 'server' },
    { src: '@/plugins/client/plgDao', mode: 'client' },
    { src: '@/plugins/server/plgDao', mode: 'server' },
    { src: '@/plugins/client/plgEventBus', mode: 'client' }
  ],
  /*
   ** Nuxt.js dev-modules
   */
  buildModules: [
    // Документація: https://github.com/nuxt-community/eslint-module
    '@nuxtjs/eslint-module'
  ],
  /*
   ** Nuxt.js modules
   */
  modules: [
    // Документація: https://bootstrap-vue.js.org
    'bootstrap-vue/nuxt',
    // Документація: https://axios.nuxtjs.org/usage
    '@nuxtjs/axios',
    // https://www.npmjs.com/package/cookie-universal-nuxt
    'cookie-universal-nuxt'
  ],
  /*
   ** Axios module configuration
   ** See https://axios.nuxtjs.org/options
   */
  axios: {},
  /*
   ** Build configuration
   */
  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {}
  },
  // каталог з вихідним кодом
  srcDir: 'nuxt-20',
  // маршрутизатор
  router: {
    // кореневий каталог додатка URL
    base: '/nuxt-20/',
    // проміжне програмне забезпечення для маршрутизації
    middleware: ['routing']
  },
  // сервер
  server: {
    // порт служби, за замовчуванням 3000
    port: 81,
    // мережеві адреси, на яких відбувається прослуховування, за замовчуванням localhost: 127.0.0.1
    // 0.0.0.0 = усі мережеві адреси комп’ютера
    host: 'localhost'
  },
  // середовище
  env: {
    // конфігурація axios
    timeout: 2000,
    withCredentials: true,
    baseURL: 'http://localhost/php7/scripts-web/impots/version-14',
    // конфігурація сесійного файлу cookie [nuxt]
    maxAge: 60 * 5
  }
}

Потім виконуємо [build] для проекту:

Image

Виникли такі помилки:

1
2
3
4
5
6
7
Module not found: Error: Can't resolve '../assets/logo.jpg' @ ./nuxt-20/layouts/default.vue?...
Module not found: Error: Can't resolve './FormCalculImpot' @ ./nuxt-20/pages/calcul-impot.vue?...
Module not found: Error: Can't resolve './Layout' @ ./nuxt-20/pages/_.vue?...
Module not found: Error: Can't resolve './Layout' @ ./nuxt-20/pages/liste-des-simulations...
Module not found: Error: Can't resolve './Layout' @ ./nuxt-20/pages/calcul-impot.vue?...
Module not found: Error: Can't resolve './Menu' @ ./nuxt-20/pages/_.vue?...
Module not found: Error: Can't resolve './Menu' @ ./nuxt-20/pages/calcul-impot.vue
  • помилка в рядку 1 вказує на посилання на зображення, якого не існує. Його буде виправлено в [vuejs-22];
  • помилка у рядку 2 вказує на те, що компонент [./FormCalculImpot] не існує. Дійсно, цей компонент тепер знаходиться у [@/components/form-calcul-impot];
  • помилки в рядках [3-5] вказують на те, що компонент [./Layout] не існує. Дійсно, цей компонент тепер знаходиться у [@/components/layout];
  • помилки в рядках [6-7] свідчать про те, що компонент [./Menu] не існує. Дійсно, тепер він називається [@/components/menu];

Додаємо зображення [assets/logo.jpg] до проєкту [nuxt-20]:

Image

Крім того, на всіх сторінках ми виправимо шляхи до компонентів. Візьмемо для прикладу сторінку [calcul-impot]:


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

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

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

Три [import] у рядках 26–28 стають такими:


// імпорт
import FormCalculImpot from '@/components/form-calcul-impot'
import Menu from '@/components/menu'
import Layout from '@/components/layout'

Таким чином перевіряємо та, за необхідності, виправляємо [import] у всіх компонентах, макетах та сторінках. Після внесення цих виправлень можна спробувати виконати новий [build]. Зазвичай помилок більше не буде.

Після цього можна спробувати виконати:

Image

З’являються помилки:

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

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

Видно, що команда [dev] у поєднанні з модулем [eslint] є більш суворою з точки зору синтаксису, ніж команда [build]. Тут вимагається, щоб оператор порівняння [!=] був записаний як [!==], що є більш суворим оператором (він також перевіряє тип операндів). Ці помилки виникають на сторінці [index.vue].

Виправимо вищезазначені помилки та запустимо виконання проекту знову. Після цього з’являється попередження від модуля [eslint]:

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

Image

Виправимо цю помилку за допомогою модуля [Quick fix] з модулів [eslint] та [2].

Запускаємо проект знову. Помилки компіляції більше немає. Далі завантажуємо URL та [http://localhost:81/nuxt-20/] у браузері. Отримуємо помилку виконання:

Image

Помилка міститься у файлі [index.vue] [2]. Помилка [1] пов’язана з тим, що у файлі [vuejs-22] шар [dao] був доступний у [this.$dao], тоді як у [nuxt-12], інфраструктуру якого ми перейняли, він доступний у функції [this.$dao()].

Помилка міститься у функції [created] життєвого циклу сторінки [index]:

Image

Наразі ми просто перейменовуємо [created] на [created2], щоб функція життєвого циклу [created] не виконувалася [3].

Зберігаємо зміни та перезавантажуємо сторінку [index] у браузері. Цього разу все працює:

Image

17.3. крок 2

Сторінки проєкту [vuejs-22] використовували такі вставлені елементи:

  • $dao: для шару [dao] клієнта [vue.js];
  • $session: для сеансу, збереженого в [localStorage] браузера;

Ці елементи більше не існують в інфраструктурі проєкту [nuxt-12], яку ми скопіювали:

  • тепер існує два рівні [dao]: один для клієнта [nuxt], інший — для сервера [nuxt]. Обидва шари доступні через вбудовану функцію під назвою [$dao]. Це означає, що на сторінках додатка [this.$dao] слід замінити на [this.$dao()];
  • сесія [nuxt], що керується додатком [nuxt-20], більше не має нічого спільного з об’єктом [$session] удодатку [vuejs-22], де поняття сесійного файлу cookie не існувало. Проте вони мають аналогічну функціональність: зберігання постійної інформації під час дій користувача. Сесія [nuxt] зберігає інформацію у сховищі, а не безпосередньо в сесії. На сторінках додатка [this.$session] слід замінити на [this.$store], коли йдеться про збереження інформації в сесії, та на [this.$session()], коли йдеться про роботу з самою сесією;
  • щоб дізнатися стан властивості P сховища, потрібно написати [this.$store.state.P];
  • щоб змінити властивість P сховища, потрібно написати [this.$store.commit(‘replace’, {P:value}];

Ми вносимо ці зміни на сторінці [index]:


<!-- визначення 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>
        <!-- перший рядок -->
        <b-form-group label="Nom d'utilisateur" label-for="user" label-cols="3">
          <!-- поле введення користувача -->
          <b-col cols="6">
            <b-form-input id="user" v-model="user" type="text" placeholder="Nom d'utilisateur" />
          </b-col>
        </b-form-group>
        <!-- 2-й рядок -->
        <b-form-group label="Mot de passe" label-for="password" label-cols="3">
          <!-- поле введення пароля -->
          <b-col cols="6">
            <b-input id="password" v-model="password" type="password" placeholder="Mot de passe" />
          </b-col>
        </b-form-group>
        <!-- третій рядок -->
        <b-alert v-if="showError" show variant="danger" class="mt-3">L'erreur suivante s'est produite : {{ message }}</b-alert>
        <!-- кнопка типу [submit] у третьому рядку -->
        <b-row>
          <b-col cols="2">
            <b-button :disabled="!valid" variant="primary" type="submit">Valider</b-button>
          </b-col>
        </b-row>
      </b-form>
    </template>
  </Layout>
</template>

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

  // розраховані властивості
  computed: {
    // дійсні введені дані
    valid() {
      return this.user && this.password && this.$store.state.started
    }
  },
  // життєвий цикл: компонент щойно створено
  mounted() {
    // eslint-disable-next-line
    console.log("Authentification mounted");
    // чи може користувач проводити симуляції?
    if (this.$store.state.started && this.$store.state.authenticated && this.$métier.taxAdminData) {
      // тоді користувач може проводити симуляції
      this.$router.push({ name: 'calculImpot' })
      // повернення до циклу подій
      return
    }
    // якщо сесія jSON вже запущена, її не запускають знову
    if (!this.$store.state.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.$store.commit('replace', { started: true })
          console.log('[authentification], session=', this.$session())
        })
        // у разі помилки
        .catch((error) => {
          // помилка передається до подання [Main]
          this.$emit('error', error)
        })
        // у будь-якому випадку
        .finally(() => {
          // сесія зберігається
          this.$session().save()
        })
    }
  },

  // обробники подій
  methods: {
    // ----------- автентифікація
    async login() {
      try {
        // початок очікування
        this.$emit('loading', true)
        // аутентифікація ще не відбулася
        this.$store.commit('replace', { 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.$store.commit('replace', { 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.$store.commit('replace', { métier: this.$métier })
        // зберігаємо сесію
        this.$session().save()
      }
    }
  }
}
</script>

Зверніть увагу на наступні моменти:

  • рядок 69: функцію [created2] було перейменовано на [mounted], щоб сервер [nuxt] не виконував її (він не виконує ані [beforeMount], ані [mounted]). Лише клієнт [nuxt] виконає її, як це було у випадку з прикладом [vuejs-22];
  • рядок 73: згадується [this.$métier], який на даний момент не існує;
  • рядок 75: ми ніколи не використовували цей метод у додатку [nuxt]. Треба перевірити, чи він працює в контексті [nuxt];
  • рядки 112, 172: у [vuejs-22] сесія проєкту зберігалася саме таким чином. У проєкті [nuxt-20] метод [save] повинен отримувати поточний контекст. Відомо, що на сторінці [nuxt] об’єкт [context] доступний у [this.$nuxt.context];

Тому рядки 112 та 172 переписуються наступним чином:


this.$session().save(this.$nuxt.context)

Слід зауважити, що цей код не є оптимізованим. Замість того, щоб кілька разів використовувати функцію [this.$session()], краще було б написати:


const session=this.$session()

а потім використовувати змінну [session]. Такий самий підхід можна застосувати до функції [this.$dao()].

Внісши ці виправлення, ми можемо завантажити URL [http://localhost:81/nuxt-20/] у браузері. Ми як і раніше отримуємо ту саму сторінку:

Image

Давайте подивимося на логи браузера:

Image

Лог [1] — це останній запис, зроблений клієнтом [nuxt]. У [2] бачимо, що властивість [started] має значення [vrai], що означає, що функція [mounted] успішно запустила сесію jSON із сервером розрахунку податку. Також видно, що сховище має властивості, які потрібно або видалити, або перейменувати. Нагадаємо, що ми використовуємо сховище з прикладу [nuxt-12].

Тепер спробуємо знову звернутися до URL [http://localhost:81/nuxt-20/], коли сервер розрахунку податків не запущений. Спочатку слід видалити файл cookie сеансу [nuxt]:

Image

Наведений вище знімок екрана зроблено в браузері Chrome. Після цього файли URL та [http://localhost:81/nuxt-20/] дають такий результат:

Image

Помилка була правильно оброблена проектом [vuejs-22]. Вона й надалі правильно обробляється проектом [nuxt-20].

17.4. Крок 3

Тепер, коли ми маємо сторінку автентифікації, потрібно розглянути код, що виконується, коли користувач натискає кнопку [Valider]:


// обробники подій
  methods: {
    // ----------- автентифікація
    async login() {
      try {
        // початок очікування
        this.$emit('loading', true)
        // аутентифікація ще не відбулася
        this.$store.commit('replace', { 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.$store.commit('replace', { 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.$store.commit('replace', { métier: this.$métier })
        // зберігаємо сесію
        this.$session().save(this.$nuxt.context)
      }
    }
  }

Головною проблемою тут, здається, є відсутність даних [this.$métier]. Щоб це виправити, ми:

  • додамо клас [Métier] із прикладу [vuejs-22]. Ми розмістимо його в папці [api];
  • вставимо функцію [$métier] у контекст клієнта [nuxt], що надасть доступ до цього класу;

Спочатку скопіюємо клас [Métier] у папку [api]:

Image

Після додавання класу [Métier] до проєкту створюємо новий плагін для клієнта [nuxt]. Цей плагін під назвою [pluginMétier] вбудує функцію [$métier], яка надасть доступ до класу [Métier]:


/* eslint-disable no-console */
// створено точку доступу до шару [métier]
import Métier from '@/api/client/Métier'
export default (context, inject) => {
  // інстанціювання шару [métier]
  const métier = new Métier()
  // введення функції [$métier] у контекст
  inject('métier', () => métier)
  // журнал
  console.log('[fonction client $métier créée]')
}

Після цього ми можемо виправити сторінку [index]:


// життєвий цикл: компонент щойно створено
  mounted() {
    // eslint-disable-next-line
    console.log("Authentification mounted");
    // чи може користувач проводити симуляції?
    if (this.$store.state.started && this.$store.state.authenticated && this.$métier().taxAdminData) {
      // тоді користувач може проводити симуляції
      this.$router.push({ name: 'calcul-impot' })
      // повернення до циклу подій
      return
    }
    // якщо сесія jSON вже запущена, її не запускають знову
    ...
  },

  // обробники подій
  methods: {
    // ----------- автентифікація
    async login() {
      try {
        // початок очікування
        this.$emit('loading', true)
        // аутентифікація ще не виконана
        this.$store.commit('replace', { 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.$store.commit('replace', { 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: 'calcul-impot' })
      } catch (error) {
        // помилку передаємо до головного компонента
        this.$emit('error', error)
      } finally {
        // оновлення та збереження
        this.$store.commit('replace', { métier: this.$métier() })
        // зберігаємо сесію
        this.$session().save(this.$nuxt.context)
      }
    }
  }
  • у рядках 43, 61, 69 [this.$métier] замінено на [this.$métier()];
  • рядки 8, 63: назва сторінки [CalculImpot] у проєкті [vuejs-22] стала сторінкою [calcul-impot] у проєкті [nuxt-20];

Після внесення цих виправлень можна спробувати підтвердити сторінку автентифікації:

Image

Отримана сторінка виглядає так:

Image

Ми дійсно отримали сторінку розрахунку податку. Тепер подивимося на журнали:

Image

У [2] бачимо, що дані про авторизацію були правильно збережені. У [3-4] бачимо, що було отримано дані [taxAdminData], які дозволяють розрахувати податок за допомогою класу [Métier].

17.5. Крок 4

Розглянемо отриману нами сторінку [calcul-impot]:


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

<script>
// імпортні дані
import FormCalculImpot from '@/components/form-calcul-impot'
import Menu from '@/components/menu'
import Layout from '@/components/layout'

export default {
  // використані компоненти
  components: {
    Layout,
    FormCalculImpot,
    Menu
  },
  // внутрішній звіт
  data() {
    return {
      // параметри меню
      options: [
        {
          text: 'Liste des simulations',
          path: '/liste-des-simulations'
        },
        {
          text: 'Fin de session',
          path: '/fin-session'
        }
      ],
      // результат розрахунку податку
      résultat: '',
      résultatObtenu: false
    }
  },
  // життєвий цикл
  created() {
    // eslint-disable-next-line
    console.log("CalculImpot created");
  },
  // методи обробки подій
  methods: {
    // результат розрахунку податку
    handleResultatObtenu(résultat) {
      // результат формується у вигляді рядка HTML
      const impôt = "Montant de l'impôt : " + résultat.impôt + ' euro(s)'
      const décôte = 'Décôte : ' + résultat.décôte + ' euro(s)'
      const réduction = 'Réduction : ' + résultat.réduction + ' euro(s)'
      const surcôte = 'Surcôte : ' + résultat.surcôte + ' euro(s)'
      const taux = "Taux d'imposition : " + résultat.taux
      this.résultat = impôt + '<br/>' + décôte + '<br/>' + réduction + '<br/>' + surcôte + '<br/>' + taux
      // виведення результату
      this.résultatObtenu = true
      // ---- оновлення сховища [Vuex]
      // симуляція додавання
      this.$store.commit('addSimulation', résultat)
      // збереження сесії
      this.$session.save()
    }
  }
}
</script>
  • рядки 44 та 48: посилання в меню навігації правильні. Сторінка [/fin-session] не існує. Проєкт [vuejs-22] вирішував цю проблему за допомогою маршрутизації. Ми зробимо те саме з проєктом [nuxt-20];
  • рядок 76: згадується зміна [addSimulation], яка наразі не існує. Ми її створимо;
  • рядок 78: як і на сторінці [index], потрібно вказати [this.$session().save(this.$nuxt.context)];

Змінимо стор [store/index]. Він успадкований від проєкту [nuxt-12] і наразі має такий вигляд:


/* eslint-disable no-console */

// стан сховища
export const state = () => ({
  // сесія jSON розпочато
  jsonSessionStarted: false,
  // користувач автентифікований
  userAuthenticated: false,
  // сесійний файл cookie PHP
  phpSessionCookie: '',
  // adminData
  adminData: ''
})

// зміни в сховищі
export const mutations = {
  // заміна стану
  replace(state, newState) {
    for (const attr in newState) {
      state[attr] = newState[attr]
    }
  },
  // скидання сховища
  reset() {
    this.commit('replace', { jsonSessionStarted: false, userAuthenticated: false, phpSessionCookie: '', adminData: '' })
  }
}

// дії з регістром
export const actions = {
  nuxtServerInit(store, context) {
    // хто виконує цей код?
    console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
    // ініціалізація сесії
    initStore(store, context)
  }
}

function initStore(store, context) {
  // store — це магазин, який потрібно ініціалізувати
  // отримуємо сесію
  const session = context.app.$session()
  // чи сесія вже була ініціалізована?
  if (!session.value.initStoreDone) {
    // запускаємо новий store
    console.log("nuxtServerInit, initialisation d'un nouveau store")
    // зберігаємо store у сесії
    session.value.store = store.state
    // сховище тепер ініціалізовано
    session.value.initStoreDone = true
  } else {
    console.log("nuxtServerInit, reprise d'un store existant")
    // зберігання оновлюється за допомогою зберігання сесії
    store.commit('replace', session.value.store)
  }
  // збереження сеансу
  session.save(context)
  // журнал
  console.log('initStore terminé, store=', store.state)
}
  • рядки 3–27: ми перенесемо стан та зміни з додатка [vuejs-22] (див. документ [3]):

// стан жалюзі
export const state = () => ({
  // сесія jSON розпочато
  started: false,
  // користувач автентифікований
  authenticated: false,
  // сесійний файл cookie PHP
  phpSessionCookie: '',
  // список симуляцій
  simulations: [],
  // номер останнього моделювання
  idSimulation: 0,
  // шар [métier]
  métier: null
})

// зміни в списку
export const mutations = {
  // заміна стану
  replace(state, newState) {
    for (const attr in newState) {
      state[attr] = newState[attr]
    }
  },
  // скидання регістра
  reset() {
        this.commit('replace', { started: false, authenticated: false, phpSessionCookie: '', idSimulation: 0, simulations: [], métier: null })
  },
  // видалення рядка № індекс
  deleteSimulation(state, index) {
    // eslint-disable-next-line no-console
    console.log('mutation deleteSimulation')
    // видалення рядка № [index]
    state.simulations.splice(index, 1)
    console.log('store simulations', state.simulations)
  },
  // додано симуляцію
  addSimulation(state, simulation) {
    // eslint-disable-next-line no-console
    console.log('mutation addSimulation')
    // номер симуляції
    state.idSimulation++
    simulation.id = state.idSimulation
    // додаємо симуляцію до таблиці симуляцій
    state.simulations.push(simulation)
  }
}
  • рядки 4 та 6: ми вводимо властивості, які вже використовувалися;
  • рядок 8: зберігаємо сесійний файл cookie PHP. Це є надзвичайно важливим для того, щоб клієнт і сервер [nuxt] мали одну й ту саму сесію PHP із сервером розрахунку податку;
  • рядок 10: список моделювань, виконаних користувачем;
  • рядок 12: номер останнього моделювання, виконаного користувачем;
  • рядок 14: шар [métier];
  • рядки 30–47: зміни, що містяться у сховищі проєкту [vuejs-22] і на які посилаються сторінки додатка. Проєкт [vuejs-22] мав зміну під назвою [clear], яка очищала список симуляцій. Її не додаємо, оскільки вже наявна мутація [reset] має вирішити це питання;
  • рядки 26–28: мутація [reset] змінена з урахуванням нового вмісту стану;

Сторінка [calcul-impot] використовує такий компонент [form-calcul-impot]:


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

<!-- скрипт -->
<script>
export default {
  // внутрішній стан
  data() {
    return {
      // одружений чи ні
      marié: 'non',
      // кількість дітей
      enfants: '',
      // річний дохід
      salaire: ''
    }
  },
  // розрахований внутрішній статус
  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*$/))
    }
  },
  // життєвий цикл
  created() {
    // журнал
    // eslint-disable-next-line
    console.log("FormCalculImpot created");
  },
  // обробник подій
  methods: {
    calculerImpot() {
      // розрахунок податку здійснюється за допомогою шару [métier]
      const résultat = this.$métier.calculerImpot(this.marié, Number(this.enfants), Number(this.salaire))
      // eslint-disable-next-line
      console.log("résultat=", résultat);
      // доповнюється результат
      résultat.marié = this.marié
      résultat.enfants = this.enfants
      résultat.salaire = this.salaire
      // генерується подія [resultatObtenu]
      this.$emit('resultatObtenu', résultat)
    }
  }
}
</script>
  • рядки 65, 89: посилання [this.$métier] слід змінити на [this.$métier()];

Після внесення цих виправлень можна спробувати виконати симуляцію:

Image

Отримуємо таку відповідь:

Image

Якщо переглянути журнали:

Image

  • у [9-10], то бачимо, що перше моделювання дійсно міститься у [store];
  • у [5] номер останнього моделювання дійсно збільшився;

17.6. крок 5

Тепер, коли ми провели симуляцію, натиснімо на посилання [Liste des simulations]. Ми отримаємо таку сторінку:

Image

Маршрутизація клієнта [nuxt] відбулася успішно. Давайте подивимося на код сторінки [liste-des-simulations]:


<!-- визначення 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 :items="simulations" :fields="fields" striped hover responsive>
            <template v-slot:cell(action)="data">
              <b-button @click="supprimerSimulation(data.index)" variant="link">Supprimer</b-button>
            </template>
          </b-table>
        </template>
      </template>
      <!-- меню навігації у лівій колонці -->
      <Menu slot="left" :options="options" />
    </Layout>
  </div>
</template>

<script>
// імпорт
import Layout from '@/components/layout'
import Menu from '@/components/menu'
export default {
  // компоненти
  components: {
    Layout,
    Menu
  },
  // внутрішній стан
  data() {
    return {
      // параметри меню навігації
      options: [
        {
          text: "Calcul de l'impôt",
          path: '/calcul-impot'
        },
        {
          text: 'Fin de session',
          path: '/fin-session'
        }
      ],
      // параметри таблиці HTML
      fields: [
        { label: '#', key: 'id' },
        { label: 'Marié', key: 'marié' },
        { label: "Nombre d'enfants", key: 'enfants' },
        { label: 'Salaire', key: 'salaire' },
        { label: 'Impôt', key: 'impôt' },
        { label: 'Décôte', key: 'décôte' },
        { label: 'Réduction', key: 'réduction' },
        { label: 'Surcôte', key: 'surcôte' },
        { label: '', key: 'action' }
      ]
    }
  },
  // розрахований внутрішній стан
  computed: {
    // список симуляцій, взятих із сховища Vuex
    simulations() {
      return this.$store.state.simulations
    }
  },
  // життєвий цикл
  created() {
    // eslint-disable-next-line
    console.log("ListeSimulations created");
  },
  // методи
  methods: {
    supprimerSimulation(index) {
      // eslint-disable-next-line
      console.log("supprimerSimulation", index);
      // видалення симуляції № [index]
      this.$store.commit('deleteSimulation', index)
      // збереження сесії
      this.$session.save()
    }
  }
}
</script>
  • рядки 47–56: посилання в меню навігації правильні;
  • рядок 75: шторка правильно посилається;
  • рядок 89: використовується мутація [deleteSimulation], яку ми інтегрували на попередньому етапі;
  • рядок 91: цей рядок потрібно переписати як [this.$session().save(this.$nuxt.context)];

Вносимо необхідні зміни, а потім намагаємося видалити відображену симуляцію:

Image

У результаті отримуємо таку сторінку:

Image

Переглянемо журнали:

Image

  • у [6] видно, що таблиця моделювання порожня;

Тепер повернемося до форми розрахунку податку:

Image

Ми отримуємо таку сторінку:

Image

Отже, маршрутизація спрацювала.

17.7. Крок 6

Залишилося налаштувати опцію навігації [Fin de session] у меню навігації:


// параметри меню
      options: [
        {
          text: 'Liste des simulations',
          path: '/liste-des-simulations'
        },
        {
          text: 'Fin de session',
          path: '/fin-session'
        }
]
  • у рядку 9 сторінка [/fin-session] не існує. Проєкт [vuejs-22] обробляв цей випадок за допомогою правил маршрутизації у файлі [router.js]:

// імпорт
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: 'authentification2', 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
  • рядки 64–76 обробляли особливий випадок маршруту до шляху [/fin-session];
  • рядок 66: очищення поточної сесії;
  • рядки 68–70: відображається вікно [authentification];

Спробуємо зробити щось подібне у файлі маршрутизації клієнта [nuxt]:

Image

Скрипт [client/routing.js] виглядає наступним чином:


/* eslint-disable no-console */
export default function(context) {
  // хто виконує цей код?
  console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
  // управління сесійним файлом cookie PHP у браузері
  // сесійний файл cookie PHP у браузері має збігатися з тим, що знайдено в сесії Nuxt
  // дія [fin-session] отримує новий файл cookie PHP (як сервер, так і клієнт Nuxt)
  // якщо його отримує сервер, клієнт повинен передати його браузеру
  // для власного обміну даними з сервером PHP
  //— тут відбувається маршрутизація на стороні клієнта

  // отримуємо сесійний файл cookie PHP
  const phpSessionCookie = context.store.state.phpSessionCookie
  if (phpSessionCookie) {
    // якщо він існує, сесійний файл cookie PHP присвоюється браузеру
    document.cookie = phpSessionCookie
  }

  // куди рухаємося?
  const to = context.route.path
  if (to === '/fin-session') {
    // очищаємо сесію
    const session = context.app.$session()
    session.reset(context)
    // перенаправляємо на сторінку index
    context.redirect({ name: 'index' })
  }
}
  • до існуючого коду додано рядки [19-27];
  • рядок 20: отримуємо [path] із цільового об’єкта поточного маршруту;
  • рядок 21: перевіряється, чи це [/fin-session]. Якщо так:
    • рядки 23–24: сесія скидається;
    • рядок 26: клієнт [nuxt] перенаправляється на головну сторінку;

Метод [session.reset(context)] (рядок 24) сесії виглядає так:


// скидання сесії
  reset(context) {
    console.log('nuxt-session reset')
    // скидання сховища
    context.store.commit('reset')
    // збереження нового сховища в сесії та збереження сесії
    this.save(context)
}

Метод [context.store.commit('reset')] (рядок 5) має такий вигляд:


// скидання сховища
  reset() {
        this.commit('replace', { started: false, authenticated: false, phpSessionCookie: '', idSimulation: 0, simulations: [], métier: null })
}

Тепер, коли ми використовуємо посилання [Fin de session], головна сторінка відображається з такими записами у журналі:

Image

  • у [3] видно, що автентифікація більше не діє;
  • у [4] видно, що сесія jSON запущена;
  • у [6] рівень [métier] більше не присутній у сховищі (він все ще присутній на сторінках із [this.$métier()]);
  • у [5, 7] симуляцій більше немає;

Необхідно чітко розуміти, що відбувається під час завершення сеансу:

  • сесія [nuxt] скидається: властивість [started] сховища змінюється на [false];
  • відбувається перенаправлення на сторінку [index];
  • виконується метод [mounted] сторінки [index]. Цей метод запускає нову сесію jSON із сервером розрахунку податку. Якщо операція завершилася успішно, властивість [started] сховища змінюється на [true];

17.8. крок 7

На цьому етапі додаток [nuxt-20] має всі функції додатка [vuejs-22]. Перенесення, здається, завершено.

Ми підемо трохи далі в дусі [nuxt]. Метод [mounted] на сторінці [index] створює проблему. Він запускає асинхронну операцію, завершення якої пошукова система не буде чекати. Відомо, що в такому випадку асинхронну операцію потрібно помістити у функцію [asyncData], оскільки тоді сервер [nuxt], який її виконує, чекатиме на її завершення, перш ніж надати сторінку пошуковій системі.

Тут ми використовуємо функцію [asyncData], написану в додатку [nuxt-12] для сторінки [index]:


export default {
  name: 'InitSession',
  // використані компоненти
  components: {
    Layout,
    Navigation
  },
  // асинхронні дані
  async asyncData(context) {
    // журнал
    console.log('[index asyncData started]')
    // не виконуємо дії двічі, якщо сторінка вже була запитана
    if (process.server && context.store.state.jsonSessionStarted) {
      console.log('[index asyncData canceled]')
      return { result: '[succès]' }
    }
    try {
      // запускаємо сесію jSON
      const dao = context.app.$dao()
      const response = await dao.initSession()
      // журнал
      console.log('[index asyncData response=]', response)
      // отримуємо сесійний файл cookie PHP для наступних запитів
      const phpSessionCookie = dao.getPhpSessionCookie()
      // зберігаємо сесійний файл cookie PHP у сесії [nuxt]
      context.store.commit('replace', { phpSessionCookie })
      // Чи сталася помилка?
      if (response.état !== 700) {
        // помилка міститься в response.réponse
        throw new Error(response.réponse)
      }
      // відзначається, що сесія jSON розпочалася
      context.store.commit('replace', { jsonSessionStarted: true })
      // повертається результат
      return { result: '[succès]' }
    } catch (e) {
      // журнал
      console.log('[index asyncData error=]', e)
      // фіксується факт, що сесія jSON не розпочалася
      context.store.commit('replace', { jsonSessionStarted: false })
      // повідомляється про помилку
      return { result: '[échec]', showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // зберігаємо дані
      const session = context.app.$session()
      session.save(context)
      // журнал
      console.log('[index asyncData finished]')
    }
  },
  // життєвий цикл
  beforeCreate() {
    console.log('[index beforeCreate]')
  },
  created() {
    console.log('[index created]')
  },
  beforeMount() {
    console.log('[index beforeMount]')
  },
  mounted() {
    console.log('[index mounted]')
    // тільки для клієнта
    if (this.showErrorLoading) {
      console.log('[index mounted, showErrorLoading=true]')
      this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
    }
}
  • рядки 13, 33, 40: потрібно змінити властивість [jsonSessionStarted] на [started];
  • рядок 13: у додатку [nuxt-12] лише сервер [nuxt] виконував сторінку [index] та її функцію [asyncData]. Клієнт [nuxt] виконував сторінку [index] лише після її отримання від сервера [nuxt], і при цьому не виконував функцію [asyncData]. У [nuxt-20] ситуація інша: посилання [Fin de session] відобразить сторінку [index] у середовищі клієнта [nuxt]. Тоді буде виконано функцію [asyncData]. Однак, коли таким чином відбувається перехід на сторінку [index], сесія [nuxt] тим часом була скинута, а властивість [started] сховища має значення [false], і умова в рядку 13 неминуче буде хибною. Отже, можна залишити [process.server], і таким чином клієнт [nuxt] не виконуватиме цю перевірку;
  • рядки 15, 35, 42: властивість [result] додається до властивостей [data] сторінки [index]. У [nuxt-20] ця властивість не використовуватиметься, тому ми видалимо її з результату, що повертається функцією;
  • рядки 61–67: цей метод [mounted] слід зберегти, оскільки саме він дозволяє клієнту [nuxt] відображати повідомлення про помилку. Проте спосіб обробки помилки буде змінено;

На поточній сторінці [index] ми інтегруємо наведену вище функцію [asyncData] замість старої функції [mounted] і додаємо нову функцію [mounted]. Код сторінки [index] з прикладу [nuxt-20] тепер виглядає так:


...

<!-- динаміка подання -->
<script>
/* eslint-disable no-console */
import Layout from '@/components/layout'
export default {
  // використані компоненти
  components: {
    Layout
  },
  // стан компонента
  data() {
    return {
      // користувач
      user: '',
      // його пароль
      password: '',
      // відображення помилки
      showError: false
    }
  },

  // розраховані властивості
  computed: {
    // дійсні введені дані
    valid() {
      return this.user && this.password && this.$store.state.started
    }
  },
  // асинхронні дані
  async asyncData(context) {
    // журнал
    console.log('[index asyncData started]')
    // не виконувати операцію двічі, якщо сторінка вже була запитана
    if (process.server && context.store.state.started) {
      console.log('[index asyncData canceled]')
      return
    }
    try {
      // запускається сесія jSON
      const dao = context.app.$dao()
      const response = await dao.initSession()
      // журнал
      console.log('[index asyncData response=]', response)
      // отримуємо сесійний файл cookie PHP для наступних запитів
      const phpSessionCookie = dao.getPhpSessionCookie()
      // зберігаємо сесійний файл cookie PHP у сесії [nuxt]
      context.store.commit('replace', { phpSessionCookie })
      // Чи сталася помилка?
      if (response.état !== 700) {
        // помилка міститься в response.réponse
        throw new Error(response.réponse)
      }
      // відзначається, що сесія jSON розпочалася
      context.store.commit('replace', { started: true })
      // результатів немає
      return
    } catch (e) {
      // журнал
      console.log('[index asyncData error=]', e.message)
      // фіксується, що сесія jSON не розпочалася
      context.store.commit('replace', { started: false })
      // повідомляється про помилку
      return { showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // зберігається магазин даних
      const session = context.app.$session()
      session.save(context)
      // журнал
      console.log('[index asyncData finished]')
    }
  },
  // життєвий цикл
  beforeCreate() {
    console.log('[index beforeCreate]')
  },
  created() {
    console.log('[index created]')
  },
  beforeMount() {
    // тільки для клієнта
    console.log('[index beforeMount]')
    // обробка можливої помилки
    if (this.showErrorLoading) {
      // журнал
      console.log('[index beforeMount, showErrorLoading=true]')
      // помилка передається до головного компонента [default]
      this.$emit('error', new Error(this.errorLoadingMessage))
    }
  },
  mounted() {
    console.log('[index mounted]')
  },

  // обробники подій
  methods: {
    // ----------- автентифікація
    async login() {
      ...
}
</script>
  • рядки 58, 65: функція [asyncData] більше не робить властивість [result] невикористаною в цьому місці;
  • рядок 81: метод [beforeMount] клієнта [nuxt]. Його було обрано замість методу [mounted] для обробки можливої помилки [asyncData];
  • рядок 85: перевіряється, чи встановлено властивість [errorLoading]. Її може встановити лише функція [asyncData];
  • рядки 85–90: якщо функція [asyncData] повідомила про помилку, її передають на сторінку [default] через подію [error]. Саме так колишня функція [created], яку ми щойно замінили, обробляла можливі помилки;

Давайте проведемо кілька тестів.

Спочатку видаляємо сесійний файл cookie [nuxt] та сесійний файл cookie PHP, якщо вони існують. Потім ми завантажуємо сторінку [http://localhost:81/nuxt-20/], коли сервер розрахунку податку не запущений. Ми отримуємо таку сторінку:

Image

Ми перезавантажуємо ту саму сторінку після запуску сервера розрахунку податку:

Image

Давайте подивимося на логи:

Image

  • у [2-3] бачимо, що сесія jSON була запущена;
  • у [4] бачимо сесійний файл cookie PHP, який сервер [nuxt] отримав під час обміну даними з сервером розрахунку податку. Відтепер клієнт [nuxt] буде його використовувати;

Тепер пройдемо аутентифікацію:

Image

Ми отримуємо таку сторінку:

Image

У [1] ми отримали повідомлення про помилку. Це означає, що браузер не надіслав правильний файл cookie сеансу PHP, запущеного сервером [nuxt] на попередньому етапі. У [nuxt-12] передача сесійного файлу cookie PHP від сервера [nuxt] до клієнта [nuxt] відбувалася в процесі маршрутизації клієнта [nuxt] зі скрипту [middleware/client/routing] :


/* eslint-disable no-console */
export default function(context) {  // хто виконує цей код?
  console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
  // управління сесійним файлом cookie PHP у браузері
  // сесійний файл cookie PHP у браузері має збігатися з тим, що знайдено в сесії Nuxt
  // акція [fin-session] отримує новий файл cookie PHP (як сервер, так і клієнт Nuxt)
  // якщо його отримує сервер, клієнт повинен передати його браузеру
  // для власного обміну даними з сервером PHP
  //— тут відбувається маршрутизація на стороні клієнта

  // отримуємо сесійний файл cookie PHP
  const phpSessionCookie = context.store.state.phpSessionCookie
  if (phpSessionCookie) {
    // якщо він існує, сесійний файл cookie PHP присвоюється браузеру
    document.cookie = phpSessionCookie
  }

  // куди рухаємося?
  const to = context.route.path
  if (to === '/fin-session') {
    // очищаємо сесію
    const session = context.app.$session()
    session.reset(context)
    // перенаправляємо на сторінку index
    context.redirect({ name: 'index' })
  }
}

Саме рядки 13–17 дозволяють клієнту [nuxt] отримати файл cookie сеансу PHP із сервера [nuxt].

Проблема полягає в тому, що при натисканні кнопки [Valider] не відбувається маршрутизація клієнта [nuxt]. Отже, його функція маршрутизації не викликається. Проблему вирішують шляхом дублювання рядків 12–17 на початку методу автентифікації сторінки [index]:


// обробники подій
  methods: {
    // ----------- автентифікація
    async login() {
      // отримуємо сесійний файл cookie PHP із сховища
      const phpSessionCookie = this.$store.state.phpSessionCookie
      if (phpSessionCookie) {
        // якщо він існує, сесійний файл cookie PHP присвоюється браузеру
        document.cookie = phpSessionCookie
      }
      try {
        // початок очікування
        this.$emit('loading', true)
        // аутентифікація ще не відбулася

У рядках 5–10 із сховища витягується файл cookie сеансу PHP, ініційованого сервером [nuxt]. Після внесення цієї зміни сторінка розрахунку податку завантажується успішно, що означає, що автентифікація пройшла успішно.

17.9. Крок 8

Ми маємо функціональний додаток, який працює у дусі [nuxt]. Як і у випадку з додатком [nuxt-13], ми розглянемо навігацію сервера [nuxt]. Як уже зазначалося, користувач не повинен вручну вводити URL додатка. Він повинен використовувати посилання, що йому пропонуються і які виконуються клієнтом [nuxt], який у цей момент працює в режимі SPA. Проте ми подбаємо про те, щоб навігація по серверу [nuxt] завжди залишала додаток у стабільному стані.

З дослідження, проведеного для [nuxt-13] (див. параграф «посилання»), ми знаємо, що необхідно:

  • змінити скрипт [midleware/routing];
  • додати скрипт [middleware/server/routing];

Image

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


/* eslint-disable no-console */

// імпортуємо серверні та клієнтські проміжні модулі
import serverRouting from './server/routing'
import clientRouting from './client/routing'

export default function(context) {
  // хто виконує цей код?
  console.log('[middleware], process.server', process.server, ', process.client=', process.client)
  if (process.server) {
    // маршрутизація на сервері
    serverRouting(context)
  } else {
    // маршрутизація на стороні клієнта
    clientRouting(context)
  }
}
  • рядок 4: імпортується скрипт маршрутизації з сервера [nuxt];
  • рядки 10–12: якщо код виконує сервер [nuxt], використовується його функція маршрутизації;

Скрипт [middleware/server/routing] має такий вигляд:


/* eslint-disable no-console */
export default function(context) {
  // хто виконує цей код?
  console.log('[middleware server], process.server', process.server, ', process.client=', process.client)

  // збираємо трохи інформації тут і там
  const store = context.store
  // звідки ми прийшли?
  const from = store.state.from || 'nowhere'
  // куди ми йдемо?
  let to = context.route.name
  
  // особливий випадок /fin-session, який не має атрибута [name]
  if (context.route.path === '/fin-session') {
    to = 'fin-session'
  }
  
  // можливе перенаправлення
  let redirection = ''
  // управління маршрутизацією завершено
  let done = false
  
  // чи вже відбувається перенаправлення з сервера [nuxt]?
  if (store.state.serverRedirection) {
    // нічого робити не потрібно
    done = true
  }
  
  // чи це перезавантаження сторінки?
  if (!done && from === to) {
    // нічого не потрібно робити
    done = true
  }
  
  // контроль навігації сервера [nuxt]
  // використовується меню навігації клієнта

  // спочатку обробляється випадок завершення сеансу
  if (!done && store.state.started && store.state.authenticated && to === 'fin-session') {
    // очищаємо сесію
    const session = context.app.$session()
    session.reset(context)
    // перенаправляємо на сторінку index
    redirection = 'index'
    // робота завершена
    done = true
  }

  // випадок, коли сесія PHP не розпочалася
  if (!done && !store.state.started && to !== 'index') {
    // перенаправлення на [index]
    redirection = 'index'
    // робота завершена
    done = true
  }

  // у разі, якщо користувач не пройшов автентифікацію
  if (!done && store.state.started && !store.state.authenticated && to !== 'index') {
    redirection = 'index'
    // робота завершена
    done = true
  }

  // випадок, коли [adminData] не отримано
  if (!done && store.state.started && store.state.authenticated && !store.state.métier.taxAdminData && to !== 'index') {
    // перенаправлення на [index]
    redirection = 'index'
    // робота завершена
    done = true
  }

  // у разі, якщо [adminData] було отримано
  if (
    !done &&
    store.state.started &&
    store.state.authenticated &&
    store.state.métier.taxAdminData &&
    to !== 'calcul-impot' &&
    to !== 'liste-des-simulations'
  ) {
    // залишаємося на тій самій сторінці
    redirection = from
    // робота завершена
    done = true
  }

  // зазвичай всі перевірки виконано ---------------------
  // перенаправлення?
  if (redirection) {
    // перенаправлення зафіксовано в сховищі
    store.commit('replace', { serverRedirection: true })
  } else {
    // перенаправлення відсутнє
    store.commit('replace', { serverRedirection: false, from: to })
  }
  // зберігаємо сховище в сесії [nuxt]
  const session = context.app.$session()
  session.value.store = store.state
  session.save(context)
  // виконується можливе перенаправлення з сервера [nuxt]
  if (redirection) {
    context.redirect({ name: redirection })
  }
}
  • у цьому скрипті ми використовуємо ідеї, які вже були розроблені та застосовані в маршрутизації сервера [nuxt] додатка [nuxt-13];
  • ми додаємо дві властивості до сховища додатка:
    • [from]: назва останньої відображеної сторінки. Ми знаємо, що клієнт [nuxt] має цю інформацію, але сервер [nuxt] — ні. Ми додамо цю інформацію, зберігаючи у сховищі при кожному маршрутизуванні сервера [nuxt] назву сторінки, яка буде відображена. Те саме ми зробимо під час кожного маршрутизації клієнта [nuxt]. Таким чином, під час наступної маршрутизації сервер [nuxt] знайде у сховищі назву останньої сторінки, відображеної додатком;
    • [serverRedirection]: коли сервер [nuxt] відхилить пункт призначення маршрутизації, він здійснить перенаправлення. Тоді він вкаже в сховищі, що наступним пунктом призначення сервера [nuxt] є сторінка перенаправлення. Це перенаправлення спричинить нове виконання маршрутизатора сервера [nuxt]. Якщо він виявить, що поточний пункт призначення є результатом перенаправлення, він дозволить це;
  • рядки 6–11: отримуємо інформацію, необхідну для маршрутизації;
  • рядки 13–16: ціль [/fin-session] не пов’язана зі сторінкою під назвою [fin-session]. Отже, вона не має імені. Їй присвоюється ім’я;
  • рядок 19: ціль можливого перенаправлення;
  • рядок 21: [done=true] після завершення тестів маршрутизації;
  • рядки 23–27: як уже зазначалося, якщо поточна маршрутизація є наслідком перенаправлення, нічого робити не потрібно. Адже під час попередньої маршрутизації маршрутизатор вирішив, що потрібно перенаправити браузер клієнта. Немає підстав переглядати це рішення;
  • рядки 29–33: якщо йдеться про перезавантаження сторінки, то це залишають без змін. Це не є аксіомою, що діє для всіх додатків [nuxt]: для кожної сторінки потрібно перевіряти наслідки перезавантаження. У даному випадку перезавантаження сторінок [index, calcul-impot, liste-des-simulations] не спричиняє небажаних наслідків;
  • рядки 35–85: маршрутизація сервера [nuxt] повторює маршрутизацію клієнта [nuxt]. Коли користувач перебуває на сторінці, маршрутизація сервера [nuxt] має відображати меню навігації, яке пропонує клієнт [nuxt] під час перебування на цій сторінці;
  • рядки 38–47: спочатку розглядається випадок, коли ціль [fin-session] не відповідає жодній існуючій сторінці. Якщо умови виконані (сесія розпочата, користувач автентифікований), сесія очищується, а користувач перенаправляється на сторінку [index];
  • рядки 49–55: якщо сесія jSON із сервером розрахунку податку не розпочалася, то єдиним можливим пунктом призначення є сторінка [index];
  • рядки 57–62: якщо сесія jSON була розпочата, а користувач не пройшов автентифікацію і не запросив сторінку автентифікації, то відбувається перенаправлення на сторінку автентифікації, якою є сторінка [index];
  • рядки 64–70: якщо користувач пройшов автентифікацію, але дані [adminData] не були отримані, то відбувається перенаправлення на сторінку автентифікації. Аутентифікація виконує дві дії: вона перевіряє автентичність користувача, а в разі успішної аутентифікації додатково запитує дані [adminData]. Якщо ці дані не отримано, то аутентифікацію потрібно повторити;
  • рядки 72–85: якщо отримано дані [adminData], то єдиними можливими цілями є [calcul-impot] та [liste-des-simulations]. Якщо це не так, маршрутизація відхиляється;
  • рядки 88–95: оновлюємо сховище залежно від того, чи відбудеться перенаправлення;
  • рядок 94: перенаправлення відсутнє. Отже, поточний [to] стане [from] для наступної маршрутизації;
  • рядки 96–99: інформація зі сховища зберігається у сесійному файлі cookie [nuxt];
  • рядки 100–103: якщо потрібно виконати перенаправлення, то його виконують;

Для проведення тестів слід подбати про те, щоб почати з «чистого аркуша», видаливши сесійний файл cookie [nuxt] та сесійний файл cookie PHP на сервері розрахунку податку:

Image

Щоб перевірити маршрутизацію сервера [nuxt], на кожній сторінці спробуйте всі можливі варіанти URL та [/, /calcul-impot, /liste-des-simulations]. Щоразу додаток має залишатися в узгодженому стані.

17.10. Крок 9

На етапі 9 відбувається розгортання додатка [nuxt-20]. Для цього потрібен хостинг, що забезпечує середовище [node.js] для запуску сервера [nuxt]. У мене його немає. Читач може скористатися інструкціями, описаними в розділі «Посилання», щоб розгорнути додаток [nuxt-20] на своїй машині для розробки та захистити його за допомогою протоколу HTTPS.

17.11. Conclusion

Перенесення додатка [vuejs-22] у додаток [nuxt-20] завершено. Звернемо увагу на кілька моментів цього перенесення:

  • сторінки [vuejs-22] було збережено;
  • асинхронні операції, що існували на сторінках [vuejs-22], було перенесено до функції [asyncData];
  • у [nuxt-20] довелося керувати двома об’єктами: клієнтом [nuxt] та сервером [nuxt]. Останній об’єкт не існував у [vuejs-22]. Щоб забезпечити узгодженість між цими двома об’єктами, нам знадобилася сесія [nuxt];
  • нам довелося налагодити маршрутизацію сервера [nuxt];

На практиці, безсумнівно, краще відразу розпочати роботу з архітектурою [nuxt], ніж створювати архітектуру [vue.js], яку потім переносити в середовище [nuxt].