5. Приклад [nuxt-02]: серверні та клієнтські сторінки
У цьому проєкті ми показуємо:
- що сторінка, побудована клієнтом, може візуально відрізнятися від тієї, що отримана від сервера. У результаті відбувається швидка зміна сторінки, помітна для користувача, що негативно впливає на ергономіку додатка. Тому такого варіанту слід уникати;
- рішення, за якого клієнтська сторінка відтворює ту саму сторінку, що й та, яку надіслав сервер;
Проєкт [nuxt-02] спочатку отримано шляхом копіювання проєкту [nuxt-01].

До проекту додано папку [store], а також дві нові сторінки. Ми повернемося до цього пізніше.
5.1. Сторінка [index]
5.1.1. Код сторінки
Код сторінки [index] виглядає наступним чином:
<!-- головна сторінка -->
<template>
<Layout :left="true" :right="true">
<!-- навігація -->
<Navigation slot="left" />
<!-- повідомлення-->
<b-alert slot="right" show variant="warning"> Home - value= {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Home',
// використані компоненти
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// життєвий цикл
beforeCreate() {
// клієнт і сервер
console.log('[home beforeCreate]')
},
created() {
// клієнт і сервер
console.log('[home created]')
// тільки сервер
if (process.server) {
this.value = 10
}
// клієнт і сервер
console.log('value=', this.value)
},
beforeMount() {
// тільки клієнт
console.log('[home beforeMount]')
},
mounted() {
// тільки клієнт
console.log('[home mounted]')
}
}
</script>
Коментарі
- рядок 7: сторінка [index] відображатиме значення своєї властивості [value] (рядок 28);
- рядки 36–45: тут слід пам’ятати, що функція [created] виконується як на стороні сервера, так і на стороні клієнта. У рядках 40–42 сервер змінить значення властивості [value] на 10. Клієнт, у свою чергу, не змінює це значення. Ми просто хочемо дізнатися, чи зберігається це значення на стороні клієнта. Ми побачимо, що ні;
5.1.2. Виконання
Ми змінюємо файл [/nuxt.config.js], щоб виконати проект [nuxt-02]:
...
// каталог вихідного коду
srcDir: 'nuxt-02',
// маршрутизатор
router: {
// кореневий каталог URL додатка
base: '/nuxt-02/'
},
// сервер
server: {
// порт служби, за замовчуванням 3000
port: 81,
// мережеві адреси, на яких відбувається прослуховування, за замовчуванням localhost: 127.0.0.1
// 0.0.0.0 = усі мережеві адреси комп’ютера
host: 'localhost'
}
...
Виконуємо проект [1]:

Після цього відображається сторінка [index] [2-3]. На ній протягом кількох секунд відображається значення [10], а потім — значення [0]. Що сталося?
Крок 1
Сервер запускається першим. Він виконує код сторінки [index]:
export default {
name: 'Home',
// використовувані компоненти
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// життєвий цикл
beforeCreate() {
// клієнт і сервер
console.log('[home beforeCreate]')
},
created() {
// клієнт і сервер
console.log('[home created]')
// тільки сервер
if (process.server) {
this.value = 10
}
// клієнт і сервер
console.log('value=', this.value)
},
beforeMount() {
// тільки клієнт
console.log('[home beforeMount]')
},
mounted() {
// тільки клієнт
console.log('[home mounted]')
}
}
- через рядок 23 властивість [value] у рядку 10 набуває значення 10;
Це можна перевірити, переглянувши вихідний код сторінки, отриманої браузером (опція [code source] у браузері):
<!doctype html>
<html data-n-head-ssr>
<head>
<title>Introduction à [nuxt.js]</title>
<meta data-n-head="ssr" charset="utf-8">
<meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
<meta data-n-head="ssr" data-hid="description" name="description" content="ssr routing loading asyncdata middleware plugins store">
<link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
<base href="/nuxt-02/">
<link rel="preload" href="/nuxt-02/_nuxt/runtime.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/commons.app.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/vendors.app.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/app.js" as="script">
....
</head>
<body>
<div data-server-rendered="true" id="__nuxt">
<div id="__layout">
<div class="container">
<div class="card">
<div class="card-body">
<div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-success">
<h4>[nuxt-02] : page serveur, page client</h4>
</div> <div>
<div class="row">
<div class="col-2">
<ul class="nav flex-column">
<li class="nav-item">
<a href="/nuxt-02/" target="_self" class="nav-link active nuxt-link-active">
Home
</a>
</li>
<li class="nav-item">
<a href="/nuxt-02/page1" target="_self" class="nav-link">
Page 1
</a>
</li>
<li class="nav-item">
<a href="/nuxt-02/page2" target="_self" class="nav-link">
Page 2
</a>
</li>
</ul>
</div> <div class="col-10">
<div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-warning">
Home - value= 10
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>window.__NUXT__ = ....;</script>
<script src="/nuxt-02/_nuxt/runtime.js" defer></script>
<script src="/nuxt-02/_nuxt/commons.app.js" defer></script>
<script src="/nuxt-02/_nuxt/vendors.app.js" defer></script>
<script src="/nuxt-02/_nuxt/app.js" defer></script>
</body>
</html>
- рядок 46: на отриманій сторінці [value] мав значення 10;
крок 2
Відомо, що після отримання сторінки скрипти в рядках 57–60 беруть контроль і змінюють поведінку отриманої сторінки, а також інформацію, що відображається, як у цьому випадку. Ці скрипти утворюють клієнт, який також виконує код сторінки [index] — той самий код, що й на сервері:
export default {
name: 'Home',
// використовувані компоненти
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// життєвий цикл
beforeCreate() {
// клієнт і сервер
console.log('[home beforeCreate]')
},
created() {
// клієнт і сервер
console.log('[home created]')
// тільки сервер
if (process.server) {
this.value = 10
}
// клієнт і сервер
console.log('value=', this.value)
},
beforeMount() {
// тільки клієнт
console.log('[home beforeMount]')
},
mounted() {
// лише клієнт
console.log('[home mounted]')
}
}
- щоб зрозуміти, що відбувається, потрібно усвідомити, що клієнт [nuxt] не виконуватиме рядки 22–24 (process.server=false);
- у класичному додатку [vue] властивість [value] у рядку 10 залишається рівною 0. Тому, коли клієнт переходить на отриману сторінку, відображуване значення стає [0];
Значення, згенероване сервером [nuxt] для властивості [value], виявилося марним.
5.2. Сторінка [page1]
5.2.1. Магазин [Vuex]
Ми додали папку [store] до проєкту [nuxt-02]:

