学习笔记:Vue3 + Pinia 实现微头条登录与状态管理
学习笔记:Vue3 + Pinia 实现微头条登录与状态管理
今天完成了一个基于 Vue3 + Vue Router + Pinia 的微头条小项目,实现了登录验证、用户状态管理和基础的新闻发布/展示功能,把整个学习过程和踩坑记录整理成笔记分享给大家。
一、项目背景与目标
这次的目标是在之前 Vue Router 路由的基础上,引入 Pinia 进行全局状态管理,解决用户登录状态共享的问题,实现以下核心功能:
- 登录页面:表单验证、模拟登录、错误提示
- 首页:展示用户信息、新闻列表、发布/删除/点赞功能
- 状态管理:用 Pinia 统一管理用户登录状态,退出登录时清除状态并跳转登录页
二、核心代码实现与踩坑记录
1. 登录页面(Login.vue)
核心功能
- 用
ref声明用户名、密码响应式变量,实现双向绑定 - 模拟登录验证:输入用户名
w、密码1时登录成功 - 登录成功后:保存 token 到 localStorage、调用 Pinia 的
setUser方法存储用户信息、跳转到首页 - 清空按钮一键重置表单内容
关键踩坑
- ❌ 错误:一开始写
username_value访问 ref 变量,一直报错 - ✅ 解决:Vue3 中 ref 声明的变量必须用
.value访问,正确写法是username.value - ❌ 错误:忘记引入 Pinia 的
useUserStore,导致登录后无法存储用户状态 - ✅ 解决:引入
useUserStore并实例化,登录成功后调用userStore.setUser()
完整代码
<template>
<div class="login-page">
<div class="login-card">
<h2>微头条登录</h2>
<input type="text" placeholder="用户名" v-model="username" />
<input type="password" placeholder="密码" v-model="password" />
<button @click="handleLogin">登录</button>
<button @click="clearForm">清空</button>
<div class="register-link">
<a href="#">立即注册</a>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '../stores/user'
const username = ref('')
const password = ref('')
const router = useRouter()
const userStore = useUserStore()
const handleLogin = () => {
if (username.value === 'w' && password.value === '1') {
localStorage.setItem('token', 'fake-token')
userStore.setUser(username.value, 'fake-token')
router.push('/home')
} else {
alert('用户名或密码错误')
}
}
const clearForm = () => {
username.value = ''
password.value = ''
}
onMounted(() => {
document.body.style.overflow = 'hidden'
})
onUnmounted(() => {
document.body.style.overflow = ''
})
</script>
<style scoped>
.login-page {
height: 100vh;
width: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.login-card {
width: 360px;
background: white;
padding: 30px;
border-radius: 16px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.login-card h2 {
text-align: center;
margin-bottom: 24px;
color: #333;
}
.login-card input {
width: 100%;
padding: 12px;
margin-bottom: 16px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
box-sizing: border-box;
transition: border-color 0.2s;
}
.login-card input:focus {
outline: none;
border-color: #667eea;
}
.login-card button {
width: 100%;
padding: 12px;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
}
.login-card button:first-of-type {
background: #667eea;
color: white;
}
.login-card button:first-of-type:hover {
background: #5a67d8;
}
.login-card button:last-of-type {
background: #e2e8f0;
color: #333;
margin-top: 8px;
}
.login-card button:last-of-type:hover {
background: #cbd5e0;
}
.register-link {
text-align: center;
margin-top: 20px;
font-size: 14px;
color: #666;
}
.register-link a {
color: #667eea;
text-decoration: none;
}
.register-link a:hover {
text-decoration: underline;
}
</style>
2. 首页(Home.vue)
核心功能
- 从 Pinia 中读取
userStore.username,在页面显示欢迎语 - 退出登录按钮:调用 Pinia 的
logout方法,清除用户状态并跳转到登录页 - 新闻列表功能:发布、删除、点赞新闻
- 响应式布局:固定头部,内容区域可滚动
关键踩坑
- ❌ 错误:一开始直接操作
localStorage.removeItem('token')退出登录,状态没有同步更新 - ✅ 解决:改为调用
userStore.logout(),由 Pinia 统一处理状态清除,保证所有组件的状态同步 - ❌ 错误:忘记引入
useUserStore,导致页面无法读取用户名 - ✅ 解决:引入
useUserStore并实例化,在模板中直接使用{{ userStore.username }}
完整代码
<template>
<div class="home-container">
<div class="header">
<h2>微头条首页</h2>
<div>欢迎, {{ userStore.username }}</div>
<button @click="logout">退出登录</button>
</div>
<div class="scroll-area">
<div class="content-wrapper">
<div class="publish-area">
<input type="text" v-model="newTitle" placeholder="新闻标题" @keyup.enter="addNews" />
<input type="text" v-model="newContent" placeholder="新闻内容" />
<button @click="addNews">+ 发布微头条</button>
</div>
<div class="news-list">
<div v-if="newsList.length === 0" class="empty">
暂无新闻,快去发布一条吧
</div>
<div v-for="item in newsList" :key="item.id" class="news-item">
<h3>{{ item.title }}</h3>
<div class="time">{{ item.time }}</div>
<div class="content">{{ item.content }}</div>
<div class="actions">
<a href="javascript:;">📚 查看详情</a>
<button @click="deleteNews(item.id)">🗑 删除</button>
<button @click="likeNews(item.id)">💗 点赞 {{ item.likes }}</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { ref, onMounted, onUnmounted } from 'vue'
import { useUserStore } from '../stores/user'
const router = useRouter()
const userStore = useUserStore()
const newsList = ref([
{
id: 1,
title: '微头条1.0 正式发布',
time: '2025-06-01 10:30',
content: '微头条是一个轻量级信息分享平台,采用 Vue3 + SpringBoot 技术栈。',
likes: 0
},
{
id: 2,
title: 'Vue3 组合式 API 学习心得',
time: '2025-06-02 15:20',
content: 'setup 语法糖非常简洁,ref和reactive让数据响应式变得容易。',
likes: 2
}
])
const newTitle = ref('')
const newContent = ref('')
let nextId = 3
const addNews = () => {
if (!newTitle.value.trim()) {
alert('请输入标题')
return
}
const newNews = {
id: nextId++,
title: newTitle.value,
content: newContent.value || '暂无内容',
time: new Date().toLocaleString(),
likes: 0
}
newsList.value.unshift(newNews)
newTitle.value = ''
newContent.value = ''
}
const deleteNews = (id) => {
newsList.value = newsList.value.filter(item => item.id !== id)
}
const likeNews = (id) => {
const item = newsList.value.find(item => item.id === id)
if (item) {
item.likes++
}
}
const logout = () => {
userStore.logout()
router.push('/login')
}
onMounted(() => {
document.body.style.overflow = 'hidden'
})
onUnmounted(() => {
document.body.style.overflow = ''
})
</script>
<style scoped>
.home-container {
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f5f5;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.header {
flex-shrink: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02);
border-bottom: 5px solid #e0e0e0;
}
.header h2 {
margin: 0;
color: #333;
}
.header button {
background: #ff4d4f;
color: white;
border: none;
border-radius: 20px;
padding: 6px 16px;
cursor: pointer;
}
.header button:hover {
background: #ff7875;
}
.scroll-area {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.content-wrapper {
max-width: 800px;
margin: 0 auto;
}
.publish-area {
display: flex;
gap: 10px;
margin-top: 20px;
margin-bottom: 20px;
}
.publish-area input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 24px;
}
.publish-area button {
display: block;
width: 180px;
background: #1890ff;
color: white;
border: none;
border-radius: 24px;
padding: 10px 0;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
}
.publish-area button:hover {
background: #0c7bdf;
}
.news-list {
margin-top: 20px;
}
.news-item {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: transform 0.2s, box-shadow 0.2s;
}
.news-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.news-item h3 {
margin-bottom: 8px;
color: #333;
}
.time {
font-size: 12px;
color: #999;
margin-bottom: 12px;
}
.content {
font-size: 14px;
color: #666;
line-height: 1.5;
margin-bottom: 12px;
}
.actions a {
color: #1890ff;
text-decoration: none;
margin-right: 10px;
font-size: 14px;
}
.actions a:hover {
text-decoration: underline;
}
.actions button {
margin-right: 10px;
background: none;
border: none;
color: #e5152d;
cursor: pointer;
}
.actions button:hover {
text-decoration: underline;
}
.actions button:last-child {
color: #0ae411;
}
.empty {
text-align: center;
color: #999;
padding: 40px;
}
</style>
3. Pinia 用户状态管理(stores/user.js)
核心功能
- 用
defineStore创建用户仓库,定义username和token状态 - 实现
setUser方法:登录时保存用户信息和 token - 实现
logout方法:退出时清除状态和 localStorage 中的 token
完整代码
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
username: '',
token: ''
}),
actions: {
setUser(username, token) {
this.username = username
this.token = token
},
logout() {
this.username = ''
this.token = ''
localStorage.removeItem('token')
}
}
})
三、学习总结与心得
- Pinia 状态管理真香:相比 Vuex,Pinia 的 API 更简洁,没有 mutations 的概念,直接在 actions 中修改状态,组件中直接调用即可,代码更清爽,状态同步也更方便。
- Vue3 组合式 API 细节要注意:ref 变量必须用
.value访问,这个小细节踩了好几次坑,一定要记牢。 - 状态和路由配合的重要性:登录状态需要跨组件共享,用 Pinia 统一管理后,退出登录时所有组件的状态都会同步更新,不会出现“页面退出了但状态没清掉”的问题。
- 项目结构更清晰了:把状态管理单独放在 stores 文件夹里,组件只负责页面渲染和业务逻辑,代码职责更分明,维护起来更方便。
这次的项目让我对 Vue3 + Pinia 的状态管理有了更直观的理解,从基础的登录验证到全局状态共享,一步步踩坑、解决问题,收获满满!后续可以再优化一下路由守卫,实现未登录自动跳转到登录页的功能,让项目更完整。
浙公网安备 33010602011771号