核心思考:理清前后端边界与状态流转
从纯粹依赖外部代码片段提示,走向自主重构与组件设计,对 Prisma 数据建模、前端 Pinia 状态管理以及路由架构有了更深的认知。
-
路由的物理与逻辑边界: 从功能定位上理清了前端与后端路由的职责:
- 前端路由 (Vue Router):控制视图层展现。基于 HTML5 History API 拦截 URL 变化并映射至对应视图组件,规避了全局重载。
- 后端路由 (Express Router):控制资源与数据分发。遵循 RESTful 或其他规范对应至特定的业务逻辑/ Controller。 两者的解耦是现代前后端分离协作的地基。
-
单一数据源 (Single Source of Truth): 在重构跨路由媒体播放器状态(播放、暂停、进度)过程中,再次印证了状态集约化管理的收效。
- 散列化反模式:视图容器、
Audio实例本身以及 LocalStorage 互相监听同步,将不可逆地引发状态撕裂和循环依赖。 - 集约化正解:Pinia 承担绝对且唯一的状态代理(大脑),播放器组件仅执行响应,LocalStorage 负责挂载/卸载时的数据快照。所有变更一律流经 Store 声明的 Action 函数。
- 散列化反模式:视图容器、
核心功能重构与实现路径解析
1. 全栈开发流程 (Prisma + Express + Vue)
场景:实现一个“用户列表”功能。
-
Step 1: 数据建模 (后端) 在
schema.prisma定义模型,这是数据的源头。model User {id Int @id @default(autoincrement())username String @uniquerole String @default("user")}执行
npx prisma db push同步数据库。 -
Step 2: 接口开发 (后端) 在
userController.ts中写逻辑,在index.ts注册路由。// controllerexport const getUsers = async (req, res) => {const users = await prisma.user.findMany();res.json(users);};// routerapp.get('/api/users', getUsers); -
Step 3: 状态管理 (前端) 在
stores/user.ts定义 Store。export const useUserStore = defineStore('user', () => {const userList = ref([]);const fetchUsers = async () => {const res = await fetch('/api/users');userList.value = await res.json();};return { userList, fetchUsers };}); -
Step 4: 视图渲染 (前端) 组件三部曲:Store -> Action -> Template。
<script setup>const userStore = useUserStore();onMounted(() => userStore.fetchUsers());</script><template><div v-for="user in userStore.userList" :key="user.id">{{ user.username }}</div></template>
2. 播放器状态持久化 (Pinia + LocalStorage)
场景:刷新页面后,歌曲能接着上次的进度播放。
-
核心逻辑:
- 存:监听切歌 (
watch) 和页面关闭 (beforeunload),将currentSong和currentTime存入 localStorage。 - 取:Store 初始化时 (
restoreState) 读取 localStorage。 - 同步:利用
<audio>的loadedmetadata事件,在音频元数据加载完成后,立即设置currentTime。
- 存:监听切歌 (
-
关键代码 (Store):
// 保存状态const saveState = () => {localStorage.setItem('player-state', JSON.stringify({song: currentSong.value,time: currentTime.value}));};// 恢复状态const restoreState = () => {const saved = localStorage.getItem('player-state');if (saved) {const { song, time } = JSON.parse(saved);currentSong.value = song;currentTime.value = time; // 恢复进度变量}};// 自动保存策略watch(currentSong, saveState); // 切歌存window.addEventListener('beforeunload', saveState); // 关窗存restoreState(); // 启动取 -
关键代码 (组件):
<audioref="audioRef"@timeupdate="(e) => playerStore.currentTime = e.target.currentTime" <!-- DOM -> Store -->@loadedmetadata="audioRef.currentTime = playerStore.currentTime" <!-- Store -> DOM (断点续传) -->></audio>
3. 每日推荐 (前端算法 + 缓存)
场景:每天展示 10 首随机歌曲,当天刷新不变,第二天自动刷新。
-
实现原理:
- 检查缓存:对比
localStorage里的日期和今天是否一致。 - 一致:直接用缓存里的 ID 列表去 Store 里找歌。
- 不一致:执行洗牌算法,生成新列表,存入缓存。
- 检查缓存:对比
-
关键代码:
const generateDailyRecommend = () => {const today = dayjs().format('YYYY-MM-DD');const cachedDate = localStorage.getItem('daily_date');if (cachedDate !== today) {// 洗牌算法:随机排序后取前10const randomSongs = [...songList.value].sort(() => 0.5 - Math.random()).slice(0, 10);// 更新缓存localStorage.setItem('daily_date', today);localStorage.setItem('daily_ids', JSON.stringify(randomSongs.map(s => s.id)));recommendSongs.value = randomSongs;} else {// 走缓存逻辑...}};
4. 全局播放器架构 (Layout 提升)
场景:切换路由(如从首页进专辑页),音乐不中断。
-
问题根因: 原结构中,播放器组件 (
AppFooterPlayerBar) 放在各个页面 (HomeView) 内部。路由切换会导致页面销毁 -> 播放器销毁 -> 音乐停止。 -
解决方案: 将播放器提升到
App.vue或全局 Layout 中,包裹RouterView。 -
架构变更:
<!-- App.vue (新结构) --><template><BasicLayout><!-- 播放器在这里,永远不销毁 --><RouterView /> <!-- 只有这里的内容在变 --></BasicLayout></template>
总结
阶段性的重构充分说明,“能够跑通”只满足工程阶段其一,组件树和全局状态树从一开始的合理化设计,对后续重构调试的增益要远远大于补丁代码产生的心智消耗。
部分信息可能已经过时