Наявність цього файлу призводить до того, що [nuxt] автоматично реалізує сховище [Vuex]. Цей магазин реалізується файлом [index.js]. Тут файл [index.js] має такий вигляд:
export const state = () => ({
counter: 0
})
export const mutations = {
increment(state, inc) {
state.counter += inc
}
}
[nuxt] реалізує магазин [Vuex] на основі вмісту [index.js]:
- рядки 1–3: визначення стану [state] сховища. Цей стан повертається функцією. Тут стан має лише одну властивість — лічильник у рядку 2. Експортована функція повинна називатися [state];
- рядки 5–9: можливі операції над станом магазину. Вони називаються [mutations]. Тут операція [increment] дозволяє збільшити значення властивості [counter] на величину [inc]. Експортований об’єкт повинен називатися [mutations];
[store] Vuex, реалізований [nuxt], доступний у різних місцях. У поданнях він доступний у властивості [this.$store].
5.2.2. Код сторінки
Як і сторінка [index], сторінка [page1] відображатиме значення лічильника Vuex:
<!-- сторінка 1 -->
<template>
<Layout :left="true" :right="true">
<!-- навігація -->
<Navigation slot="left" />
<!-- повідомлення-->
<b-alert slot="right" show variant="primary"> Page 1 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page1',
// використані компоненти
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// життєвий цикл
beforeCreate() {
// клієнт і сервер
console.log('[home beforeCreate]')
},
created() {
// клієнт і сервер
console.log('[home created]')
// тільки сервер
if (process.server) {
this.$store.commit('increment', 25)
}
// клієнт і сервер
this.value = this.$store.state.counter
console.log('value=', this.value)
},
beforeMount() {
// тільки клієнт
console.log('[home beforeMount]')
},
mounted() {
// тільки клієнт
console.log('[home mounted]')
}
}
</script>
Коментарі
- рядки 38–40: сервер збільшить лічильник на 25;
- рядок 42: і сервер, і клієнт виведуть значення лічильника;
- рядок 7: значення лічильника відображається;
Читаючи цей код, слід розуміти дві речі:
- код, що виконується, однаковий як для сервера, так і для клієнта;
- але об’єкт [this] не є однаковим: на стороні сервера існує версія [this], а на стороні клієнта — інша, [client];
Ми намагаємося з’ясувати, чи [this.$store] на сервері є тим самим, що й [this.$store] на стороні клієнта. Оскільки сервер запускається першим (під час запуску програми), це зводиться до такого питання: чи передається клієнту [store], ініціалізований сервером?
5.2.3. Виконання
Запускаємо проект [nuxt-02] і вручну вводимо [localhost:81/nuxt-02/page1], щоб звернутися до сервера. Як і під час запуску сторінки [index]:
- сервер виконує сторінку [page1.vue];
- надсилає згенеровану сторінку до браузера. Вона відображається;
- вбудовані в надіслану сторінку клієнтські скрипти беруть управління на себе та знову виконують сторінку [page1.vue];
- відображена сторінка при цьому змінюється;
Кінцевий результат такий:

