zl程序教程

您现在的位置是:首页 >  前端

当前栏目

【笔记】Vue Element+Node.js开发企业通用管理后台系统——用户登录(中)

2023-09-27 14:26:51 时间


用户登录(中) | 「小慕读书」管理后台


一、登录组件分析

1.布局分析

登录组件 login.vue 布局要点如下:

  • el-form 容器,包含 usernamepassword 两个 el-form-itemel-form 主要属性:
    • ref 用来获取组件实例,在表单提交时,即点击登录按钮时触发handleLogin事件(this.$refs.loginForm.validate(valid => {}),这里的loginForm就是refloginForm的组件实例,validate方法用作表单验证,rules属性绑定的就是验证规则

    • modelloginForm,在data中,是对象形式,包含了表单的初始化数据
      在这里插入图片描述

    • rulesloginRules,绑定的验证规则在data中,是对象形式,直接返回,这个对象里面的属性名对应的是el-form-itemprop属性,里面的每一个属性对应一组验证规则,每组验证规则可以有多个验证项,验证项也是一个对象,包含的属性如下:

      • required:是否必须项
      • trigger:触发方式,常见的值为:blur焦点离开时触发;change内容改变时触发…
      • validator:校验器,绑定了验证项的具体验证规则,本框架默认引用了src/utils/validate.js里面对应方法名的验证规则,比如:
        • validUsername(value),将表单填入的值带到方法里验证,通过返回的bollean值判断是否符合规则,这里我们需要使用自己的规则,因此取消引入src/utils/validate.js,修改为:只判undefined或空
        • 修改密码不能少于四位value.length < 4
    • autocomplete:自动填充;

    • label-positionlable标签位置(案例中并没有使用label
      在这里插入图片描述
      这里我们需要修改一下校验规则:

const validateUsername = (rule, value, callback) => {
  if (!value || value.length === 0) {
    callback(new Error('请输入用户名'))
  } else {
    callback()
  }
}
const validatePassword = (rule, value, callback) => {
        if (value.length < 4) {
          callback(new Error('密码不能少于4位'))
        } else {
          callback()
        }
      }

如果某一部分有错误提示,按下alt+enter => Expand tag => Disable inspection,就会关闭错误检查

  • password使用了 el-tooltip提示,当用户打开大小写时,会进行提示,主要属性:
    • manual:手动控制模式,设置为 true 后,mouseentermouseleave 事件将不会生效
    • placement:提示出现的位置
  • password 对应的 el-input 主要属性:
    • @keyup.native="checkCapslock" 键盘按键时绑定 checkCapslock 事件
    • @keyup.enter.native="handleLogin" 监听键盘 enter 按下后的事件

这里绑定 @keyup 事件时需要添加 .native 修饰符,这是因为我们的事件绑定在 el-input 组件上,所以如果不添加 .native 修饰符,事件将无法绑定到原生的 input 标签上

  • 包含一个 el-button,点击时调用 handleLogin 方法,并触发 loading 效果

2.checkCapslock 方法

checkCapslock 方法的主要用途是监听用户键盘输入,显示提示文字的判断逻辑如下:

  • 按住 shift 时输入小写字符
  • 未按 shift 时输入大写字符
    当按下 CapsLock 按键时,如果按下后是小写模式,则会立即消除提示文字
checkCapslock({ shiftKey, key } = {}) {
  if (key && key.length === 1) {
    if (shiftKey && (key >= 'a' && key <= 'z') || !shiftKey && (key >= 'A' && key <= 'Z')) {
      this.capsTooltip = true
    } else {
      this.capsTooltip = false
    }
  }
  if (key === 'CapsLock' && this.capsTooltip === true) {
    this.capsTooltip = false
  }
}

3.handleLogin 方法

handleLogin 方法处理流程如下:

  • 调用 el-formvalidate 方法对 rules 进行验证;
  • 如果验证通过,则会调用 vuexuser/login action 进行登录验证;
  • 登录验证通过后,会重定向到 redirect 路由,如果 redirect 路由不存在,则直接重定向到 / 路由

这里需要注意:由于 vuex 中的 user 指定了 namespaced 为 true,所以 dispatch 时需要加上 namespace,否则将无法调用 vuex 中的 action

handleLogin() {
  this.$refs.loginForm.validate(valid => {
    if (valid) {
      this.loading = true
      this.$store.dispatch('user/login', this.loginForm)
        .then(() => {
          this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
          this.loading = false
        })
        .catch(() => {
          this.loading = false
        })
    } else {
      console.log('error submit!!')
      return false
    }
  })
}

它的请求前缀是VUE_APP_BASE_API = '/dev-api'vue-element-admin\.env.development
在这里插入图片描述

4.user/login 方法

user/login 方法调用了 login API,传入 usernamepassword 参数,请求成功后会从 response 中获取 token,然后将 token 保存到 Cookie 中,之后返回。如果请求失败,将调用 reject 方法,交由我们自定义的 request 模块来处理异常

login({ commit }, userInfo) {
    const { username, password } = userInfo
    return new Promise((resolve, reject) => {
      login({ username: username.trim(), password: password }).then(response => {
        const { data } = response	// 拿到data
        commit('SET_TOKEN', data.token)	// 拿到data里的token
        setToken(data.token)	// 把token保存到cookies中
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
}

里面的SET_TOKENmutations的一个属性方法:

SET_TOKEN: (state, token) => {
  state.token = token	// 将状态保存到state中
},

setTokentoken保存到cookies中(src/utils/auth.js):

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

在这里插入图片描述
login API 的方法如下:

import request from '@/utils/request'

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

这里使用 request 方法,它是一个基于 axios 封装的库,目前我们的 /user/login 接口是通过 mock 实现的,用户数据位于 /mock/user.js;

二、axios 用法分析

request 库(src/utils/request.js)使用了 axios 的手动实例化方法 create 来封装请求,要理解其中的用法,我们需要首先学习 axios 库的用法

axios 官网:axios中文文档|axios中文网 | axios

1.axios 基本案例

我们先从一个普通的 axios 示例开始:

import axios from 'axios'

const url = 'https://test.youbaobao.xyz:18081/book/home/v2?openId=1234'
axios.get(url).then(response => {
  console.log(response)
})

上述代码可以改为:

const url = 'https://test.youbaobao.xyz:18081/book/home/v2'
axios.get(url, { 
  params: { openId: '1234' }	// params中的内容会自动添加到url后,组成一条请求
})

如果我们在请求时需要在 http header 中添加一个 token,需要将代码修改为:

const url = 'https://test.youbaobao.xyz:18081/book/home/v2'
axios.get(url, { 
  params: { openId: '1234' },
  headers: { token: 'abcd' }
}).then(response => {
  console.log(response)
})

如果要捕获服务端抛出的异常,即返回非 200 请求,需要将代码修改为:

const url = 'https://test.youbaobao.xyz:18081/book/home/v2'
axios.get(url, { 
  params: { openId: '1234' },
  headers: { token: 'abcd' }
}).then(response => {
  console.log(response)
}).catch(err => {
  console.log(err)
})

这样改动可以实现我们的需求,但是有两个问题:

  • 每个需要传入 token 的请求都需要添加 headers 对象,会造成大量重复代码
  • 每个请求都需要手动定义异常处理,而异常处理的逻辑大多是一致的,如果将其封装成通用的异常处理方法,那么每个请求都要调用一遍

2.axios.create 示例

axios.get是一个静态方法,axios.create是一个动态方法)
下面我们使用 axios.create 对整个请求进行重构:

const url = '/book/home/v2'
const request = axios.create({
  baseURL: 'https://test.youbaobao.xyz:18081',
  timeout: 5000
})
request({
  url, 
  method: 'get',
  params: {
    openId: '1234'
  }
})

首先我们通过 axios.create 生成一个函数,该函数是 axios 实例,通过执行该方法完成请求,它与直接调用 axios.get 区别如下:

  • 需要传入 url 参数,axios.get 方法的第一个参数是 url
  • 需要传入 method 参数,axios.get 方法已经表示发起 get 请求

3.axios 请求拦截器

上述代码完成了基本请求的功能,下面我们需要为 http 请求的 headers 中添加 token,同时进行白名单校验,如 /login 不需要添加 token,并实现异步捕获和自定义处理:

const whiteUrl = [ '/login', '/book/home/v2' ]
const url = '/book/home/v2'
const request = axios.create({
  baseURL: 'https://test.youbaobao.xyz:18081',
  timeout: 5000
})
request.interceptors.request.use(
  config => {
    // throw new Error('error...')
    const url = config.url.replace(config.baseURL, '')
      if (whiteUrl.some(wl => url === wl)) {
        return config
      }
    config.headers['token'] = 'abcd'
    return config
  },
  error => {
    return Promise.reject(error)
  }
)
request({
  url, 
  method: 'get',
  params: {
    openId: '1234'
  }
}).catch(err => {
  console.log(err)
})

这里核心是调用了 request.interceptors.request.use 方法,即 axios 的请求拦截器,该方法需要传入两个参数:

  • 第一个参数是拦截器方法,包含一个 config 参数,我们可以在这个方法中修改 config 并且进行回传;
  • 第二个参数是异常处理方法,我们可以使用 Promise.reject(error) 将异常返回给用户进行处理,所以我们在 request 请求后可以通过 catch 捕获异常进行自定义处理
    在这里插入图片描述

4.axios 响应拦截器

下面我们进一步增强 axios 功能,我们在实际开发中除了需要保障 http statusCode 为 200,还需要保证业务代码正确,上述案例中,我定义了 error_code 为 0 时,表示业务返回正常,如果返回值不为 0 则说明业务处理出错,此时我们通过 request.interceptors.response.use 方法定义响应拦截器,它仍然需要2个参数,与请求拦截器类似,注意第二个参数主要处理 statusCode 非 200 的异常请求,源码如下:

const whiteUrl = [ '/login', '/book/home/v2' ]
const url = '/book/home/v2'
const request = axios.create({
  baseURL: 'https://test.youbaobao.xyz:18081',
  timeout: 5000
})
request.interceptors.request.use(
  config => {
    const url = config.url.replace(config.baseURL, '')
    if (whiteUrl.some(wl => url === wl)) {
      return config
    }
    config.headers['token'] = 'abcd'
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

request.interceptors.response.use(
  response => {
    const res = response.data
    if (res.error_code != 0) {
      alert(res.msg)
      return Promise.reject(new Error(res.msg))
    } else {
      return res
    }
  },
  error => {
    return Promise.reject(error)
  }
)

request({
  url, 
  method: 'get',
  params: {
    openId: '1234'
  }
}).then(response => {
  console.log(response)
}).catch(err => {
  console.log(err)
})

三、request 库源码分析

有了上述基础后,我们再看 request 库源码就非常容易了(相对源码部分修改)

vue-element-admin\src\utils\request.js

const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // 配置项中的baseurl
  timeout: 5000
})
// 请求拦截器
service.interceptors.request.use(
  config => {
    // 如果存在 token 则附带在 http header 中
    if (store.getters.token) {
      config.headers['X-Token'] = getToken()
    }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)
// 响应拦截器
service.interceptors.response.use(
  response => {
    const res = response.data

    if (res.code !== 20000) {
      Message({
        message: res.message || 'Error',
        type: 'error',
        duration: 5 * 1000
      })
      // 判断 token 失效的场景
      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
        // 如果 token 失效,则弹出确认对话框,用户点击后,清空 token 并返回登录页面
        MessageBox.confirm('Token 已失效,是否重新登录?', '确认登出', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          store.dispatch('user/resetToken').then(() => {
            location.reload()
          })
        })
      }
      return Promise.reject(new Error(res.message || 'Error'))
    } else {
      return res
    }
  },
  error => {
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)
// 暴露接口
export default service

在这里插入图片描述

四、登录细节分析

1.细节一:页面启动后自动聚焦

检查用户名或密码是否为空,如果发现为空,则自动聚焦:

mounted() {
    if (this.loginForm.username === '') {
      this.$refs.username.focus()
    } else if (this.loginForm.password === '') {
      this.$refs.password.focus()
    }
}

2.细节二:显示密码后自动聚焦

切换密码显示状态后,自动聚焦 password 输入框:

showPwd() {
  if (this.passwordType === 'password') {
    this.passwordType = ''
  } else {
    this.passwordType = 'password'
  }
  this.$nextTick(() => {
    this.$refs.password.focus()
  })
}

3.细节三:通过 reduce 过滤对象属性

const query = {
  redirect: '/book/list',
  name: 'sam',
  id: '1234'
}
// 直接删除 query.redirect,会直接改动 query
// delete query.redirect

// 通过浅拷贝实现属性过滤
// const _query = Object.assign({}, query)
// delete _query.redirect

const _query = Object.keys(query).reduce((acc, cur) => {
    if (cur !== 'redirect') {
      acc[cur] = query[cur]
    }
    return acc
  }, {})
console.log(query, _query)

五、关闭 Mock 接口

  • 去掉 main.jsmock 相关代码:
import { mockXHR } from '../mock'
if (process.env.NODE_ENV === 'production') {
  mockXHR()
}

或是

if (process.env.NODE_ENV === 'production') {
  const { mockXHR } = require('../mock')
  mockXHR()
}
  • 删除 src/api 目录下 2 个 api 文件:
article.js
qiniu.js
  • 删除 vue.config.js 中的相关配置:
proxy: {
  // change xxx-api/login => mock/login
  // detail: https://cli.vuejs.org/config/#devserver-proxy
  [process.env.VUE_APP_BASE_API]: {
    target: `http://127.0.0.1:${port}/mock`,
    changeOrigin: true,
    pathRewrite: {
      ['^' + process.env.VUE_APP_BASE_API]: ''
    }
  }
},
after: require('./mock/mock-server.js')

或是:

before: require('./mock/mock-server.js')

修改后我们的项目里就不能使用 mock 接口,会直接请求到 http 接口,我们需要打开 SwitchHosts 配置 host 映射,让域名映射到本地 node 项目:

127.0.0.1	book.youbaobao.xyz

也可以直接修改 /etc/hosts 文件

六、修改接口地址

我们将发布到开发环境和生产环境,所以需要修改 .env.development.env.production 两个配置文件:

VUE_APP_BASE_API = 'https://book.youbaobao.xyz:18082'
# VUE_APP_BASE_API = '/dev-api'

有两点需要注意:

这里使用了域名 book.youbaobao.xyz,大家可以将其替换为自己注册的域名,如果还没注册域名,用 localhost 也是可行的,但如果要发布到互联网需要注册域名
如果没有申请 https 证书,可以采用 http 协议,同样可以实现登录请求,但是如果你要发布到互联网上建议使用 https 协议安全性会更好
重新启动项目后,发现登录接口已指向我们指定的接口:

Request URL: https://book.youbaobao.xyz:18082/user/login