【Vue】Vue小项目记录1:初始化及登录验证

环境前提:

  1. 安装node:控制台输入 node -v,如果出现相应的版本号,则说明安装成功。
  2. 安装淘宝镜像:npm install -g cnpm --registry=https://registry.npm.taobao.org
  3. 安装webpacknpm install webpack@3.8.1 -gwebpack -v查看版本号
  4. 安装vue-cli脚手架构建工具:npm install vue-cli -g
    安装完成之后输入 vue -V(注意这里是大写的“V”),如果出现相应的版本号,则说明安装成功。

1. 初始化项目:

vue init webpack [项目名]
创建一个基于 webpack 模板的新项目,输入命令: vue init webpack my_vue ,my_vue是文件夹的名字。项目初始化及环境配置见另一篇博文 nvm安装配置/node安装配置/vue项目初始化

2. 项目目录

项目目录(在自动生成目录基础上自行修改):

projectName
├── build                      // 构建相关  
├── config                     // 配置相关
├── src                        // 源代码
│   ├── api                    // 所有请求
│   ├── assets                 // 主题 字体等静态资源
|		└──img				   // 项目所有图片子u按
│   ├── components             // 全局公用组件
│   ├── directive              // 全局指令
│   ├── filtres                // 全局 filter               
│   ├── lang                   // 国际化 language
│   ├── mock                   // 项目mock 模拟数据
│   ├── router                 // 路由
│   ├── store                  // 全局 store管理
│   ├── styles                 // 全局样式
│   ├── utils                  // 全局公用方法
│   ├── pages                  // 页面
│   ├── App.vue                // 入口页面
│   ├── main.js                // 入口 加载组件 初始化等
│   └── permission.js          // 权限管理
├── static                     // 第三方不打包资源
├── .babelrc                   // babel-loader 配置
├── eslintrc.js                // eslint 配置项
├── .gitignore                 // git 忽略项
├── index.html                 // html模板
└── package.json               // package.json

在pages问价夹下新建login.vue文件并随便输入内容,修改router指向后,运行,看是否可以正确加载。

3. 基础配置:

3.1 安装element-UI

安装:npm install element-ui

引入:

// main.js
import ElementUI from 'element-ui'
Vue.use(ElementUI, { size: 'mini'});
import('element-ui/lib/theme-chalk/index.css')

3.2 Vuex

使用vuex进行数据管理

录token,用户信息、全局个人偏好设置等用vuex管理

安装vuex:npm install vuex --save
引入:

// main.js
import store from './store/'

new Vue({
   el: '#app',
   store,
   components: { App },
   template: '<App/>'
})

3.3 封装 axios

安装axios : npm install --save axios

// src/utils/axios.js

import axios from 'axios'
import { Message, MessageBox } from 'element-ui'
import store from '../store'
import { getToken } from '@/utils/auth'

// 创建axios实例
let service =  axios.create({
  baseURL: process.env.BASE_API, // api的base_url
  timeout: 5000 // 请求超时时间
})
// request拦截器
service.interceptors.request.use(config => {
  if (store.getters.token) {
    config.headers = {
      'Authorization' : "Token " + getToken('Token'), //携带权限参数
     };
  }
  return config
}, error => {
  Promise.reject(error)
})

// respone拦截器
service.interceptors.response.use(
  response => {
   /**
    * code:200,接口正常返回;
    */
    const res = response.data
    if (res.code !== 200) {
      Message({
        message: res.message,
        type: 'error',
        duration: 5 * 1000
      })
      // 根据服务端约定的状态码:5001:非法的token; 5002:其他客户端登录了; 5004:Token 过期了;
      if (res.code === 5001 || res.code === 5002 || res.code === 5004) {
          MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
            confirmButtonText: '重新登录',
            cancelButtonText: '取消',
            type: 'warning'
          }).then(() => {
            store.dispatch('LogOut').then(() => {
              location.reload()// 为了重新实例化vue-router对象 避免bug
            })
          })
      }
      return Promise.reject('error')
    } else { // res.code === 200,正常返回数据
        return response.data
    }
  },
  error => {
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

export default service

3.4 封装使用 阿里icon插件

index.html中,引用

 <script src="//at.alicdn.com/t/font_1258069_e40c6mwl0x8.js"></script>

创建 icon-component 组件

// components/iconSvg/IconSvg.vue
<template>
  <svg class="svg-icon" aria-hidden="true">
    <use :xlink:href="iconName"></use>
  </svg>
</template>
<script>
export default {
  name: 'icon-svg',
  props: {
    iconClass: {
      type: String,
      required: true
    }
  },
  computed: {
    iconName() {
      return `#${this.iconClass}`
    }
  }
}
</script>

<style>
.svg-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}
</style>
// components/iconSvg/index.js