Цього разу відображене значення дійсно відповідає тому, що було встановлено сервером, і візуально не помітно, що сторінка «підскакує» через зміну клієнтом значення, яке відображає сервер. Що ж сталося цього разу?
Сервер виконав наступну сторінку [page1]:
...
<script>
/* eslint-disable no-console */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page1',
// використовувані компоненти
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// життєвий цикл
beforeCreate() {
// клієнт і сервер
console.log('[page1 beforeCreate]')
},
created() {
// клієнт і сервер
console.log('[page1 created]')
// тільки сервер
if (process.server) {
this.$store.commit('increment', 25)
}
// клієнт і сервер
this.value = this.$store.state.counter
console.log('value=', this.value)
},
beforeMount() {
// тільки клієнт
console.log('[page1 beforeMount]')
},
mounted() {
// тільки клієнт
console.log('[page1 mounted]')
}
}
</script>
- рядки 30–32 були виконані без помилок. Це означає, що й на стороні сервера [this.$store] також вказує на змінну [Vuex]. Рядок 31 змінив значення лічильника цієї змінної на 25;
- після чого сторінка була надіслана клієнту;
Якщо подивитися на сторінку, отриману клієнтом, можна побачити такі елементи:
<!doctype html>
<html data-n-head-ssr>
<head>
<title>Introduction à [nuxt.js]</title>
<meta data-n-head="ssr" charset="utf-8">
<meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
<meta data-n-head="ssr" data-hid="description" name="description" content="ssr routing loading asyncdata middleware plugins store">
<link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
<base href="/nuxt-02/">
<link rel="preload" href="/nuxt-02/_nuxt/runtime.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/commons.app.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/vendors.app.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/app.js" as="script">
...
</head>
<body>
<div data-server-rendered="true" id="__nuxt">
<div id="__layout">
<div class="container">
<div class="card">
<div class="card-body">
<div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-success">
<h4>[nuxt-02] : page serveur, page client</h4>
</div>
<div>
<div class="row">
<div class="col-2">
<ul class="nav flex-column">
<li class="nav-item">
<a href="/nuxt-02/" target="_self" class="nav-link">
Home
</a>
</li>
<li class="nav-item">
<a href="/nuxt-02/page1" target="_self" class="nav-link active nuxt-link-active">
Page 1
</a>
</li>
<li class="nav-item">
<a href="/nuxt-02/page2" target="_self" class="nav-link">
Page 2
</a>
</li>
</ul>
</div> <div class="col-10">
<div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-primary">
Page 1 - value = 25
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
window.__NUXT__ = (function (a, b, c) {
return {
layout: "default", data: [{}], error: null, state: { counter: 25 }, serverRendered: true,
logs: [
{ date: new Date(1574085336802), args: ["[home beforeCreate]"], type: a, level: b, tag: c },
{ date: new Date(1574085336839), args: ["[home created]"], type: a, level: b, tag: c },
{ date: new Date(1574085336869), args: ["value=", "25"], type: a, level: b, tag: c }
]
}
}("log", 2, ""));</script>
<script src="/nuxt-02/_nuxt/runtime.js" defer></script>
<script src="/nuxt-02/_nuxt/commons.app.js" defer></script>
<script src="/nuxt-02/_nuxt/vendors.app.js" defer></script>
<script src="/nuxt-02/_nuxt/app.js" defer></script>
</body>
</html>
- рядок 47: значення, надіслане сервером;
- рядок 60: бачимо, що стан сховища [Vuex] було вбудовано у сторінку. Це дозволить клієнту, який запуститься після отримання сторінки, відтворити нове сховище [Vuex] із початковим значенням лічильника 25;
Після отримання та відображення сторінки, отриманої від сервера, клієнт бере на себе управління і, у свою чергу, виконує сторінку [page1]:
...
<script>
/* eslint-disable no-console */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page1',
// використовувані компоненти
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// життєвий цикл
beforeCreate() {
// клієнт і сервер
console.log('[page1 beforeCreate]')
},
created() {
// клієнт і сервер
console.log('[page1 created]')
// тільки сервер
if (process.server) {
this.$store.commit('increment', 25)
}
// клієнт і сервер
this.value = this.$store.state.counter
console.log('value=', this.value)
},
beforeMount() {
// тільки клієнт
console.log('[page1 beforeMount]')
},
mounted() {
// тільки клієнт
console.log('[page1 mounted]')
}
}
</script>
- рядок 34: властивість [value] у рядку 18, у свою чергу, отримує значення лічильника 25;
Таким чином, магазин даних [nuxt] дозволяє серверу передавати інформацію клієнту під час початкового завантаження сторінки, коли її шукають на сервері. Нагадаємо, що після отримання цієї сторінки сервер більше не задіюється, і додаток працює як звичайний додаток [vue] у режимі SAP.
5.3. Сторінка [page2]
На сторінці [page2] ми демонструємо інший спосіб, за допомогою якого:
- сервер включає обчислені дані на сторінку;
- клієнт не змінював цю інформацію;
5.3.1. Код сторінки
Код сторінки [page2] змінюється наступним чином:
<!-- сторінка 2 -->
<template>
<Layout :left="true" :right="true">
<!-- навігація -->
<Navigation slot="left" />
<!-- повідомлення -->
<b-alert slot="right" show variant="secondary"> Page 2 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page2',
// використані компоненти
components: {
Layout,
Navigation
},
asyncData(context) {
// хто виконує цей код?
console.log('asyncData, client=', process.client, 'serveur=', process.server)
// тільки для сервера
if (process.server) {
// повертається обіцянка
return new Promise(function(resolve, reject) {
// зазвичай тут використовується асинхронна функція
// її імітують за допомогою очікування протягом однієї секунди
setTimeout(() => {
// цей результат буде включено до властивостей [data]
resolve({ value: 87 })
// log
console.log('asynData terminée')
}, 1000)
})
}
},
// життєвий цикл
beforeCreate() {
// клієнт і сервер
console.log('[page2 beforeCreate]')
},
created() {
// клієнт і сервер
console.log('[page2 created]')
},
beforeMount() {
// тільки клієнт
console.log('[page2 beforeMount]')
},
mounted() {
// тільки клієнт
console.log('[page2 mounted]')
}
}
</script>
- рядок 7: сторінка відображає значення властивості з іменем [value];
- властивість [value] не існує як елемент об’єкта, що повертається функцією [data]. У даному випадку ця функція не існує. Властивість [value] створюється динамічно у рядку 36;
- рядок 25: функція [asyncData] є функцією [nuxt]. Як випливає з назви, зазвичай це асинхронна функція. Її звичайне призначення — отримувати зовнішні дані. [nuxt] гарантує, що сторінка не буде надіслана до браузера клієнта, доки функція [asyncData] не поверне свої асинхронні дані;
- функція [asyncData] отримує як параметр контекст [nuxt]. Цей об’єкт містить велику кількість інформації про додаток [nuxt]. Ми дізнаємося про це в наступних розділах;
- рядок 31: реалізуємо функцію [asyncData] із використанням [Promise] (див. документ |Вступ до мови ECMASCRIPT 6 на прикладі|). Конструктор цього класу приймає як параметр асинхронну функцію, яка:
- позначає успіх, повертаючи дані за допомогою функції [resolve]. Об’єкт, повернений цією функцією, автоматично включається до властивостей [data] сторінки;
- повідомляє про невдачу, повертаючи помилку за допомогою функції [reject];
- рядок 34: імітується асинхронна функція за допомогою функції [setTimeout]. Ця функція повертає об’єкт [{ value: 87 }] (рядок 36) через секунду (рядок 31) завдяки функції [resolve], яка повідомляє про успішне виконання функції [Promise]. Об’єкт, повернений асинхронною функцією, автоматично включається до властивостей [data] сторінки. І саме цю властивість відображає рядок 7;
- рядок 27: ми побачимо, що функція [asyncData] виконується сервером, а не клієнтом;
- рядок 29: ініціалізація властивості [value] здійснюється сервером;
Примітка: об’єкт [this] невідомий у функції [asyncData], оскільки об’єкт, що інкапсулює компонент [vue], ще не створено;
5.3.2. Виконання
Запускаємо проект [nuxt-02] і вручну вводимо [localhost:81/nuxt-02/page2], щоб сервер отримав запит. Як і під час запуску сторінки [index]:
- сервер виконує сторінку [page2.vue];
- надсилає згенеровану сторінку до браузера. Вона відображається;
- вбудовані в надіслану сторінку клієнтські скрипти беруть на себе управління та знову виконують сторінку [page2.vue];
- відображена сторінка при цьому змінюється;
Кінцевий результат такий:

Цього разу відображене значення дійсно відповідає тому, що було встановлено сервером, і візуально не помітно, що сторінка «стрибає» через зміну клієнтом значення, яке відображає сервер. Що ж сталося цього разу?
Сервер виконав наступну сторінку [page2]:
<!-- сторінка 2 -->
<template>
<Layout :left="true" :right="true">
<!-- навігація -->
<Navigation slot="left" />
<!-- повідомлення -->
<b-alert slot="right" show variant="secondary"> Page 2 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page2',
// використані компоненти
components: {
Layout,
Navigation
},
asyncData(context) {
// хто виконує цей код?
console.log('asyncData, client=', process.client, 'serveur=', process.server)
// лише для сервера
if (process.server) {
// повертається обіцянка
return new Promise(function(resolve, reject) {
// зазвичай тут використовується асинхронна функція
// її імітують за допомогою очікування протягом однієї секунди
setTimeout(() => {
// цей результат буде включено до властивостей [data]
resolve({ value: 87 })
// log
console.log('asynData terminée')
}, 1000)
})
}
},
// життєвий цикл
beforeCreate() {
// клієнт і сервер
console.log('[page2 beforeCreate]')
},
created() {
// клієнт і сервер
console.log('[page2 created]')
},
beforeMount() {
// тільки клієнт
console.log('[page2 beforeMount]')
},
mounted() {
// тільки клієнт
console.log('[page2 mounted]')
}
}
</script>
Саме рядок 36 визначив значення, яке відображається у рядку 7. Отже, саме це отримав браузер клієнта. Точніше кажучи, він отримує таку сторінку:
<!doctype html>
<html data-n-head-ssr>
<head>
<title>Introduction à [nuxt.js]</title>
<meta data-n-head="ssr" charset="utf-8">
<meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
<meta data-n-head="ssr" data-hid="description" name="description" content="ssr routing loading asyncdata middleware plugins store">
<link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
<base href="/nuxt-02/">
<link rel="preload" href="/nuxt-02/_nuxt/runtime.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/commons.app.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/vendors.app.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/app.js" as="script">
...
</head>
<body>
<div data-server-rendered="true" id="__nuxt">
<div id="__layout">
<div class="container">
<div class="card">
<div class="card-body">
<div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-success">
<h4>[nuxt-02] : page serveur, page client</h4>
</div>
<div>
<div class="row">
<div class="col-2">
<ul class="nav flex-column">
<li class="nav-item">
<a href="/nuxt-02/" target="_self" class="nav-link">
Home
</a>
</li>
<li class="nav-item">
<a href="/nuxt-02/page1" target="_self" class="nav-link">
Page 1
</a>
</li>
<li class="nav-item">
<a href="/nuxt-02/page2" target="_self" class="nav-link active nuxt-link-active">
Page 2
</a>
</li>
</ul>
</div>
<div class="col-10">
<div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-secondary">
Page 2 - value = 87
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
window.__NUXT__ = (function (a, b, c) {
return {
layout: "default", data: [{ value: 87 }], error: null, state: { counter: 0 }, serverRendered: true,
logs: [
{ date: new Date(1574096608555), args: ["asyncData, client=", "false", "serveur=", "true"], type: a, level: b, tag: c },
{ date: new Date(1574096608575), args: ["[page2 beforeCreate]"], type: a, level: b, tag: c },
{ date: new Date(1574096608599), args: ["[page2 created]"], type: a, level: b, tag: c }
]
}
}("log", 2, ""));</script>
<script src="/nuxt-02/_nuxt/runtime.js" defer></script>
<script src="/nuxt-02/_nuxt/commons.app.js" defer></script>
<script src="/nuxt-02/_nuxt/vendors.app.js" defer></script>
<script src="/nuxt-02/_nuxt/app.js" defer></script>
</body>
</html>
- рядок 48: бачимо, що значення на отриманій сторінці дорівнює 87;
- рядок 61: у відповіді сервера бачимо два об’єкти: [data] та [state]:
- [state] — це стан сховища [Vuex]. Воно було створено на основі вмісту папки [store] додатка [nuxt-02];
- [data] містить властивості, створені сервером за допомогою функції [asyncData]. Тут ми бачимо властивість [value : 87], створену сервером. Скрипти клієнта інтегрують цю властивість у властивості сторінки [page2];
Повернемося до коду сторінки [page2]:
<!-- сторінка 2 -->
<template>
<Layout :left="true" :right="true">
<!-- навігація -->
<Navigation slot="left" />
<!-- повідомлення -->
<b-alert slot="right" show variant="secondary"> Page 2 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page2',
// використані компоненти
components: {
Layout,
Navigation
},
asyncData(context) {
// хто виконує цей код?
console.log('asyncData, client=', process.client, 'serveur=', process.server)
// лише для сервера
if (process.server) {
// повертається обіцянка
return new Promise(function(resolve, reject) {
// зазвичай тут використовується асинхронна функція
// її імітують за допомогою очікування протягом однієї секунди
setTimeout(() => {
// цей результат буде включено до властивостей [data]
resolve({ value: 87 })
// log
console.log('asynData terminée')
}, 1000)
})
}
},
// життєвий цикл
beforeCreate() {
// клієнт і сервер
console.log('[page2 beforeCreate]')
},
created() {
// клієнт і сервер
console.log('[page2 created]')
},
beforeMount() {
// тільки клієнт
console.log('[page2 beforeMount]')
},
mounted() {
// тільки клієнт
console.log('[page2 mounted]')
}
}
</script>
- у рядку 7 використовується властивість [value]. Однак на сторінці не визначено жодної властивості з іменем [value]. Проте клієнтські скрипти автоматично створили цю властивість завдяки об’єкту [data: [{ value: 87 }]], отриманому від сервера;
Крім того, журнали показують, що функція [asyncData] не була виконана клієнтом:

Функція [asyncData] була виконана сервером [1], але не клієнтом [2]. Крім того, слід зазначити, що функції життєвого циклу не виконуються сервером до завершення функції [asyncData]. Щоб перевірити це, можна збільшити час очікування у функції [asyncData].
5.4. Сторінка [page3]
Ми додаємо до нашого додатка нову сторінку [page3]:

5.4.1. Компонент [navigation]
Компонент [vavigation] змінено, щоб забезпечити перехід на нову сторінку:
<template>
<!-- меню Bootstrap із трьома пунктами -->
<b-nav vertical>
<b-nav-item to="/" exact exact-active-class="active">
Home
</b-nav-item>
<b-nav-item to="/page1" exact exact-active-class="active">
Page 1
</b-nav-item>
<b-nav-item to="/page2" exact exact-active-class="active">
Page 2
</b-nav-item>
<b-nav-item to="/page3" exact exact-active-class="active">
Page 3
</b-nav-item>
</b-nav>
</template>
5.4.2. Код [page3]
Код сторінки [page3] такий:
<!-- сторінка 3 -->
<template>
<Layout :left="true" :right="true">
<!-- навігація -->
<Navigation slot="left" />
<!-- повідомлення -->
<b-alert slot="right" show variant="secondary"> Page 3 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page3',
// використані компоненти
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
fetch(context) {
// хто виконує цей код?
console.log('fetch, client=', process.client, 'serveur=', process.server)
// лише для сервера
if (process.server) {
// повертається обіцянка
return new Promise(function(resolve, reject) {
// зазвичай тут використовується асинхронна функція
// її імітують за допомогою очікування протягом однієї секунди
setTimeout(() => {
// успіх
resolve()
}, 1000)
}).then(() => {
// змінюємо значення в змінній
context.store.commit('increment', 28)
})
}
},
// життєвий цикл
beforeCreate() {
// клієнт і сервер
console.log('[page3 beforeCreate]')
},
created() {
// клієнт і сервер
this.value = this.$store.state.counter
console.log('[page3 created], value=', this.value)
},
beforeMount() {
// тільки клієнт
console.log('[page3 beforeMount]')
},
mounted() {
// тільки клієнт
console.log('[page3 mounted]')
}
}
</script>
- рядок 30: функція [fetch] працює аналогічно до функції [asyncData]:
- вона виконується перед функціями життєвого циклу;
- об’єкт [this] у цій функції не визначений;
- її робота є асинхронною;
- життєвий цикл не починається, доки асинхронна функція не поверне свій результат;
- результат тут повертається методом [then] функції [Promise], рядок 43;
- функція [fetch] отримує параметр [context]. Він представляє поточний контекст [nuxt];
- рядок 30: серед своїх численних властивостей об’єкт [context] має властивість [store], яка представляє стор [Vuex] додатка;
- рядок 41: штучно позначаємо успішне виконання [Promise] через секунду (див. документ |Вступ до мови ECMASCRIPT 6 на прикладі|);
- рядок 45: після цього виконується метод [then]. У ньому збільшується лічильник [store];
5.4.3. Виконання
Виконуємо проект [nuxt-02] і вручну вводимо [localhost:81/nuxt-02/page3], щоб сервер отримав запит. Як і під час запуску сторінки [index]:
- сервер виконує сторінку [page3.vue];
- надсилає згенеровану сторінку до браузера. Вона відображається;
- вбудовані в надіслану сторінку клієнтські скрипти беруть контроль і знову виконують сторінку [page3.vue];
- відображена сторінка при цьому змінюється;
Кінцевий результат такий:

Відображене значення дійсно відповідає тому, що було встановлено сервером, і візуально не помітно, що сторінка «підскакує» через зміну клієнтом значення, яке відображає сервер. Що ж сталося цього разу?
Сервер виконав наступну сторінку [page3]:
<!-- сторінка 3 -->
<template>
<Layout :left="true" :right="true">
<!-- навігація -->
<Navigation slot="left" />
<!-- повідомлення -->
<b-alert slot="right" show variant="secondary"> Page 3 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page3',
// використані компоненти
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
fetch(context) {
// хто виконує цей код?
console.log('fetch, client=', process.client, 'serveur=', process.server)
// лише для сервера
if (process.server) {
// повертається обіцянка
return new Promise(function(resolve, reject) {
// зазвичай тут використовується асинхронна функція
// її імітують за допомогою очікування протягом однієї секунди
setTimeout(() => {
// успіх
resolve()
}, 1000)
}).then(() => {
// змінюємо значення в змінній
context.store.commit('increment', 28)
// журнал
console.log('fetch commit terminé')
})
}
},
// життєвий цикл
beforeCreate() {
// клієнт і сервер
console.log('[page3 beforeCreate]')
},
created() {
// клієнт і сервер
this.value = this.$store.state.counter
console.log('[page3 created], value=', this.value)
},
beforeMount() {
// тільки клієнт
console.log('[page3 beforeMount]')
},
mounted() {
// тільки клієнт
console.log('[page3 mounted]')
}
}
</script>
- рядок 45: асинхронна функція [fetch] є першою з наведених вище функцій, що виконується. Вона отримує як параметр об’єкт із назвою [context], який є поточним контекстом [nuxt]. Серед численних властивостей цього об’єкта властивість [context.store] представляє магазин [Vuex];
- рядок 45: у асинхронній функції [fetch] сервер встановлює лічильник сховища на 28;
- рядок 56: під час виконання функції [created] [nuxt] гарантує, що асинхронна функція [fetch] завершила свою роботу;
- рядок 58: значення лічильника жалюзі присвоюється властивості [value] з рядка 27;
- рядок 7: відображення значення [value], тобто лічильника змінної;
Браузер клієнта отримує таку сторінку:
<!doctype html>
<html data-n-head-ssr>
<head>
<title>Introduction à [nuxt.js]</title>
<meta data-n-head="ssr" charset="utf-8">
<meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
<meta data-n-head="ssr" data-hid="description" name="description" content="ssr routing loading asyncdata middleware plugins store">
<link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
<base href="/nuxt-02/">
<link rel="preload" href="/nuxt-02/_nuxt/runtime.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/commons.app.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/vendors.app.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/app.js" as="script">
...
</head>
<body>
<div data-server-rendered="true" id="__nuxt">
<div id="__layout">
<div class="container">
<div class="card">
<div class="card-body">
<div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-success">
<h4>[nuxt-02] : page serveur, page client</h4>
</div>
<div>
<div class="row">
<div class="col-2">
<ul class="nav flex-column">
<li class="nav-item">
<a href="/nuxt-02/" target="_self" class="nav-link">
Home
</a>
</li>
<li class="nav-item">
<a href="/nuxt-02/page1" target="_self" class="nav-link">
Page 1
</a>
</li>
<li class="nav-item">
<a href="/nuxt-02/page2" target="_self" class="nav-link">
Page 2
</a>
</li>
<li class="nav-item">
<a href="/nuxt-02/page3" target="_self" class="nav-link active nuxt-link-active">
Page 3
</a>
</li>
</ul>
</div> <div class="col-10">
<div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-secondary">
Page 3 - value = 28
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
window.__NUXT__ = (function (a, b, c) {
return {
layout: "default", data: [{}], error: null, state: { counter: 28 }, serverRendered: true,
logs: [
{ date: new Date(1574169916025), args: ["fetch, client=", "false", "serveur=", "true"], type: a, level: b, tag: c },
{ date: new Date(1574169917038), args: ["fetch commit terminé"], type: a, level: b, tag: c },
{ date: new Date(1574169917137), args: ["[page3 beforeCreate]"], type: a, level: b, tag: c },
{ date: new Date(1574169917167), args: ["[page3 created], value=", "28"], type: a, level: b, tag: c }
]
}
}("log", 2, ""));</script>
<script src="/nuxt-02/_nuxt/runtime.js" defer></script>
<script src="/nuxt-02/_nuxt/commons.app.js" defer></script>
<script src="/nuxt-02/_nuxt/vendors.app.js" defer></script>
<script src="/nuxt-02/_nuxt/app.js" defer></script>
</body>
</html>
- рядок 52: бачимо, що значення на отриманій сторінці дорівнює 28;
- рядок 65: у відповіді сервера видно, що сервер надіслав клієнту стан [state] сховища [Vuex]. Завдяки цій інформації клієнтські скрипти зможуть відтворити сховище [Vuex];
Скрипти клієнта, у свою чергу, виконають код сторінки [page3]:
<!-- сторінка 3 -->
<template>
<Layout :left="true" :right="true">
<!-- навігація -->
<Navigation slot="left" />
<!-- повідомлення -->
<b-alert slot="right" show variant="secondary"> Page 3 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page3',
// використані компоненти
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
fetch(context) {
// хто виконує цей код?
console.log('fetch, client=', process.client, 'serveur=', process.server)
// лише для сервера
if (process.server) {
// повертається обіцянка
return new Promise(function(resolve, reject) {
// зазвичай тут використовується асинхронна функція
// її імітують за допомогою очікування протягом однієї секунди
setTimeout(() => {
// успіх
resolve()
}, 1000)
}).then(() => {
// змінюємо значення в змінній
context.store.commit('increment', 28)
// журнал
console.log('fetch commit terminé')
})
}
},
// життєвий цикл
beforeCreate() {
// клієнт і сервер
console.log('[page3 beforeCreate]')
},
created() {
// клієнт і сервер
this.value = this.$store.state.counter
console.log('[page3 created], value=', this.value)
},
beforeMount() {
// тільки клієнт
console.log('[page3 beforeMount]')
},
mounted() {
// тільки клієнт
console.log('[page3 mounted]')
}
}
</script>
- рядок 58: функція [created], що виконується клієнтом, присвоює значення лічильника властивості [value] у рядку 27;
- у рядку 7 відображається це значення. Оскільки воно збігається з тим, що надіслано сервером, сторінка не «підскакує» внаслідок зміни;
Крім того, журнали показують, що функція [fetch] не була виконана клієнтом:

