发布于 2025-12-07 0 阅读
0

如何在 Vue3 应用程序中构建身份验证 如何在 Vue3 应用程序中构建身份验证

如何在 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 }
  }
})


Enter fullscreen mode Exit fullscreen mode

相比 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()
);


Enter fullscreen mode Exit fullscreen mode

您可以使用该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


Enter fullscreen mode Exit fullscreen mode

整个use[Something]模式感觉有点像React Hook,一开始让我感觉有点像“如果我想使用 Hooks,那么我就可以只使用 React”——但这种想法随着时间的推移已经消失了,现在它是有意义的。

API 交互

为了与 API 交互,我们将使用axois包。



npm i --save axios


Enter fullscreen mode Exit fullscreen mode

我们可以创建一个具有一些基本配置的 API 实例,该实例将在整个应用程序中使用。


 ts
// src/modules/api.ts
export const api = axios.create({
  baseURL: process.env.VUE_APP_API || 'http://localhost:3000/'
})


Enter fullscreen mode Exit fullscreen mode

更好的是,为了避免重复调用 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
  }
}


Enter fullscreen mode Exit fullscreen mode

要更新这些变量,您可以.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
  }
})


Enter fullscreen mode Exit fullscreen mode

验证错误时,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' ] }
  }
})


Enter fullscreen mode Exit fullscreen mode

最后,我们可以创建一个方法来包装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)
}


Enter fullscreen mode Exit fullscreen mode

综合起来,该函数看起来如下:


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


Enter fullscreen mode Exit fullscreen mode

现在我们有一个钩子,当我们需要向 API 发送请求时,它可以在整个应用程序中使用。

注册用户

POST /auth/register端点需要输入邮箱、密码、出生日期,并可选地接受名字和姓氏。由于我们正在构建一个 TypeScript 应用程序,我们可以将其定义为一个接口,以确保代码的一致性:


 ts
// src/views/Register.vue
interface RegisterPayload {
  email: string;
  password: string;
  dateOfBirth: Date;
  firstName?: string;
  lastName?: string;
}


Enter fullscreen mode Exit fullscreen mode

在 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,
    }
  },
});


Enter fullscreen mode Exit fullscreen mode

post我们钩子中的方法需要useApi一个有效载荷,因此我们可以在 setup 函数中初始化它们。之前,我们使用该ref函数创建单独的响应式属性,但这在解构时会变得有点笨拙。

相反,我们可以使用reactive导出的函数——这将省去在将每个属性传递给函数时调用vue它们的麻烦。当将这些属性传递给组件时,我们可以使用该函数将它们转换回响应式属性。.valueposttoRefs


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


Enter fullscreen mode Exit fullscreen mode

然后,我们可以创建一个submit可在组件内使用的方法,以触发对 API 的请求。这将调用从 导出的 post 方法useApi,该方法在底层触发请求并更新errorloadingpost


 ts
const submit = () => {
  post(payload).then(() => {
    // Update user information in global state

    // Redirect to the home page
  });
};


Enter fullscreen mode Exit fullscreen mode

我将省略此查询的整个<template>部分,但变量的使用方式与 Vue 2 应用程序相同。例如,使用以下命令将电子邮件和密码分配给输入v-model,并将提交函数分配给标签@submit上的事件<form>


 html
<form @submit.prevent="send">
    <input v-model="email" />
    <input v-model="password" />
    <!-- etc... -->
</form>


Enter fullscreen mode Exit fullscreen mode

登记表

在此处查看组件代码...

将用户保存到全局状态

为了在整个应用程序中使用用户的身份验证详细信息,我们可以创建另一个引用全局状态对象的钩子。同样,这是 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;
}


Enter fullscreen mode Exit fullscreen mode

下一步是为模块创建初始状态:


 ts
// src/modules/auth.ts
const state = reactive<AuthState>({
    authenticating: false,
    user: undefined,
    error: undefined,
})


Enter fullscreen mode Exit fullscreen mode

然后,我们可以创建一个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
  }
}


Enter fullscreen mode Exit fullscreen mode

然后我们可以使用以下函数将组件组合在一起:



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


Enter fullscreen mode Exit fullscreen mode

记住用户

上面的 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
  })
}


Enter fullscreen mode Exit fullscreen mode

登录

登录表单

登录组件的设置功能几乎相同,只是我们调用了不同的 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),
};


Enter fullscreen mode Exit fullscreen mode

使用组件中的数据

要在组件内使用用户的信息,我们可以导入相同的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 }
  }
})


Enter fullscreen mode Exit fullscreen mode

注销

我们已经在 的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' }))
  }
})


Enter fullscreen mode Exit fullscreen mode

保护路线

在此应用程序中,除非用户已登录,否则应限制用户只能使用登录或注册路由。由于我们在此应用程序中使用vue-router,因此我们可以使用路由元字段来定义应保护哪些路由:



// src/router/index.ts
const routes = [
  {
    path: '/',
    name: 'home',
    component: Home,
    meta: { requiresAuth: true },
  },
  // ...
}


Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode




结论

我越习惯新的 Composition API,就越喜欢它。Vue 3 还处于早期阶段,目前的示例还不多,所以到时候你可能会发现这篇文章的内容并非最佳方案。如果你有不同的做法,请在评论区告诉我。

我将在Neo4j Twitch 频道的直播中演示该应用程序的构建过程。欢迎每周二英国标准时间 13:00、欧洲中部夏令时间 14:00 加入我的直播,或者在Neo4j YouTube 频道观看视频

流期间构建的所有代码均可在 Github 上找到

文章来源:https://dev.to/adamcowley/how-to-build-an-authentication-into-a-vue3-application-200b