import Vue from 'vue'
import IconSvg from './IconSvg'
//全局注册icon-svg
Vue.component('icon-svg', IconSvg)

main.js中引入

import './components/iconSvg' // iconSvg

3.5 Cookies封装

登录验证 采取方式为cookie验证 , 在utils文件夹下新建auth.js,用来对cookie进行操作

安装:npm install --save js-cookie

auth.js

// utils/auth.js
// cookies的获取、设置、移除
import Cookies from 'js-cookie'

export function getToken(TokenKey) {
  return Cookies.get(TokenKey)
}

export function setToken(TokenKey,token) {
  return Cookies.set(TokenKey, token)
}

export function removeToken(TokenKey) {
  return Cookies.remove(TokenKey)
}

4. 登录

思路:针对前后端完全分离的情况下,实现对Vue的Token登录验证,大致思路如下:

  • 点击登录后,前端调后端的登陆接口,发送用户名和密码;
  • 后端收到请求,验证用户名和密码,在验证成功返回token;
  • 前端拿到token,将token存储到localStorage和vuex中,并跳转路由页面
  • 前端每次跳转路由,就判断 localStroage 中有无 token ,没有就跳转到登录页面,有则跳转到对应路由页面
  • 每次调后端接口,都要在请求头中加token,后端判断请求头中有无token:有token,就拿到token并验证token,验证成功就返回数据,验证失败就返回401;无token也返回401。
  • 前端拿到状态码为401,就清除token信息并跳转到登录页面

4.1 mockjs模拟后台用户信息:

mockjs/user.js

import Mock from 'mockjs'

let List = []
const count = 1000
let typelist = ['联通', '移动', '电信', '铁通']
for (let i = 0; i < count; i++) {
  List.push(Mock.mock({
    id: Mock.Random.guid(),
    sortnum: i + 1,
    username: Mock.Random.cname(),
    address: Mock.mock('@county(true)'),
    createTime: Mock.Random.datetime(),
    updateTime: Mock.Random.now(),
    ip: Mock.mock('@ip'),
    region: Mock.mock('@region'),
    areaId: /\d{7}/,
    email: Mock.Random.email(),
    'isp|1': typelist
  }))
}

let users = {
  code:0,
  data:[
    {
      id:1,
      usertitle: '管理员',
      username: 'admin',
      password: '123456',
      roles: ['admin'],
      token: 'admin',
      avatar: '../../static/img/avatar.png'
    },{
      id:2,
      usertitle: '编辑',
      username: 'editor',
      password: '112233',
      roles: ['editor'],
      token: 'editor',
      avatar:'../../static/img/avatar.png'
    },
  ]
}

