Nuxt - Vue en Modo Fácil

24 de marzo de 2022

Vue.js ha ido ganando popularidad a lo largo de estos años, uno de los motivos es el ecosistema de librerías a su alrededor.

En este articulo vamos a explicarte más en detalle qué es y qué nos ofrece Nuxt.js, un meta-framework que se basa en Vue para ayudarte a construir de forma sencilla: SPA's, webs con contenido estático pre-renderizado ( SSG - Server Side Generated ), webs renderizadas en el lado servidor ( SSR - Server Side Rendering ), además de brindarnos otras ventajas que veremos más adelante.

¿Qué es un meta-framework? 🧐

Normalmente hablamos de frameworks, herramientas que nos facilitan una estructura del código, unos patrones de diseño, además de aportarnos funcionalidades.

El concepto de meta-framework, se basa en abstraer una capa más del framework al desarrollador, con el objetivo de simplificar algunas de sus partes en pos de la experiencia de desarrollo. Uno de los más populares por la parte de React es Next.js.

Suena confuso y complicado, pero veremos que es bastante más sencillo de lo que parece.

¿Qué es Nuxt?

Como habíamos introducido antes, Nuxt es un meta-framework que nos permite crear aplicaciones web hechas en Vue, de forma simplificada y estructurada, además de proveernos de multiples ventajas y funcionalidades.

Algunas de estas funcionalidades son:

Formas de renderizar el contenido de una página web

Antes de continuar tenemos que explicar las distintas formas de renderizar el contenido web, ya que hablaremos de las ventajas de cada una después.

SPA - Single Page Application

El sistema más conocido en los últimos años. Por ejemplo este sistema es el que utilizamos cuando generamos nuestra web con vue-cli o create-react-app.

Al construir la aplicación, lo que genera es un archivo HTML sin contenido, que llamará a todos los js necesarios para construir todo el contenido del DOM de forma dinámica en el lado cliente. Esto significa que la respuesta que enviamos al cliente es simplemente un "esqueleto" que va a tomar forma conforme se cargue el Js y se vaya modificando el DOM.

SSR - Server Side Rendering

Este sistema es similar al que han usado las aplicaciones web monolíticas durante años (también conocidas como universales).

Aquí tenemos un servidor que por cada petición, sirve exactamente el HTML que necesita el cliente, es decir al cliente le llega una página web completamente cargada y renderizada, la cual al cargar los archivos javascript correspondientes, se "hidrata" y a partir de ese momento será completamente interactiva.

Para este sistema necesitamos un host web que se encargue de construir las respuestas de las peticiones, con el coste computacional que eso conlleva.

SSG - Server Side Generated

Este sistema está a medio camino entre una aplicación SPA y una aplicación SSR. Funciona de forma similar a cuando hacemos SSR, la diferencia es que en este caso solo renderizamos el HTML del contenido una vez, de forma anticipada, pero de toda nuestra aplicación.

Luego, esos HTML son los que servimos de forma estática. Al cliente le llega el HTML ya pre-renderizado y luego pasa al proceso de "hidratar" la web, para añadirle la interactividad.

Este sistema se utiliza por ejemplo en los JAMStack (JavaScript, API y Markup)

¿Qué beneficios aporta utilizar Nuxt en lugar de solo Vue?

¡Buena pregunta! Lo primero es que Nuxt, como la mayoría de librerías y frameworks, solo deberían ser usados cuando nos aportan algo para solucionar nuestro problema.

En este caso se añade una capa de complejidad que tendremos que entender y valorar si encaja en nuestro proyecto.

La parte positiva es que nos proporciona una gran ayuda en varios aspectos:

🔎 SPA's y el SEO

Las SPA ( Single Page Application ) han ganado mucha popularidad en los últimos años debido al auge de frameworks como Angular, React y Vue, y a las ventajas desde el punto de vista del usuario, al tener una experiencia más fluida y con menos cargas explícitas.

Todo eso suena genial, pero existe un problema con esa arquitectura, y es algo que según los requisitos que tenga nuestro proyecto no vamos a poder ignorar, el SEO.

El SEO ( Search Engine Optimization ) es un arte para algunos y un misterio para otros, aunque eso es un tema para otro post. Vamos con el problema que nos atañe.

