这是一篇学习笔记,记录课程中前端JWT的实现代码
慕课网 vue3+ts 仿知乎专栏项目
首先页面大概分为三类
- redirectAlreadyLogin:登录后下无法访问,包括登录页和注册页,如果登录后访问需要跳转到首页。
- requiredLogin: 只有登录后才能访问的页面
- 其他: 谁都能看的页面,比如首页
有两个 action 来发起请求
- login: 用户提交登录,获取token,将token存入vuex(vue的全局状态管理工具),localStorage 和Authorization请求头
- fetchCurrentUser: 携带token获取用户信息
但是因为用户登录后必定需要获取用户信息,所以创建一个额外的action loginAndFetch 来同时完成login和fetchCurrentUser
// vuex
import { createStore, Commit } from 'vuex'
import axios from 'axios'
// ts描述用户信息
export interface UserProps {
isLogin: boolean;
nickName?: string;
_id?: string;
column?: string;
email?: string;
}
// ts描述错误信息
export interface GlobalErrorProps {
status: boolean;
message?: string;
}
// ts描述store信息
export interface GlobalDataProps {
error: GlobalErrorProps;
token: string;
loading: boolean;
user: UserProps;
}
// 用于发送get信息,并commit mutations
const getAndCommit = async (url: string, mutationName: string, commit: Commit) => {
const { data } = await axios.get(url)
commit(mutationName, data)
return data
}
// 用于发送post信息,并commit mutations
const postAndCommit = async (url: string, mutationName: string, commit: Commit, payload: any) => {
const { data } = await axios.post(url, payload)
commit(mutationName, data)
return data
}
const store = createStore<GlobalDataProps>({
state: {
error: { status: false }, // 错误信息
token: localStorage.getItem('token') || '', // 从localStorage中获取token
loading: false, // loading状态
user: { isLogin: false } // 用户信息
},
mutations: {
// setLoading 和 setError: 分别用于设置 loading 和 error
// login:用户登录后,将token写入 state,localStorage 和 Authorization请求头 中
// fetchCurrentUser: 请求用户信息成功后,将登录状态切换为true, 用户信息写入 state
// logout: token伪造或者用户退出登录时执行,与login的操作相反
setLoading(state, status) {
state.loading = status
},
setError(state, e: GlobalErrorProps) {
state.error = e
},
fetchCurrentUser(state, rawData) {
state.user = { isLogin: true, ...rawData.data }
},
login(state, rawData) {
const { token } = rawData.data
state.token = token
localStorage.setItem('token', token)
axios.defaults.headers.Authorization = `Bearer ${token}`
},
logout(state) {
state.token = ''
state.user = { isLogin: false }
localStorage.removeItem('token')
delete axios.defaults.headers.common.Authorization
}
},
actions: {
// 从上面的流程图就可以发现,用户登录,服务器返回token后,必定需要请求用户信息
// 那么在 loginAndFetch 中将 login 和 fetchCurrentUser 两个 action 合并
// 这样,只要 store.dispatch('loginAndFetch') 就可以同时完成登录加获取信息两个请求了
fetchCurrentUser({ commit }) {
return getAndCommit('/user/current', 'fetchCurrentUser', commit)
},
login({ commit }, payload) {
return postAndCommit('/user/login', 'login', commit, payload)
},
async loginAndFetch({ dispatch }, loginData) {
await dispatch('login', loginData)
return dispatch('fetchCurrentUser')
}
}
})
export default store
// router 管理路由,使用 meta 将页面进行分类,设置路由导航守卫
import Home from './views/Home.vue'
import Login from './views/Login.vue'
import Signup from './views/Signup.vue'
import ColumnDetail from './views/ColumnDetail.vue'
import CreatePost from './views/CreatePost.vue'
import store from './store'
import axios from 'axios'
const routerHistory = createWebHistory()
const router = createRouter({
history: routerHistory,
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/login',
name: 'login',
component: Login,
meta: { redirectAlreadyLogin: true }
},
{
path: '/signup',
name: 'signup',
component: Signup,
meta: { redirectAlreadyLogin: true }
},
{
path: '/create',
name: 'create',
component: CreatePost,
meta: { requiredLogin: true }
},
{
path: '/column/:id',
name: 'column',
component: ColumnDetail
}
]
})
router.beforeEach((to, from, next) => {
const { user, token } = store.state
const { requiredLogin, redirectAlreadyLogin } = to.meta
if (!user.isLogin) {
if (token) {
axios.defaults.headers.common.Authorization = `Bearer ${token}`
store.dispatch('fetchCurrentUser').then(() => {
if (redirectAlreadyLogin) {
next('/')
} else {
next()
}
}).catch(e => {
store.commit('logout')
next('login')
})
} else {
if (requiredLogin) {
next('login')
} else {
next()
}
}
} else {
if (redirectAlreadyLogin) {
next('/')
} else {
next()
}
}
})
export default router
// Login.vue 用户登录
<script lang="ts">
import { defineComponent, ref } from 'vue'
import { useStore } from 'vuex'
import { useRouter } from 'vue-router'
import createMessage from '../components/createMessage'
export default defineComponent({
name: 'Login',
setup() {
const router = useRouter()
const store = useStore()
const userName = ref('')
const password = ref('')
// 用户点击登录
// loginAndFetch 会依次执行 login 和 fetchCurrentUser
const onFormSubmit = async (result: boolean) => {
if (result) {
const payload = {
email: emailVal.value,
password: passwordVal.value
}
try {
await store.dispatch('loginAndFetch', payload)
createMessage('登录成功', 'success')
router.push('/')
} catch (e) {
console.log('登录失败了...')
}
}
}
return {
//.......
}
}
})
</script>
// 退出登录只需要commit('logout')就行了
store.commit('logout')
// main.ts
// axios 的配置项,主要是通过拦截器设置 loading 和 error 状态
import { createApp } from 'vue'
import axios from 'axios'
import router from './router'
import store from './store'
import App from './App.vue'
axios.defaults.baseURL = '********'
axios.interceptors.request.use(config => {
// request时 开启loading, 复位 error
store.commit('setLoading', true)
store.commit('setError', { status: false, message: '' })
return config
})
axios.interceptors.response.use(config => {
// response时 关闭 Loading, 如果失败设置 error
store.commit('setLoading', false)
return config
}, e => {
const { error } = e.response.data
store.commit('setError', { status: true, message: error })
store.commit('setLoading', false)
return Promise.reject(error)
})
const app = createApp(App)
app.use(router)
app.use(store)
app.mount('#app')