export default {
  // 用户登录
  login: config => {
    let data = JSON.parse(config.body);

    let flag = false;
    let localToken='';
    let localName='';

    for(let value of users.data){
      if((data.username === value.username)&&(data.username === value.username)){
        console.log('密码正确')
        flag = !flag
        localToken = value.token
        localName = value.username
      }
    }
    if(flag){
      flag = !flag
      return {
        code: 200,
        data: {
          userInfo: {
            token: localToken,
            name: localName,
          },
          status: 'success'
        }
      }    
    }else {
      return {
        code: -1,
        data: {
          msg: "用户名错误",
          status: 'fail'
        }
      }
    }
  },
  // 用户登出
  logout: config => {
    return {
      code: 200,
      data: {
        userList: ""
      }
    }
  },
  // 获取登录用户信息
  getUserInfo: config => {
    let data = JSON.parse(config.body);
    let flag = false;
    let localroles='';
    let localName='';
    let localAvatar='';

      
    for(let value of users.data){
      if (data.token === value.token) {
        console.log('密码正确')
        flag = !flag
        localroles = value.roles;
        localName = value.username;
        localAvatar = value.avatar;
      }
    }

    if(flag){
      flag = !flag
      return {
        code: 200,
        data: {
          userInfo: {
            roles: localroles,
            name: localName,
            avatar: localAvatar,
          }
        }
      }
    }else {
      console.log('data',data)
      console.log('value',value)
      console.log('后台验证到token过期')
      return {
        code: 401,
        data: {
          userInfo: {
          }
        }
      }
    }
  },

  /**
   * 获取用户列表
   * 要带参数 name, page, limt; name可以不填, page,limit有默认值。
   * @param name, page, limit
   * @return {{code: number, count: number, data: *[]}}
   */
  getUserList: config => {
    const {
      limit,
      page
    } = JSON.parse(config.body);
    let mockList = List;
    const userList = mockList.filter((item, index) => index < limit * page && index >= limit * (page - 1))
    return {
      code: 200,
      data: {
        total: mockList.length,
        userList: userList
      }
    }
  }
}

4.2 axios封装

调用axios获取mockjs模拟数据。

// api/user.js 
import request from '@/utils/axios'

export function login(params) {
  return request({
    url: '/user/login',
    method: 'get',
    data:params
  })
}
export function logout(params) {
  return request({
    url: '/user/logout',
    method: 'get',
    data:params
  })
}

4.3 登录验证

提交用户账号和密码,向后端验证通过之后,返回用户唯一标识的token(mockjs/user.js中模拟设置),拿到token之后,将这个token存贮到cookie中,保证刷新页面后能记住用户登录状态。然后根据token再去拉取一个 user_info 的接口来获取用户的详细信息(如用户权限,用户名等等信息)。

// login.vue
submitForm(loginForm) {
    this.$refs[loginForm].validate((valid) => {
        if (valid) {
            let userinfo = this.loginForm;
            window.console.log("this.loginForm",userinfo)

            login(userinfo).then(res => {
                const data = res.data
                if(res.code == 200){
                    console.log('登录成功')
                    let userInfo = data.userInfo;
                    // token存贮到cookie
                    setToken("Token",userInfo.token)
                    this.$router.push({ path: '/' })
                    this.$store.dispatch('initLeftMenu'); //设置左边菜单始终为展开状态
                }else if(data.code == -1){
                    alert(res.data.msg)
                }else{
                    console.log('登录失败') 
                }
            })
        }
    });
},

存储用户信息到vuex

// store/user.js
import { getStore } from '@/utils/mUtils'
import { getUserInfo } from '@/api/user'  // 导入用户信息相关接口
import { getToken, setToken, removeToken } from '@/utils/auth'

const user  = {
    state : {
        name:'',
        avatar:'',
        token: getToken('Token'),
        roles: [],
        browserHeaderTitle: getStore('browserHeaderTitle') || 'ColdCoder'
    },
    getters : {
        token: state => state.token,
        roles: state => state.roles,
        avatar: state => state.avatar,
        name: state => state.name,
        browserHeaderTitle: state => state.browserHeaderTitle,
    },
    mutations: {
        SET_ROLES: (state, roles) => {
            state.roles = roles
        },
        SET_BROWSERHEADERTITLE: (state, action) => {
            state.browserHeaderTitle = action.browserHeaderTitle
        },
        SET_NAME: (state, name) => {
        state.name = name
        },
        SET_AVATAR: (state, avatar) => {
        state.avatar = avatar
        }
    },
    ......
}

4.4 获取用户信息及权限判断

用户登录成功之后,我们会在全局钩子router.beforeEach中拦截路由,判断是否已获得token,在获得token后,通过 token 获取用户的 role ,动态根据用户的 role 动态生成相应权限的路由表,再通过router.addRoutes动态挂载路由,这属于前端页面级的权限控制。