La forma en la que los motores de búsqueda indexan el contenido se basa en hacer peticiones a las páginas web y analizar el contenido devuelto.

Aquí es donde nos encontramos con el problema: al construir nuestra SPA, este es el HTML que verán las motores de búsqueda:

<!doctype html>
<html lang="">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="icon" href="/favicon.ico">
    <title>vue-spa</title>
    <script defer="defer" src="/js/chunk-vendors.6f73f3f7.js"></script>
    <script defer="defer" src="/js/app.1546678f.js"></script>
    <link href="/css/app.2cf79ad6.css" rel="stylesheet">
</head>

<body><noscript><strong>We're sorry but vue-spa doesn't work properly without JavaScript enabled. Please enable it to
            continue.</strong></noscript>
    <div id="app"></div>
</body>

</html>

Como podemos ver, es un HTML prácticamente vacío, no contiene la información que queremos que el motor de búsqueda relacione con nuestra página. Por lo tanto eso impactará de forma negativa en nuestro SEO.

Al abrir la página en un navegador, este usará estos assets js y css para empezar a modificar el DOM, con objetivo de darle la forma real de nuestra web.

<script defer="defer" src="/js/chunk-vendors.6f73f3f7.js"></script>
<script defer="defer" src="/js/app.1546678f.js"></script>
<link href="/css/app.2cf79ad6.css" rel="stylesheet">

Ahora bien, ¿cómo se ve una aplicación (SSG) creada con Nuxt desde el punto de vista de un motor de búsqueda?

Este es el resultado:

<!doctype html>
<html data-n-head-ssr lang="en" data-n-head="%7B%22lang%22:%7B%22ssr%22:%22en%22%7D%7D">

