5. Ejemplo [nuxt-02]: páginas del servidor y del cliente
En este proyecto, mostramos:
- que la página generada por el cliente puede ser visualmente diferente de la recibida del servidor. Esto provoca un cambio rápido de página, perceptible para el usuario, lo que perjudica la usabilidad de la aplicación. Por lo tanto, es una opción que debe evitarse;
- una solución para que la página del cliente recree la misma página que la enviada por el servidor;
El proyecto [nuxt-02] se obtiene inicialmente al copiar el proyecto [nuxt-01].

Se agrega una carpeta [store] al proyecto, así como dos nuevas páginas. Volveremos sobre esto más adelante.
5.1. La página [index]
5.1.1. El código de la página
El código de la página [index] queda así:
<!-- página principal -->
<template>
<Layout :left="true" :right="true">
<!-- navegación -->
<Navigation slot="left" />
<!-- mensaje-->
<b-alert slot="right" show variant="warning"> Home - value= {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-desactivar nuxt/no-env-in-hooks */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Home',
// componentes utilizados
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// ciclo de vida
beforeCreate() {
// cliente y servidor
console.log('[home beforeCreate]')
},
created() {
// cliente y servidor
console.log('[home created]')
// solo servidor
if (process.server) {
this.value = 10
}
// cliente y servidor
console.log('value=', this.value)
},
beforeMount() {
// solo cliente
console.log('[home beforeMount]')
},
mounted() {
// solo cliente
console.log('[home mounted]')
}
}
</script>
Comentarios
- línea 7: la página [index] mostrará el valor de su propiedad [value] (línea 28);
- líneas 36-45: hay que recordar aquí que la función [created] se ejecuta tanto en el lado del servidor como en el lado del cliente. En las líneas 40-42, el servidor establecerá en 10 el valor de la propiedad [value]. El cliente, por su parte, no modifica este valor. Simplemente queremos saber si el cliente conserva este valor. Descubriremos que no;
5.1.2. Ejecución
Modificamos el archivo [/nuxt.config.js] para ejecutar el proyecto [nuxt-02]:
...
// directorio del código fuente
srcDir: 'nuxt-02',
// enrutador
router: {
// raíz de los URL de la aplicación
base: '/nuxt-02/'
},
// servidor
server: {
// puerto de servicio, 3000 por defecto
port: 81,
// direcciones de red en las que escucha, por defecto localhost: 127.0.0.1
// 0.0.0.0 = todas las direcciones de red de la máquina
host: 'localhost'
}
...
Ejecutamos el proyecto [1]:

A continuación, se muestra la página [index] [2-3]. Muestra el valor [10] durante unos instantes y luego muestra el valor [0]. ¿Qué pasó?
Paso 1
El servidor es el primero en ejecutarse. Ejecuta el código de la página [index]:
export default {
name: 'Home',
// componentes utilizados
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// ciclo de vida
beforeCreate() {
// cliente y servidor
console.log('[home beforeCreate]')
},
created() {
// cliente y servidor
console.log('[home created]')
// solo servidor
if (process.server) {
this.value = 10
}
// cliente y servidor
console.log('value=', this.value)
},
beforeMount() {
// solo cliente
console.log('[home beforeMount]')
},
mounted() {
// solo cliente
console.log('[home mounted]')
}
}
- debido a la línea 23, la propiedad [value] de la línea 10 toma el valor 10;
Esto se puede verificar al revisar el código fuente de la página recibida por el navegador (opción [code source] en el navegador):
<!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>
- línea 46: en la página recibida, [value] tenía el valor 10;
paso 2
Sabemos que, tras recibir la página, los scripts de las líneas 57 a 60 toman el control y modifican el comportamiento de la página recibida, e incluso la información que se muestra, como en este caso. Estos scripts conforman el cliente, que a su vez ejecuta el código de la página [index], el mismo código que el servidor:
export default {
name: 'Home',
// componentes utilizados
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// ciclo de vida
beforeCreate() {
// cliente y servidor
console.log('[home beforeCreate]')
},
created() {
// cliente y servidor
console.log('[home created]')
// solo servidor
if (process.server) {
this.value = 10
}
// cliente y servidor
console.log('value=', this.value)
},
beforeMount() {
// solo cliente
console.log('[home beforeMount]')
},
mounted() {
// solo cliente
console.log('[home mounted]')
}
}
- Para entender lo que está sucediendo, hay que tener en cuenta que el cliente [nuxt] no ejecutará las líneas 22 a 24 (process.server=false);
- en una aplicación [vue] convencional, la propiedad [value] de la línea 10 permanece en 0. Por eso, una vez que el cliente ha pasado a la página recibida, el valor que se muestra pasa a ser [0];
El valor generado por el servidor [nuxt] para la propiedad [value] no sirvió de nada.
5.2. La página [page1]
5.2.1. La tienda [Vuex]
Hemos agregado una carpeta [store] al proyecto [nuxt-02]:

