En esta sección, tenemos dos opciones, crear una SPA desde Vue CLI totalmente independiente de Laravel o crear una SPA dentro de Laravel. Lo haremos dentro de Laravel.

Herramientas que vamos a utilizar:

  • Vue.js
  • VueRouter
  • Vuex
  • Bulma

No vamos a detenernos demasiado en explicar cada herramienta, este artículo supone que tienes un conocimiento básico y has trabajado antes con Vue.js.
Bulma es simplemente un framework puro de CSS.

Lo que sucede al crear una Vue SPA con Laravel como backend es:

  • El primer request llega al servidor, por lo tanto es procesado por el router de Laravel.
  • Laravel renderiza la SPA.
  • Los siguientes requests utilizan la API history.pushState para la navegación sin recargar la página (Vue router).

Configuraremos Vue router para utilizar history mode, lo que significa que necesitamos crear una ruta en Laravel que sea capaz de responder a todas las posibles URL y dirigirlas a la SPA.
Por ejemplo si el usuario quiere ingresar a /escritorio necesitamos responder a esa ruta con la SPA, luego Vue router será el encargado de renderizar el componente necesario.

Configurar el backend (Laravel)

Lo primero es definir en el archivo routes/web.php la ruta encargada de responder a todas las solicitudes que se realicen:

<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/{any}', 'SpaController@index')->where('any', '.*');

Crearemos el controlador SpaController:

php artisan make:controller SpaController

Una vez creado el controlador, añadiremos un método index:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class SpaController extends Controller
{
    public function index()
    {
        return view('spa');
    }
}

La instalación de Laravel, crea una vista predeterminada en resources/views/welcome.blade.php. La eliminaremos y crearemos una nueva vista en resources/views/spa.blade.php y añadiremos el siguiente código:

<!DOCTYPE html>
<html class="has-aside-left has-aside-expanded">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Vue SPA Demo</title>
    <link href="{{ mix('/css/app.css') }}" rel="stylesheet" />
</head>

<body>
    <div id="app">
        <app></app>
    </div>
    <script src="{{ mix('js/app.js') }}"></script>
</body>

</html>

Instalación de paquetes

Continuando con el proyecto de Laravel creado en la parte 1:

npm install vue vue-router vuex bulma

¿NECESITAS AYUDA CON TU WEB?

Si buscas mejorar tu página web o tienda online, estás en el lugar adecuado. Trabajemos juntos para impulsar tu presencia en línea y hacer realidad tus objetivos.

¡HABLEMOS DE TU PROYECTO!

Añadir Bulma.io

Para añadir este framework CSS, lo que haremos es abrir el archivo resources/scss/app.scss:

//load bulma
@import "~bulma/bulma";

Configuración de los archivos JS

En la carpeta resources/js encontraremos dos archivos app.js y bootstrap.js.

app.js será nuestro punto de entrada a la SPA, por lo que allí realizaremos toda la configuración necesaria incluyendo la configuración de Vue router y de Vuex.

El siguiente paso sera crear una carpeta y archivos:

  • resources/router.js – encargado de las rutas (Vue router)
  • resources/store.js – encargado del estado de la aplicación (Vuex)
  • resources/views – carpeta que contendrá las vistas (páginas)
  • resources/views/App.vue
  • resources/views/NotFound.vue
  • resources/views/Login.vue
  • resources/views/Logout.vue
  • resources/views/Dashboard.vue
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)

// Pages
import NotFound from './views/NotFound'
import Login from './views/Login'
import Logout from './views/Logout'
import Dashboard from './views/Dashboard'

// Routes
const router = new VueRouter({
    mode: 'history',
    linkActiveClass: 'is-active',
    routes: [
        {
            path: '/login',
            name: 'login',
            component: Login,
        },
        {
            path: '/logout',
            name: 'logout',
            component: Logout,
            meta: {
                requiresAuth: true,
            }
        },
        {
            path: '/dashboard',
            name: 'dashboard',
            component: Dashboard,
            meta: {
                requiresAuth: true,
            }
        },
        { 
            path: '/404', 
            name: '404', 
            component: NotFound,
        },
        { 
            path: '*', 
            redirect: '/404', 
        },
    ],
});

export default router

Hemos configurado 4 rutas en el router:

  • /404 – para errores 404
  • /login – encargada de realizar el login y contendrá el formulario de ingreso
  • /logout – encargada de desconectarnos
  • /dashboard – nuestra ruta protegida

