likes
comments
collection
share

手摸手创建一个 Vue + Ts 项目(九)—— 登录页面及登录校验

作者站长头像
站长
· 阅读数 11

系列目录

封装用户登录API

定义用户登录出入参类型

  • api/types/authTypes.ts

    import { UserInfo } from './userTypes.ts'
    export interface LoginRequest {
      username: string,
      password: string
    }
    
    export interface LoginResponse {
      userInfo: UserInfo,
      token: string
    }
    

定义登录API

  • api/authApi.ts

    import { LoginRequest, LoginResponse } from './types/authTypes.ts'
    import request from '@/utils/http'
    
    export const login = (data: LoginRequest) => {
      return request.post<LoginRequest, LoginResponse>('auth/login', data)
    }
    

开发登录页面

开发一个登录页面

首先,第一步,实现一个登录表单。在 views 文件夹下,创建 login 文件夹和 index.vue

  • views/login/index.vue

    <template>
      <div>
        <div class="e-login-form">
          <n-form
            ref="formRef"
            label-placement="left"
            size="large"
            :model="formModel"
            :rules="formRules"
          >
            <n-form-item path="username">
              <n-input
                v-model:value="formModel.username"
                placeholder="请输入用户名"
              >
                <template #prefix>
                  <n-icon size="18" color="#808695">
                    <PersonOutline />
                  </n-icon>
                </template>
              </n-input>
            </n-form-item>
            <n-form-item path="password">
              <n-input v-model:value="formModel.password" placeholder="请输入密码">
                <template #prefix>
                  <n-icon size="18" color="#808695">
                    <LockClosedOutline />
                  </n-icon>
                </template>
              </n-input>
            </n-form-item>
            <n-form-item>
              <n-button type="primary" size="large" @click="toLogin" block> 登录 </n-button>
            </n-form-item>
          </n-form>
        </div>
      </div>
    </template>
    
    <script setup lang="ts">
    import { ref, Ref } from "vue";
    import { LoginRequest } from "@/api/types/authTypes.ts";
    import { login } from "@/api/authApi";
    
    const formRef = ref();
    
    const formModel: Ref<LoginRequest> = ref({
      username: "",
      password: "",
    });
    
    const formRules = {
      username: { required: true, message: "请输入用户名", trigger: "blur" },
      password: { required: true, message: "请输入密码", trigger: "blur" },
    };
    
    const toLogin = (e) => {
      e.preventDefault();
      formRef.value.validate(async (errors: any) => {
        // 如果没有校验异常
        if (!errors) {
          login(formModel.value)
            .then(res => {
              console.log('res', res)
            })
        }
      }); 
    };
    </script>
    
    <style scoped></style>
    

然后,配置登录页面的路由。在 router/modules 文件夹下,创建 login.ts

import { RouteRecord } from '@/router/type'

const loginRoutes: RouteRecord[] = [
  {
    path: '/login',
    name: 'Login',
    meta: {
      hidden: true
    },
    component: () => import('@/views/login/index.vue')
  }
]

export default loginRoutes

访问登录页面:

手摸手创建一个 Vue + Ts 项目(九)—— 登录页面及登录校验

输入用户名和密码后,点击登录,控制台中显示了返回结果:

手摸手创建一个 Vue + Ts 项目(九)—— 登录页面及登录校验

全局管理 token 和用户信息

当登录之后,首先要做的就是将 token 和用户信息全局管理起来,方便在任意页面中获取。这里采用 Store(Pinia)和 LocalStorage 的方案来全局管理。

首先,在 store/modules 目录下,创建 user.ts

import { defineStore } from 'pinia'
import { UserInfo } from '@/api/types/userTypes'
import { LoginRequest } from '@/api/types/authTypes'
import { login } from '@/api/authApi'

import { useStorage } from '@vueuse/core'