La presencia de este archivo hace que, automáticamente, [nuxt] implemente un almacén [Vuex]. El archivo [index.js] es el que implementa este almacén. Aquí, el archivo [index.js] es el siguiente:
export const state = () => ({
counter: 0
})
export const mutations = {
increment(state, inc) {
state.counter += inc
}
}
[nuxt] implementa un almacén [Vuex] a partir del contenido de [index.js]:
- líneas 1-3: definición del estado [state] del almacén. Este estado es devuelto por una función. Aquí, el estado solo tiene una propiedad: el contador de la línea 2. La función exportada debe llamarse [state];
- líneas 5-9: las operaciones posibles sobre el estado del almacén. Se denominan [mutations]. Aquí, la mutación [increment] permite incrementar la propiedad [counter] en una cantidad [inc]. El objeto exportado debe llamarse [mutations];
El [store] Vuex implementado por [nuxt] está disponible en diferentes lugares. En las vistas, está disponible en la propiedad [this.$store].
5.2.2. El código de la página
Al igual que la página [index], la página [page1] mostrará un valor, el del contador del store de Vuex:
<!-- página 1 -->
<template>
<Layout :left="true" :right="true">
<!-- navegación -->
<Navigation slot="left" />
<!-- mensaje-->
<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',
// componentes utilizados
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// ciclo de vida
beforeCreate() {
// cliente y servidor
console.log('[home beforeCreate]')
},
created() {
// cliente y servidor
console.log('[home created]')
// solo servidor
if (process.server) {
this.$store.commit('increment', 25)
}
// cliente y servidor
this.value = this.$store.state.counter
console.log('value=', this.value)
},
beforeMount() {
// solo cliente
console.log('[home beforeMount]')
},
mounted() {
// solo cliente
console.log('[home mounted]')
}
}
</script>
Comentarios
- líneas 38-40: el servidor incrementará el contador en 25;
- línea 42: tanto el servidor como el cliente mostrarán el valor del contador;
- línea 7: se muestra el valor del contador;
Al leer este código, hay que entender dos cosas:
- el código que se ejecuta es el mismo tanto para el servidor como para el cliente;
- el objeto [this] no es el mismo: hay una versión [this] del lado del servidor y otra del lado del cliente, [client];
Queremos saber si el [this.$store] del servidor es el mismo que el [this.$store] del cliente. Dado que el servidor es el que se ejecuta primero (al iniciar la aplicación), esto equivale a preguntarnos: ¿se transmite al cliente el [store] inicializado por el servidor?
5.2.3. Ejecución
Ejecutamos el proyecto [nuxt-02] y escribimos [localhost:81/nuxt-02/page1] manualmente para que se solicite al servidor. Al igual que al inicio con la página [index]:
- el servidor ejecuta la página [page1.vue];
- envía la página generada al navegador. Esta se muestra;
- los scripts de cliente integrados en la página enviada toman el control y vuelven a ejecutar la página [page1.vue];
- la página mostrada se modifica entonces;
El resultado final es el siguiente:

Esta vez, el valor que se muestra es efectivamente el establecido por el servidor y, visualmente, no se ve que la página «titubee» debido a un cambio por parte del cliente del valor mostrado por el servidor. ¿Qué pasó esta vez?
El servidor ejecutó la siguiente página [page1]:
...
<script>
/* eslint-disable no-console */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page1',
// componentes utilizados
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// ciclo de vida
beforeCreate() {
// cliente y servidor
console.log('[page1 beforeCreate]')
},
created() {
// cliente y servidor
console.log('[page1 created]')
// solo servidor
if (process.server) {
this.$store.commit('increment', 25)
}
// cliente y servidor
this.value = this.$store.state.counter
console.log('value=', this.value)
},
beforeMount() {
// solo cliente
console.log('[page1 beforeMount]')
},
mounted() {
// solo cliente
console.log('[page1 mounted]')
}
}
</script>
- las líneas 30-32 se ejecutaron sin errores. Esto significa que, también del lado del servidor, [this.$store] hace referencia al almacén [Vuex]. La línea 31 incrementó el contador del almacén a 25;
- una vez hecho esto, la página se envió al cliente;
Si observamos la página recibida por el cliente, encontramos los siguientes elementos:
<!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>
- línea 47: el valor enviado por el servidor;
- línea 60: se observa que el estado del almacén [Vuex] se ha incorporado a la página. Esto permitirá al cliente, que se ejecutará tras recibir la página, reconstruir un nuevo almacén [Vuex] con 25 como valor inicial del contador;
Tras recibir y mostrar la página recibida del servidor, el cliente toma el control y, a su vez, ejecuta la página [page1]:
...
<script>
/* eslint-disable no-console */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page1',
// componentes utilizados
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// ciclo de vida
beforeCreate() {
// cliente y servidor
console.log('[page1 beforeCreate]')
},
created() {
// cliente y servidor
console.log('[page1 created]')
// solo servidor
if (process.server) {
this.$store.commit('increment', 25)
}
// cliente y servidor
this.value = this.$store.state.counter
console.log('value=', this.value)
},
beforeMount() {
// solo cliente
console.log('[page1 beforeMount]')
},
mounted() {
// solo cliente
console.log('[page1 mounted]')
}
}
</script>
- línea 34: la propiedad [value] de la línea 18 recibe a su vez el valor 25 del contador;
Por lo tanto, el almacén de [nuxt] permite que el servidor transmita información al cliente durante la carga inicial de la página, cuando esta se solicita al servidor. Cabe recordar que, una vez obtenida esta página, ya no se solicita más al servidor y la aplicación funciona como una aplicación [vue] convencional, en modo SAP.
5.3. La página [page2]
En la página [page2], mostramos otra forma en que:
- el servidor incluya información calculada en la página;
- el cliente no modifique dicha información;
5.3.1. El código de la página
El código de la página [page2] evoluciona de la siguiente manera:
<!-- página 2 -->
<template>
<Layout :left="true" :right="true">
<!-- navegación -->
<Navigation slot="left" />
<!-- mensaje -->
<b-alert slot="right" show variant="secondary"> Page 2 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
/* eslint-desactivar nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page2',
// componentes utilizados
components: {
Layout,
Navigation
},
asyncData(context) {
// ¿Quién ejecuta este código?
console.log('asyncData, client=', process.client, 'serveur=', process.server)
// solo para el servidor
if (process.server) {
// se devuelve una promesa
return new Promise(function(resolve, reject) {
// Normalmente aquí hay una función asíncrona
// la simulamos con una espera de un segundo
setTimeout(() => {
// este resultado se incluirá en las propiedades de [data]
resolve({ value: 87 })
// registro
console.log('asynData terminée')
}, 1000)
})
}
},
// ciclo de vida
beforeCreate() {
// cliente y servidor
console.log('[page2 beforeCreate]')
},
created() {
// cliente y servidor
console.log('[page2 created]')
},
beforeMount() {
// solo cliente
console.log('[page2 beforeMount]')
},
mounted() {
// solo cliente
console.log('[page2 mounted]')
}
}
</script>
- línea 7: la página muestra el valor de una propiedad llamada [value];
- la propiedad [value] no existe como elemento de un objeto devuelto por la función [data]. En este caso, dicha función no existe. La propiedad [value] se crea dinámicamente en la línea 36;
- línea 25: la función [asyncData] es una función [nuxt]. Como su nombre lo indica, normalmente es una función asíncrona. Su función habitual es recuperar datos externos. [nuxt] garantiza que la página no se envíe al navegador del cliente hasta que la función [asyncData] haya devuelto sus datos asíncronos;
- la función [asyncData] recibe como parámetro el contexto [nuxt]. Este objeto es muy completo y brinda acceso a mucha información sobre la aplicación [Nuxt]. Lo veremos en las secciones siguientes;
- línea 31: se implementa la función [asyncData] con un [Promise] (véase el documento |Introducción al lenguaje ECMASCRIPT 6 con ejemplos|). El constructor de esta clase acepta como parámetro una función asíncrona que:
- indica éxito al devolver datos mediante la función [resolve]. El objeto devuelto por esta función se incluye automáticamente en las propiedades [data] de la página;
- indica un error devolviendo un error mediante la función [reject];
- línea 34: se simula una función asíncrona con la función [setTimeout]. Esta función devuelve el objeto [{ value: 87 }] (línea 36) al cabo de un segundo (línea 31) gracias a la función [resolve], que indica que la función [Promise] se ejecutó correctamente. El objeto devuelto por la función asíncrona se incluye automáticamente en las propiedades [data] de la página. Y es precisamente esta propiedad la que muestra la línea 7;
- línea 27: veremos que la función [asyncData] es ejecutada por el servidor, pero no por el cliente;
- línea 29: la inicialización de la propiedad [value] la realiza el servidor;
Nota: el objeto [this] no se reconoce en la función [asyncData] porque aún no se ha creado el objeto que encapsula el componente [vue];
5.3.2. Ejecución
Se ejecuta el proyecto [nuxt-02] y se escribe [localhost:81/nuxt-02/page2] manualmente para que se solicite al servidor. Al igual que al inicio con la página [index]:
- el servidor ejecuta la página [page2.vue];
- envía la página generada al navegador. Esta se muestra;
- los scripts de cliente integrados en la página enviada toman el control y vuelven a ejecutar la página [page2.vue];
- la página mostrada se modifica entonces;
El resultado final es el siguiente:

Esta vez, el valor que se muestra es efectivamente el establecido por el servidor y, visualmente, no se ve que la página «titubee» debido a un cambio por parte del cliente del valor mostrado por el servidor. ¿Qué pasó esta vez?
El servidor ejecutó la siguiente página [page2]:
<!-- página 2 -->
<template>
<Layout :left="true" :right="true">
<!-- navegación -->
<Navigation slot="left" />
<!-- mensaje -->
<b-alert slot="right" show variant="secondary"> Page 2 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
/* eslint-desactivar nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page2',
// componentes utilizados
components: {
Layout,
Navigation
},
asyncData(context) {
// ¿Quién ejecuta este código?
console.log('asyncData, client=', process.client, 'serveur=', process.server)
// solo para el servidor
if (process.server) {
// se devuelve una promesa
return new Promise(function(resolve, reject) {
// Normalmente aquí hay una función asíncrona
// la simulamos con una espera de un segundo
setTimeout(() => {
// este resultado se incluirá en las propiedades de [data]
resolve({ value: 87 })
// registro
console.log('asynData terminée')
}, 1000)
})
}
},
// ciclo de vida
beforeCreate() {
// cliente y servidor
console.log('[page2 beforeCreate]')
},
created() {
// cliente y servidor
console.log('[page2 created]')
},
beforeMount() {
// solo cliente
console.log('[page2 beforeMount]')
},
mounted() {
// solo cliente
console.log('[page2 mounted]')
}
}
</script>
Fue la línea 36 la que estableció el valor mostrado por la línea 7. Por lo tanto, eso es lo que recibió el navegador del cliente. Más precisamente, recibió la siguiente página:
<!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>
- línea 48: se observa que el valor en la página recibida es 87;
- línea 61: en la respuesta del servidor, se ven dos objetos: [data] y [state]:
- [state] es el estado del almacén [Vuex]. Este se ha instanciado a partir del contenido de la carpeta [store] de la aplicación [nuxt-02];
- [data] contiene las propiedades creadas por el servidor mediante la función [asyncData]. Encontramos la propiedad [value : 87] creada por el servidor. Los scripts del cliente integrarán esta propiedad en las de la página [page2];
Volvamos al código de la página [page2]:
<!-- página 2 -->
<template>
<Layout :left="true" :right="true">
<!-- navegación -->
<Navigation slot="left" />
<!-- mensaje -->
<b-alert slot="right" show variant="secondary"> Page 2 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
/* eslint-desactivar nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page2',
// componentes utilizados
components: {
Layout,
Navigation
},
asyncData(context) {
// ¿Quién ejecuta este código?
console.log('asyncData, client=', process.client, 'serveur=', process.server)
// solo para el servidor
if (process.server) {
// se devuelve una promesa
return new Promise(function(resolve, reject) {
// Normalmente aquí hay una función asíncrona
// la simulamos con una espera de un segundo
setTimeout(() => {
// este resultado se incluirá en las propiedades de [data]
resolve({ value: 87 })
// registro
console.log('asynData terminée')
}, 1000)
})
}
},
// ciclo de vida
beforeCreate() {
// cliente y servidor
console.log('[page2 beforeCreate]')
},
created() {
// cliente y servidor
console.log('[page2 created]')
},
beforeMount() {
// solo cliente
console.log('[page2 beforeMount]')
},
mounted() {
// solo cliente
console.log('[page2 mounted]')
}
}
</script>
- la línea 7 utiliza la propiedad [value]. Sin embargo, la página no define ninguna propiedad llamada [value]. No obstante, los scripts del cliente crearon automáticamente esta propiedad gracias al objeto [data: [{ value: 87 }]] recibido del servidor;
Además, los registros muestran que la función [asyncData] no fue ejecutada por el cliente:

La función [asyncData] fue ejecutada por el servidor [1], pero no por el cliente [2]. Además, cabe señalar que el servidor no ejecuta las funciones del ciclo de vida hasta que finaliza la función [asyncData]. Se puede aumentar el tiempo de espera dentro de la función [asyncData] para verificarlo.
5.4. La página [page3]
Agregamos una nueva página [page3] a nuestra aplicación:

5.4.1. El componente [navigation]
Se modifica el componente [vavigation] para permitir la navegación hacia la nueva página:
<template>
<!-- menú Bootstrap de tres opciones -->
<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. El código de [page3]
El código de la página [page3] es el siguiente:
<!-- página 3 -->
<template>
<Layout :left="true" :right="true">
<!-- navegación -->
<Navigation slot="left" />
<!-- mensaje -->
<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',
// componentes utilizados
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
fetch(context) {
// ¿Quién ejecuta este código?
console.log('fetch, client=', process.client, 'serveur=', process.server)
// solo para el servidor
if (process.server) {
// se devuelve una promesa
return new Promise(function(resolve, reject) {
// Normalmente aquí hay una función asíncrona
// la simulamos con una espera de un segundo
setTimeout(() => {
// éxito
resolve()
}, 1000)
}).then(() => {
// se modifica el store
context.store.commit('increment', 28)
})
}
},
// ciclo de vida
beforeCreate() {
// cliente y servidor
console.log('[page3 beforeCreate]')
},
created() {
// cliente y servidor
this.value = this.$store.state.counter
console.log('[page3 created], value=', this.value)
},
beforeMount() {
// solo cliente
console.log('[page3 beforeMount]')
},
mounted() {
// solo cliente
console.log('[page3 mounted]')
}
}
</script>
- línea 30: la función [fetch] tiene un comportamiento similar al de la función [asyncData]:
- se ejecuta antes que las funciones del ciclo de vida;
- el objeto [this] no se reconoce en esta función;
- su funcionamiento es asíncrono;
- el ciclo de vida no comienza hasta que la función asíncrona haya devuelto su resultado;
- el resultado se devuelve aquí mediante el método [then] de [Promise], línea 43;
- la función [fetch] recibe el parámetro [context]. Este representa el contexto [nuxt] del momento;
- línea 30: entre sus numerosas propiedades, el objeto [context] tiene una propiedad [store] que representa el almacén [Vuex] de la aplicación;
- línea 41: de manera artificial, se indica que la [Promise] se completó con éxito al cabo de un segundo (véase el documento |Introducción al lenguaje ECMASCRIPT 6 con ejemplos|);
- línea 45: a continuación, se ejecuta el método [then]. En él se incrementa el contador de [store];
5.4.3. Ejecución
Se ejecuta el proyecto [nuxt-02] y se escribe [localhost:81/nuxt-02/page3] manualmente para que se solicite al servidor. Al igual que al inicio con la página [index]:
- el servidor ejecuta la página [page3.vue];
- envía la página generada al navegador. Esta se muestra;
- los scripts de cliente integrados en la página enviada toman el control y vuelven a ejecutar la página [page3.vue];
- la página mostrada se modifica entonces;
El resultado final es el siguiente:

El valor que se muestra es, efectivamente, el establecido por el servidor y, visualmente, no se nota que la página «titubee» debido a un cambio por parte del cliente del valor mostrado por el servidor. ¿Qué pasó esta vez?
El servidor ejecutó la siguiente página [page3]:
<!-- página 3 -->
<template>
<Layout :left="true" :right="true">
<!-- navegación -->
<Navigation slot="left" />
<!-- mensaje -->
<b-alert slot="right" show variant="secondary"> Page 3 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
/* eslint-desactivar nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page3',
// componentes utilizados
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
fetch(context) {
// ¿Quién ejecuta este código?
console.log('fetch, client=', process.client, 'serveur=', process.server)
// solo para el servidor
if (process.server) {
// se devuelve una promesa
return new Promise(function(resolve, reject) {
// Normalmente aquí hay una función asíncrona
// la simulamos con una espera de un segundo
setTimeout(() => {
// éxito
resolve()
}, 1000)
}).then(() => {
// se modifica el almacén
context.store.commit('increment', 28)
// registro
console.log('fetch commit terminé')
})
}
},
// ciclo de vida
beforeCreate() {
// cliente y servidor
console.log('[page3 beforeCreate]')
},
created() {
// cliente y servidor
this.value = this.$store.state.counter
console.log('[page3 created], value=', this.value)
},
beforeMount() {
// solo cliente
console.log('[page3 beforeMount]')
},
mounted() {
// solo cliente
console.log('[page3 mounted]')
}
}
</script>
- línea 45: la función asíncrona [fetch] es la primera de las funciones anteriores en ejecutarse. Recibe como parámetro un objeto llamado [context], que es el contexto [nuxt] del momento. Entre las numerosas propiedades de este objeto, la propiedad [context.store] representa el almacén [Vuex];
- línea 45: en la función asíncrona [fetch], el servidor establece el contador del almacén en 28;
- línea 56: cuando se ejecuta la función [created], [nuxt] garantiza que la función asíncrona [fetch] haya terminado su trabajo;
- línea 58: el valor del contador de la persiana se asigna a la propiedad [value] de la línea 27;
- línea 7: se muestra el valor de [value], es decir, el contador del almacén;
El navegador del cliente recibe la siguiente página:
<!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>
- línea 52: se observa que el valor en la página recibida es 28;
- línea 65: en la respuesta del servidor, se observa que el servidor envió al cliente el estado [state] del almacén [Vuex]. Gracias a esta información, los scripts del cliente podrán reconstruir un almacén [Vuex];
A su vez, los scripts del cliente ejecutarán el código de la página [page3]:
<!-- página 3 -->
<template>
<Layout :left="true" :right="true">
<!-- navegación -->
<Navigation slot="left" />
<!-- mensaje -->
<b-alert slot="right" show variant="secondary"> Page 3 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
/* eslint-desactivar nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page3',
// componentes utilizados
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
fetch(context) {
// ¿Quién ejecuta este código?
console.log('fetch, client=', process.client, 'serveur=', process.server)
// solo para el servidor
if (process.server) {
// se devuelve una promesa
return new Promise(function(resolve, reject) {
// Normalmente aquí hay una función asíncrona
// la simulamos con una espera de un segundo
setTimeout(() => {
// éxito
resolve()
}, 1000)
}).then(() => {
// se modifica el almacén
context.store.commit('increment', 28)
// registro
console.log('fetch commit terminé')
})
}
},
// ciclo de vida
beforeCreate() {
// cliente y servidor
console.log('[page3 beforeCreate]')
},
created() {
// cliente y servidor
this.value = this.$store.state.counter
console.log('[page3 created], value=', this.value)
},
beforeMount() {
// solo cliente
console.log('[page3 beforeMount]')
},
mounted() {
// solo cliente
console.log('[page3 mounted]')
}
}
</script>
- línea 58: la función [created] ejecutada por el cliente asigna el valor del contador a la propiedad [value] de la línea 27;
- la línea 7 muestra este valor. Dado que es el mismo que el enviado por el servidor, no se observa que la página «titule» debido a un cambio;
Además, los registros muestran que la función [fetch] no fue ejecutada por el cliente:

La función [fetch] fue ejecutada por el servidor [1], pero no por el cliente [2]. Además, cabe señalar que el servidor no ejecuta las funciones del ciclo de vida antes de que finalice la función [fetch] [3]. Se puede aumentar el tiempo de espera dentro de la función [fetch] para verificarlo.
Las páginas [page1] y [page3] mostraron dos métodos que utilizan el almacén [Vuex] para transmitir información del servidor al cliente. Cabe preguntarse si son equivalentes. Creamos una página [page4] para verificarlo.
5.5. La página [page4]
Agregamos una nueva página [page4] a nuestra aplicación:

5.5.1. El componente [navigation]
Se modifica el componente [navigation] para permitir la navegación hacia la nueva página:
<template>
<!-- menú Bootstrap de cinco opciones -->
<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. El código de [page4]
El código de la página [page4] es el siguiente:
<!-- página 4 -->
<template>
<Layout :left="true" :right="true">
<!-- navegación -->
<Navigation slot="left" />
<!-- mensaje -->
<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',
// componentes utilizados
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// ciclo de vida
async beforeCreate() {
// cliente y servidor
console.log('[page4 beforeCreate]')
// solo para el servidor
if (process.server) {
// se ejecuta la función asíncrona
const valeur = await new Promise(function(resolve, reject) {
// Normalmente aquí hay una función asíncrona
// se simula con una espera de 10 segundos
setTimeout(() => {
// éxito: se devuelve el valor del contador
resolve(52)
}, 10000)
})
// Se modifica el store
this.$store.commit('increment', valeur)
// registro
console.log('[page4 beforeCreate], fonction asynchrone terminée, compteur=', this.$store.state.counter)
}
},
created() {
// cliente y servidor
this.value = this.$store.state.counter
console.log('[page4 created], value=', this.value)
},
beforeMount() {
// solo cliente
console.log('[page4 beforeMount]')
},
mounted() {
// solo cliente
console.log('[page4 mounted]')
}
}
</script>
- línea 30: lo que antes se realizaba en la función [fetch] ahora se realiza en el método [beforeCreate]. Se utiliza el par async (línea 30) / await (línea 36) para esperar a que finalice la función asíncrona;
- línea 36: se recupera el resultado de la función asíncrona, que se devuelve en la línea 41 después de 10 segundos (línea 42);
- líneas 50-54: en el método [created], que se ejecuta tanto en el lado del servidor como en el lado del cliente, el contador se asigna a la propiedad [value] de la página;
5.5.3. Ejecución
Se ejecuta el proyecto [nuxt-02] y se ingresa manualmente [localhost:81/nuxt-02/page4] para que se solicite al servidor. Al igual que al inicio con la página [index]:
- el servidor ejecuta la página [page4.vue];
- envía la página generada al navegador. Esta se muestra;
- los scripts de cliente integrados en la página enviada toman el control y vuelven a ejecutar la página [page4.vue];
- la página mostrada se modifica entonces;
El resultado final es el siguiente:

Contrario a lo esperado, el valor que se muestra en [2] no es 52. ¿Qué pasó?
Los registros son los siguientes:

Se puede observar que en [1] no se mostró el registro de finalización de la acción asincrónica. La función [created], que muestra el valor del contador, muestra 0. Todo esto sugiere que [nuxt] no esperó a que terminara la acción asíncrona.
Si regresamos al terminal de VSCode que se utilizó para iniciar la aplicación, encontramos los registros de [3-4]. Se observa que la función asíncrona sí se ejecutó en el lado del servidor.
Al final, la función [beforeCreate] se ejecutó por completo del lado del servidor, pero [nuxt] no esperó a que terminara su ejecución para enviar la página al navegador del cliente, mientras que sí espera a que termine la función [fetch]. Por lo tanto, este es el método que se debe utilizar si se desea que el servidor inicialice un almacén [Vuex].
5.6. Navegación en la aplicación [vue]
Hemos mostrado lo que sucedía cuando el servidor cargaba inicialmente cada una de las páginas [index, page1, page2, page3, page4]. En la práctica, esto no es lo que ocurre: en un funcionamiento normal, solo se busca la página [index] en el servidor. Veamos las tres páginas en este caso:
página [index]

Ya hemos explicado este resultado en el apartado sobre enlaces.
Ahora hagamos clic en el enlace [Page 1]:

El valor que se muestra es 0. Era 25 cuando se solicitó la página por primera vez al servidor al escribir manualmente su URL. La explicación es sencilla. El código que se ejecuta es el siguiente:
created() {
// cliente y servidor
console.log('[page1 created]')
// solo servidor
if (process.server) {
this.$store.commit('increment', 25)
}
// cliente y servidor
this.value = this.$store.state.counter
console.log('value=', this.value)
},
Era la línea 6 la que ponía el contador en 25. Como la página no se solicitó al servidor, las líneas 5 a 7 no se ejecutaron y el contador del store [Vuex] se mantuvo en 0.
Ahora, hagamos clic en el enlace [Page 2]:

Esta vez, no se muestra ningún valor y, además, aparece una advertencia en los registros de la consola:

- En [1], vemos que la función [asyncData] fue ejecutada por el cliente. Siempre es así:
- la ejecuta el servidor si la página se solicita al servidor; en ese caso, no la ejecuta el cliente;
- y cada vez que la página es el destino de la ruta actual del cliente;
- en [2]: [nuxt] emite una advertencia porque en la plantilla de la página hay una expresión reactiva {{ value }}, mientras que la página no tiene la propiedad [value];
Recordemos el código ejecutado por el cliente:
asyncData() {
// ¿Quién ejecuta este código?
console.log('asyncData, client=', process.client, 'serveur=', process.server)
// solo para el servidor
if (process.server) {
// se devuelve una promesa
return new Promise(function(resolve, reject) {
// Normalmente aquí habría una función asíncrona, pero no es el caso
// este resultado se incluirá en las propiedades de [data]
resolve({ value: 87 })
})
}
},
- en la línea 3 se pudo observar que la función [asyncData] fue ejecutada por el cliente incluso antes que las funciones del ciclo de vida;
- la línea 5 impidió la ejecución del resto del código que creaba la propiedad [value], ya que este código es ejecutado por el cliente;
Ahora pasemos a la página [page3]:

- en [2], teníamos 28 cuando la página fue proporcionada por el servidor. Aquí no es así;
- en [4], vemos que la función [fetch] se ejecutó del lado del cliente;
Analicemos el código ejecutado por el cliente:
...
fetch(context) {
// ¿Quién ejecuta este código?
console.log('fetch, client=', process.client, 'serveur=', process.server)
// solo para el servidor
if (process.server) {
// se devuelve una promesa
return new Promise(function(resolve, reject) {
// Normalmente aquí hay una función asíncrona
// la simulamos con una espera de un segundo
setTimeout(() => {
// éxito
resolve()
}, 1000)
}).then(() => {
// se modifica el almacén
context.store.commit('increment', 28)
// registro
console.log('fetch commit terminé')
})
}
},
...
- el cliente ejecuta el método [fetch], línea 2;
- las líneas 6 a 21 no se ejecutan porque la condición [process.server] es falsa. Por lo tanto, la línea 17, que establece el contador en 28, no se ejecuta. El contador permanece en cero. Por eso el cliente muestra 0 en lugar de 28;
Pasemos ahora a la página [page4]. Obtenemos el siguiente resultado:

- en [2], el valor del contador;
- en [3], los registros del cliente;
El código ejecutado por el cliente es el siguiente:
...
data() {
return {
value: 0
}
},
// ciclo de vida
async beforeCreate() {
// cliente y servidor
console.log('[page4 beforeCreate]')
// solo para el servidor
if (process.server) {
// se ejecuta la función asíncrona
const valeur = await new Promise(function(resolve, reject) {
// Aquí normalmente hay una función asíncrona
// se simula con una espera de 10 segundos
setTimeout(() => {
// éxito: se devuelve el valor del contador
resolve(52)
}, 10000)
})
// Se modifica el store
this.$store.commit('increment', valeur)
// registro
console.log('[page4 beforeCreate], fonction asynchrone terminée, compteur=', this.$store.state.counter)
}
},
created() {
// cliente y servidor
this.value = this.$store.state.counter
console.log('[page4 created], value=', this.value)
},
...
- las líneas 12 a 26 no son ejecutadas por el cliente, ya que la condición [process.server] de la línea 12 es falsa. Por lo tanto, el contador del almacén [Vuex] tiene un valor de 0 (su valor inicial en el almacén) y es este valor el que muestra la página;
Cabe preguntarse qué sucede cuando se comenta la condición [if] de las líneas 12 y 26. Esta es la respuesta:
- esta vez, el cliente ejecuta las líneas 14-25, pero [nuxt] no espera a que termine la función asíncrona (como en el caso del servidor) y, por lo tanto, deja el contador en 0;
- al cabo de 10 s, la función asíncrona finaliza y el contador se establece en 52 en la línea 23;
- cuando se vuelve a navegar a la página [page4], es entonces el valor 52 el que se muestra;
5.7. Résumé
De nuestras diferentes pruebas, destacamos los siguientes puntos:
- si la página [index] debe contener datos externos, el servidor puede buscarlos mediante la función [asyncData];
- si el servidor debe inicializar un almacén [Vuex] con datos externos al cargar la página [index], lo hará en la función [fetch];
- la página generada por el servidor y la página generada por el cliente deben ser idénticas si se quiere evitar el efecto de «salto» provocado por el hecho de que la página del cliente reemplaza visualmente a la página enviada por el servidor y mostrada inicialmente;
Vamos a explorar otros aspectos de [nuxt] a través de un nuevo ejemplo.