尚品汇-登录注册流程


前台项目

注册

首先是静态页面:

src/pages/Register/index.vue

<template>
  <div class="register-container">
    <!-- 注册内容 -->
    <div class="register">
      <h3>
        注册新用户
        <span class="go"
          >我有账号,去 <a href="login.html" target="_blank">登陆</a>
        </span>
      </h3>
      <div class="content">
        <label>手机号:</label>
        <input
          placeholder="请输入你的手机号"
          v-model="phone"
          name="phone"
          v-validate="{ required: true, regex: /^1\d{10}$/ }"
          :class="{ invalid: errors.has('phone') }"
        />
        <span class="error-msg">{{ errors.first("phone") }}</span>
      </div>
      <div class="content">
        <label>验证码:</label>
        <input
          placeholder="请输入验证码"
          v-model="code"
          name="code"
          v-validate="{ required: true, regex: /^\d{6}$/ }"
          :class="{ invalid: errors.has('code') }"
        />
        <button
          style="width: 100px; height: 38px; margin-left: 5px"
          @click="getCode"
        >
          获取验证码
        </button>
        <span class="error-msg">{{ errors.first("code") }}</span>
      </div>
      <div class="content">
        <label>登录密码:</label>
        <input
          placeholder="请输入登录密码"
          v-model="password"
          name="password"
          v-validate="{ required: true, regex: /^[0-9A-Za-z]{8,20}$/ }"
          :class="{ invalid: errors.has('password') }"
        />
        <span class="error-msg">{{ errors.first("password") }}</span>
      </div>
      <div class="content">
        <label>确认密码:</label>
        <input
          placeholder="请输入确认密码"
          v-model="password1"
          name="password1"
          v-validate="{ required: true, is: password }"
          :class="{ invalid: errors.has('password1') }"
        />
        <span class="error-msg">{{ errors.first("password1") }}</span>
      </div>
      <div class="controls">
        <input
          type="checkbox"
          name="agree"
          :checked="agree"
          v-validate="{
            required: true,
            agree: true,
          }"
          :class="{ invalid: errors.has('agree') }"
        />
        <span>同意协议并注册《尚品汇用户协议》</span>
        <span class="error-msg">{{ errors.first("agree") }}</span>
      </div>
      <div class="btn">
        <button @click="userRegister">完成注册</button>
      </div>
    </div>

    <!-- 底部 -->
    <div class="copyright">
      <ul>
        <li>关于我们</li>
        <li>联系我们</li>
        <li>联系客服</li>
        <li>商家入驻</li>
        <li>营销中心</li>
        <li>手机尚品汇</li>
        <li>销售联盟</li>
        <li>尚品汇社区</li>
      </ul>
      <div class="address">地址:北京市昌平区宏福科技园综合楼6层</div>
      <div class="beian">京ICP备19006430号</div>
    </div>
  </div>
</template>

<script>
export default {
  name: "Register",
  data() {
    return {
      phone: "",
      // 验证码
      code: "",
      // 密码
      password: "",
      // 确认密码
      password1: "",
      // 是否同意
      agree: true,
    };
  },
  methods: {
    async getCode() {
      try {
        const { phone } = this;
        phone && (await this.$store.dispatch("getCode", this.phone));
        this.code = this.$store.state.user.code;
      } catch (error) {
        alert(error.message);
      }
    },
    async userRegister() {
      const success = await this.$validator.validateAll();
      if (!success) {
        return;
      }
      const { phone, code, password, password1 } = this;
      try {
        await this.$store.dispatch("userRegister", { phone, code, password });
        // 注册成功,跳转登录页面
        this.$router.push("/login");
      } catch (error) {
        alert(error.message);
      }
    },
  },
};
</script>

这里表单验证采用的是VeeValidate依赖(仅作了解),在main.js中引入src/plugins/validate.js文件:

import Vue from "vue";
import VeeValidate from "vee-validate";
import zh_CN from "vee-validate/dist/locale/zh_CN";
Vue.use(VeeValidate);

// 表单验证
VeeValidate.Validator.localize("zh_CN", {
  messages: {
    ...zh_CN.messages,
    is: (field) => `${field}必须与密码相同`,
  },
  attributes: {
    phone: "手机号",
    code: "验证码",
    password: "密码",
    password1: "确认密码",
    agree: "协议",
  },
});

// 自定义校验规则
VeeValidate.Validator.extend("agree", {
  validate: (value) => {
    return value;
  },
  getMessage: (field) => field + "必须同意",
});

那么我们再看看这两个方法:getCode()userRegister()

getCode()