<head>
  <title>nuxt-example</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="">
  <meta data-n-head="ssr" name="format-detection" content="telephone=no">
  <link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
  <link rel="preload" href="/_nuxt/ac2e9d1.js" as="script">
  <link rel="preload" href="/_nuxt/af55298.js" as="script">
  <link rel="preload" href="/_nuxt/bf25a64.js" as="script">
  <link rel="preload" href="/_nuxt/38f9c50.js" as="script">
  <link rel="preload" href="/_nuxt/a78706c.js" as="script">
  <style data-vue-ssr-id="fa7ff0ca:0">.nuxt-progress {position: fixed;top: 0;left: 0;right: 0;height: 2px;width: 0;opacity: 1;transition: width .1s, opacity .4s;background-color: #000;z-index: 999999}.nuxt-progress.nuxt-progress-notransition {transition: none}.nuxt-progress-failed {background-color: red}</style>
  <link rel="preload" href="/_nuxt/static/1648032673/payload.js" as="script">
  <link rel="preload" href="/_nuxt/static/1648032673/manifest.js" as="script">
</head>

<body>
  <div data-server-rendered="true" id="__nuxt">
    <div id="__layout">
      <div class="relative flex items-top justify-center min-h-screen bg-gray-100 sm:items-center sm:pt-0">
        <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.1.2/dist/tailwind.min.css" rel="stylesheet">
        <div class="max-w-4xl mx-auto sm:px-6 lg:px-8"><a href="https://nuxtjs.org" target="_blank"
            class="flex justify-center pt-8 sm:pt-0">
            <svg width="218" height="45" viewBox="0 0 159 30" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M55.5017 6.81866H60.1727L70.0719 22.9912V6.81866H74.3837V29.7345H69.7446L59.8135 13.5955V29.7345H55.5017V6.81866Z" fill="#003543"></path> <path d="M93.657 29.7344H89.6389V27.1747C88.7241 28.9761 86.8628 29.9904 84.5113 29.9904C80.7869 29.9904 78.3684 27.3059 78.3684 23.4423V13.2339H82.3865V22.5976C82.3865 24.8566 83.7594 26.4276 85.8171 26.4276C88.0712 26.4276 89.6389 24.6598 89.6389 22.2377V13.2339H93.657V29.7344Z" fill="#003543"></path> <path d="M107.64 29.7344L103.784 24.2342L99.9291 29.7344H95.6492L101.596 21.1242L96.1074 13.2339H100.485L103.784 17.9821L107.051 13.2339H111.461L105.94 21.1242L111.886 29.7344H107.64Z" fill="#003543"></path> <path d="M120.053 8.25848V13.2339H124.627V16.6063H120.053V24.7974C120.053 25.0725 120.162 25.3363 120.356 25.531C120.55 25.7257 120.813 25.8353 121.087 25.8357H124.627V29.728H121.98C118.386 29.728 116.035 27.6323 116.035 23.9687V16.6095H112.801V13.2339H114.83C115.776 13.2339 116.327 12.6692 116.327 11.7349V8.25848H120.053Z" fill="#003543"></path> <path d="M134.756 24.5446V6.81866H139.066V23.1864C139.066 27.6067 136.943 29.7345 133.349 29.7345H128.332V25.8421H133.461C133.804 25.8421 134.134 25.7054 134.377 25.4621C134.619 25.2188 134.756 24.8888 134.756 24.5446Z" fill="#003543"></path> <path d="M141.649 22.0409H145.799C146.029 24.6006 147.728 26.2308 150.472 26.2308C152.923 26.2308 154.623 25.2501 154.623 23.2199C154.623 18.3085 142.331 21.7129 142.331 12.9395C142.334 9.17515 145.568 6.55945 150.215 6.55945C155.05 6.55945 158.317 9.34153 158.516 13.6306H154.388C154.193 11.6341 152.632 10.2918 150.207 10.2918C147.953 10.2918 146.548 11.3397 146.548 12.9427C146.548 18.0173 159 14.2226 159 23.1576C159 27.4131 155.504 30 150.474 30C145.279 30 141.882 26.8563 141.654 22.0441" fill="#003543"></path> <path d="M24.7203 29.704H41.1008C41.6211 29.7041 42.1322 29.5669 42.5828 29.3061C43.0334 29.0454 43.4075 28.6704 43.6675 28.2188C43.9275 27.7672 44.0643 27.2549 44.0641 26.7335C44.0639 26.2121 43.9266 25.6999 43.6662 25.2485L32.6655 6.15312C32.4055 5.70162 32.0315 5.32667 31.581 5.06598C31.1305 4.8053 30.6195 4.66805 30.0994 4.66805C29.5792 4.66805 29.0682 4.8053 28.6177 5.06598C28.1672 5.32667 27.7932 5.70162 27.5332 6.15312L24.7203 11.039L19.2208 1.48485C18.9606 1.03338 18.5864 0.658493 18.1358 0.397853C17.6852 0.137213 17.1741 0 16.6538 0C16.1336 0 15.6225 0.137213 15.1719 0.397853C14.7213 0.658493 14.3471 1.03338 14.0868 1.48485L0.397874 25.2485C0.137452 25.6999 0.000226653 26.2121 2.8053e-07 26.7335C-0.000226092 27.2549 0.136554 27.7672 0.396584 28.2188C0.656614 28.6704 1.03072 29.0454 1.48129 29.3061C1.93185 29.5669 2.44298 29.7041 2.96326 29.704H13.2456C17.3195 29.704 20.3239 27.9106 22.3912 24.4118L27.4102 15.7008L30.0986 11.039L38.1667 25.0422H27.4102L24.7203 29.704ZM13.0779 25.0374L5.9022 25.0358L16.6586 6.36589L22.0257 15.7008L18.4322 21.9401C17.0593 24.2103 15.4996 25.0374 13.0779 25.0374Z" fill="#00DC82"></path> </svg></a>
          <div class="mt-8 bg-white overflow-hidden shadow sm:rounded-lg p-6">
            <h2 class="text-2xl leading-7 font-semibold">
              Welcome to your Nuxt Application
            </h2>
            <p class="mt-3 text-gray-600">
              We recommend you take a look at the
              <a href="https://nuxtjs.org" target="_blank" class="button--doc text-green-500 hover:underline">Nuxt
                documentation</a>, whether you are new or have previous experience with the
              framework.<br>
            </p>
            <p class="mt-4 pt-4 text-gray-800 border-t border-dashed">
              To get started, remove
              <code class="bg-gray-100 text-sm p-1 rounded border">components/Tutorial.vue</code>
              and start coding in
              <code class="bg-gray-100 text-sm p-1 rounded border">pages/index.vue</code>. Have fun!
            </p>
          </div>
          <div class="flex justify-center pt-4 space-x-2">
            <a href="https://github.com/nuxt/nuxt.js" target="_blank">
              <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" class="w-6 h-6 text-gray-600 hover:text-gray-800 button--github"> <path d="M12 2.247a10 10 0 0 0-3.162 19.487c.5.088.687-.212.687-.475c0-.237-.012-1.025-.012-1.862c-2.513.462-3.163-.613-3.363-1.175a3.636 3.636 0 0 0-1.025-1.413c-.35-.187-.85-.65-.013-.662a2.001 2.001 0 0 1 1.538 1.025a2.137 2.137 0 0 0 2.912.825a2.104 2.104 0 0 1 .638-1.338c-2.225-.25-4.55-1.112-4.55-4.937a3.892 3.892 0 0 1 1.025-2.688a3.594 3.594 0 0 1 .1-2.65s.837-.262 2.75 1.025a9.427 9.427 0 0 1 5 0c1.912-1.3 2.75-1.025 2.75-1.025a3.593 3.593 0 0 1 .1 2.65a3.869 3.869 0 0 1 1.025 2.688c0 3.837-2.338 4.687-4.563 4.937a2.368 2.368 0 0 1 .675 1.85c0 1.338-.012 2.413-.012 2.75c0 .263.187.575.687.475A10.005 10.005 0 0 0 12 2.247z" fill="currentColor"></path> </svg>
            </a>
            <a href="https://twitter.com/nuxt_js" target="_blank">
              <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" class="w-6 h-6 text-gray-600 hover:text-gray-800"> <path d="M22.46 6c-.77.35-1.6.58-2.46.69c.88-.53 1.56-1.37 1.88-2.38c-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29c0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15c0 1.49.75 2.81 1.91 3.56c-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.22 4.22 0 0 1-1.93.07a4.28 4.28 0 0 0 4 2.98a8.521 8.521 0 0 1-5.33 1.84c-.34 0-.68-.02-1.02-.06C3.44 20.29 5.7 21 8.12 21C16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56c.84-.6 1.56-1.36 2.14-2.23z" fill="currentColor"></path> </svg>
            </a>
          </div>
        </div>
      </div>
    </div>
  </div>
  <script>window.__NUXT__ = { staticAssetsBase: "/_nuxt/static/1648032673", layout: "default", error: null, serverRendered: !0, routePath: "/", config: { _app: { basePath: "/", assetsPath: "/_nuxt/", cdnURL: null } } }</script>
  <script src="/_nuxt/ac2e9d1.js" defer></script>
  <script src="/_nuxt/a78706c.js" defer></script>
  <script src="/_nuxt/af55298.js" defer></script>
  <script src="/_nuxt/bf25a64.js" defer></script>
  <script src="/_nuxt/38f9c50.js" defer></script>
</body>

</html>

Como podemos ver, es un HTML mucho más extenso, con todo el contenido de nuestra página visible, por lo tanto el motor de búsqueda será capaz de entender y categorizar nuestro contenido. Además el usuario verá el contenido ligeramente más rápido, ya que el HTML ya viene construido desde el principio, y no hace falta construirlo a partir de modificaciones en el DOM

Otra ventaja que nos proporciona Nuxt en este aspecto es la posibilidad de generar/modificar las etiquetas <meta> de forma global, dinámica y por página, algo muy importante para el SEO.

Además, como veremos es muy sencillo:

export default {
  ...
  head: {
    title: 'my website title',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      {
        hid: 'description',
        name: 'description',
        content: 'my website description'
      }
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
  }
  ...
}
<template>
  <h1>{{ title }}</h1>
  <input v-model="title" placeholder="edit the page title">
</template>
<script>
  export default {
    data() {
      return {
        title: 'Home page'
      }
    },
    head() {
      return {
        title: this.title,
        meta: [
          {
            hid: 'description',
            name: 'description',
            content: 'Home page description'
          }
        ]
      }
    }
  }
</script>

🛣️ Router automático

Otra comodidad que nos ofrece Nuxt es su sistema de rutas automáticas basadas en la estructura de nuestras carpetas dentro del directorio pages

Vue.js cuenta con una librería centrada en proveer al desarrollador de herramientas para crear un sistema de rutas y la posibilidad de moverse entre ellas a través de enlaces sin recargar toda la página, llamada Vue Router.

Este es un ejemplo de cómo se generan las rutas de una aplicación usando Vue Router

// 0. If using a module system (e.g. via vue-cli), import Vue and VueRouter
// and then call `Vue.use(VueRouter)`.

// 1. Define route components.
import Foo from '@/example/Foo'
import Bar from '@/example/Bar'

// 2. Define some routes
// Each route should map to a component. The "component" can
// either be an actual component constructor created via
// `Vue.extend()`, or just a component options object.
// We'll talk about nested routes later.
const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar }
]