// src/permission.js
 router.beforeEach((to, from, next) => {
  // 加载进度条
  NProgress.start()
  // 设置浏览器头部标题
  const browserHeaderTitle = to.meta.title
  store.commit('SET_BROWSERHEADERTITLE', {
    browserHeaderTitle: browserHeaderTitle
  })

  // 点击登录时,拿到了token并存入了cookie,保证页面刷新时,始终可以拿到token
  if (getToken('Token')) {
    if (to.path === '/login') {
      next({
        path: '/'
      })
      NProgress.done()
    } else {
      // 用户登录成功之后,每次点击路由都进行了角色的判断;
      if (store.getters.roles.length === 0) {
        let token = getToken('Token');
        getUserInfo({"token": token}).then().then(res => { // 根据token拉取用户信息
          if (res.code == 401){
            store.dispatch('LogOut').then(() => {
              Message.error(err || 'Token was  expired, please login again')
              next({ path: '/' })
            })
          }
          if (res.code == 200) {
            let userList = res.data.userInfo;
            console.log('(userList.avatar',userList.avatar)
            store.commit("SET_ROLES", userList.roles);
            store.commit("SET_NAME", userList.name);
            store.commit("SET_AVATAR", userList.avatar);
            store.dispatch('GenerateRoutes', {
              "roles": userList.roles
            }).then(() => { // 根据roles权限生成可访问的路由表
              router.addRoutes(store.getters.addRouters) // 动态添加可访问权限路由表
              next({
                ...to,
                replace: true
              }) // hack方法 确保addRoutes已完成
            })
          }
        }).catch((err) => {
          store.dispatch('LogOut').then(() => {
            Message.error(err || 'Verification failed, please login again')
            next({ path: '/' })
          })
        })
      } else {
        // 没有动态改变权限的需求可直接next() 删除下方权限判断
        next()
      }
    }
  } else {
    if (whiteList.indexOf(to.path) !== -1) {
      // 点击退出时,会定位到这里
      next()
    } else {
      next('/login')
      NProgress.done()
    }
  }
})

4.5 路由控制及动态路由生成

根据不同角色登录后台返回权限以及目录信息,动态的生成路由以及菜单目录,设计思路如下:

  • 路由配置
    将路由分为两个部分:权限路由、非权限路由。初始化时,将非权限路由对象赋值给Router;同时设置权限路由的中meta:{roles:[‘admin’,‘editor’]},标识对应用户的权限。
  • 全局前置守卫中动态添加路由,校验页面权限。用户登录成功之后返回的roles值,进行路由的匹配并生成新的路由对象,调用router.addRoutes(store.getters.addRouters)添加用户可访问的路由,用户成功登录并跳转到首页时,使用vuex管理路由表,根据vuex中可访问的路由渲染侧边栏组件。
store/modules/permission.js
用户是否有roles信息,进行不同的业务逻辑:
- 初始情况下,用户roles信息为空:通过getUserInfo()函数,根据token拉取用户信息;并通过store将该用户roles,name,avatar信息存储于vuex;通过store.dispatch('GenerateRoutes', { roles })去重新过滤和生成路由,通过router.addRoutes()合并路由表;如果在获取用户信息接口时出现错误,则调取store.dispatch('LogOut')接口,返回到login页面。
- 用户已经拥有roles信息:点击页面路由,通过roles权限判断 hasPermission()。如果用户有该路由权限,直接跳转对应的页面;如果没有权限,则跳转至401提示页面;

// store/modules/permission.js
import { asyncRouterMap, constantRouterMap } from '@/router'
import * as mutils from '@/utils/mUtils'

/**
 * 通过meta.role判断是否与当前用户权限匹配
 * @param roles
 * @param route
 */
function hasPermission(roles, route) {
  // roles为权限身份数组
  if (route.meta && route.meta.roles) {
    return roles.some(role => route.meta.roles.indexOf(role) >= 0)
  } else {
    return true
  }
}

/**
 * 递归过滤异步路由表,返回符合用户角色权限的路由表
 * @param asyncRouterMap
 * @param roles
 */
function filterAsyncRouter(asyncRouterMap, roles) {
  // 返回满足条件的子路由对象
  const accessedRouters = asyncRouterMap.filter(route => {
    if (hasPermission(roles, route)) {
      if (route.children && route.children.length) {
        // route.children重新过滤赋值;
        route.children = filterAsyncRouter(route.children, roles)
      }
      return true // 返回该权限路由对象;
    }
    return false
  })
  return accessedRouters
}

