Vue3中实现登录功能,通常涉及到创建一个表单,用户输入用户名和密码,然后将信息发送到后端进行验证,得到响应结果后作出相应操作。
一、创建项目
这里他用pnpm进行项目的创建的,所以需要事先全局安装pnpm(在安装pnpm前,先确认本地电脑上是否已安装nodejs),命令如下:
npm install -g pnpm
1.1 创建vue
使用pnpm创建vue项目命令如下:
pnpm create vue
执行后作出相应选择,选择你项目中需要的选项即可。示例如下:
D:\workspace\vscode>pnpm create vue
Vue.js - The Progressive JavaScript Framework
√ 请输入项目名称: ... vue-project
√ 是否使用 TypeScript 语法? ... 否 / 是
√ 是否启用 JSX 支持? ... 否 / 是
√ 是否引入 Vue Router 进行单页面应用开发? ... 否 / 是
√ 是否引入 Pinia 用于状态管理? ... 否 / 是
√ 是否引入 Vitest 用于单元测试? ... 否 / 是
√ 是否要引入一款端到端(End to End)测试工具? » 不需要
√ 是否引入 ESLint 用于代码质量检测? ... 否 / 是
√ 是否引入 Prettier 用于代码格式化? ... 否 / 是
√ 是否引入 Vue DevTools 7 扩展用于调试? (试验阶段) ... 否 / 是
正在初始化项目 D:\workspace\vscode\vue-project...
项目初始化完成,可执行以下命令:
cd vue-project
pnpm install
pnpm format
pnpm dev
1.2 初始化项目
当项目创建完成后,根据提示命令对项目进行初始化,先输入命令进入项目目录:
cd vue-project
然后执行install命令,初始化项目:
D:\workspace\vscode\vue-project>pnpm install
╭──────────────────────────────────────────────────────────────────╮
│ │
│ Update available! 9.7.0 → 9.10.0. │
│ Changelog: https://github.com/pnpm/pnpm/releases/tag/v9.10.0 │
│ Run "pnpm add -g pnpm" to update. │
│ │
│ Follow @pnpmjs for updates: https://x.com/pnpmjs │
│ │
╰──────────────────────────────────────────────────────────────────╯
WARN 5 deprecated subdependencies found: @humanwhocodes/config-array@0.11.14,
@humanwhocodes/object-schema@2.0.3, glob@7.2.3, inflight@1.0.6, rimraf@3.0.2
Packages: +425
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
++++++++++++++++++++++++++++++
Progress: resolved 463, reused 272, downloaded 153, added 425, done
node_modules/.pnpm/esbuild@0.21.5/node_modules/esbuild: Running postinstall script...
node_modules/.pnpm/esbuild@0.21.5/node_modules/esbuild: Running postinstall script,
done in 341msstinstall script, done
in 209ms
dependencies:
+ pinia 2.2.2
+ vue 3.5.3
+ vue-router 4.4.3
devDependencies:
+ @rushstack/eslint-patch 1.10.4
+ @tsconfig/node20 20.1.4
+ @types/jsdom 21.1.7
+ @types/node 20.16.5 (22.5.4 is available)
+ @vitejs/plugin-vue 5.1.3
+ @vitejs/plugin-vue-jsx 4.0.1
+ @vue/eslint-config-prettier 9.0.0
+ @vue/eslint-config-typescript 13.0.0
+ @vue/test-utils 2.4.6
+ @vue/tsconfig 0.5.1
+ eslint 8.57.0 (9.10.0 is available)
+ eslint-plugin-vue 9.28.0
+ jsdom 24.1.3 (25.0.0 is available)
+ npm-run-all2 6.2.2
+ prettier 3.3.3
+ typescript 5.4.5 (5.6.2 is available)
+ vite 5.4.3
+ vite-plugin-vue-devtools 7.4.4
+ vitest 1.6.0 (2.0.5 is available)
+ vue-tsc 2.1.6
Done in 9.8s
初始化项目后,则可以运行项目了,当执行pnpm dev后您会发现,瞬间项目就运行起来了,比vue2+webpack时快了很多。
D:\workspace\vscode\vue-project>pnpm dev
> vue-project@0.0.0 dev D:\workspace\vscode\vue-project
> vite
VITE v5.4.3 ready in 1285 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ Vue DevTools: Open http://localhost:5173/__devtools__/ as a separate window
➜ Vue DevTools: Press Alt(⌥)+Shift(⇧)+D in App to toggle the Vue DevTools
➜ press h + enter to show help
二、Pinia
在创建项目时,我们就已经选择安装了Pinia,这里就不重复安装了。
pinia是Vue的存储库,它允许您跨组件/页面共享状态。它Vuex的替代品,其功能和性能更优于vuex,在后期使用中会逐渐被大家发现。
这里我们先使用pinia全局存储用户信息、接口访问令牌、菜单列表等数据,当然这些只是作为一个项目的基础部分,其他共享信息可根据你们的项目需求进行添加。
关于这部分,就不细讲了,之前一篇已详细讲解过,地址:Vue3入门 - Pinia使用(替代Vuex)-CSDN博客
Pinia官方文档:Pinia 中文文档
三、路由创建
在创建Vue项目里,如果你已在选项中选择安装了vue-router,则此时您的目录中肯定已有router/index.ts文件,代码如下:
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue')
}
]
})
export default router
此时,我们将目录中多余的文件删除,创建首页、登录页面、404页面、layout文件等,如下图:
- components/LayoutIndex.vue:主界面公共框架页面
- views/NotFound.vue:404页面
- views/HomeIndex.vue:首页
- views/LoginIndex.vue:登录界面
3.1 App.vue
首页清理App.vue中代码,将多余代码全部清除,保留RouterView视图组件,代码如下:
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>
<style>
*{ margin: 0; padding: 0; }
#app{
width: 100%;
min-height: 100%;
font-size: 12px;
font-family: 'Segoe UI', Tahoma, Verdana, sans-serif;
}
</style>
3.2 LayoutIndex.vue
在components目录中创建LayoutIndex.vue组件,用于编写主界面框,及主界面头部、左侧导航后期都将在此组件中引用并展示。由于该篇只讲登录界面,还未涉及主界面,所以先添加视图组件留位占坑,代码如下:
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>
<template>
<RouterView></RouterView>
</template>
3.3 NotFound.vue
当路由错误,找不到对应页面时,通过路由匹配跳转到404页面,显示页面不存在等错误提示。由于是演示界面,样式就随便写了,大家可自行优化。代码如下:
<template>
<div class="error-wrap">
<h3>错误:页面不存在~</h3>
<p class="intro">404</p>
</div>
</template>
<style scoped>
.error-wrap{
width: 100%;
min-height: 100vh;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.error-wrap h3{ font-size: 30px; margin-bottom: 20px; }
.error-wrap p.intro{ font-size: 62px; font-weight: bold; }
</style>
3.4 HomeIndex.vue
首页也先留坑占位,等后续讲了主界面时,再详细说明。代码如下:
<template>
<div>
首页
</div>
</template>
3.5 LoginIndex.vue
登录界面也先留坑,待下面讲到“登录界面开发”时再细讲,代码如下:
<template>
<div>
<h3>登录页面</h3>
</div>
</template>
3.6 路由配置
当基本页面创建完成后,我们就可以进入router/index.ts文件中,对路由进行配置了。代码如下:
import { createRouter, createWebHistory } from 'vue-router'
import LayoutIndex from '@/components/LayoutIndex.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'index',
component: LayoutIndex,
redirect: "/home",
children: [
{
path: '/home',
name: 'home',
meta: {
requiresAuth: true, // 标识该路由需要校验用户是否登录
title: "首页"
},
component: () => import('@/views/HomeIndex.vue')
}
]
},
{
path: '/login',
name: 'login',
meta: {
title: "登录界面"
},
component: () => import('@/views/LoginIndex.vue')
},
{
path: '/:catchAll(.*)',
name: "NotFound",
component: () => import('@/views/NotFound.vue')
}
]
})
export default router
当上述代码配置完成后,则可以通过浏览器地址栏,进行路由跳转了。
注意:Vue3中是使用 :pathMach(.*) 或者 :catchAll(.*) 匹配所有路径,Vue2中是使用"*"号。如果在Vue3中使用星号,则会报错:Error: Catch all routes ("*") must now be defined using a param with a custom regexp.
3.7 导航守卫
正如其名,vue-router提供的导航守卫主要用来通过跳转或取消的方式守卫导航。这里有很多方式植入路由导航中:全局的、单个路由独享的、或者组件级的。
这里使用是全局前置守卫,可以使用router.beforeEach注册,示例如下:
const router = createRouter({ ... })
router.beforeEach((to, from) => {
// ...
// 返回 false 以取消导航
return false
})
守卫方法参数:
序号 | 字段 | 说明 |
---|---|---|
1 | to | 即将要进入的目标 |
2 | from | 当前导航正要离开的路由 |
3 | next | 可选参数。建议使用Vue3新写法,通过返回值进行跳转。 |
返回的值如下:
序号 | 返回值 | 说明 |
---|---|---|
1 | false | 取消当前的导航。如果浏览器URL改变了(可能是用户手动或者浏览器后退按钮),那么URL地址会重置到from路由对应的址址。 |
2 | 路由地址 | 通过一个路由地址重写向到一个不同的地址,如果调用router.push(),且可以传入诸如replace: true或name: 'home' 之类的选项。它会中断当前的导航,同时用相同的from创建一个新导航。 |
3.7.1 新增是否登录校验
这里我们在stores/modules/login.ts文件中,新增校验用户是否登录的计算属性,代码如下:
import { defineStore } from 'pinia'
import type { UserInfoType } from '@/types/global'
import { useUserInfoStore, useAccessTokenStore } from '../global'
import { computed } from 'vue'
// 登录统一处理用户信息和访问令牌
export const useLoginStore = defineStore('login', () => {
const { userInfoChange, user_info } = useUserInfoStore()
const { accessTokenChange, access_token } = useAccessTokenStore()
// 定义getters,判断用户是否登录(当用户id不等于undefined存在时,并且访问令牌不为空时,表示用户已登录)
const is_login = computed(() => 'undefined' !== typeof user_info.value['id'] && access_token.value != '')
// 定义actions修改用户信息和访问令牌令牌
const loginChange = (_userInfo: UserInfoType, _token: string) => {
userInfoChange(_userInfo)
accessTokenChange(_token)
}
return { is_login, loginChange }
})
3.7.2 守卫校验登录状态
在router/index.ts文件中,引入useLoginStore,并在导航卫士执行时,判断该用户是否登录并作出相应操作,代码如下:
import { createRouter, createWebHistory } from 'vue-router'
import LayoutIndex from '@/components/LayoutIndex.vue'
import { useLoginStore } from '@/stores'
const router = createRouter({
// 略...
})
// 登录路由地址
const loginPath = '/login'
// 路由卫士
router.beforeEach((to) => {
const stores = useLoginStore() // 获取用户状态库
if(to.meta){
// 修改界面标题
if('undefined' !== typeof to.meta.title) document.title = to.meta.title
// 该路由需要校验登录状态,并且用户未登录,则跳转到登录界面
if (to.meta.requiresAuth && !stores.is_login && to.path != loginPath) return loginPath
}
})
export default router
此时上述代码在 document.title = to.meta.title处会报错:Type "{} | null" cannot be assigned to type "string".Cannot assign type "null" to type "string"。意思这里可能会给document.title赋值一个{}或null值,将其作以下修改:
document.title = to.meta.title || ''
此时则会报错:Cannot assign type '{}' to type 'string'。意思不能将类型"{}"分配给类型“string”。官方给出的解决方式是扩展RouteMeta接口来输入meta字段。代码如下:
// 在 src/types目录下, 新建 vue-router.d.ts类型声明文件
import "vue-router"
declare module 'vue-router' {
interface RouteMeta {
title?: string // 可选
}
}
上述问题都解决后,输入不存在路由则会跳转到404页面;当输入“/home”主页路径则会直接跳转到登录界面,这是因为首页路由在meta中添加requiresAuth为true,当进入该页面前,导航守卫中判断用户是否登录,未登录则会跳转到登录界面。
四、Element Plus
在讲axios请求前,我们先讲下Element Plus的安装;写过vue2的朋友肯定知道,在Vue2中一般是使用Element-UI组件库,在Vue3中开始使用Element Plus。
4.1 安装
pnpm add element-plus
4.2 引用
打开main.ts文件,在内部添加Element Plus的样式引入,以及组件安装。代码如下:
import { createApp } from 'vue'
import pinia from '@/stores'
import App from './App.vue'
import router from './router'
// 引入Element Plus样式
import 'element-plus/dist/index.css'
// 引入Element Plus
import ElementPlus from 'element-plus'
const app = createApp(App)
app.use(pinia)
app.use(router)
app.use(ElementPlus)
app.mount('#app')
4.3 自动导入
按需导入需要使用额外的插件来导入要使用的组件,首先需要安装unplugin-vue-components 和 unplugin-auto-import这两款插件,代码如下:
pnpm add -D unplugin-vue-components unplugin-auto-import
本次演示项目使用的是vite,配置如下:
// vite.config.ts
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
// ...
plugins: [
// ...
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})
如果使用webpack可去官方查看,地址:https://element-plus.org/zh-CN/guide/quickstart.html
当上述配置完成后,我们在components目录中按驼峰模式命名的组件,则会按需直接在页面使用(注意:页面使用时,将驼峰模式修改为 - 分隔使用),代码如下:
<header-compt></header-compt>
五、axios封装
对于一个项目,与后台交互,进行Http请求是基本需求。在Vue3中使用axios普遍,所以要进行后续开发,需要先安装Axios。
5.1 安装axios
pnpm add axios
5.2 封装axios
在项目中,可以在组件中直接引用axios发送请求,但通常将axios作为底层库,再将其封装一个axios实例对象导出。代码如下:
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { useAccessTokenStore } from '@/stores'
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
const instance = axios.create({
baseURL: 'API请求地址',
timeout: 1000 * 60
})
// 添加请求拦截器
instance.interceptors.request.use(
(config) => {
// 获取访问令牌
const { access_token } = useAccessTokenStore()
// 如果令牌存在,则追加到header中
if (access_token) config.headers['accesstoken'] = access_token
return config
},
(error) => {
// 对请求错误做些什么
return Promise.reject(error)
}
)
// 添加响应拦截器
instance.interceptors.response.use(
(response) => {
if (response.data.code == 200) {
return response.data
}
ElMessage.error(response.data.message) // 错误提示
return Promise.reject(response.data)
},
(error) => {
// 对响应错误做点什么
return Promise.reject(error)
}
)
export default instance
5.2 创建API
当axios封装好后,在src目录下创建api文件夹,用于定义项目中所有api请求函数。示例如下:
import Request from '@/utils/request'
/**
* 登录接口
* @param {*} params
* @returns
*/
export const loginService = (params) => {
return Request.post("/login", params)
}
六、SASS安装
接下来就要编写登录界面了,这里我们将要使用到CSS的预编译工具,例如有Styus、Less、Sass等,这里将使用Sass进行样式编写。安装命令如下:
pnpm add sass
安装成功后,我们将3.3 NotFound.vue中的样式重新书写,在标签style的属性lang中赋值scss即可。代码如下:
<template>
<div class="error-wrap">
<h3>错误:页面不存在~</h3>
<p class="intro">404</p>
</div>
</template>
<style lang="scss" scoped>
.error-wrap{
width: 100%;
min-height: 100vh;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
h3{
font-size: 30px;
margin-bottom: 20px;
}
p.intro{
font-size: 62px;
font-weight: bold;
}
}
</style>
七、登录界面开发
7.1 页面编写
这里css单独定义scss文件,通过@import引入到登录界面,代码如下:
.login-wrap{ width: 100%;
.right-large-thumb, .login-box{
height: 100vh; display: flex; justify-content: center; align-items: center;
}
.right-large-thumb{ background-color: aqua;
h3{ font-size: 36px; }
}
.login-box{ padding: 30px; box-sizing: border-box;
.ruleForm-box{ width: 50%; }
}
}
vue页面,代码如下:
<script setup lang="ts">
import { reactive, ref } from 'vue'
import type { ComponentSize, FormInstance, FormRules } from 'element-plus'
import type { RuleForm } from '@/interface/user'
import { loginService } from '@/api/index'
defineOptions({
name: 'LoginIndex'
})
const formSize = ref<ComponentSize>('default')
const ruleFormRef = ref<FormInstance>()
const ruleForm = reactive<RuleForm>({
name: '',
password: ''
})
const rules = reactive<FormRules<RuleForm>>({
name: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
})
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
// do something...
} else {
console.log('error submit!', fields)
}
})
}
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
}
</script>
<template>
<div class="login-wrap">
<el-row>
<el-col :span="12">
<div class="right-large-thumb">
<h3>DEMO系统</h3>
</div>
</el-col>
<el-col :span="12">
<div class="login-box">
<el-form
ref="ruleFormRef"
:model="ruleForm"
:rules="rules"
label-width="auto"
class="ruleForm-box"
:size="formSize"
status-icon
>
<el-form-item label="用户" prop="name">
<el-input v-model="ruleForm.name" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="ruleForm.password" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm(ruleFormRef)"> 登录 </el-button>
<el-button @click="resetForm(ruleFormRef)">重置</el-button>
</el-form-item>
</el-form>
</div>
</el-col>
</el-row>
</div>
</template>
<style lang="scss" scoped>
@import './index.scss';
</style>
页面样式效果如下:
7.2 API请求
上述代码中,完成登录界面框架搭建、输入框校验等操作,当用户输入符合要求的登录信息后,将输入信息提交到后台校验,待接口响应最终结果。如果登录失败则提示用户, 要是登录成功将接口返回的用户信息和访问令牌存储到useStore中,完成登录操作步骤。
上篇中已详细讲解过用户信息和访问令牌的useStore定义和数据持久化(地址:Vue3入门 - Pinia使用(替代Vuex)-CSDN博客),并且定义的userLoginStore统一存储用户信息和访问令牌,这里不再阐述。
代码如下:
import { reactive, ref } from 'vue'
import type { ComponentSize, FormInstance, FormRules } from 'element-plus'
import type { RuleForm } from '@/interface/user'
import { loginService } from '@/api/index'
import { useLoginStore } from '@/stores/index'
import { useRouter } from 'vue-router'
// 略...
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
console.log('submit!')
loginService(ruleForm)
.then((res) => {
console.log('success', res)
if(res.code == 200){
useLoginStore(res.data.userInfo, res.data.accesstoken)
setTimeout(() => {
const router = useRouter()
// 返回首页
router.push('/')
}, 1200)
}
})
.catch((e) => {
console.error('e', e)
})
} else {
console.log('error submit!', fields)
}
})
}
// 略...
当用户信息缓存后,则在3.7.1中讲到的is_login则为true,此时经过导航守卫则校验通过,会直接跳转到主界面。
登录部分就讲到这,希望对大家有所帮助~