17. Приклад [nuxt-20]: адаптація прикладу [vuejs-22]
17.1. Présentation
Тут ми пропонуємо перенести приклад [vuejs-22], який був додатком [vue.js] типу SPA, у контекст [nuxt] SSR. [vuejs-22] був клієнтським додатком сервера розрахунку податків, який містив такі вікна:
Перший екран — екран автентифікації:

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

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

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

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

Ми будемо поступово переносити додаток [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]:

Потім видаляються сторінки та компоненти, які стали непотрібними [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]:

- у [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] для проекту:

Виникли такі помилки:
- помилка в рядку 1 вказує на посилання на зображення, якого не існує. Його буде виправлено в [vuejs-22];
- помилка у рядку 2 вказує на те, що компонент [./FormCalculImpot] не існує. Дійсно, цей компонент тепер знаходиться у [@/components/form-calcul-impot];
- помилки в рядках [3-5] вказують на те, що компонент [./Layout] не існує. Дійсно, цей компонент тепер знаходиться у [@/components/layout];
- помилки в рядках [6-7] свідчать про те, що компонент [./Menu] не існує. Дійсно, тепер він називається [@/components/menu];
Додаємо зображення [assets/logo.jpg] до проєкту [nuxt-20]:

Крім того, на всіх сторінках ми виправимо шляхи до компонентів. Візьмемо для прикладу сторінку [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]. Зазвичай помилок більше не буде.
Після цього можна спробувати виконати:

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

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

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

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

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/] у браузері. Ми як і раніше отримуємо ту саму сторінку:

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

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

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

Помилка була правильно оброблена проектом [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]:

Після додавання класу [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];
Після внесення цих виправлень можна спробувати підтвердити сторінку автентифікації:

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

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

У [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()];
Після внесення цих виправлень можна спробувати виконати симуляцію:

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

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

- у [9-10], то бачимо, що перше моделювання дійсно міститься у [store];
- у [5] номер останнього моделювання дійсно збільшився;
17.6. крок 5
Тепер, коли ми провели симуляцію, натиснімо на посилання [Liste des simulations]. Ми отримаємо таку сторінку:

Маршрутизація клієнта [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)];
Вносимо необхідні зміни, а потім намагаємося видалити відображену симуляцію:

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

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

- у [6] видно, що таблиця моделювання порожня;
Тепер повернемося до форми розрахунку податку:

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

Отже, маршрутизація спрацювала.
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]:

Скрипт [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], головна сторінка відображається з такими записами у журналі:

- у [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/], коли сервер розрахунку податку не запущений. Ми отримуємо таку сторінку:

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

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

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

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

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

Скрипт [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 на сервері розрахунку податку:

Щоб перевірити маршрутизацію сервера [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].