登录组件分析

  • src/views/login/index.vue

登录组件布局要点如下:

el-form 容器,包含 usernamepassword 两个 el-form-itemel-form 主要属性:

  • ref :用来获取 el-form 表单实例

    在表单提交时,通过该实例调用 validate 方法

  • model :对应 data 中的 loginForm

  • rules :表单的验证项,验证规则主要才是用 async-validator

    详细用法可以查看我这篇文章中的 async-validator Element-UI入门

  • autocomplete :自动填充

  • label-positionlabel 标签位置

需要修改一下校验规则

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()
  }
}

password 使用了 el-tooltip 提示,当用户打开大小写时,会进行提示,主要属性:

  • manual :手动控制模式,设置为 true 后,mouseentermouseleave 事件将不会生效
  • placement :提示出现的位置

  • 这里绑定 @keyup 事件时需要添加 .native 修饰符

    如果不添加 .native 修饰符,事件会绑定在 el-input 组件上

    添加 .native 修饰符,事件将绑定到原生的 input 标签上

checkCapslock 方法

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

  • 按住 shift 时输入小写字符
  • 为按住 shift 时输入大写字符
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
  }
}

// 新版本直接判断key是否是大写
checkCapslock(e) {
  const { key } = e
  this.capsTooltip = key && key.length === 1 && key >= 'A' && key <= 'Z'
}

handleLogin 方法

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

这里需要注意:由于 vuexuser 指定了 namespacedtrue,所以 dispatch 时需要加上其命名空间,否则将无法调用 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
    }
  })
}

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
      commit('SET_TOKEN', data.token)
      setToken(data.token)
      resolve()
    }).catch(error => {
      reject(error)
    })
  })
}

login API 方法如下:

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

这里使用 request 方法,它是一个基于 axios 封装的库

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
})

baseURL 取的是 .env.development 下的变量 VUE_APP_BASE_API

# base api
VUE_APP_BASE_API = '/dev-api'

接口是通过 mock 实现的,用户数据位于 /mock/user.js

axios 用法分析

  • vue-element-adminrequest 方法使用了 axios 的手动实例化方法 create 来封装请求

axios 基本案例

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

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

上述代码可以改为:

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

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

const url = 'https://127.0.0.1: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 对象,会造成大量重复代码
  • 每个请求都需要手动定义异常处理,而异常处理逻辑大多是一致的,如果将其封装成通用的异常处理方法,那么每个请求都要调用一遍

axios.create 示例

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

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

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

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 捕获异常进行自定义处理

axios 响应拦截器

在实际开发中除了需要保障 http statusCode 为 200,还需要保证业务代码正确,上述案例

  • 如果 error_code 为 0 时,表示业务正常
  • 如果返回值不为 0,这说明业务处理错误,此时我们通过 request.interceptors.response.use 方法定义响应拦截器,它仍然需要 2 个参数,与请求拦截器类似
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 库源码分析

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

const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 5000
})

service.interceptors.request.use(
  config => {
    // 如果存在 token 则附带在 http header 中
    if (store.getters.token) {
      config.headers['X-Token'] = getToken()
    }
    return config
  },
  error => {
    console.log(error) // for debug
    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 => {
    console.log('err' + error) // for debug
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

export default service

登录细节分析

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

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

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

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

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

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

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

直接删除 query.redirect 会直接改动 query,可以通过浅拷贝实现属性过滤

  • const _query = Object.assign({}, query)

    delete _query.redirect

watch: {
  $route: {
    handler: function(route) {
      const query = route.query
      if (query) {
        this.redirect = query.redirect
        this.otherQuery = this.getOtherQuery(query)
      }
    },
    immediate: true
  }
}

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

关闭 Mock 接口

去掉 main.jsmock 相关代码:

if (process.env.NODE_ENV === 'production') {
  const { mockXHR } = require('../mock')
  mockXHR()
}
// 或
import { mockXHR } from '../mock'
if (process.env.NODE_ENV === 'production') {
  mockXHR()
}

删除 src/api 目录下的 article.jsqiniu.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 接口,我们需要配置 hosts 映射,让域名映射到本地 node 项目

127.0.0.1 book.llmysnow.top

修改接口地址

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

VUE_APP_BASE_API = 'https://book.llmysnow.top:18082'

重新启动项目后,发现登录接口已指向我们指定的接口:https://book.llmysnow.top:18082/vue-element-admin/user/login

Logo

前往低代码交流专区

更多推荐