Required Tools

  • Vue Cli 3.*
  • Node

Installing Required Tools

Installing Node Environment

Go to Node official website to download

Installing Vue Cli

npm install -g @vue/cli
# OR
yarn global add @vue/cli

After installation, you need to

Creating a Project

vue create YOUR_PROJECT_NAME
# OR
vue ui
George-2:Node George$ vue create spa


Vue CLI v3.8.2
┌───────────────────────────┐
│  Update available: 3.8.4  │
└───────────────────────────┘
? Please pick a preset: default (babel, eslint)


Vue CLI v3.8.2
✨  Creating project in /Users/George/Develop/Node/spa.
🗃  Initializing git repository...
⚙  Installing CLI plugins. This might take a while...

yarn install v1.13.0
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
✨  Done in 207.89s.
🚀  Invoking generators...
📦  Installing additional dependencies...

yarn install v1.13.0
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...

success Saved lockfile.
✨  Done in 53.93s.
⚓  Running completion hooks...

📄  Generating README.md...

🎉  Successfully created project spa.
👉  Get started with the following commands:

 $ cd spa
 $ yarn serve

George-2:Node George$ 

Installing Dependencies

vue add router
vue add vuex
vue add axios

The above commands add vue-router, vuex, and axios dependencies to the project.

Project Directory Structure

Project Implementation

User State Management

Since users can have two states, logged in or not logged in, how does the frontend store the user’s login state? Here we can use Vuex for storage. But Vuex alone is not enough, because when the user manually refreshes the browser, Vue’s entire lifecycle is reloaded. So we also need to use the browser’s Local Storage or Session Storage to ensure that the user state is not lost after refreshing.

import Vue from 'vue'
import Vuex from 'vuex'


Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    access_token: localStorage.getItem('access_token'),
  },
  mutations: {
    SET_ACCESS_TOKEN: (state, data) => {
      if(data === false) {
        Vue.set(state, 'access_token', '');
        localStorage.removeItem('access_token');
      } else {
        Vue.set(state, 'access_token', data.access_token);
        localStorage.setItem('access_token', data.access_token);
      }
    }
  },
  actions: {
    signIn({commit}, params) {
      return new Promise((resolve,reject) => {
        Vue.axios.post('/signin', params).then(res => {
          commit('SET_ACCESS_TOKEN', res.data);
          resolve(res);
        }).catch(err => {
          reject(err);
        })
      });
    }
  }
});

System State Management

Saving Layout information globally

import Vue from 'vue'
import api from '../../apis'
import * as types from '../types'

export default {
  state: {
    layout: {
      current: "guest"
    },
    menu: {
      active: "/"
    },
    installed: false,
    version: ""
  },
  mutations: {
    SET_SYSTEM: (state, data) => {
      Vue.set(state, 'installed', data.installed);
      Vue.set(state, 'version', data.version);
    },
    SET_MENU_ACTIVE: (state, data) => {
      Vue.set(state.menu, 'active', data)
    },
    SET_LAYOUT_CURRENT: (state, data) => {
      Vue.set(state.layout, 'current', data)
    }
  },
  actions: {
    fetchSystem({ commit }) {
      api.system.fetch().then(response => {
        commit(types.SET_SYSTEM, response.data)
      });
    }
  }
}

Request 401 Interception

"use strict";

import Vue from 'vue';
import axios from 'axios'
import store from '../store'
import router from '../router'

// Full config:  https://github.com/axios/axios#request-config
// axios.defaults.baseURL = process.env.baseURL || process.env.apiUrl || '';
// axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
// axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';

let config = {
  baseURL: process.env.baseURL || process.env.apiUrl || "",
  timeout: 60 * 1000, // Timeout
  withCredentials: true, // Check cross-site Access-Control
};

const request = axios.create(config);

// Define request interceptor
request.interceptors.request.use(config => {
  // Iterate through request parameters
  for(let key in config.params) {
    // If the parameter value is empty, delete this key
    if (config.params.hasOwnProperty(key) && (config.params[key] === "" || config.params[key] === null)) {
      delete config.params[key];
    }
  }
  // If the user's access token exists, add it to the request header
  if (store.state.account.access_token) {
    config.headers.Authorization = `Bearer ${store.state.account.access_token}`;
  }
  return config;
}, error => {
  return Promise.reject(error);
});