async getCode() {
      try {
        const { phone } = this;
        phone && (await this.$store.dispatch("getCode", this.phone));
        this.code = this.$store.state.user.code;
      } catch (error) {
        alert(error.message);
      }
    },

这段代码的关键:

  1. 获取并判断变量phone是否存在,
  2. 使用await关键字等待this.$store.dispatch("getCode", this.phone)函数的执行结果,该函数会将手机号码作为参数传递给名为getCodeaction,用于获取验证码。
  3. this.$store.state.user.code的值赋给变量code,该变量用于存储获取到的验证码。

我们去store中看看getCode做了什么:

let state = {
     //验证码
     code: '',
};
let actions = {
     //获取验证码
     async getCode({ commit, state, dispatch }, phone) {
          let result = await reqGetCode(phone);
         //获取验证码接口
//export const reqGetCode = (phone)=>requests({url:`/user/passport/sendCode/${phone}`,method:'get'});
          if (result.code == 200) {
               commit('GETCODE', result.data);
               return 'ok';
          } else {
               return Promise.reject();
          }
     }
     }
let mutations = {
     GETCODE(state, code) {
          state.code = code;
     },
};

userRegister()

async register() {
      //解构出参数
      const { phone, code, password, password1 } = this;
      //目前不做表单验证
      if (phone && code && password == password1) {
        //通知vuex发请求,进行用户的注册
        try {
          //注册成功
          await this.$store.dispatch("registerUser", { phone, code, password });
          //让用户跳转到登录页面进行登录
          this.$router.push('/login');
        } catch (error) {
          //注册失败
          alert(error.message);
        }
      }
    },

代码很简单,就是解构对象与发起注册请求,注册成功后跳转登录页面。

我们去store中看看registerUser做了什么:

//注册用户的地方
    async registerUser({ commit, state, dispatch }, obj) {
         //注册接口没有返回data,不需要提交mutation
         let result = await reqRegister(obj);
         //export const reqRegister = (data)=>requests({url:`/user/passport/register`,method:'post',data});
         if (result.code == 200) {
              //注册成功
              return 'ok';
         } else {
              //注册失败
              return Promise.reject(new Error(result.message));
         }
    }

登录

src/pages/Login/index.vue

<template>
  <div class="login-container">
    <!-- 登录 -->
    <div class="login-wrap">
      <div class="login">
        <div class="loginform">
          <ul class="tab clearFix">
            <li>
              <a href="##" style="border-right: 0">扫描登录</a>
            </li>
            <li>
              <a href="##" class="current">账户登录</a>
            </li>
          </ul>

          <div class="content">
            <!-- 登录密码与账号输入的地方 -->
            <form action="##">
              <div class="input-text clearFix">
                <span></span>
                <input
                  type="text"
                  placeholder="邮箱/用户名/手机号"
                  v-model="phone"
                />
              </div>
              <div class="input-text clearFix">
                <span class="pwd"></span>
                <input
                  type="text"
                  placeholder="请输入密码"
                  v-model="password"
                />
              </div>
              <div class="setting clearFix">
                <label class="checkbox inline">
                  <input name="m1" type="checkbox" value="2" checked="" />
                  自动登录
                </label>
                <span class="forget">忘记密码?</span>
              </div>
              <!-- 
                电脑登录按钮:会触发form表单默认行为
                stop:阻止事件的传播
                prevent:阻止默认事件
                once:事件仅仅触发一次
              -->
              <button class="btn" @click.prevent="login">
                登&nbsp;&nbsp;录
              </button>
            </form>

            <div class="call clearFix">
              <ul>
                <li><img src="./images/qq.png" alt="" /></li>
                <li><img src="./images/sina.png" alt="" /></li>
                <li><img src="./images/ali.png" alt="" /></li>
                <li><img src="./images/weixin.png" alt="" /></li>
              </ul>
              <router-link class="register" to="/register"
                >立即注册</router-link
              >
            </div>
          </div>
        </div>
      </div>
    </div>
    <!-- 底部 -->
    <div class="copyright">
      <ul>
        <li>关于我们</li>
        <li>联系我们</li>
        <li>联系客服</li>
        <li>商家入驻</li>
        <li>营销中心</li>
        <li>手机尚品汇</li>
        <li>销售联盟</li>
        <li>尚品汇社区</li>
      </ul>
      <div class="address">地址:北京市昌平区宏福科技园综合楼6层</div>
      <div class="beian">京ICP备19006430号</div>
    </div>
  </div>
</template>