Hemos incluido también un campo meta dentro de alguna rutas, este será nuestro guardián. Lo definiremos luego en el archivo app.js.

import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    token: localStorage.getItem('access_token') || null
  },
  getters: {
    loggedIn(state) {
      return state.token !== null
    }
  },
  mutations: {
    retrieveToken(state, token) {
      state.token = token
    },
    destroyToken(state) {
      state.token = null
    }
  },
  actions: {
    retrieveToken(context, credentials) {

      return new Promise((resolve, reject) => {
        axios.post('/api/login', {
          username: credentials.username,
          password: credentials.password,
        })
          .then(response => {
            //console.log(response)
            const token = response.data.access_token
            localStorage.setItem('access_token', token)
            context.commit('retrieveToken', token)

            resolve(response)
          })
          .catch(error => {
            //console.log(error)
            reject(error)
          })
      })

    },
    destroyToken(context) {
      
      if (context.getters.loggedIn){
        
        return new Promise((resolve, reject) => {
          axios.post('/api/logout', '', {
              headers: { Authorization: "Bearer " + context.state.token }
            })
            .then(response => {
              //console.log(response)
              localStorage.removeItem('access_token')
              context.commit('destroyToken')
  
              resolve(response)
            })
            .catch(error => {
              //console.log(error)
              localStorage.removeItem('access_token')
              context.commit('destroyToken')

              reject(error)
            })
        })

      }
    }
  }
})

export default store

Vamos a explicar un poco que hace este archivo store.js.

Como verán, hemos definido state, getters, mutations y actions. Basicamente definiremos:

  • state es el contenedor donde se almacena el estado de la aplicación.
  • getters aquí ingresaremos funciones que se ejecutarán en varias partes de nuestra aplicación.
  • mutations definiremos funciones que modificarán el estado de la aplicación.
  • actions las acciones son similares a las mutaciones, la diferencia es que pueden contener operaciones asincrónicas, como por ejemplo una consulta a una API. Las acciones cometen mutaciones.
require('./bootstrap');

import Vue from 'vue'
import router from './router'
import store from './store'
import App from './views/App'

router.beforeEach((to, from, next) => {
    if (to.matched.some(record => record.meta.requiresAuth)) {
      // this route requires auth, check if logged in
      // if not, redirect to login page.
      if (!store.getters.loggedIn) {
        next({
          name: 'login',
        })
      } else {
        next()
      }
    } else {
      next() // make sure to always call next()!
    }
  })

const app = new Vue({
    el: '#app',
    components: { App },
    router,
    store
});

En nuestro archivo app.js cargaremos lo necesario para que nuestra SPA funcione correctamente, Vue, Vue router, Vuex y crearemos la instancia de la aplicación.

Creación de las vistas

<template>
  <div>
    <section class="section">
      <main>
        <router-view></router-view>
      </main>
    </section>
  </div>
</template>

<script>
export default {};
</script>

App.vue es el contenedor de la aplicación, solo tenemos dentro el componente <router-view> de Vue router.

<template>
  <h1 class="title">This is your Dashboard, you are logged in :)</h1>
</template>

<script>
export default {};
</script>

Dashboard.vue es una vista protegida a la que queremos llegar una vez realizado el login.

<template>
  <div>
    <div class="container">
      <div class="column is-4 is-offset-4">
        <div class="box">
          <h1 class="title">Login</h1>
          <div class="notification is-danger" v-if="error">
            <p>{{error}}</p>
          </div>
          <form autocomplete="off" @submit.prevent="login" method="post">
            <div class="field">
              <div class="control">
                <input type="email" class="input" placeholder="[email protected]" v-model="username" />
              </div>
            </div>
            <div class="field">
              <div class="control">
                <input type="password" class="input" v-model="password" />
              </div>
            </div>
            <button type="submit" class="button is-primary">Sign in</button>
          </form>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      username: null,
      password: null,
      error: null
    };
  },
  methods: {
    login() {
      this.$store
        .dispatch("retrieveToken", {
          username: this.username,
          password: this.password
        })
        .then(response => {
          this.$router.push({ name: "dashboard" });
        })
        .catch(error => {
          this.error = error.response.data;
        });
    }
  }
};
</script>

Login.vue es una vista un poco más extensa, encargada de renderizar el formulario para ingresar el usuario y password. Tiene una responsabilidad importante que es enviar la solicitud a nuestra API hecha previamente en la parte 1 y recuperar un access-token de acceso. Una vez recuperado el access-token lo almacenaremos en el state (estado) de la aplicación, para tener ese dato en todo momento y acceder a nuestras vistas protegidas.