// 3. Create the router instance and pass the `routes` option
// You can pass in additional options here, but let's
// keep it simple for now.
const router = new VueRouter({
  routes // short for `routes: routes`
})

// 4. Create and mount the root instance.
// Make sure to inject the router with the router option to make the
// whole app router-aware.
const app = new Vue({
  router
}).$mount('#app')

// Now the app has started!

Como podemos ver, no es algo excesivamente complicado, pero de esta forma tenemos que escribirlo y gestionarlo nosotros a mano.

Cuando usamos Nuxt la configuración de nuestro router se genera automáticamente en base a la estructura de la carpeta pages. Veamos un ejemplo:

Con esta estructura de carpetas:

|-- pages,
  |-- about.vue
  |-- index.vue
  |-- users
      |-- _username.vue
  |-- help
      |-- details.vue
      |-- index.vue

Tendríamos disponibles los siguientes endpoints en nuestra aplicación:

http://myhost/                     -  Acceder aquí nos mostrará el componente: index.vue de la carpeta /pages 
http://myhost/about                -  Acceder aquí nos mostrará el componente: about.vue de la carpeta /pages 
http://myhost/help                 -  Acceder aquí nos mostrará el componente: index.vue de la carpeta /pages/help 
http://myhost/details              -  Acceder aquí nos mostrará el componente: details.vue de la carpeta /pages/help