// Define response interceptor
request.interceptors.response.use(
  response => {
    return response.data;
  },
  error => {
    // If the status code is 401, clear the state in Vuex and redirect to the login page
    if (error.response.status === 401) {
      store.commit('SET_PROFILE', false);
      store.commit('SET_ACCESS_TOKEN', false);
      router.push("/signin");
    }
    return Promise.reject(error.response);
  }
);

Plugin.install = function(Vue) {
  Vue.axios = request;
  window.axios = request;
  Object.defineProperties(Vue.prototype, {
    axios: {
      get() {
        return request;
      }
    },
    $axios: {
      get() {
        return request;
      }
    },
  });
};

Vue.use(Plugin);

export default Plugin;

Frontend Route Interception

Implementing API 401 status code interception alone is not enough; we also need to authenticate the frontend routes.

import Vue from 'vue'
import store from '../store'
import Router from 'vue-router'

Vue.use(Router);

const router = new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'dashboard',
      meta: {
        requiresAuth: true
      },
      component: () => import('../views/Dashboard.vue'),
    },
    {
      path: '/signin',
      name: 'signin',
      meta: {
        requiresAuth: false
      },
      component: () => import('../views/SignIn.vue')
    },
    {
      // Will match all paths
      path: '*',
      name: "notfound",
      meta: {
        requiresAuth: false
      },
      component: () => import('../views/NotFound.vue')
    }
  ]
});

/**
 * Routing to intercept
 */
router.beforeEach((to, from, next) => {
  // If redirecting to NotFound page, set the view's Layout to guest in advance
  if (to.name === 'notfound') {
    store.commit('SET_LAYOUT_CURRENT', 'guest');
  }

  // If returning from NotFound page and authentication is required, set the view's Layout to backend
  if (from.name === 'notfound' && to.meta.requiresAuth === true) {
    store.commit('SET_LAYOUT_CURRENT', 'backend')
  }

  // When the user's access token exists and is valid, and the route is the login page, redirect directly to the homepage
  if (store.state.account.access_token && to.path === '/signin') {
    next({
      path: "/"
    })
  }

  /**
   * Main logic for route authentication
   */
  if (to.matched.some(record => record.meta.requiresAuth)) {
    const access_token = store.state.account.access_token;
    // Determine if the user is logged in through access_token
    if (!access_token) {
      next({
        path: '/signin'
      })
    } else {
      next()
    }
  } else {
    next()
  }
});

export default router;

Page Layout

Since the layouts of the login page and backend page are different and cannot be reused, to avoid re-rendering common modules such as menus, top navigation bars, and other components after login and route navigation, we need to implement logic in App.vue to dynamically switch page layouts based on the user’s login status.

<template>
  <div id="app">
    <component :is="layout.current"></component>
  </div>
</template>

<script>
  import {mapState} from 'vuex'
  import Guest from './layouts/Guest'
  import Backend from './layouts/Backend'

  export default {
    name: 'app',
    data() {
      return {
      }
    },
    methods: {},
    components: {
      guest: Guest,
      backend: Backend
    },
    computed: {
      ...mapState({
        layout: state => state.system.layout,
        profile: state => state.account.profile,
        access_token: state => state.account.access_token
      })
    },
    watch: {
      // Watch for user information changes to switch page layouts
      profile(value) {
        if (value.id === '') {
          this.$store.commit('SET_LAYOUT_CURRENT', 'guest');
        } else {
          this.$store.commit('SET_LAYOUT_CURRENT', 'backend');
        }
      },
      /**
       * When the access_token state changes
       * it means the user login information verification is successful and the access_token has been obtained
       * at this point, call the fetchProfile action to get user information
       */
      access_token(value) {
        if (value == null) {
          window.console.log(value);
        }
      },
      '$route' (to, from) {
        if (from.path === '/' && to.name === 'notfound') {
          this.$store.commit('SET_LAYOUT_CURRENT', 'guest');
        }

        if (from.path === '/') {
          if (to.name !== 'notfound') {
            this.$store.commit('SET_LAYOUT_CURRENT', 'backend');
          }
        }

        if (to.name === 'signin') {
          this.$store.commit('SET_LAYOUT_CURRENT', 'guest');
        }

        if (from.name === 'notfound') {
          this.$store.commit('SET_MENU_ACTIVE', to.path);
        }

        this.$store.commit('SET_MENU_ACTIVE', to.path);
      }
    }
  }
</script>

<style lang="scss">

</style>

At this point, the frontend logic related to user authentication has been implemented. All that remains is to focus on writing business logic!

I hope this is helpful, Happy hacking…