Функція [fetch] була виконана сервером [1], але не клієнтом [2]. Крім того, можна зазначити, що функції життєвого циклу не виконуються сервером до завершення функції [fetch] [3]. Щоб це перевірити, можна збільшити час очікування у функції [fetch].
На сторінках [page1] та [page3] було продемонстровано два методи використання сховища [Vuex] для передачі інформації від сервера до клієнта. Можна задатися питанням, чи є вони еквівалентними. Щоб це перевірити, ми створюємо сторінку [page4].
5.5. Сторінка [page4]
Ми додаємо нову сторінку [page4] до нашого додатка:

5.5.1. Компонент [navigation]
Компонент [navigation] змінено, щоб забезпечити перехід на нову сторінку:
<template>
<!-- меню Bootstrap з п’ятьма пунктами -->
<b-nav vertical>
<b-nav-item to="/" exact exact-active-class="active">
Home
</b-nav-item>
<b-nav-item to="/page1" exact exact-active-class="active">
Page 1
</b-nav-item>
<b-nav-item to="/page2" exact exact-active-class="active">
Page 2
</b-nav-item>
<b-nav-item to="/page3" exact exact-active-class="active">
Page 3
</b-nav-item>
<b-nav-item to="/page4" exact exact-active-class="active">
Page 4
</b-nav-item>
</b-nav>
</template>
5.5.2. Код [page4]
Код сторінки [page4] такий:
<!-- сторінка 4 -->
<template>
<Layout :left="true" :right="true">
<!-- навігація -->
<Navigation slot="left" />
<!-- повідомлення -->
<b-alert slot="right" show variant="secondary"> Page 4 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page4',
// використані компоненти
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// життєвий цикл
async beforeCreate() {
// клієнт і сервер
console.log('[page4 beforeCreate]')
// лише для сервера
if (process.server) {
// виконується асинхронна функція
const valeur = await new Promise(function(resolve, reject) {
// зазвичай тут використовується асинхронна функція
// її імітують за допомогою очікування протягом 10 секунд
setTimeout(() => {
// успіх — повертаємо значення лічильника
resolve(52)
}, 10000)
})
// змінюємо значення змінної
this.$store.commit('increment', valeur)
// журнал
console.log('[page4 beforeCreate], fonction asynchrone terminée, compteur=', this.$store.state.counter)
}
},
created() {
// клієнт і сервер
this.value = this.$store.state.counter
console.log('[page4 created], value=', this.value)
},
beforeMount() {
// тільки клієнт
console.log('[page4 beforeMount]')
},
mounted() {
// тільки клієнт
console.log('[page4 mounted]')
}
}
</script>
- рядок 30: те, що раніше виконувалося у функції [fetch], тепер виконується у методі [beforeCreate]. Використовується пара async (рядок 30) / await (рядок 36) для очікування завершення асинхронної функції;
- рядок 36: отримуємо результат асинхронної функції, який повертається у рядку 41 через 10 секунд (рядок 42);
- рядки 50–54: у методі [created], що виконується як на стороні сервера, так і на стороні клієнта, лічильник присвоюється властивості [value] сторінки;
5.5.3. Виконання
Виконуємо проект [nuxt-02] і вручну вводимо [localhost:81/nuxt-02/page4], щоб звернутися до сервера. Як і під час запуску сторінки [index]:
- сервер виконує сторінку [page4.vue];
- надсилає згенеровану сторінку до браузера. Вона відображається;
- вбудовані в надіслану сторінку клієнтські скрипти беруть контроль і знову виконують сторінку [page4.vue];
- відображена сторінка при цьому змінюється;
Кінцевий результат такий:

На відміну від очікуваного, значення, що відображається в [2], не дорівнює 52. Що сталося?
Журнали мають такий вигляд:

Можна помітити, що в [1] не відобразився запис про завершення асинхронної дії. Функція [created], яка виводить значення лічильника, показує 0. Все це дозволяє припустити, що [nuxt] не дочекався завершення асинхронної дії.
Якщо повернутися до терміналу VSCode, який використовувався для запуску додатка, можна знайти журнали [3-4]. Видно, що асинхронна функція була успішно виконана на стороні сервера.
У підсумку функція [beforeCreate] була повністю виконана на стороні сервера, але [nuxt] не дочекалася завершення її виконання, щоб надіслати сторінку до браузера клієнта, хоча вона дочекалася завершення функції [fetch]. Отже, саме цей метод слід використовувати, якщо потрібно, щоб сервер ініціалізував магазин [Vuex].
5.6. Навігація в додатку [vue]
Ми показали, що відбувається, коли кожна зі сторінок [index, page1, page2, page3, page4] завантажується сервером спочатку. На практиці все відбувається інакше: під час нормальної роботи на сервері шукається лише сторінка [index]. Розглянемо ці три сторінки в цьому випадку:
сторінка [index]