El método login() es el encargado de ejecutar una acción retrieveToken de Vuex, declarada en el archivo store.js.
Si la respuesta a la promesa es satisfactoria, ejecutaremos this.$router.push({ name: "dashboard" }); que nos llevará a la ruta que seleccionemos luego de realizar el login, en este caso a /dashboard.

<template></template>

<script>
export default {
  created() {
    this.$store.dispatch("destroyToken").then(response => {
      this.$router.push({ name: "login" });
    });
  }
};
</script>

Logout.vue tiene como única finalidad, enviar una solicitud para destruir el access-token del estado de la aplicación. Por lo tanto ya no podremos ingresar a vistas protegidas. Una vez destruido el access-token redirigiros al usuario a /login.

<template>
  <div>
    <h1 class="title">Not Found</h1>
    <p>Woops! Looks like the page you requested cannot be found.</p>
  </div>
</template>

<script>
export default {};
</script>

NotFound.vue es la vista que mostraremos cuando un request devuelva un error 404.

Probando la SPA

Para realizar la prueba vamos a recordar los datos de usuario y password que estábamos utilizando en la parte 1:

Antes que nada, vamos a ejecutar en el terminal:

npm run watch

Si todo sale bien, se crearán los archivos CSS y JS.

El siguiente paso es abrir el navegador e ir a http://spa-auth.test/login. Ahí se nos mostrará la vista Login.vue definida previamente con el formulario.

Si por alguna razón ingresamos a http://spa-auth.test nos dará un error 404 ya que no tenemos definida ninguna ruta para /.

Conclusión

Hemos creado una Single Page Application (SPA) con Vue.js dentro de Laravel. A su vez configuramos el router de Laravel para que responda a cualquier petición enviándola a la SPA, donde actúa el Vue router.

Hemos creado vistas con Vue, algunas protegidas, otras públicas.
Almacenamos en el estado de la aplicación, el access-token que necesitaremos para realizar consultas a nuestra API en un futuro.
No solo estamos autenticados en nuestra SPA sino que tambien haremos solicitudes de manera autenticada.

Por ejemplo, en un futuro podríamos crear las vistas:

/clients para listar clientes
/clients/create para crear un cliente
/clients/:id para mostrar los datos de un cliente
/clients/:id/edit para editar un cliente

Cualquiera de esas vistas deberá comunicarse con nuestra API, recuperando y enviando datos. Para cualquier intercambio de datos, lo haremos pasando nuestro access-token.

Este trabajo más la parte 1, la hemos incluido en un repo para que puedas ver y experimentar: https://github.com/danielcharrua/laravelvue-boilerplate

Si te he ayudado, comparte! 🤓

HAGAMOS DESPEGAR TU NEGOCIO ONLINE 🚀

Te ayudo a potenciar tu página web o tienda online con estos servicios.

DESARROLLO WEB

MANTENIMIENTO WEB

CONSULTORÍA WEB

OPTIMIZACIÓN WPO

DESINFECCIONES

SOPORTE WEB

Publicaciones Similares