<script>
export default {
  name: "Login",
  data() {
    return {
      //收集账号与密码
      phone: "",
      password: "",
    };
  },
  methods: {
    //登录按钮
    async login() {
      //整理参数
      const { phone, password } = this;
      //在发登录请求
      try {
        //登录成功
        await this.$store.dispatch("userLogin", { phone, password });

        let goPath = this.$route.query.redirect||'/home';
        //跳转到首页
        this.$router.push(goPath);
      } catch (error) {
        alert(error.message);
      }
    },
  },
};
</script>

store中的登录操作比较特殊:

//登录、注册模块的小仓库
import { reqGetCode, reqRegister, reqUserLogin, reqUserInfo, reqUserLogout } from '@/api';
let state = {
     //验证码
     code: '',
     //身份标识符很重要【存储在vuex】
     token: localStorage.getItem("TOKEN"),
     //用户名
     nickName: ''
};
let mutations = {
     GETCODE(state, code) {
          state.code = code;
     },
     SET_TOKEN(state, token) {
          state.token = token;
     },
     SET_USERINFO(state, nickName) {
          state.nickName = nickName;
     },
     CLEAR(state) {
          //清除仓库相关用户信息
          state.token = '';
          state.nickName = '';
          //本地存储令牌清空
          localStorage.removeItem('TOKEN');
     }
};
let actions = {

     //用户登录的地方:非常非常重要
     async userLogin({ commit, state, dispatch }, data) {
          /*
            举例子
             {
                   code:200,
                   data:{
                        token:'1e4vdadhajkhdakj6sahdajk'
                   },
                   message:'登录成功'
             }
          */
          let result = await reqUserLogin(data);
          //登录成功
          if (result.code == 200) {
               commit('SET_TOKEN', result.data.token);
               //以后开发的时候:经常的登录的成功获取token【持久化存储】
               localStorage.setItem('TOKEN', result.data.token);
               return 'ok';
          } else {
               //登录失败
               return Promise.reject(new Error(result.message));
          }

     }
     ,
     //获取用户信息
     async getUserInfo({ commit, state, dispatch }) {
          let result = await reqUserInfo();
          if (result.code == 200) {
               commit('SET_USERINFO', result.data.nickName);
               return 'ok';
          } else {
               return Promise.reject();
          }
     },
     //退出登录的业务
     async logout({ commit, state, dispatch }) {
          //发请求通知服务器销毁当前token
          let result = await reqUserLogout();
          if (result.code == 200) {
               commit('CLEAR');
               return 'ok';
          } else {
               return Promise.reject(new Error(result.message));
          }
     }
};
let getters = {};

//对外暴露
export default {
     state,
     mutations,
     actions,
     getters
}

但是登录功能最重要的还是权限控制,即部分页面在未登录状态下是不可以被访问的,这里我们可以使用全局路由守卫实现:

//全局守卫:只要项目中有任何路由变化,全局守卫都会进行拦截【符合条件走你,不符合条件不能访问】

//全局守卫:全局前置守卫【访问之前进行触发】

//全局前置守卫
router.beforeEach(async (to, from, next) => {
    //to:去的那个路由的信息
    //from:从那个路由而来的信息
    //next:放行函数!!!!!! 
    //第一种:next(),放行函数,放行到它想去的路由!!!
    //第二种:next(path),守卫指定放行到那个路由去?

    //用户是否登录:取决于仓库里面是否有token!!!
    //每一次路由跳转之前需要用有用户信息在跳转,没有发请求获取用户信息在跳转!!!!
    //token
    let hasToken = store.state.user.token;
    //用户信息
    let hasNickName = store.state.user.nickName;
    //用户登录
    if (hasToken) {
        //用户登录了,不能去login
        if (to.path == "/login") {
            next('/home');
        } else {
            //用户登陆了(拥有token),而且还有用户信息【去的并非是login】
            if (hasNickName) {
                next();
            } else {
                //用户登陆了(拥有token),但是没有用户信息 
                try {
                    //发请求获取用户信息以后在放行
                    await store.dispatch('getUserInfo');
                    next();
                } catch (error) {
                    //用户没有信息,还携带token发请求获取用户信息【失败】
                    //token【学生证失效了】
                    //token失效:本地清空数据、服务器的token通知服务器清除
                    await store.dispatch('logout');
                    //回到登录页,重新获取一个新的学生证
                    next('/login');
                }
            }
        }
    } else {
        //用户未登录||目前的判断都是放行.将来这里会'回手掏'增加一些判断
        //用户未登录:不能进入/trade、/pay、/paysuccess、/center、/center/myorder  /center/teamorder
        let toPath = to.path;
        if (toPath.indexOf('trade') != -1 || toPath.indexOf('pay') != -1 || toPath.indexOf('center') != -1) {
            next('/login?redirect='+toPath);
        } else {
            next();
        }
    }
});