Ми вже пояснювали цей результат у розділі «Посилання».
Тепер натиснімо на посилання [Page 1]:

Відображене значення дорівнює 0. Воно дорівнювало 25, коли сторінка була вперше запрошена на сервер шляхом ручного введення її адреси URL. Пояснення просте. Виконується такий код:
created() {
// клієнт і сервер
console.log('[page1 created]')
// тільки сервер
if (process.server) {
this.$store.commit('increment', 25)
}
// клієнт і сервер
this.value = this.$store.state.counter
console.log('value=', this.value)
},
Саме рядок 6 встановлював лічильник на 25. Оскільки сторінка не була запитана у сервера, рядки 5–7 не були виконані, і лічильник у регістрі [Vuex] залишився на 0.
Тепер натиснімо на посилання [Page 2]:

Цього разу значення не відображається, а в журналах консолі з’являється попередження:

- У [1] ми бачимо, що функція [asyncData] була виконана клієнтом. Це завжди так:
- вона виконується сервером, якщо сторінка запитується на сервері; у цьому випадку вона не виконується клієнтом;
- а потім щоразу, коли сторінка є ціллю поточного маршруту клієнта;
- у [2]: [nuxt] видає попередження, оскільки в шаблоні сторінки є реактивний вираз {{ value }}, тоді як сторінка не має властивості [value];
Нагадаємо код, що виконується клієнтом:
asyncData() {
// хто виконує цей код?
console.log('asyncData, client=', process.client, 'serveur=', process.server)
// тільки для сервера
if (process.server) {
// повертається обіцянка
return new Promise(function(resolve, reject) {
// зазвичай тут має бути асинхронна функція — але тут її немає
// цей результат буде включено до властивостей [data]
resolve({ value: 87 })
})
}
},
- у рядку 3 видно, що функція [asyncData] була виконана клієнтом ще до функцій життєвого циклу;
- рядок 5 заблокував виконання решти коду, який створював властивість [value], оскільки цей код виконується клієнтом;
Тепер перейдемо до сторінки [page3]:

- у [2] ми мали значення 28, коли сторінка була надана сервером. Тут це не так;
- на сторінці [4] бачимо, що функція [fetch] була виконана на стороні клієнта;
Розглянемо код, виконаний клієнтом:
...
fetch(context) {
// хто виконує цей код?
console.log('fetch, client=', process.client, 'serveur=', process.server)
// лише для сервера
if (process.server) {
// повертається обіцянка
return new Promise(function(resolve, reject) {
// зазвичай тут використовується асинхронна функція
// її імітують за допомогою очікування протягом однієї секунди
setTimeout(() => {
// успіх
resolve()
}, 1000)
}).then(() => {
// змінюємо значення в змінній
context.store.commit('increment', 28)
// журнал
console.log('fetch commit terminé')
})
}
},
...
- клієнт виконує метод [fetch], рядок 2;
- рядки 6–21 не виконуються, оскільки умова [process.server] є хибною. Отже, рядок 17, який встановлює лічильник на 28, не виконується. Він залишається на нулі. Саме тому клієнт відображає 0 замість 28;
Тепер перейдемо до сторінки [page4]. Отримуємо такий результат:

- у [2] — значення лічильника;
- у [3] — журнали клієнта;
Код, що виконується клієнтом, такий:
...
data() {
return {
value: 0
}
},
// життєвий цикл
async beforeCreate() {
// клієнт і сервер
console.log('[page4 beforeCreate]')
// тільки для сервера
if (process.server) {
// виконується асинхронна функція
const valeur = await new Promise(function(resolve, reject) {
// зазвичай тут використовується асинхронна функція
// її імітують із затримкою в 10 секунд
setTimeout(() => {
// успіх — повертаємо значення лічильника
resolve(52)
}, 10000)
})
// змінюємо значення змінної
this.$store.commit('increment', valeur)
// журнал
console.log('[page4 beforeCreate], fonction asynchrone terminée, compteur=', this.$store.state.counter)
}
},
created() {
// клієнт і сервер
this.value = this.$store.state.counter
console.log('[page4 created], value=', this.value)
},
...
- рядки 12–26 не виконуються клієнтом, оскільки умова [process.server] у рядку 12 є хибною. Отже, лічильник у регістрі [Vuex] дорівнює 0 (його початкове значення в регістрі), і саме це значення відображається на сторінці;
Можна запитати, що відбувається, коли закомментувати [if] у рядках 12 та 26. Ось відповідь:
- цього разу клієнт виконує рядки 14–25, але [nuxt] не чекає на завершення асинхронної функції (як у випадку з сервером) і, отже, залишає лічильник на 0;
- через 10 с асинхронна функція завершується, і лічильник встановлюється на 52 у рядку 23;
- коли знову переходимо на сторінку [page4], відображається саме значення 52;
5.7. Résumé
З наших різних випробувань слід винести такі висновки:
- якщо сторінка [index] має містити зовнішні дані, сервер може їх отримати за допомогою функції [asyncData];
- якщо сервер повинен ініціалізувати магазин [Vuex] зовнішніми даними під час завантаження сторінки [index], він зробить це у функції [fetch];
- сторінка, згенерована сервером, та сторінка, згенерована клієнтом, мають бути ідентичними, щоб уникнути ефекту «стрибка», спричиненого тим, що сторінка клієнта візуально замінює сторінку, надіслану сервером і спочатку відображену;
Ми розглянемо інші аспекти функції [nuxt] на новому прикладі.