18 comentarios

  1. Hola. muy interesante laravel passport con vue js, pero tengo un inconveniente al usar php artisan serve mi request se queda en estado pending y se cuelga la aplicacion.

    Espero de su ayuda.. Gracias!

  2. Estimado, puedes hacer los ejemplos que indicas?

    /clients para listar clientes
    /clients/create para crear un cliente
    /clients/:id para mostrar los datos de un cliente
    /clients/:id/edit para editar un cliente

    Si no es mucho pedir… excelente tu publicación!

  3. Buenos días,

    En primer lugar felicitarte por tan excelente y clara guía. Mi consulta va relacionada a la creación del proyecto Vue: hay alguna consideración a tener en cuenta si decido crear el proyecto vue de manera independiente?

    1. Hola Jano, hay pequeñas consideraciones si haces el proyecto de manera independiente, por ejemplo, podrías saltarte completamente el paso nº1, en el paso nº2 instala a mayores Axios, en el paso nº4 verifica las URL de la API en el archivo store.js y elimina del archivo app.js la primera línea.
      Algo que puede sucederte seguramente es que si tienes alojada la app de Vue.js en un dominio diferente a la API de Laravel, vas a obtener un error de CORS (Cross Origin Resource Sharing), para solucionarlo puedes utilizar un paquete de composer: barryvdh/laravel-cors (en la instalación de Laravel). Espero que con estos tips haya solucionado tu consulta 🙂

  4. Hola buen dia!

    La verdad ambos post estan muy buenos sin embargo estoy teniendo un pequeño problema entendiendo la ultima parte.

    Para que nosotros mandemos nuestro token de authorization con axios por defecto, tenemos que definirlo cuando obtenemos el token sin errores . Sin embargo no se define el default de axios en retrieveToken sino en destroyToken. Si no estoy siguiendo mal la logica, en el momento que se re-define el parametro por defecto, el token todavia tiene un valor asignado ya que nunca se hizo el commit

    1. Hola Santiago, tienes razón.

      De nada sirve definir globalmente un header global de Axios en destroyToken, se me habrá quedado haciendo pruebas, lo que si necesitamos es definir un header en ese request en particular, ya que como detalla la parte 1, la ruta /api/logout necesita autenticación. Lo puedes ver en el punto 4.1, donde se define el método logout().
      Estoy cambiando en este mismo momento el texto y los archivos.

      Lo lógico como dices, sería definir ese header global de Axios en retrieveToken solo si nuestra SPA estará haciendo más requests a la API hecha en Laravel. No olvidemos destruir ese header global al hacer logout en destroyToken. En este caso no lo haré ya que no tenemos mas endpoints en la API para visitar de manera autenticada.

      ¡Gracias por la observación!

  5. hola, muchísimas gracias por tu trabajo, se nota la dedicación y el esmero con el que lo haces.

    sin embargo aunque he seguido la guía al pie de la letra me encontrado con un error que no puedo resolver la pagina login no carga se queda en la promesa y no pasa de allí.
    me podrías colaborar con eso

    1. Hola Elias, gracias por tus comentarios.
      Deberías ver en tu consola de JS si tienes algún error. Al no ver lo que tienes delante no puedo ayudarte. Revisa que tengas realizado todo exactamente como está en el artículo, quizá te falto algo.
      Pero insisto nuevamente, mira la consola que seguramente tengas información allí. Un saludo!

  6. Daniel excelente trabajo lo realice con un vue independiente desde el CLI y funciono a la perfección con las consideraciones que comentaste

  7. Hola buenas! La verdad que me ha servido de mucho, felicidades. Pero en mi caso tengo un pequeño problema, cuando hago login y paso a la view dashboard y actualizo la web no me guarda la sesion por lo que tengo que volver al login y volver a logearme, la cosa es que el token lo guarda bien en el lado del cliente.
    Espero haberme explicado.

  8. Hola a todos, tengo problemas para realizar esta parte de manera independiente. Leí que @Angel Infanti pudo lograrlo… podrias ayudarme? o mostrarme tu codigo para ver donde estoy errándole. Muchas gracias!

  9. Buenas noches, al final pude realizarlo. Sólo me queda la duda de como volver a enviar el token a la api para realizar otra llamada por ejemplo usuarios o noticias.

  10. Hola actualmente ando realizando este curso, pero con un SPA fuera de Laravel, la API me funciona como un encanto cuando la consulto desde postman, pero al momento de realizar el request post con axios como lo haces tu, me lanza error de unauthorized y no llega siquiera a darme el token, el back es algo distinto al tuyo, pero en teoría debería funcionar igual puesto que en Postman si sirve todo bien, he estado investigando y me dice que es por el problema del token csrf desde axios pero no tengo la más menor idea de como implementarlo, ya el problema del CORS lo pasé pero estoy atascado en esto, alguien podría darme una mano?

  11. Hola amigo excelente ejemplo integrando las tecnologías de Laravel, vue, vue router y vuex, tengo 02 dudas espero pueda despejarlas
    La primera pregunta: es como puedo agregar mas criterios para iniciar sesión, es decir por ejemplo en la tabla Users tengo el campo «estado», supongo debe ser en el «form_params»?? o como seria y que nombre recibiría ya que este lleva a la segunda pregunta: En mi base de datos tengo el campo «email» y «password», como LaravelPassport entiende que el campo «username» es equivalente al campo «email» de mi base de datos en donde se encuentra esa configuración, para poder escoger otro campo para iniciar sesión como el campo «name» en lugar del «email»
    Saludos amigo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Responsable: charrua.es. Finalidad: Poder contestar tu solicitud. Almacenamiento de los datos: Los datos son almacenados en un servidor alojado dentro de la UE y gestionado por charrua.es. Derechos: En cualquier momento puedes limitar, recuperar y borrar tu información.