需要判断的情况有以下几种:

  1. 用户已登录(有token)且有用户信息:直接放行到目标路由。

  2. 用户已登录(有token)但没有用户信息:发起请求获取用户信息,如果获取成功则放行到目标路由,如果获取失败则清空本地数据、通知服务器清除token,并重定向到登录页。

    那么为什么用户已登录但没有用户信息这种情况会发生呢?

    • 可能是由于网络连接问题或服务器端错误导致的。
    • 可能是由于服务器端的操作或其他因素导致用户信息被删除或清除。
    • 这可能是由于恶意攻击或其他安全问题导致用户信息被修改或篡改。
  3. 用户未登录且目标路由是需要登录才能访问的页面(如tradepaycenter等):重定向到登录页并携带重定向参数。

  4. 用户未登录且目标路由不是需要登录才能访问的页面:直接放行到目标路由。

后台项目

登录

src/views/login/index.vue

<template>
  <div class="login-container">
    <!-- el-form组件:elementUI插件里面的一个组件,经常展示表单元素  model:用于收集表单数据  rules:表单验证规则-->
    <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left">

      <div class="title-container">
        <h3 class="title">登录</h3>
      </div>

      <el-form-item prop="username">
        <span class="svg-container">
          <svg-icon icon-class="user" />
        </span>
        <el-input
          ref="username"
          v-model="loginForm.username"
          placeholder="Username"
          name="username"
          type="text"
          tabindex="1"
          auto-complete="on"
        />
      </el-form-item>

      <el-form-item prop="password">
        <span class="svg-container">
          <svg-icon icon-class="password" />
        </span>
        <el-input
          :key="passwordType"
          ref="password"
          v-model="loginForm.password"
          :type="passwordType"
          placeholder="Password"
          name="password"
          tabindex="2"
          auto-complete="on"
          @keyup.enter.native="handleLogin"
        />
        <span class="show-pwd" @click="showPwd">
          <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
        </span>
      </el-form-item>

      <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">登录</el-button>

      <div class="tips">
        <span style="margin-right:20px;">username: admin</span>
        <span> password: any</span>
      </div>

    </el-form>
  </div>
</template>

<script>
import { validUsername } from '@/utils/validate'

export default {
  name: 'Login',
  data() {
    //先不用在意:这里面在进行表单验证,验证用户名与密码操作
    //回首在看这里
    const validateUsername = (rule, value, callback) => {
      if (!validUsername(value)) {
        callback(new Error('Please enter the correct user name'))
      } else {
        callback()
      }
    }
    const validatePassword = (rule, value, callback) => {
      if (value.length < 6) {
        callback(new Error('The password can not be less than 6 digits'))
      } else {
        callback()
      }
    }
    return {
      loginForm: {
        username: 'admin',
        password: '111111'
      },
      loginRules: {
        // username: [{ required: true, trigger: 'blur', validator: validateUsername }],
        // password: [{ required: true, trigger: 'blur', validator: validatePassword }]
      },
      loading: false,
      passwordType: 'password',
      redirect: undefined
    }
  },
  watch: {
    $route: {
      handler: function(route) {
        this.redirect = route.query && route.query.redirect
      },
      immediate: true
    }
  },
  methods: {
    showPwd() {
      if (this.passwordType === 'password') {
        this.passwordType = ''
      } else {
        this.passwordType = 'password'
      }
      this.$nextTick(() => {
        this.$refs.password.focus()
      })
    },
    //登录业务:发请求,带着用户名与密码给服务器(成功与失败)
    handleLogin() {
      //这里是在验证表单元素(用户名与密码)的是否符合规则
      this.$refs.loginForm.validate(valid => {
        //如果符合验证规则
        if (valid) {
          //按钮会有一个loading效果
          this.loading = true;
          //派发一个action:user/login,带着用户名与密码的载荷
          this.$store.dispatch('user/login', this.loginForm).then(() => {
            //登录成功进行路由的跳转
            this.$router.push({ path: this.redirect || '/' });
            //loading效果结束
            this.loading = false
          }).catch(() => {
            this.loading = false
          })
        } else {
          console.log('error submit!!')
          return false
        }
      })
    }
  }
}
</script>