http://myhost/users/usuario1       -  Acceder aquí nos mostrarán el componente: _username.vue de la carpeta /pages,
http://myhost/users/usuario2          además desde la página tendremos acceso al valor que haya pasado el usuario
http://myhost/users/usuarioN          en la ruta, en estos casos sería (usuario1, usuario2, usuarioN),
                                      el cual podemos usar para mostrar la información específica de cada usuario

Esto nos ahorra varias líneas de código, además de facilitarnos el mantenimiento al modificar o añadir rutas. Y en caso de necesitar alguna configuración más específica de Vue Router, Nuxt nos da la posibilidad de extender las configuraciones por defecto para esos casos.

Para ello tendremos que modificar la propiedad router del archivo nuxt.config.js

export default {
  ...
  router: {
    // customize the Nuxt router
  }
  ...
}

Para más detalles sobre esto podemos acceder a la documentación oficial aquí

📦 Módulos

Similares a los plugins de Vue, Nuxt nos proporciona la posibilidad de crear o usar módulos que pueden extender la funcionalidad de Nuxt de varias maneras. En caso de que queramos crear nuestro propio módulo, en este enlace de la documentación se explica en detalle.

A modo de ejemplo, estos son algunos de los módulos más populares:

Si necesitamos algún otro módulo podemos buscarlo en la lista oficial, que podemos consultar aquí

💻 Facilidades al desarrollar

Además de todo lo que hemos visto, Nuxt también nos proporciona otras comodidades más a la hora del desarrollo. Algunas de estas son:

🔮 Mirando hacia el futuro

Como sabemos, Vue lanzó su version 3 en Septiembre de 2020, y desde entonces todo el ecosistema a su alrededor ha ido actualizándose para utilizar la última versión.

Desde el 7 de Febrero de 2022, la versión por defecto de Vue es la 3

Nuxt también se encuentra en este proceso. Actualmente la versión 3 se encuentra en beta y tienen planificado lanzar su primera versión candidata a definitiva en Marzo de 2022. Esta versión, además de utilizar ya Vue 3, con su composition API, también nos ofrecerá otras ventajas como:

¡Estaremos a la espera de su lanzamiento para poder disfrutar de todas esas novedades!


Créditos

Foto por micheile || visual stories en Unsplash.

Sobre el autor: Bryan de Oliveira Brettas

Desarrollador full-stack en Mimacom, enfocándose hacia el frontend sin olvidar el otro lado

Comments
Únete a nosotros