export const useUserStore = defineStore('user', () => {
  const userInfoRef = useStorage('USER_INFO', {} as UserInfo, sessionStorage)
  const tokenRef = useStorage('TOKEN', '', sessionStorage)

  function setUserInfo(userInfo: UserInfo) {
    userInfoRef.value = userInfo
  }
  
  function setToken(token: string) {
    tokenRef.value = token
  }

  async function toLogin(param: LoginRequest) {
    await login(param)
      .then(res => {
        setToken(res.token)
        setUserInfo(res.userInfo)
      })
  }

  function getUserId() {
    return userInfoRef.value.userId
  }

  function getUsername() {
    return userInfoRef.value.username
  }

  function getToken() {
    return tokenRef.value
  }

  return { toLogin, getToken, getUserId, getUsername }
})

之后,再调整登录页面:

import { useUserStore } from "@/store/modules/user";

const userStore = useUserStore()

const toLogin = (e) => {
  e.preventDefault();
  formRef.value.validate(async (errors: any) => {
    // 如果没有校验异常
    if (!errors) {
      await userStore.toLogin(formModel.value)
      window.$notification.success({
        content: '欢迎回来 - ' + userStore.getNickName(),
        duration: 2000
      })
	  // 登录成功后,跳转到原来的页面
      const redirect = route.query.redirect as string
      if (redirect) {
        router.replace(redirect)
      } else {
        router.replace('/')
      }
    }
  }); 
};

登录校验

登录校验一般分为两部分,第一是路由跳转时,如果没有登录,则直接跳转到登录页面;第二是与 API 约定一个固定的异常编码,当返回改异常时,则认为需要登录。

路由跳转校验

首先,实现路由跳转时的校验,这里用到的是 vue-router 中的[导航卫士](导航守卫 | Vue Router (vuejs.org))功能。

找到我们定义路由的地方,注册一个全局前置卫士:

  • router/index.ts

    router.beforeEach((to, from, next) => {
      const userStore = useUserStore();
      const isAuthenticated = userStore.getToken() && userStore.getToken() !== "";
      if (isAuthenticated) {
        next();
      } else {
        if (to.name === "Login") {
          next();
        } else {
          next({ name: "Login", query: { redirect: to.fullPath } });
        }
      }
    });
    

    这里判断是否登录,判断方式是 token 是否存在,如果存在,则认为是已经登录过,如果没有登录的话,跳转到登录页面,并且把当前要跳转的路径作为参数带过去。

API 响应校验

接下来,实现接口层面校验,当服务端返回异常状态编码为未登录时,跳转到登录页面。

首先,在每次接口请求时,都把 token 携带到 Header 中。

AxiosRequest.ts 中的请求拦截器中,添加相关代码:

private interceptRequest(): void {
    this.axiosInstance.interceptors.request.use(
      async (config: ExpandInternalAxiosRequestConfig) => {
        // token
        const userStore = useUserStore()
        const token = userStore.getToken()
        if (token) {
          // 这里的 _tt 是与服务端约定的 token 键名
          config.headers._tt = token
        }
		
        // ……
			
        return config;
      },
      errorHandler
    );
}

store/modules/user.ts 中,增加注销 logout 方法,主要实现清除浏览器中存储的 token 和用户信息,同时跳转到登录页面。

function toLogout() {
    setUserInfo({} as UserInfo);
    setToken("");
    window.location.href = '/login'
}

找到 AxiosRequest.ts 文件,定义异常编码和处理异常编码的方法:

import { useUserStore } from '@/store/modules/user.ts'

const notLoginErrorCode = "999900011";

const handleErrorCode = (errorCode: string) => {
    if (notLoginErrorCode === errorCode) {
        const userStore = useUserStore()
        userStore.logout()
    }
}

之后,在其响应拦截器中,添加处理异常编码的逻辑:

private interceptResponse(): void {
    this.axiosInstance.interceptors.response.use(
      async (response: ExpandAxiosResponse): Promise<any> => {
        // ……  

        const { code, errorCode, message, data } = response.data;

        if (code === successCode) {
          return data;
        } else {
		  // ……
          handleErrorCode(errorCode);
        }

        return Promise.reject(response.data);
      },
      errorHandler
    );
}