我们逐行拆解一下:

  1. 登录页面使用了el-form组件,展示表单元素:

    • model:用于收集表单数据

      此处默认值为:

      loginForm: {
              username: 'admin',
              password: '111111'
            },

      同时在el-form-item中的el-input标签中:

      <el-input
                ref="username"
                v-model="loginForm.username"
                placeholder="Username"
                name="username"
                type="text"
                tabindex="1"
                auto-complete="on"
              />
      //- ref="username":给输入框设置一个引用名称,可以在代码中通过该引用名称访问和操作输入框。 
      //- v-model="loginForm.username":将输入框的值与Vue实例中的loginForm对象的username属性进行双向绑定,实现数据的同步更新。 
      //- placeholder="Username":设置输入框的占位符文本,当输入框为空时显示该文本。 
      //- name="username":设置输入框的名称,用于表单提交时标识该字段。 
      //- type="text":设置输入框的类型为文本输入。 
      //- tabindex="1":设置输入框的Tab键顺序,表示在表单中的焦点切换顺序。 
      //- auto-complete="on":启用输入框的自动完成功能。 
    • rules:表单验证规则

      import { validUsername } from '@/utils/validate'
      loginRules: {
              username: [{ required: true, trigger: 'blur', validator: validateUsername }],
              password: [{ required: true, trigger: 'blur', validator: validatePassword }]
            },
       
      //检验规则
      const validateUsername = (rule, value, callback) => {
            if (!validUsername(value)) {
              callback(new Error('Please enter the correct user name'))
            } else {
              callback()
            }
          }
          const validatePassword = (rule, value, callback) => {
            if (value.length < 6) {
              callback(new Error('The password can not be less than 6 digits'))
            } else {
              callback()
            }
          }

      其中validUsername规则单独引入

      /**
       * @param {string} str
       * @returns {Boolean}
       */
      export function validUsername(str) {
        const valid_map = ['admin', 'editor']
        return valid_map.indexOf(str.trim()) >= 0
      }
      //函数的主体部分是一行代码,它使用了数组的indexOf方法来检查给定的字符串是否在valid_map数组中。
      //首先,通过调用str.trim()方法去除字符串两端的空格,然后使用indexOf方法检查该字符串在valid_map数组中的索引位置。
      //如果索引大于等于0,说明该字符串在数组中存在,函数返回true,否则返回false。 
  2. 密码切换显示

    <el-form-item prop="password">
            <span class="svg-container">
              <svg-icon icon-class="password" />
            </span>
            <el-input
              :key="passwordType"
              ref="password"
              v-model="loginForm.password"
              :type="passwordType"
              placeholder="Password"
              name="password"
              tabindex="2"
              auto-complete="on"
              @keyup.enter.native="handleLogin"
            />
            <span class="show-pwd" @click="showPwd">
              <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
            </span>
          </el-form-item>
    /**
       - :key="passwordType":当密码类型(passwordType)发生变化时,重新渲染输入框。 
       - ref="password":给输入框设置一个引用,方便在代码中获取输入框的值。 
       - v-model="loginForm.password":将输入框的值绑定到Vue实例中的loginForm对象的password属性上。 
       - :type="passwordType":根据密码类型(passwordType)设置输入框的类型(password或text)。 
       - placeholder="Password":设置输入框的占位符为"Password"。 
       - name="password":设置输入框的名称为"password"。 
       - tabindex="2":设置输入框的tab索引为2。 
       - auto-complete="on":启用输入框的自动完成功能。 
       - @keyup.enter.native="handleLogin":当用户在输入框中按下回车键时,调用Vue实例中的handleLogin方法。
    */
     showPwd() {
          if (this.passwordType === 'password') {
            this.passwordType = ''
          } else {
            this.passwordType = 'password'
          }
          this.$nextTick(() => {
            this.$refs.password.focus()
          })
        },
    //首先,它会检查密码框的类型是否为"password"。如果是,则将其类型设置为空字符串,即将密码框的内容显示出来。如果不是,则将其类型设置为"password",即将密码框的内容隐藏起来。 然后,它会使用$nextTick方法来等待DOM更新完成后,再执行后续操作。在DOM更新完成后,它会使用$refs属性来获取名为"password"的元素,并调用其focus方法,将焦点设置在密码框上。
  3. 发起登录请求

    <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">登录</el-button>
    //登录业务:发请求,带着用户名与密码给服务器(成功与失败)
        handleLogin() {
          //这里是在验证表单元素(用户名与密码)的是否符合规则
          this.$refs.loginForm.validate(valid => {
            //如果符合验证规则
            if (valid) {
              //按钮会有一个loading效果
              this.loading = true;
              //派发一个action:user/login,带着用户名与密码的载荷
              this.$store.dispatch('user/login', this.loginForm).then(() => {
                //登录成功进行路由的跳转
                this.$router.push({ path: this.redirect || '/' });
                //loading效果结束
                this.loading = false
              }).catch(() => {
                this.loading = false
              })
            } else {
              console.log('error submit!!')
              return false
            }
          })
        }

    src\store\modules\user.js

    //actions
    const actions = {
      //这里在处理登录业务
      async login({ commit }, userInfo) {
        //解构出用户名与密码
        const { username, password } = userInfo;
        let result = await login({ username: username.trim(), password: password });
        if(result.code==20000){
          //vuex存储token
          commit('SET_TOKEN',result.data.token);
          //本地持久化存储token
          setToken(result.data.token);
          return 'ok';
        }else{
          return Promise.reject(new Error('faile'));
        }
      },
      }

    src\utils\auth.js

    import Cookies from 'js-cookie'
    
    const TokenKey = 'vue_admin_template_token'
    
    export function getToken() {
      return Cookies.get(TokenKey)
    }
    
    export function setToken(token) {
      
      return Cookies.set(TokenKey, token)
    }
    
    export function removeToken() {
      return Cookies.remove(TokenKey)
    }

    src\utils\request.js

    import axios from 'axios'
    import { MessageBox, Message } from 'element-ui'
    import store from '@/store'
    import { getToken } from '@/utils/auth'
    
    // create an axios instance
    const service = axios.create({
      baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
      // withCredentials: true, // send cookies when cross-domain requests
      timeout: 5000 // request timeout
    })
    
    //请求拦截器:携带的token字段
    service.interceptors.request.use(
      config => {
        // do something before request is sent
    
        if (store.getters.token) {
          // let each request carry token
          // ['X-Token'] is a custom headers key
          // please modify it according to the actual situation
          config.headers['token'] = getToken()
        }
        return config
      },
      error => {
        // do something with request error
        console.log(error) // for debug
        return Promise.reject(error)
      }
    )
    
    //响应拦截器
    service.interceptors.response.use(
      /**
       * If you want to get http information such as headers or status
       * Please return  response => response
      */
    
      /**
       * Determine the request status by custom code
       * Here is just an example
       * You can also judge the status by HTTP Status Code
       */
      response => {
        const res = response.data
    
        //服务器响应失败在干什么,因为咱们真实服务器返回code  20000也有可能200
        if (res.code !== 20000 && res.code!=200) {
          Message({
            message: res.message || 'Error',
            type: 'error',
            duration: 5 * 1000
          })
    
          // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
          if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
            // to re-login
            MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
              confirmButtonText: 'Re-Login',
              cancelButtonText: 'Cancel',
              type: 'warning'
            }).then(() => {
              store.dispatch('user/resetToken').then(() => {
                location.reload()
              })
            })
          }
          return Promise.reject(new Error(res.message || 'Error'))
        } else {
        //服务器相应成功干什么
          return res
        }
      },
      error => {
        console.log('err' + error) // for debug
        Message({
          message: error.message,
          type: 'error',
          duration: 5 * 1000
        })
        return Promise.reject(error)
      }
    )
    
    export default service
  4. 权限控制

    src\permission.js

    import router from './router'
    import store from './store'
    import { Message } from 'element-ui'
    import NProgress from 'nprogress' // progress bar
    import 'nprogress/nprogress.css' // progress bar style
    import { getToken } from '@/utils/auth' // get token from cookie
    import getPageTitle from '@/utils/get-page-title'
    
    NProgress.configure({ showSpinner: false }) // NProgress Configuration
    
    const whiteList = ['/login'] // no redirect whitelist
    
    router.beforeEach(async(to, from, next) => {
      // start progress bar
      NProgress.start()
    
      // set page title
      document.title = getPageTitle(to.meta.title)
    
      // determine whether the user has logged in
      const hasToken = getToken()
    
      if (hasToken) {
        if (to.path === '/login') {
          // if is logged in, redirect to the home page
          next({ path: '/' })
          NProgress.done()
        } else {
          const hasGetUserInfo = store.getters.name
          if (hasGetUserInfo) {
            next()
          } else {
            try {
              // get user info
              await store.dispatch('user/getInfo')
              next({...to})
            } catch (error) {
              // remove token and go to login page to re-login
              await store.dispatch('user/resetToken')
              Message.error(error || 'Has Error')
              next(`/login?redirect=${to.path}`)
              NProgress.done()
            }
          }
        }
      } else {
        /* has no token*/
    
        if (whiteList.indexOf(to.path) !== -1) {
          // in the free login whitelist, go directly
          next()
        } else {
          // other pages that do not have permission to access are redirected to the login page.
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    })
    
    router.afterEach(() => {
      // finish progress bar
      NProgress.done()
    })
    1. 开始进度条:调用NProgress.start()方法开始显示进度条。
    2. 设置页面标题:根据目标路由的meta.title属性,调用getPageTitle()方法设置页面标题。
    3. 判断用户是否已登录:调用getToken()方法判断用户是否已登录,将结果保存在hasToken变量中。
    4. 如果用户已登录:
      • 如果目标路径是登录页:调用next()方法重定向到首页,并调用NProgress.done()方法结束进度条的显示。
      • 否则,判断是否已获取用户信息:
        • 如果已获取用户信息:调用next()方法继续路由跳转。
        • 否则,获取用户信息并跳转到目标路径:调用store.dispatch('user/getInfo')方法获取用户信息并调用next({...to})方法跳转到目标路径。
      • 如果获取用户信息出错:调用store.dispatch('user/resetToken')方法移除token;调用Message.error()方法显示错误信息;跳转到登录页,并将目标路径作为参数传递;调用NProgress.done()方法结束进度条的显示。
    5. 如果用户未登录:
      • 如果目标路径在白名单中:调用next()方法继续路由跳转。
      • 否则,跳转到登录页,并将目标路径作为参数传递,并调用NProgress.done()方法结束进度条的显示。
  5. 登出

    src\layout\components\Navbar.vue

    <el-dropdown-item divided @click.native="logout">
                <span style="display:block;">退出</span>
              </el-dropdown-item>
    async logout() {
          await this.$store.dispatch('user/logout')
          this.$router.push(`/login?redirect=${this.$route.fullPath}`)
        }

    src\store\modules\user.js

    //引入登录|退出登录|获取用户信息的接口函数
    import { login, logout, getInfo } from '@/api/user'
    // 获取token|设置token|删除token的函数
    import { getToken, setToken, removeToken } from '@/utils/auth'
    //路由模块当中重置路由的方法
    import { anyRoutes, resetRouter,asyncRoutes,constantRoutes} from '@/router';
    import router from '@/router';
    import cloneDeep from 'lodash/cloneDeep'
    
    ...
    // user logout
      logout({ commit, state }) {
        return new Promise((resolve, reject) => {
          logout(state.token).then(() => {
            removeToken() // must remove  token  first
            resetRouter()
            commit('RESET_STATE')
            resolve()
          }).catch(error => {
            reject(error)
          })
        })
      },

    src\router\index.js

    //任意理由:
    
    const createRouter = () => new Router({
      // mode: 'history', // require service support
      scrollBehavior: () => ({ y: 0 }),
      //因为注册的路由是‘死的’,‘活的’路由如果根据不同用户(角色)可以展示不同菜单
      routes: constantRoutes
    })
    
    const router = createRouter()
    
    // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
    export function resetRouter() {
      const newRouter = createRouter()
      router.matcher = newRouter.matcher // reset router
    }

    resetRouter()放置在removeToken()之后,可以确保在移除token后立即重置路由。这样做有以下几个好处:

    1. 顺序性:按照先移除token,再重置路由的顺序,能够确保在重置路由时不会因为仍然存在token而导致路由的未授权访问。
    2. 完整性:移除token后,重置路由能够清理应用程序中与用户身份验证相关的状态,确保应用程序从一个干净的状态开始。

    注意:在Vue Router中,路由的匹配器(matcher)是用来根据路由配置和当前URL路径进行匹配的核心机制。匹配器负责解析URL路径,找到与之匹配的路由配置,并执行相应的操作。在重置路由时,我们希望将之前创建的Router实例的匹配器重置为新创建的Router实例的匹配器。这样做的目的是使整个应用程序重新使用新的路由配置进行路由匹配,而不是保留旧的路由状态。

路由控制

src\store\modules\user.js

//引入登录|退出登录|获取用户信息的接口函数
import { login, logout, getInfo } from '@/api/user'
// 获取token|设置token|删除token的函数
import { getToken, setToken, removeToken } from '@/utils/auth'
//路由模块当中重置路由的方法
import { anyRoutes, resetRouter,asyncRoutes,constantRoutes} from '@/router';
import router from '@/router';
import cloneDeep from 'lodash/cloneDeep'

//箭头函数
const getDefaultState = () => {
  return {
    //获取token
    token: getToken(),
    //存储用户名
    name: '',
    //存储用户头像
    avatar: '',
    //服务器返回的菜单信息【根据不同的角色:返回的标记信息,数组里面的元素是字符串】
    routes:[],
    //角色信息
    roles:[],
    //按钮权限的信息
    buttons:[],
    //对比之后【项目中已有的异步路由,与服务器返回的标记信息进行对比最终需要展示的理由】
    resultAsyncRoutes:[],
    //用户最终需要展示全部路由
    resultAllRputes:[]
  }
}

const state = getDefaultState()

//唯一修改state的地方
const mutations = {
  //重置state
  RESET_STATE: (state) => {
    Object.assign(state, getDefaultState())
  },
  //存储token
  SET_TOKEN: (state, token) => {
    state.token = token
  },
  //存储用户信息
  SET_USERINFO:(state,userInfo)=>{
    //用户名
     state.name = userInfo.name;
     //用户头像
     state.avatar = userInfo.avatar;
     //菜单权限标记
     state.routes = userInfo.routes;
     //按钮权限标记
     state.buttons = userInfo.buttons;
     //角色
     state.roles = userInfo.roles;
  },
  //最终计算出的异步路由
  SET_RESULTASYNCROUTES:(state,asyncRoutes)=>{
     //vuex保存当前用户的异步路由,注意,一个用户需要展示完成路由:常量、异步、任意路由。常量路由在创建路由时已经加入到路由器当中了
     state.resultAsyncRoutes = asyncRoutes;
     //计算出当前用户需要展示所有路由
     state.resultAllRputes = constantRoutes.concat(state.resultAsyncRoutes,anyRoutes);
     //给路由器添加新的路由
      router.addRoutes(state.resultAllRputes)
  }
}


//定义一个函数:两个数组进行对比,对比出当前用户到底显示哪些异步路由
 const computedAsyncRoutes = (asyncRoutes,routes)=>{
     //过滤出当前用户【超级管理|普通员工】需要展示的异步路由
    return asyncRoutes.filter(item=>{
         //数组当中没有这个元素返回索引值-1,如果有这个元素返回的索引值一定不是-1 
        if(routes.indexOf(item.name)!=-1){
          //递归:别忘记还有2、3、4、5、6级路由
          if(item.children&&item.children.length){
              item.children = computedAsyncRoutes(item.children,routes);
          }
          return true;
        }
     })
 }

//actions
const actions = {

  //获取用户信息
  getInfo({ commit, state }) {
    return new Promise((resolve, reject) => {
      getInfo(state.token).then(response => {
        //获取用户信息:返回数据包含:用户名name、用户头像avatar、routes[返回的标志:不同的用户应该展示哪些菜单的标记]、roles(用户角色信息)、buttons【按钮的信息:按钮权限用的标记】
        const { data } = response;
        //vuex存储用户全部的信息
        commit('SET_USERINFO',data);
        commit('SET_RESULTASYNCROUTES',computedAsyncRoutes(cloneDeep(asyncRoutes),data.routes));
          //在这段代码中使用深拷贝的原因是为了确保对asyncRoutes数组进行操作时不会影响原始的asyncRoutes数组。
          //asyncRoutes是一个包含路由配置对象的数组。当我们在对路由进行操作时,比如过滤、修改或删除某些路由项时,我们需要确保对asyncRoutes数组的操作不会影响到原始的路由配置。 
          //在这个场景中,我们需要对asyncRoutes数组进行过滤,根据用户权限或其他条件来确定哪些路由项应该显示。如果我们直接对asyncRoutes数组进行操作,那么原始的路由配置也会被修改,可能会导致不可预期的结果。
        resolve(data)
      }).catch(error => {
        reject(error)
      })
    })
  },
  }

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

总结:

  1. 动态路由配置:通过从服务器获取的菜单信息,根据不同的角色,返回的标记信息,构建了异步路由配置。这些异步路由配置包含了不同角色所能访问的页面和功能。这样做可以根据用户的角色动态生成路由,实现了动态路由的功能。
  2. 路由过滤与对比:通过 computedAsyncRoutes 函数,将异步路由配置与当前用户的路由进行对比,过滤出当前用户需要展示的异步路由。该函数使用递归的方式对比路由的子路由,确保所有层级的路由都可以正确匹配。
  3. 路由添加与重置:通过 SET_RESULTASYNCROUTES mutation,将过滤后的异步路由添加到Vue Router实例中。这样,用户在登录后,根据其角色权限,可以动态生成并展示相应的路由。
  4. 路由重置与注销:在用户注销或退出登录时,通过调用 resetRouter 函数重置路由,将路由恢复到初始状态。这样做可以确保用户退出登录后,再次登录时从一个干净的状态开始。

文章作者: QT-7274
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 QT-7274 !
评论
  目录