最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue + TypeScript 实战项目(三)

    正文概述 掘金(不梨)   2021-01-08   488

    本系列将从零开始创建一个项目,文章将持续更新

    项目代码https://github.com/no-pear/edu-fed.git

    登录和认证

    一、登录

    1)页面基本布局

    2)登录接口封装

    import request from '@/utils/request'
    import qs from 'qs'
    // import store from '@/store'
    
    interface User {
        phone: string;
        password: string;
    }
    
    // 登录
    export const login = (form: User) => {
      return request({
        method: 'POST',
        url: '/front/user/login',
        // headers: { 'content-type': 'application/x-www-form-urlencoded' },
        /**
         * 如果 data 是普通对象, 则 Content-Type 是 application/json
         * 如果 data 是 qs.stringfity(data) 转换之后的数据:key=value&key=value, 则 Content-Type 会被设置成 application/x-www-form-urlencoded
         * 如果 data 是 FormData 对象,则 Content-Type 是 multipart/form-data
         */
        data: qs.stringify(form) // axios 默认发送的是 application/json 格式的数据
      })
    }
    

    3)登录处理

    • 表单验证
    • 验证通过提交表单
    • 处理请求结果
      • 成功:跳转到首页
      • 失败:给出提示

    src/login/index.vue

    <template>
      <div class="login">
        <!-- :model="ruleForm" :rules="rules" ref="ruleForm" -->
        <el-form ref="form" :model="form" :rules="rules" label-width="80px" label-position='top'>
          <el-form-item label="手机号" prop="phone">
            <el-input v-model="form.phone"></el-input>
          </el-form-item>
          <el-form-item label="密码" prop="password">
            <el-input v-model="form.password" type="password"></el-input>
          </el-form-item>
          <el-form-item>
            <el-button :loading='isLoginLoading' type="primary" @click="onSubmit">登录</el-button>
          </el-form-item>
        </el-form>
      </div>
    </template>
    
    <script lang="ts">
    import Vue from 'vue'
    // import request from '@/utils/request'
    // import qs from 'qs'
    import { Form } from 'element-ui'
    import { login } from '@/services/user'
    
    export default Vue.extend({
      name: 'LoginIndex',
      data () {
        return {
          form: {
            phone: '15510792995',
            password: '111111'
          },
          isLoginLoading: false,
          rules: {
            phone: [
              { required: true, message: '请输入手机号', trigger: 'blur' },
              { pattern: /^1\d{10}$/, message: '请输入正确的手机号', trigger: 'blur' }
            ],
            password: [
              { required: true, message: '请输入密码', trigger: 'blur' },
              { min: 6, max: 18, message: '长度在 6 到 18 个字符', trigger: 'blur' }
            ]
          }
        }
      },
      methods: {
        async onSubmit () {
          try {
            // 1. 表单验证
            await (this.$refs.form as Form).validate()
    
            // 防止登录按钮多次点击
            this.isLoginLoading = true
    
            // 2. 验证通过 - 提交表单
            // const { data } = await request({
            //   method: 'POST',
            //   url: '/front/user/login',
            //   headers: { 'content-type': 'application/x-www-form-urlencoded' },
            //   data: qs.stringify(this.form)
            // })
            const { data } = await login(this.form)
            // console.log(data)
            // 3. 处理请求结果
            // 成功:跳转到首页
            // 失败:给出提示
            if (data.state !== 1) {
              this.$message.error(data.message)
            } else {
              // 存储登录用户信息
              this.$store.commit('setUser', data.content)
              // 跳转首页
              // this.$router.push({
              //   name: 'home'
              // })
    
              // 登录成功跳转到首页或者跳转到登录前想去到的页面
              this.$router.push(this.$route.query.redirect as string || '/')
    
              this.$message.success('登录成功')
            }
    
            this.isLoginLoading = false
          } catch (error) {
            console.log('格式校验失败', false)
          }
        }
      }
    })
    </script>
    
    <style lang="scss" scoped>
    .login {
      background-color: $body-bg;
      height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      .el-form {
        background: #fff;
        padding: 20px;
        border-radius: 5px;
        width: 300px;
        .el-button {
          width: 100%;
        }
      }
    }
    </style>
    
    

    二、认证

    对于某些页面,必须得登录才能访问,所以登录的时候服务器会返回一个 access_token,以后每次访问都可以在请求头中添加上,access_token

    有效期,过了有效期后,通过 refresh_token 去再次获取 access_token

    1)路由

    /**
     * 全局前置守卫
     * to: Route: 即将要进入的目标 路由对象
     * from: Route: 当前导航正要离开的路由
     * next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
     */
    router.beforeEach((to, from, next) => {
      // console.log('to--->', to)
      // console.log('from--->', from)
      // to.matched 是一个数组(匹配到的路由信息)
      if (to.matched.some(record => record.meta.requiresAuth)) {
        if (!store.state.user) {
          next({
            path: '/login',
            query: { // 通过 url 传递查询字符串参数
              redirect: to.fullPath // 把登录成功需要返回的页面告诉登录页面
            }
          })
        } else {
          next()
        }
      } else {
        next()
      }
    })
    

    2)请求拦截器

    request.interceptors.request.use(function (config) {
      // Do something before request is sent
      // console.log(config)
      const { user } = store.state
      if (user && user.access_token) {
        config.headers.Authorization = user.access_token
      }
    
      return config
    }, function (error) {
      // Do something with request error
      return Promise.reject(error)
    })
    

    3)响应拦截器

    // 响应拦截器
    let isRefreshing = false // 控制刷新 token 的状态
    let requests: any[] = [] // 存储刷新 token 期间过来的 401 请求
    request.interceptors.response.use(function (response) { // 2xx 状态码
      // console.log('response--->', response)
    
      return response
    }, async function (error) { // 超出 2xx 范围状态码
      // console.log('error--->', error)
      if (error.response) { // 请求发出去收到响应了,但是状态码超出了 2xx 范围
        const { status } = error.response
        if (status === 400) {
          Message.error('请求参数错误')
        } else if (status === 401) {
          /**
           * token 失效
           * 如果有 refresh_token 则尝试使用 refresh_token 获取新的token
           * 如果没有,则直接跳转登录页
           */
    
          if (!store.state.user) {
            redirectLogin()
            return Promise.reject(error)
          }
    
          //  刷新 token
          if (!isRefreshing) {
            isRefreshing = true // 开启刷新状态
    
            return refreshToken().then(res => {
              if (!res.data.success) {
                throw new Error('刷新 token 失败')
              }
    
              // 刷新 token 成功
              store.commit('setUser', res.data.content)
              // 把 requests 队列中的请求发送出去
              requests.forEach(cb => {
                cb()
              })
              // 重置 requests
              requests = []
              // console.log('error config-->', error.config) // 失败请求的配置信息
              return request(error.config) // 第一个请求重新发送
            }).catch(err => {
              console.log('err-->', err)
              // 清除当前登录用户状态
              store.commit('setUser', null)
              // 回到登录页
              redirectLogin()
              return Promise.reject(error)
            }).finally(() => {
              isRefreshing = false // 重置刷新状态
            })
    
            return
          }
    
          // 刷新状态下,把请求挂起放到 requests 数组中
          return new Promise(resolve => {
            requests.push(() => {
              resolve(request(error.config))
            })
          })
    
          // try {
          //   // 尝试获取新的 token
          //   const { data } = await axios.create()({
          //     method: 'POST',
          //     url: '/front/user/refresh_token',
          //     data: qs.stringify({
          //       refreshtoken: store.state.user.refresh_token
          //     })
          //   })
          //   console.log('data:', data)
          //   /**
          //    * 把刷新拿到的新的 refresh_token 更新到容器和本地存储中
          //    * 获取新的 token 成功, 把失败的请求重新发送出去
          //    */
          //   store.commit('setUser', data.content)
          //   console.log('error config-->', error.config) // 失败请求的配置信息
          //   return request(error.config)
          // } catch (error) {
          //   // 清除当前登录用户状态
          //   store.commit('setUser', null)
          //   // 回到登录页
          //   redirectLogin()
          //   return Promise.reject(error)
          // }
        } else if (status === 403) {
          Message.error('没有权限,请联系管理员')
        } else if (status === 404) {
          Message.error('请求资源不存在')
        } else if (status >= 500) {
          Message.error('服务器错误')
        }
      } else if (error.request) { // 请求发出去没收到响应
        Message.error('请求响应失败')
      } else { // 在设置请求是发生了一些事情,触发了一个错误
        Message.error(`请求失败: ${error.message}`)
      }
    
      return Promise.reject(error)
    })
    

    起源地下载网 » Vue + TypeScript 实战项目(三)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元