const permission = {
  state: {
    routers: constantRouterMap,
    addRouters: [],
    topRouters:[],
    topTitle:'',
    menuIndex:0
  },
  getters:{
    permission_routers: state => state.routers, // 所有路由
    addRouters: state => state.addRouters,  // 权限过滤路由
    // topRouters: state => state.topRouters,  // 顶部三级路由
    topTitle:state => state.topTitle, // 顶部的title
    menuIndex:state => state.menuIndex, // 顶部菜单的index
  },
  mutations: {
    SET_ROUTERS: (state, routers) => {
      state.addRouters = routers // 权限路由
      state.routers = constantRouterMap.concat(routers) // 总路由
    },
    CLICK_INNER_LEFT_MENU:(state,data) => { // titleList:arr
        state.topRouters = data.titleList;
    },
  },
  actions: {
    // 根据角色,重新设置权限路由;并保存到vuex中,SET_ROUTERS;
    GenerateRoutes({ commit }, data) {
      return new Promise(resolve => {
        let roles = data.roles;
        window.console.log(data)
        let accessedRouters = '';
        if (roles.indexOf('admin') >= 0) {
          // 如果是管理员,直接将权限路由赋值给新路由;
          accessedRouters = asyncRouterMap
        } else {
        //   // 非管理员用户,如roles:['editor','developer'],则需要过滤权限路由数据
          accessedRouters = filterAsyncRouter(asyncRouterMap, roles)
        }
        commit('SET_ROUTERS', accessedRouters)
        resolve()
      })
    },
    ClickLeftInnerMenu({ commit },data) {
      commit('CLICK_INNER_LEFT_MENU',data)
    },
    // ClickTopMenu({ commit },data) {
    //   commit('CLICK_TOP_MENU',data)
    // }
  }
}
export default permission

前端路由控制router.js
// router.js
import Vue from 'vue'
import Router from 'vue-router'
import { Layout} from "../layout"; // 页面整体布局
// import { topRouterMap } from "./topRouter";

process.env.NODE_ENV === "development" ? Vue.use(Router) : null;

//手动跳转的页面白名单
const whiteList = [
  '/'
];

//默认不需要权限的页面
export const constantRouterMap = [{
    path: '',
    component: Layout,
    redirect: '/index/index',
    hidden: true
  },
  {
    path: '/login',
    name: 'login',
    component: () => import('@/pages/login'),
    hidden: true
  },
  {
    path: '/404',
    component: () => import('@/pages/errorPage/404'),
    hidden: true
  },
  {
    path: '/401',
    component: () => import('@/pages/errorPage/401'),
    hidden: true
  },
  {
    path: '/index',
    name: 'index',
    component: Layout,
    meta: {
      title: '首页',
      icon: 'icondashboard',
    },
    noDropdown: true,
    children: [{
      path: 'index',
      meta: {
        title: '首页',
        icon: 'icondashboard',
        routerType: 'leftmenu'
      },
      component: () => import('@/pages/index/index'),
    }]
  }
]

//注册路由
export default new Router({
  mode: 'history', // 默认为'hash'模式
  base: '/', // 添加根目录,对应服务器部署子目录
  routes: constantRouterMap
})

//异步路由(需要权限的页面)
export const asyncRouterMap = [
  {
    path: '/error',
    component: Layout,
    name: 'errorPage',
    meta: {
      title: '错误页面',
      icon: 'iconError'
    },
    children: [{
        path: '401',
        name: 'page401',
        component: () => import('@/pages/errorPage/401'),
        meta: {
          title: '401',
          noCache: true
        }
      },
      {
        path: '404',
        name: 'page404',
        component: () => import('@/pages/errorPage/404'),
        meta: {
          title: '404',
          noCache: true
        }
      }
    ]
  },
  {
    path: '*',
    redirect: '/404',
    hidden: true
  }
];

注意事项404 页面一定要在异步路由的最后加载,如果放在constantRouterMap一同声明404,页面都会被拦截到404.

代码地址

项目地址

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值