ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vue3纯页面论坛信息管理系统实战:分页筛选、权限与本地存储

Vue3纯页面论坛信息管理系统实战:分页筛选、权限与本地存储 做前端这七八年我手上攒过不少练手项目但真正被问得最多、也最愿意拿出来讲的是一个看起来平平无奇的东西——论坛信息管理系统。它没有酷炫的动效也没有三维场景可每次有人问我想练手该做什么我还是会推荐它尤其是那种不带后端服务、只做纯页面的版本。原因很实在论坛这个场景几乎把前端日常要处理的事情全塞进去了——列表分页、条件筛选、表单校验、富文本编辑、评论嵌套、角色权限、本地状态持久化一个都不少。而纯页面这三个字又把它从必须等后端接口的被动里解放出来你一个人、一台电脑、一个周末就能跑通一条完整链路。这篇内容写给三类人想找项目练手的前端新人、要准备作品集的求职者、以及习惯写服务端但想补一补页面能力的朋友。我会把项目怎么拆、数据从哪来、关键功能怎么写、哪些坑一定要绕开全部摊开讲清楚代码能直接抄。1. 项目定位与技术选型为什么纯页面反而是优势1.1 纯页面到底指什么边界在哪里先把概念钉死不然很容易做着做着就跑偏了。所谓纯页面的论坛信息管理系统指的是整个项目不依赖任何真实后端服务所有数据来源都是前端自己造的静态 JSON、本地 JS 模块、Mock 拦截层或者浏览器本地存储。它交付出去的形态是一套可以直接打开、点击、交互的界面而不是一个需要npm run serve之后再配数据库、配环境的整套工程。这个边界带来两个明显好处。第一是协作成本归零你不用跟前端和后端来回对齐字段名、状态码、分页参数格式想改一个字段名全局搜索替换三秒钟搞定。第二是演示成本极低面试的时候打开一个静态站点链接就能让对方上手点比现场跑一套需要初始化数据库的工程靠谱得多。我见过太多人的作品集评委点进去是一片 500 报错项目再牛也没机会展示。但边界也很清楚别在纯页面项目里假装自己有服务端。有些人会硬写一个 Node 中间层结果部署时又要配端口又要配转发最后变成一个半成品。如果你确实想练全栈那就大大方方承认这是全栈项目不要挂在纯页面的名下。边界感清晰项目才好收尾。1.2 技术栈选型Vue3 加 Vite 加 Element Plus 的取舍逻辑选型这件事我的判断标准只有一条能不能在最短时间内把业务逻辑跑通而不是把时间花在造轮子上。层级选择主要理由构建工具Vite冷启动秒级热更新几乎无感改样式不刷新页面框架Vue 3 组合式 API逻辑聚合度高一个功能的 ref、computed、watch 能写在一起状态管理PiniaAPI 比 Vuex 简洁天然支持 TS调试面板友好路由Vue Router 4动态路由和路由守卫成熟权限控制好落地组件库Element Plus表格、分页、表单校验、弹窗开箱即用中文文档全数据模拟本地模块 拦截层无需后端同时保留真实请求的写法习惯这里重点说为什么不选某些方案。不用 Tailwind 自己写全套 UI是因为表格的固定列、排序、虚拟滚动这些细节手写一次至少两天练手项目的时间应该花在业务建模上。不用 React 不是 React 不好而是 Element Plus 的生态在 Vue 侧更顺手你要是已经很熟 Ant Design换 React 完全没问题逻辑是相通的。还有一个容易被忽略的点保留 axios 的调用形式。哪怕数据是本地造的也建议把请求封装成request.get(/api/posts, params)的样子然后在拦截层里把 URL 映射到本地数据。这样以后真接后端只需要把拦截层删掉业务代码一行都不用改。这个习惯我在实际项目里受益过很多次。1.3 目录结构规划与模块划分很多练手项目死在结构混乱上——所有页面塞在一个 views 文件夹所有请求写在组件里改一个字段要翻五个文件。我习惯用下面这种按功能域切分的方式forum-admin/ ├── src/ │ ├── api/ # 请求封装按模块拆分 │ │ ├── request.js │ │ ├── post.js │ │ └── user.js │ ├── mock/ # 模拟数据与拦截逻辑 │ │ ├── db.js # 内存数据库 本地存储读写 │ │ └── index.js # 请求路径映射 │ ├── stores/ # Pinia 状态 │ │ ├── user.js │ │ └── app.js │ ├── router/ │ │ └── index.js │ ├── components/ # 跨页面复用组件 │ │ ├── PostTable.vue │ │ └── CommentTree.vue │ ├── views/ # 页面级组件 │ │ ├── post/ │ │ ├── board/ │ │ └── login/ │ └── utils/ │ ├── storage.js │ └── permission.js划分逻辑很简单api 层负责数据从哪来stores 层负责数据怎么共享views 层负责数据怎么展示。三层之间单向依赖页面不直接碰 mockmock 不关心里面渲染成什么样。这种结构在项目膨胀到三四十个文件的时候你依然能一眼定位问题在哪一层。别小看这一点我接手过别人的练手项目改一个分页参数要找二十分钟。2. 数据层设计没有后端论坛的数据从哪来2.1 用本地模块模拟真实接口的三种做法数据模拟这块从简单到复杂我实际用过三种方案各有适用场景。第一种静态 JSON 直读。建一个posts.json需要的地方import进来。优点是零成本缺点也明显——数据是只读的发帖、删帖这些操作做不了而且 JSON 一旦上千条打包体积会很难看。这个方案只适合做纯展示的静态原型。第二种本地模块加 Promise 延迟。这是我最推荐的练手方案。数据放在一个 JS 模块里导出读取时用setTimeout包一层模拟网络延迟让 loading 状态有机会出现。// mock/db.js const delay (ms 300) new Promise((resolve) setTimeout(resolve, ms)) let posts [ { id: 1, boardId: 2, title: 新人报到说说我入行第一年的踩坑记录, authorId: 1001, tags: [经验分享, 新人], views: 1263, likes: 48, status: published, createdAt: 2026-01-12 09:24:11 }, { id: 2, boardId: 2, title: 组件库按需引入后样式丢失排查了两个小时, authorId: 1002, tags: [疑难杂症], views: 872, likes: 31, status: published, createdAt: 2026-01-14 20:05:37 } ] export async function queryPosts({ page 1, size 10, keyword , boardId null, status }) { await delay() let list posts.slice() if (keyword) { const kw keyword.trim().toLowerCase() list list.filter((item) item.title.toLowerCase().includes(kw)) } if (boardId) { list list.filter((item) item.boardId boardId) } if (status) { list list.filter((item) item.status status) } const total list.length const start (page - 1) * size return { total, list: list.slice(start, start size) } }注意这里筛选和分页的顺序必须先筛选再分页反过来的话页码会算错这是新手最常见的逻辑错误之一。第三种拦截层方案。用 axios 拦截器或者连接本地数据映射把request.get(/api/post/list)这样的请求转发到上面的queryPosts。写法上跟真实项目完全一致后续迁移无痛。数据量大的时候我还会把这层换成基于 Service Worker 的请求拦截好处是所有请求都走真实的网络栈Network 面板里能看到完整记录演示时更有说服力。2.2 用本地存储做数据持久化让增删改查真的生效内存里的数组有个致命问题刷新页面就回到初始状态。你演示的时候刚发的帖子一按 F5 就没了体验非常割裂。解决办法是把数据落到浏览器本地存储里。// utils/storage.js const PREFIX forum_db_ export function save(key, value) { try { window.localStorage.setItem(PREFIX key, JSON.stringify(value)) return true } catch (err) { console.warn(本地写入失败可能是超出容量限制, err) return false } } export function load(key, fallback null) { const raw window.localStorage.getItem(PREFIX key) if (!raw) return fallback try { return JSON.parse(raw) } catch (err) { console.warn(本地数据解析失败已丢弃, err) return fallback } }然后在db.js里做一次初始化启动时先尝试从本地读读不到就用种子数据之后每次写操作都同步落盘。这样整个系统就具备了记忆。注意本地存储单个域名通常有 5MB 左右的上限不同浏览器略有差异。一旦你在帖子里塞图片的 base64 字符串很容易就撑爆然后写入静默失败用户以为发布成功其实数据丢了。图片一律存成 Blob URL 或者只存缩略图字符串绝对不要把原图转 base64 塞进去。还有一个实操细节写入时机要比对差异。别每次任何一个字段变都全量写一遍我一般只在增删改这三个动作后面调用保存函数列表滚动、搜索关键词这些纯视图状态不落盘。否则一个页面滚动几十次就触发几十次 JSON 序列化卡顿肉眼可见。2.3 数据结构设计帖子、板块、用户、评论的关系论坛业务的核心是四张表关系理清楚了后面的页面写起来就是顺水推舟。实体关键字段与其他实体的关系板块 boardid、name、slug、description、sort一对多关联帖子帖子 postid、boardId、title、content、authorId、tags、status、views、likes属于一个板块属于一个用户拥有多条评论评论 commentid、postId、parentId、authorId、content、createdAt通过 parentId 自关联形成树用户 userid、name、avatar、role、signature一对多关联帖子和评论这里有两个设计选择值得说清楚。第一标签用数组还是关联表。真实后端一般会拆成标签表和关联表但在纯页面项目里帖子对象直接挂一个tags: string[]就够了。好处是渲染时不用做二次查询坏处是标签重命名要遍历所有帖子。练手阶段简单优先。第二评论为什么用 parentId 而不是 children 嵌套。扁平化存储 parentId指向父评论是更稳妥的做法。嵌套结构在新增一条深层回复时要层层查找父节点写起来啰嗦扁平结构只需要 append 一条记录渲染时在内存里组装成树就行。下面这段组装逻辑我几乎每个项目都在用export function buildCommentTree(list) { const map new Map() const roots [] list.forEach((item) { map.set(item.id, { ...item, children: [] }) }) map.forEach((node) { if (node.parentId map.has(node.parentId)) { map.get(node.parentId).children.push(node) } else { roots.push(node) } }) return roots }用一次遍历建索引再一次遍历挂载时间复杂度是线性的。如果写成递归查找父节点一千条评论能把页面卡住好几秒这个差距在做数据量测试的时候非常明显。3. 核心功能实现从帖子列表到详情页的完整链路3.1 帖子列表分页、筛选、排序怎么落地列表页是整个系统的门面也是最容易做得能用但不好用的地方。我的经验是筛选条件必须同步到地址栏。原因有三用户刷新页面不丢状态可以直接把链接发给别人浏览器前进后退符合预期。// views/post/PostList.vue 片段 import { ref, computed, watch } from vue import { useRoute, useRouter } from vue-router import { queryPosts } from /api/post const route useRoute() const router useRouter() const loading ref(false) const list ref([]) const total ref(0) const query ref({ page: Number(route.query.page) || 1, size: Number(route.query.size) || 10, keyword: route.query.keyword || , boardId: route.query.boardId ? Number(route.query.boardId) : null, status: route.query.status || }) async function fetchList() { loading.value true try { const res await queryPosts(query.value) list.value res.list total.value res.total } finally { loading.value false } } watch( query, (val) { router.replace({ query: { ...val, page: String(val.page) } }) fetchList() }, { deep: true, immediate: true } )这段代码里有一个坑必须点出来搜索框不能每次输入都发请求。虽然现在是本地数据没网络开销但筛选加计算在数据量大时依然会卡而且真实项目里这就是典型的接口风暴。正确做法是加防抖三百毫秒足够。let timer null function onKeywordInput(val) { if (timer) clearTimeout(timer) timer setTimeout(() { query.value.keyword val query.value.page 1 }, 300) }搜索时把页码重置为 1 是必须的否则你在第 8 页搜一个只有两条结果的词页面会变成空白用户第一反应是系统坏了。排序和分页的交互也有讲究。切换排序字段时页码同样要归 1切换每页条数时最合理的做法是按当前第一条数据的序号去反推新页码让用户视觉上不跳走。这个细节做不做体验差距很大。3.2 发帖与编辑表单校验和草稿保存的细节发帖表单看着简单实际上藏着不少细节。我用 Element Plus 的表单组件配合自定义校验规则这一块可以直接抄。const rules { title: [ { required: true, message: 标题不能为空, trigger: blur }, { min: 4, max: 60, message: 标题长度需在 4 到 60 个字符之间, trigger: blur }, { validator: (rule, value, callback) { if (/[]/.test(value)) { callback(new Error(标题不能包含尖括号)) } else { callback() } }, trigger: blur } ], boardId: [{ required: true, message: 请选择所属板块, trigger: change }], content: [ { required: true, message: 正文不能为空, trigger: blur }, { min: 10, message: 正文至少 10 个字符, trigger: blur } ] }长度限制必须前后端一致但纯页面项目里只有前端所以这里其实是在练你的产品思维——标题 60 个字是在列表页一行的展示极限超过就会被截断成省略号正文最少 10 个字是为了拦住顶沙发这种毫无信息量的内容。规则背后要有理由不是随便填个数。草稿保存值得单独讲。用户写了八百字手滑点了关闭全没了这是最伤人的体验。我的做法是监听表单数据做防抖后写入本地存储进入页面时检测到草稿就弹提示。import { watch, onMounted } from vue import { save, load } from /utils/storage const DRAFT_KEY post_draft watch( form, (val) { if (draftTimer) clearTimeout(draftTimer) draftTimer setTimeout(() { if (val.title || val.content) { save(DRAFT_KEY, { ...val, savedAt: Date.now() }) } }, 800) }, { deep: true } ) onMounted(() { const draft load(DRAFT_KEY) if (draft draft.title) { // 这里不要用原生 confirm体验割裂用组件库的确认框 showRestoreDialog(draft) } })发布成功后一定要清掉草稿不然下次进来又问一遍恢复用户会烦。实操心得富文本编辑器的内容回填一定要在编辑器实例 ready 之后再调用设置方法。我踩过一次坑——在 created 里就赋值编辑器还没挂载内容直接丢了控制台一点报错都没有排查了很久才发现是时序问题。3.3 详情页与评论树递归组件写法与性能注意点评论树是论坛的灵魂也是递归组件的经典应用场景。Vue 3 里组件可以通过文件名自动自引用写起来很清爽。!-- components/CommentTree.vue -- template div classcomment-node div classcomment-head span classauthor{{ node.authorName }}/span span classtime{{ node.createdAt }}/span button classreply-btn clickonReply(node.id)回复/button /div p classcomment-body{{ node.content }}/p div v-ifnode.children node.children.length classcomment-children CommentTree v-forchild in node.children :keychild.id :nodechild replyonReply / /div /div /template script setup defineProps({ node: { type: Object, required: true } }) const emit defineEmits([reply]) function onReply(id) { emit(reply, id) } /script style scoped .comment-children { padding-left: 24px; border-left: 2px solid #ebeef5; } /style这段代码有几个工程化的点。第一事件逐层向上抛不要在子组件里直接调 store否则组件复用性会断掉。第二缩进层级要有上限我一般限制到 5 层再深的回复自动拍平视觉上不再继续缩进。原因是屏幕上每层缩进 24 像素五层就吃掉 120 像素手机端根本没法看。性能方面评论超过 300 条的时候整棵树一次性渲染会明显掉帧。两个优化手段一是默认只展开前两层深层用展开更多按钮控制二是如果确实要全部渲染给每条评论的节点用v-memo缓存避免父组件状态变化导致整棵树重渲染。我实测过八百条评论的页面加v-memo之后从滚动卡顿变成基本流畅。页面详情区还有个小细节阅读量自增要防抖并且只算一次。简单做法是在本地存储里记一个已读帖子 id 列表进详情页时判断没读过才加一。虽然纯页面项目里这个数字不影响什么但这个思路在真实项目里能防止刷量值得养成习惯。4. 权限与状态管理纯前端也能做出像样的权限体系4.1 用 Pinia 管理登录态与角色很多人觉得没有后端就没有权限可言其实不对。前端权限的核心是控制界面可见性和入口可用性至于安全性那是后端的事。在演示项目里把权限做出来能让作品集的说服力上一个台阶。// stores/user.js import { defineStore } from pinia import { ref, computed } from vue import { load, save } from /utils/storage export const useUserStore defineStore(user, () { const token ref(load(token, )) const profile ref(load(profile, null)) const role computed(() profile.value?.role || guest) const isLogin computed(() Boolean(token.value)) const canManagePost computed(() [admin, moderator].includes(role.value)) function login(payload) { token.value payload.token profile.value payload.profile save(token, token.value) save(profile, profile.value) } function logout() { token.value profile.value null window.localStorage.removeItem(forum_db_token) window.localStorage.removeItem(forum_db_profile) } return { token, profile, role, isLogin, canManagePost, login, logout } })这里我特意用了setup风格的 store因为组合式写法和页面里的逻辑风格一致读代码不用切换思维模式。角色设计我一般分四档admin能管所有内容moderator只管帖子和评论user只能操作自己的内容guest只能看。判断能不能操作某条数据时条件不能只写角色还要拼上归属关系function canEdit(post, user) { if (!user.isLogin) return false if (user.role admin) return true if (user.role moderator) return true return post.authorId user.profile.id }这个函数建议放在utils/permission.js里统一维护别散落在各个组件。权限逻辑一旦分散改规则的时候必然漏掉几处线上就会出现某个入口还能点进去的尴尬情况。4.2 路由守卫与按钮级权限的轻量实现路由守卫负责拦人自定义指令负责藏按钮这两层配合起来就够用了。// router/index.js router.beforeEach((to, from, next) { const userStore useUserStore() if (to.meta.requiresAuth !userStore.isLogin) { next({ name: login, query: { redirect: to.fullPath } }) return } if (to.meta.roles !to.meta.roles.includes(userStore.role)) { next({ name: forbidden }) return } next() })按钮级权限用指令实现写法干净模板里一眼能看懂// utils/permission.js import { useUserStore } from /stores/user export const permission { mounted(el, binding) { const userStore useUserStore() const required binding.value const allowed Array.isArray(required) ? required.includes(userStore.role) : required userStore.role if (!allowed) { el.parentNode el.parentNode.removeChild(el) } } }el-button v-permission[admin, moderator] typedanger批量删除/el-button注意指令里我选择直接移除 DOM而不是设置display: none。原因是隐藏元素在开发者工具里改一下样式就能点虽然前端拦不住铁了心的人但至少不要给人留下改一行样式就能进来的印象。这类细节在面试里很加分。另外记得处理权限变更后的路由刷新。用户退出登录时如果当前停留在管理页面要主动跳回首页或者登录页否则会看到一堆报错弹窗。我的做法是在退出方法里直接router.replace(/)简单粗暴但绝不出错。5. 布局自适应的实现细节大屏方案5.1 rem 与 vw 方案的对比和落地论坛后台在大屏上展示的机会其实不少比如运营大屏、展厅演示这时候固定像素布局就会显得拥挤或者空旷。主流的适配方案有三种我做个横向对比。方案原理优点缺点固定 px不做适配开发简单像素级还原大屏上元素过小小屏溢出rem 动态根字号JS 按屏幕宽度设置 html 字号兼容性好改造成本低需要额外脚本首屏可能闪一下vw clamp全部用视口单位并夹取值纯 CSS无脚本依赖部分老组件库样式需要覆盖我目前更倾向vw 加 clamp 混用因为不用额外脚本配合clamp()可以做区间限制既能在小屏上保持可用又不会在大屏上失控。给个实际的写法.forum-layout { --page-padding: clamp(12px, 1.2vw, 32px); --card-radius: clamp(6px, 0.5vw, 12px); padding: var(--page-padding); gap: clamp(8px, 0.8vw, 20px); } .post-title { font-size: clamp(15px, 1.05vw, 20px); line-height: 1.6; }用 CSS 变量兜住整套间距和字号改一处全局生效。这个习惯我是从设计规范做得比较正规的项目里学来的比在每个组件里写死数字靠谱太多。如果项目里已经大量使用 px那退回到 rem 方案更省事。核心就是动态设置根字号const BASE_WIDTH 1920 const BASE_SIZE 16 function setRootFontSize() { const width Math.min(window.innerWidth, 2560) const scale width / BASE_WIDTH document.documentElement.style.fontSize ${BASE_SIZE * scale}px } setRootFontSize() window.addEventListener(resize, () { if (resizeTimer) clearTimeout(resizeTimer) resizeTimer setTimeout(setRootFontSize, 150) })加上上限 2560 是为了防止超宽屏出现字号夸张的情况这个细节很少有人提但实际用 4K 屏演示时特别明显。5.2 表格与列表在大屏下的展示优化Element Plus 的表格在窄屏下会横向滚动在大屏下又会留下一大片空白两边都不好看。我的处理方式是按屏宽切换信息密度而不是让表格无限拉伸。具体做法是给表格列配置做成响应式的计算属性import { computed } from vue import { useWindowSize } from vueuse/core const { width } useWindowSize() const columns computed(() { const base [ { prop: title, label: 标题, minWidth: 260, showOverflowTooltip: true }, { prop: authorName, label: 作者, width: 120 }, { prop: views, label: 浏览, width: 90, sortable: true }, { prop: createdAt, label: 发布时间, width: 170, sortable: true } ] if (width.value 1600) { base.splice(2, 0, { prop: boardName, label: 所属板块, width: 140 }) base.push({ prop: likes, label: 点赞, width: 90, sortable: true }) } if (width.value 2000) { base.push({ prop: tags, label: 标签, minWidth: 180 }) } return base })思路很直白小屏保核心大屏加信息。用户在大屏上本来就希望能一次看到更多内容你把表格拉宽而列数不变反而浪费了空间。这个技巧在数据看板类项目里同样适用。还有个容易被忽略的点大屏下点击区域会显得过于紧凑。行高、按钮尺寸、间距都需要按比例放大。我一般会把表格的size属性做动态绑定宽度超过 1600 时切成large视觉上更舒展。6. 常见问题与排查实录6.1 问题速查表下面这些是我在做这类项目时反复遇到的问题整理成表方便快速定位。现象大概率原因处理方式按需引入后组件样式全丢函数式组件的样式没被自动引入手动引入对应样式文件或改用完整引入本地数据刷新后重置只在内存里改没写回本地存储在增删改后调用保存函数删除最后一页数据后页面空白页码越界未做页码纠正删除后判断当前页是否还有数据没有则页码减一富文本内容粘贴后样式错乱外部样式和内联属性一起被带进来粘贴时做标签白名单过滤只保留基础标签列表滚动卡顿一次性渲染全部节点分页或虚拟滚动深层评论默认折叠搜索输入触发频繁计算没有防抖每次输入都刷新加 300ms 防抖并在搜索时重置页码中文输入法导致搜索提前触发未处理输入法合成事件监听合成事件或使用组件库自带防抖退出登录后页面报错当前路由仍停留在权限页面登出时主动跳转首页6.2 我踩过的几个坑和对应的解法第一个坑页码越界。我在第 5 页删掉了最后一条数据列表变成空白用户一脸茫然。原因是筛选后的总数从 41 变成 40第 5 页只剩下 0 条。解决办法是在删除成功后重新计算总页数如果当前页大于总页数就把页码往前挪一位再刷新。这段逻辑我抽成了一个公共函数所有带分页的列表都复用function normalizePage(current, total, size) { const maxPage Math.max(1, Math.ceil(total / size)) return Math.min(current, maxPage) }第二个坑深拷贝不彻底。编辑帖子时我把列表里的对象直接赋给了表单用户改了半天点取消结果列表里那条数据也变了。原因是对象引用是同一个。这里必须做深拷贝简单场景用structuredClone兼容性有顾虑就用JSON.parse(JSON.stringify())。这个问题在新手项目里出现频率极高而且演示时特别容易尴尬——你改完取消标题居然变了。第三个坑递归组件的 key 用索引。我一开始图省事写:keyindex结果删除某条评论后后面的评论内容整体错位看起来像是删错了。原因就是索引 key 在列表变动时无法正确复用节点。递归组件里 key 必须用唯一 id这条铁律没有例外。第四个坑开发时数据正常打包后空白。排查半小时发现是路由用了 history 模式本地直接打开index.html时路径匹配不上。如果这个项目是要以文件形式交付的记得把路由切成 hash 模式或者老老实实配一个静态服务。这种问题在交付场景里非常常见提前想好交付形态能省很多事。6.3 关于这个项目还能怎么往下走纯页面版本做扎实之后扩展方向其实挺多的我说几个我认为性价比最高的。一是加导出功能把帖子列表导出成表格文件。这一块的难点不在生成文件而在中文编码和大量数据的分批写入做过一次之后你对二进制流和编码会有更实在的理解。二是加国际化把界面文案抽成语言包切换时观察哪些内容是硬编码的这个过程能帮你养成文案不写死在模板里的习惯。三是把数据层抽成独立包让 mock 层可以一键切换到真实接口这样这套代码既是练手项目也能直接作为真实项目的前端骨架。如果时间只够做一件事我建议是把搜索、分页、筛选这三个功能打磨到极致。它们看着最普通但真正能体现一个人对边界情况的理解程度——空状态怎么展示、加载中怎么反馈、错误了怎么恢复、条件冲突了怎么处理。这些细节堆起来才是作品集里最打动人的部分。最后分享一个我一直在用的习惯每做完一个功能就在浏览器里从头到尾点一遍专门去点那些不该点的地方——空表单直接提交、快速连点两次删除、搜一个不存在的关键词、把筛选条件全选上。这几个动作每次都能帮我揪出一两个问题比写单元测试还立竿见影。这套论坛系统的代码我是从一个空白目录一点点敲出来的中间重写了两次数据层最后一次才把本地存储和内存数据的关系理顺。你要是照着做大概也会经历同样的过程别急坑踩过一遍这些东西就真的长在你身上了。
返回列表