前言
大家好!我是小凹。最近在做一个校园论坛项目,需要实现用户登录、注册功能。为了快速验证,我选择先用前端模拟的方式(localStorage + Pinia)把整个流程跑通,后续再接入真实后端。本文记录了这个过程中的技术选型、实现步骤以及遇到的各种“坑”,希望能给同样在路上的前端初学者一些帮助。
技术栈
- Vue 3(Composition API)
- Pinia(状态管理)
- Vue Router(路由守卫)
- localStorage(数据持久化)
功能需求
- 注册:学号(10位数字)、密码(字母+数字,8-16位)、姓名(非空),学号唯一性校验。
- 登录:学号 + 密码,验证成功后保存用户状态。
- 退出登录。
- 未登录时无法访问需要认证的页面(路由守卫)。
项目结构(仅展示关键部分)
src/
├── stores/
│ └── user.js // Pinia store
├── views/
│ ├── Login.vue
│ └── Register.vue
├── router/
│ └── index.js // 路由守卫
└── App.vue // 导航栏登录/退出按钮
实现步骤
1. 创建 Pinia Store
在 stores/user.js 中定义用户状态、登录/注册/退出方法。
import { ref, watch } from 'vue'
export const useUserStore = defineStore('user', () => {
const USER_KEY = 'forum-users'
const CURRENT_USER_KEY = 'forum-current-user'
const defaultUsers = [
{ sno: '6020240921', password: 'huhanyu62', name: '胡涵钰' },
{ sno: '6020240950', password: 'congjiu55', name: '冯语涵' }
]
// 初始化用户列表
let saved = null
try {
saved = JSON.parse(localStorage.getItem(USER_KEY))
} catch (e) {
console.warn(e)
}
const users = ref(saved || defaultUsers)
// 自动持久化用户列表
watch(users, (newUsers) => {
localStorage.setItem(USER_KEY, JSON.stringify(newUsers))
}, { deep: true })
// 当前登录用户
const currentUser = ref(null)
const savedCurrent = localStorage.getItem(CURRENT_USER_KEY)
if (savedCurrent) {
currentUser.value = JSON.parse(savedCurrent)
}
watch(currentUser, (newUser) => {
if (newUser) {
localStorage.setItem(CURRENT_USER_KEY, JSON.stringify(newUser))
} else {
localStorage.removeItem(CURRENT_USER_KEY)
}
})
// 登录
function userLogin(snoId, numId) {
const found = users.value.find(u => u.sno === snoId)
if (!found) return { success: false, msg: '学号不存在!' }
if (found.password !== numId) return { success: false, msg: '密码不正确!' }
currentUser.value = { sno: found.sno, name: found.name }
return { success: true, msg: '' }
}
// 注册
function addUser(nameId, snoId, numId) {
users.value.push({ sno: snoId, password: numId, name: nameId })
}
// 退出
function logout() {
currentUser.value = null
}
return { users, currentUser, userLogin, logout, addUser }
})
2. 注册页面(Register.vue)
关键点:
- 表单使用
@submit.prevent阻止默认提交(避免页面刷新)。 - 正则验证:学号10位数字,密码字母+数字组合,长度8-16。
- 注册前检查学号是否已被注册。
import { useUserStore } from '@/stores/user'
import { useRouter } from 'vue-router'
import { ref } from 'vue'
const router = useRouter()
const userStore = useUserStore()
const nameId = ref('')
const snoId = ref('')
const numId = ref('')
function validateSno(s) {
return /^\d{10}$/.test(s)
}
function validatePassword(p) {
return /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,16}$/.test(p)
}
function handleRegister() {
if (!validateSno(snoId.value)) {
alert('学号必须是10位数字')
return
}
if (!validatePassword(numId.value)) {
alert('密码必须包含字母和数字,长度8-16位')
return
}
// 注意:组件中访问 store 的 state 不需要 .value
if (userStore.users.some(u => u.sno === snoId.value)) {
alert('该学号已被注册!')
return
}
userStore.addUser(nameId.value, snoId.value, numId.value)
alert('注册成功!请重新登录!')
router.push('/Login')
}
</script>
<template>
<div class="Registerarea">
<h2>请完成注册</h2>
<div class="Register_form">
<form @submit.prevent="handleRegister">
<ul>
<li><label>姓名:</label><input type="text" v-model="nameId" /></li>
<li><label>学号:</label><input type="text" v-model="snoId" /></li>
<li><label>密码:</label><input type="text" v-model="numId" /></li>
<li><button type="submit">确认注册</button></li>
</ul>
</form>
</div>
</div>
</template>
3. 登录页面(Login.vue)
登录时只做非空校验,具体错误由 store 返回。
import { useUserStore } from '@/stores/user'
import { useRouter } from 'vue-router'
import { ref } from 'vue'
const router = useRouter()
const userStore = useUserStore()
const snoId = ref('')
const numId = ref('')
function handleLogin() {
if (!snoId.value || !numId.value) {
alert('学号和密码不能为空')
return
}
const result = userStore.userLogin(snoId.value, numId.value)
if (result.success) {
router.push('/')
} else {
alert(result.msg)
}
}
</script>
<template>
<div class="loginarea">
<h2>请完成登录</h2>
<div class="Login_form">
<form @submit.prevent="handleLogin">
<ul>
<li><label>学号:</label><input type="text" v-model="snoId" /></li>
<li><label>密码:</label><input type="text" v-model="numId" /></li>
<li><button type="submit">确认登录</button></li>
<li>没有账号?去<router-link to="/Register">注册</router-link></li>
</ul>
</form>
</div>
</div>
</template>
4. 路由守卫(router/index.js)
保护需要登录的页面,未登录自动跳转登录页。
import { useUserStore } from '@/stores/user'
// ... 导入组件
const routes = [
{ path: '/', component: Home, meta: { requiresAuth: true } }, // 需要登录
{ path: '/about', component: About, meta: { requiresAuth: true } },
{ path: '/Login', component: Login },
{ path: '/Register', component: Register }
]
const router = createRouter({ history: createWebHistory(), routes })
router.beforeEach((to, _, next) => {
const userStore = useUserStore()
if (to.meta.requiresAuth && !userStore.currentUser) {
next('/Login')
} else {
next()
}
})
export default router
5. 导航栏显示登录/退出按钮(App.vue)
<div>
<div v-if="!userStore.currentUser">
<router-link to="/Login">登录</router-link>
</div>
<div v-else>
<button @click="userStore.logout">退出登录</button>
</div>
</div>
</template>
<script setup>
import { useUserStore } from './stores/user'
const userStore = useUserStore()
</script>
遇到的坑 & 解决方案
坑1:表单提交后页面刷新,输入框被清空
原因:<form> 内没有阻止默认提交行为,导致页面重新加载。
解决:在 <form> 上添加 @submit.prevent。
坑2:Pinia 中访问 state 时误用 .value
现象:Cannot read properties of undefined (reading 'some')
原因:在组件中通过 useUserStore() 获取 store 后,Pinia 会自动解包所有 state(ref 变成普通值),但初学者容易惯性加上 .value,导致 store.users.value 为 undefined。
解决:记住:在组件中直接使用 store.users,不要加 .value;在 store 内部(如 addUser)才需要 users.value.push(...)。
坑3:注册时学号唯一性检查不生效
原因:检查逻辑写反了 —— if (!userStore.users.some(...)) 变成了“学号不存在时提示”。
解决:去掉取反,改为 if (userStore.users.some(...))。
坑4:路由守卫中未使用的参数
原因:beforeEach 回调签名为 (to, from, next),from 未使用。
解决:用下划线占位:(to, _, next),避免 ESLint 警告。
坑5:localStorage 中存储的数据格式错误
原因:JSON.parse 时如果数据损坏或不存在会抛出异常。
解决:使用 try...catch 包裹,提供默认值。
总结
通过这个小项目,我巩固了 Pinia 的使用、理解了 Vue 路由守卫、熟悉了表单验证和本地存储。更重要的是,亲手踩了这些坑之后,下次再遇到就能快速定位。接下来我会把这个登录系统接入真实后端(Node.js + MongoDB),实现真正的用户认证。
最后分享一个感悟:不要怕犯错,每一次报错都是升级的机会。
希望这篇文章对你有所帮助!如果觉得不错,点个赞鼓励一下~