如何在 Vue3 应用程序中构建身份验证
如何在 Vue3 应用程序中构建身份验证
如何在 Vue3 应用程序中构建身份验证
我最近在Neo4j Twitch 频道上开始了一场关于使用 Neo4j 和 TypeScript 构建 Web 应用程序的直播,并为 Neoflix(一个虚构的流媒体服务)开发了一个示例项目。
我长期使用 Vue.js,但由于缺乏合适的 TypeScript 支持,我很难在 Stream 中构建基于 Vue 的前端,毕竟 Vue2 的 TypeScript 支持似乎有所欠缺。我唯一真正的选择是 Angular,但很快就让我失望了。
随着上周 Vue v3 的正式发布以及对 TypeScript 支持的改进,这给了我一个很好的理由去尝试并看看如何将其融入到 Neoflix 项目中。
Vue 3 和 Composition API
Vue 2 的一个缺点是,随着应用程序规模的扩大,复杂性也会随之增加,功能的复用和组件的可读性也会受到影响。我见过多次提到的一个例子就是排序结果或分页的问题。在 Vue 2 应用程序中,你的选择要么是跨组件复制功能,要么是使用 Mixin。Mixin 的缺点是,仍然不清楚哪些数据和方法绑定到了组件上。
新的Composition API允许我们将可重复元素提取到它们自己的文件中,这些文件可以以更合乎逻辑的方式跨组件使用。
每个组件上的新setup
函数为您提供了一种便捷的导入和复用功能的方式。从设置函数返回的任何内容都将绑定到该组件。对于搜索和分页示例,您可以编写一个组合函数来执行检索搜索结果的特定逻辑,而另一个组合函数则可以提供在 UI 中实现上一个和下一个按钮所需的更通用的功能:
ts
export default defineComponent({
setup() {
const { loading, data, getResults } = useSearch()
const { nextPage, previousPage } = usePagination()
// Anything returned here will be available in the component - eg this.loading
return { loading, data, getResults, nextPage, previousPage }
}
})
相比 Vue 2 的 Mixins,setup 功能可以让你快速查看组件绑定了哪些属性和方法,而不需要打开多个文件。
官方文档对Composition API进行了出色的描述,并且有一个关于 Composition API 的精彩 Vue Mastery 视频,很好地解释了问题和解决方案。
我假设您已经观看了视频并阅读了文档,并将直接进入一个具体的示例 -身份验证。
身份验证问题
身份验证是许多应用程序必须克服的问题。用户可能需要提供其登录凭据才能查看网站上的某些页面或订阅访问某些功能。
以 Neoflix 为例,用户需要注册并购买订阅才能观看或在线观看电影和电视节目。HTTPPOST
请求/auth/register
将创建一个新帐户,而 HTTPPOST
请求/auth/login
将向用户发送一个JWT 令牌,该令牌将传递给每个请求。
管理状态组合函数
由于用户详细信息将在多个组件中需要,我们需要将其保存到应用程序的全局状态中。在研究 Vue 2 和 Vue 3 版本之间的差异时,我偶然发现了一篇文章,其中解释了Vue 3 中可能不再需要 Vuex 进行全局状态管理,这将减少依赖项的数量。
这种模式很像React Hooks,其中你调用一个函数来创建一个引用和一个 setter 函数,然后在渲染函数中使用引用。
本文提供了此代码示例来解释其工作原理:
ts
import { reactive, provide, inject } from 'vue';
export const stateSymbol = Symbol('state');
export const createState = () => reactive({ counter: 0 });
export const useState = () => inject(stateSymbol);
export const provideState = () => provide(
stateSymbol,
createState()
);
您可以使用该inject
函数通过符号注册状态对象,然后使用该provide
函数稍后调用该状态。
或者更简单地说,您可以创建一个反应变量,然后在函数中返回它以及操作状态所需的任何方法:
import { ref } from 'vuex'
const useState = () => {
const counter = ref(1)
const increment = () => counter.value++
}
const { counter, increment } = useState()
increment() // counter will be 2
整个use[Something]
模式感觉有点像React Hook,一开始让我感觉有点像“如果我想使用 Hooks,那么我就可以只使用 React”——但这种想法随着时间的推移已经消失了,现在它是有意义的。
API 交互
为了与 API 交互,我们将使用axois包。
npm i --save axios
我们可以创建一个具有一些基本配置的 API 实例,该实例将在整个应用程序中使用。
ts
// src/modules/api.ts
export const api = axios.create({
baseURL: process.env.VUE_APP_API || 'http://localhost:3000/'
})
更好的是,为了避免重复调用 API 所需的代码,我们可以创建一个组合函数,用于整个应用程序的所有 API 交互。为此,我们可以创建一个提供程序函数,该函数公开一些有用的变量,这些变量可用于处理任何组件内部的加载状态:
loading: boolean
- 一个指示器,让我们知道钩子当前是否正在加载数据data: any
- 数据加载完成后,更新属性error?: Error
- 如果出现任何问题,在 API 中显示错误消息会很有用
为了让组件根据变量的变化进行更新,我们需要创建一个对响应式变量的引用。我们可以通过导入函数来实现。该函数接受一个可选参数,即初始状态。ref
例如,当我们使用此钩子时,loading
状态默认应为 true,并在 API 调用成功后设置为 false。在请求完成之前,数据和错误变量将处于未定义状态。
然后我们可以在对象中返回这些变量,以便在组件的setup
函数中解构它们。
ts
// src/modules/api.ts
import { ref } from 'vue'
export const useApi(endpoint: string) => {
const loading = ref(true)
const data = ref()
const error = ref()
// ...
return {
loading, data, error
}
}
要更新这些变量,您可以.value
在反应对象上进行设置 - 例如loading.value = false
。
然后,我们可以使用 Vue 导出的函数创建一些计算变量,以便在组件中使用computed
。例如,如果 API 返回错误,我们可以使用计算errorMessage
属性从 API 响应中提取消息或详细信息。
ts
// src/modules/api.ts
import { ref, computed } from 'vue'
const errorMessage = computed(() => {
if (error.value) {
return error.value.message
}
})
const errorDetails = computed(() => {
if ( error.value && error.value.response ) {
return error.value.response.data.message
}
})
验证错误时,Neoflix 的 Nest.js API 会返回一个400 Bad Request
包含各个错误信息的数组。您可以使用以下方式提取这些错误并将其转换为对象Array.reduce
:
ts
const errorFields = computed(() => {
if (error.value && Array.isArray(error.value.response.data.message)) {
return (error.value.response.data.message as string[]).reduce((acc: Record<string, any>, msg: string) => {
let [ field ] = msg.split(' ')
if (!acc[field]) {
acc[field] = []
}
acc[field].push(msg)
return acc
}, {}) // eg. { email: [ 'email is required' ] }
}
})
最后,我们可以创建一个方法来包装GET
请求POST
并在成功或错误时更新反应变量:
ts
const post = (payload?: Record<string, any>) => {
loading.value = true
error.value = undefined
return api.post(endpoint, payload)
// Update data
.then(res => data.value = res.data)
.catch(e => {
// If anything goes wrong, update the error variable
error.value = e
throw e
})
// Finally set loading to false
.finally(() => loading.value = false)
}
综合起来,该函数看起来如下:
ts
// src/modules/api.ts
export const useApi(endpoint: string) => {
const data = ref()
const loading = ref(false)
const error = ref()
const errorMessage = computed(() => { /* ... */ })
const errorDetails = computed(() => { /* ... */ })
const errorFields = computed(() => { /* ... */ })
const get = (query?: Record<string, any>) => { /* ... */ }
const post = (payload?: Record<string, any>) => { /* ... */ }
return {
data, loading, error,
errorMessage, errorDetails, errorFields,
get, post,
}
}
现在我们有一个钩子,当我们需要向 API 发送请求时,它可以在整个应用程序中使用。
注册用户
该POST /auth/register
端点需要输入邮箱、密码、出生日期,并可选地接受名字和姓氏。由于我们正在构建一个 TypeScript 应用程序,我们可以将其定义为一个接口,以确保代码的一致性:
ts
// src/views/Register.vue
interface RegisterPayload {
email: string;
password: string;
dateOfBirth: Date;
firstName?: string;
lastName?: string;
}
在 Vue 3 中,你可以defineComponent
返回一个普通的对象。在本例中,我们有一个函数,setup
它使用 Composition 函数来创建 API。
作为设置函数的一部分,我们可以调用它useApi
来与 API 进行交互。在本例中,我们需要发送一个POST
请求,/auth/register
以便使用useApi
上面的函数来提取组件中所需的变量。
ts
// src/views/Register.vue
import { useApi } from '@/modules/api'
export default defineComponent({
setup() {
// Our setup function
const {
error,
loading,
post,
data,
errorMessage,
errorDetails,
errorFields,
} = useApi('/auth/register');
// ...
return {
error,
loading,
post,
data,
errorMessage,
errorDetails,
errorFields,
}
},
});
post
我们钩子中的方法需要useApi
一个有效载荷,因此我们可以在 setup 函数中初始化它们。之前,我们使用该ref
函数创建单独的响应式属性,但这在解构时会变得有点笨拙。
相反,我们可以使用reactive
导出的函数——这将省去在将每个属性传递给函数时调用vue
它们的麻烦。当将这些属性传递给组件时,我们可以使用该函数将它们转换回响应式属性。.value
post
toRefs
ts
// src/views/Register.vue
import { reactive, toRefs } from 'vue'
const payload = reactive<RegisterPayload>({
email: undefined,
password: undefined,
dateOfBirth: undefined,
firstName: undefined,
lastName: undefined,
});
// ...
return {
...toRefs(payload), // email, password, dateOfBirth, firstName, lastName
error,
loading,
post,
data,
errorMessage,
errorDetails,
errorFields,
}
然后,我们可以创建一个submit
可在组件内使用的方法,以触发对 API 的请求。这将调用从 导出的 post 方法useApi
,该方法在底层触发请求并更新error
、loading
和post
。
ts
const submit = () => {
post(payload).then(() => {
// Update user information in global state
// Redirect to the home page
});
};
我将省略此查询的整个<template>
部分,但变量的使用方式与 Vue 2 应用程序相同。例如,使用以下命令将电子邮件和密码分配给输入v-model
,并将提交函数分配给标签@submit
上的事件<form>
。
html
<form @submit.prevent="send">
<input v-model="email" />
<input v-model="password" />
<!-- etc... -->
</form>
将用户保存到全局状态
为了在整个应用程序中使用用户的身份验证详细信息,我们可以创建另一个引用全局状态对象的钩子。同样,这是 TypeScript,所以我们应该创建接口来表示状态:
ts
// src/modules/auth.ts
interface User {
id: string;
email: string;
dateOfBirth: Date;
firstName: string;
lastName: string;
access_token: string;
}
interface UserState {
authenticating: boolean;
user?: User;
error?: Error;
}
下一步是为模块创建初始状态:
ts
// src/modules/auth.ts
const state = reactive<AuthState>({
authenticating: false,
user: undefined,
error: undefined,
})
然后,我们可以创建一个useAuth
函数,该函数将提供当前状态和方法,用于在成功验证后设置当前用户或在注销时取消设置用户。
ts
// src/modules/auth.ts
export const useAuth = () => {
const setUser = (payload: User, remember: boolean) => {
if ( remember ) {
// Save
window.localStorage.setItem(AUTH_KEY, payload[ AUTH_TOKEN ])
}
state.user = payload
state.error = undefined
}
const logout = (): Promise<void> => {
window.localStorage.removeItem(AUTH_KEY)
return Promise.resolve(state.user = undefined)
}
return {
setUser,
logout,
...toRefs(state), // authenticating, user, error
}
}
然后我们可以使用以下函数将组件组合在一起:
// src/views/Register.vue
import { useRouter } from 'vue-router'
import { useApi } from "../modules/api";
import { useAuth } from "../modules/auth";
// ...
export default defineComponent({
components: { FormValidation, },
setup() {
// Reactive variables for the Register form
const payload = reactive<RegisterPayload>({
email: undefined,
password: undefined,
dateOfBirth: undefined,
firstName: undefined,
lastName: undefined,
});
// State concerning the API call
const {
error,
loading,
post,
data,
errorMessage,
errorDetails,
errorFields,
computedClasses,
} = useApi("/auth/register");
// Function for setting the User
const { setUser } = useAuth()
// Instance of Vue-Router
const router = useRouter()
const submit = () => {
// Send POST request to `/auth/register` with the payload
post(payload).then(() => {
// Set the User in the Auth module
setUser(data.value, true)
// Redirect to the home page
router.push({ name: 'home' })
})
}
return {
...toRefs(payload),
submit,
loading,
errorMessage,
errorFields,
errorDetails,
computedClasses,
}
}
})
记住用户
上面的 auth 模块用于window.localStorage
保存用户的访问令牌(AUTH_TOKEN
)——如果用户返回网站,我们可以在用户下次访问网站时使用该值重新对其进行身份验证。
为了监视响应式变量的变化,我们可以使用该watch
函数。它接受两个参数:一个响应式变量数组和一个回调函数。我们可以使用它来调用/auth/user
端点来验证令牌。如果 API 返回有效响应,我们应该将用户状态设置为全局状态,否则从本地存储中移除令牌。
// src/modules/auth.ts
const AUTH_KEY = 'neoflix_token'
const token = window.localStorage.getItem(AUTH_KEY)
if ( token ) {
state.authenticating = true
const { loading, error, data, get } = useApi('/auth/user')
get({}, token)
watch([ loading ], () => {
if ( error.value ) {
window.localStorage.removeItem(AUTH_KEY)
}
else if ( data.value ) {
state.user = data.value
}
state.authenticating = false
})
}
登录
登录组件的设置功能几乎相同,只是我们调用了不同的 API 端点:
const {
loading,
data,
error,
post,
errorMessage,
errorFields
} = useApi("auth/login")
// Authentication details
const { setUser } = useAuth();
// Router instance
const router = useRouter();
// Component data
const payload = reactive<LoginPayload>({
email: undefined,
password: undefined,
rememberMe: false,
});
// On submit, send POST request to /auth/login
const submit = () => {
post(payload).then(() => {
// If successful, update the Auth state
setUser(data.value, payload.rememberMe);
// Redirect to the home page
router.push({ name: "home" });
});
};
return {
loading,
submit,
errorMessage,
...toRefs(payload),
};
使用组件中的数据
要在组件内使用用户的信息,我们可以导入相同的useAuth
函数并访问该user
值。
例如,我们可能希望在顶部导航中添加个性化的欢迎信息。
Neoflix 注册时不需要用户的名字,因此我们可以使用该computed
函数返回一个条件属性。如果用户有名字,我们将显示一条Hey, {firstName}
消息,否则返回通用Welcome back!
消息。
// src/components/Navigation.vue
import { computed, defineComponent } from "vue";
import { useAuth } from "../modules/auth";
export default defineComponent({
setup() {
const { user } = useAuth()
const greeting = computed(() => {
return user?.value && user.value.firstName
? `Hey, ${user.value.firstName}!`
: 'Welcome back!'
})
return { user, greeting }
}
})
注销
我们已经在 的logout
返回中添加了一个方法useAuth
。可以从新setup
组件的方法中调用该方法来清除用户信息并将其重定向回登录页面。
// src/views/Logout.vue
import { defineComponent } from "vue"
import { useRouter } from "vue-router"
import { useAuth } from "../modules/auth"
export default defineComponent({
setup() {
const { logout } = useAuth()
const router = useRouter()
logout().then(() => router.push({ name: 'login' }))
}
})
保护路线
在此应用程序中,除非用户已登录,否则应限制用户只能使用登录或注册路由。由于我们在此应用程序中使用vue-router,因此我们可以使用路由元字段来定义应保护哪些路由:
// src/router/index.ts
const routes = [
{
path: '/',
name: 'home',
component: Home,
meta: { requiresAuth: true },
},
// ...
}
如果requiresAuth
设置为 true,我们应该检查 提供的用户useAuth
。如果尚未设置用户,我们应该将用户重定向到登录页面。
user
我们可以通过访问返回的对象来判断用户是否已登录useAuth
。如果当前路由的元数据表明该路由受到限制,则应将其重定向到登录页面。
相反,如果用户在登录或注册页面但已经登录,我们应该将他们重定向回主页。
// src/router/index.ts
router.beforeEach((to, from, next) => {
const { user } = useAuth()
// Not logged into a guarded route?
if ( to.meta.requiresAuth && !user?.value ) next({ name: 'login' })
// Logged in for an auth route
else if ( (to.name == 'login' || to.name == 'register') && user!.value ) next({ name: 'home' })
// Carry On...
else next()
})
结论
我越习惯新的 Composition API,就越喜欢它。Vue 3 还处于早期阶段,目前的示例还不多,所以到时候你可能会发现这篇文章的内容并非最佳方案。如果你有不同的做法,请在评论区告诉我。
我将在Neo4j Twitch 频道的直播中演示该应用程序的构建过程。欢迎每周二英国标准时间 13:00、欧洲中部夏令时间 14:00 加入我的直播,或者在Neo4j YouTube 频道观看视频。
文章来源:https://dev.to/adamcowley/how-to-build-an-authentication-into-a-vue3-application-200b