ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JavaScript图书管理系统:从Ajax注册到Cookie登录的完整实践

JavaScript图书管理系统:从Ajax注册到Cookie登录的完整实践 简介面向JavaScript课程期末大作业的图书管理系统完整源代码包适合需要快速完成系统设计、前端交互与后端联调的学生参考。资源围绕学生登录注册、管理员登录、图书信息管理等核心模块展开注册环节采用Ajax异步验证学号唯一性配合正则表达式校验密码、手机号、邮箱等字段并用加密Cookie实现十天自动登录整体逻辑清晰可直接运行学习。包内共107个文件以java后台逻辑、js前端交互、html页面结构、css样式为主另有xml配置、png图标及sql数据库脚本等压缩包仅1.95MB结构紧凑便于部署。已有3300人学习下载尤其适合作为JavaScript期末项目的基础模板可从中提取表单验证、异步请求、会话保持等关键代码也可按需扩展图书借阅、分类统计等功能。1. 为什么这套 JavaScript 图书管理系统把注册入口设计成 Ajax解压这套 JavaScript 图书管理系统源代码包第一眼看到的不是页面而是一排样式文件bootstrap-table.css、cropper.css、default.css。源码逻辑主线很清晰从 index 展示页进入学生注册、学生登录、管理员登录最后落到图书列表和后台管理。其中让期末大作业从“能跑”变成“有价值”的核心决策是注册模块不写 form 直接提交改用 Ajax 以 JSON 格式异步请求后端学号重复、手机号格式、邮箱格式全部在这一层做前置拦截。这条设计思路值得单独拆开讲它决定了后面登录态、记住账号、表格刷新整套交互的写法。适合正在做 JavaScript 期末大作业或课程设计、想补全前后端交互细节的人也适合想把旧项目整理成可演示版本的学习者。2. Bootstrap 静态资源骨架从 css 引用顺序到 bootstrap-table 列表初始化2.1 资源包里这些文件各管什么项目正文里给出一串静态文件看着多其实分四类。第一类是 Bootstrap 体系本身bootstrap.css 是完整样式bootstrap.min.css 是压缩版glyphicons-halflings-regular.eot 是字体图标在老版本浏览器里的兜底格式。第二类是插件样式bootstrap-table.min.css 管表格分页和工具栏cropper.css 管头像或图书封面的裁剪。第三类是业务样式index.css 是首页自定义样式default.css 一般是去掉浏览器默认 margin、padding 的兼容层。第四类是页面组件menu.html 被抽出来是因为后台框架页里左侧导航在多个页面都要复用它。把资源整理成表格会更清楚文件作用域说明bootstrap.css / bootstrap.min.css全局栅格、按钮、导航、表单组件基础样式bootstrap-table.min.css列表页分页条、工具栏、排序图标cropper.css注册页图片裁剪与预览index.css首页项目介绍页的布局与配色default.css全局浏览器样式归一化glyphicons-halflings-regular.eot全局字体图标兼容格式menu.html后台左侧菜单模板这个文件分层方式在期末大作业里有一个实际收益页面数量越多越应该把公共样式抽成独立文件而不是在给每个页面都写一遍style。HBuilder 配置 html、css、javascript 时最常见的错误就是引用顺序颠倒导致 bootstrap-table 的样式被业务样式盖掉表格按钮挤在一行里换行错乱。2.2 用>table idbookTable >const rules { studentId: /^\d{10,12}$/, password: /^[\w!#$%^*]{6,16}$/, phone: /^1[3-9]\d{9}$/, email: /^[\w.%-][\w-]\.(com|cn|net|org)$/i, captcha: /^[a-zA-Z0-9]{4}$/, }; function validateField(name, value) { return rules[name] ? rules[name].test(value.trim()) : false; } console.log(validateField(phone, 13800138000)); // true console.log(validateField(email, usertest.com)); // truevalidateField 返回的是布尔值调用方只需要关注 true 或 false错误文案可以单独维护一份 map。这样做的好处是新增一个字段时只需要在 rules 里加一行正则不需要修改函数主体逻辑。3.3 学号是否重复监听 blur 后异步查一次学号不可重复这件事前端正则只能保证格式正确不能知道数据库里是否已有人注册。这个时候就要用 JavaScript 监听 blur 事件等用户离开学号输入框时向后端校验接口发一次查询。document.getElementById(studentId).addEventListener(blur, async function () { const sid this.value.trim(); if (!/^\d{10,12}$/.test(sid)) return; try { const resp await fetch(../api/checkStudentId, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ studentId: sid }), }); const data await resp.json(); if (data.status used) { document.getElementById(studentIdErr).textContent 学号已被注册; } else { document.getElementById(studentIdErr).textContent ; } } catch (err) { document.getElementById(studentIdErr).textContent 学号检测服务不可用; } });这段代码把学号通过 JSON 请求体传给后端后端返回{ status: used }表示已占用。前端根据状态切换提示文本。如果项目里已经引入 jQuery换成$.ajax同理原生 fetch 的好处是不额外依赖库搜索热词里经常出现的 JavaScript 监听说的就是这种事件与异步回调的组合。值得再补一个细节频繁切换输入框且每切换一次都发请求对性能有损耗。常见做法是加一个 300ms 的setTimeout clearTimeout防抖连续输入时只发最后一次请求。期末场景数据量小忽略防抖也不致命但写出来会成为加分项。3.4 注册提交把所有校验结果收敛成一个结果提交按钮点击时要按字段顺序做最终校验。用every方法把五个字段的布尔结果收敛成一个总开关任何一个 false 都不发起请求。document.getElementById(registerBtn).addEventListener(click, async function () { const payload { studentId: document.getElementById(studentId).value.trim(), password: document.getElementById(password).value, phone: document.getElementById(phone).value.trim(), email: document.getElementById(email).value.trim(), captcha: document.getElementById(captcha).value.trim(), }; const allValid Object.keys(payload).every((key) validateField(key, payload[key])); if (!allValid) { document.getElementById(globalMsg).textContent 请检查标红的必填项; return; } try { const resp await fetch(../api/register, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload), }); const data await resp.json(); if (data.status success) { document.getElementById(globalMsg).textContent 注册成功即将跳转登录页; setTimeout(() { location.href login.html; }, 2000); } else { document.getElementById(globalMsg).textContent data.message || 注册失败; } } catch (err) { document.getElementById(globalMsg).textContent 接口不可达请确认后端已启动; } });payload是发给后端的完整 JSON 对象键名与后端字段一一对应。Object.keys(payload).every会遍历所有键拿当前值跑 validateField全部满足才为 true。注册成功后用setTimeout延迟 2 秒跳转给用户留出看到成功提示的时间失败则停留在当前页保留用户已填写的内容。这里不推荐 form 包裹提交按钮的做法。如果页面里已经有form必须在点击事件里调用e.preventDefault()阻止默认刷新行为否则 Ajax 请求和表单提交会同时发出后端会收到两次请求造成重复注册。4. 登录持久化加密 Cookie 与十天免登录实现4.1 学生登录与管理员登录的跳转组织源代码里登录前会提供学生登录、管理员登录两个入口注册也是分开的。页面结构上我一般把 login.html 做成一个页面、两个 tab学生登录 tab 提交到 studentLogin 接口管理员登录 tab 提交到 adminLogin 接口。这样跳转逻辑简单切换 tab 时只需要修改提交地址和成功后跳转的路径不用维护两份几乎相同的登录页。学生登录页和管理员登录页之间用 location.href 互相跳转带上 role 参数可以让目标页初始化时选中正确 tablocation.href login.html?roleadmin;4.2 十天记住账号的 Cookie 结构设计“用 cookie 加密使十天内记住账号信息”这句话要落实先确定两个原则第一cookie 里绝对不能存明文密码一旦被别人拿到控制台权限密码就直接暴露第二能存的是登录凭证 token这个 token 由后端在登录成功后签发前端只负责写入和读取。加密这一步本质上应该由后端完成HMAC 签名过期时间、用户 id 生成 token前端存的是签名后的结果。function setCookie(key, value, days) { const expires new Date(Date.now() days * 24 * 60 * 60 * 1000).toUTCString(); document.cookie ${key}${encodeURIComponent(value)}; expires${expires}; path/; } function getCookie(key) { const pairs document.cookie.split(;); for (const pair of pairs) { const idx pair.indexOf(); const name pair.slice(0, idx).trim(); if (name key) { return decodeURIComponent(pair.slice(idx 1).trim()); } } return ; } setCookie(login_token, server-signed-token-value, 10); const savedToken getCookie(login_token);参数说明days参数控制有效期天数这里传 10。expires 必须使用 toUTCString 生成的 GMT 格式字符串不能直接传毫秒数。encodeURIComponent是为了防止 token 里出现特殊字符时 cookie 解析出错读取时用 decodeURIComponent 还原。cookie 属性对比如下属性作用当前设置expires过期时间10 天后path生效路径/全站HttpOnly是否禁止 JS 读取期末项目可不设SameSite跨站携带策略建议后端设置 Lax4.3 登录请求与登录状态回写登录按钮点击后先做前端非空检查再发 Ajax。请求返回成功时调用 setCookie 写入 token然后根据登录角色跳转对应页面。document.getElementById(loginBtn).addEventListener(click, async function () { const account document.getElementById(loginAccount).value.trim(); const password document.getElementById(loginPassword).value; const isAdmin document.getElementById(tab-admin).classList.contains(active); const rememberMe document.getElementById(rememberMe).checked; if (!account || !password) { document.getElementById(loginMsg).textContent 账号和密码不能为空; return; } try { const resp await fetch(isAdmin ? ../api/adminLogin : ../api/studentLogin, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ account, password }), }); const data await resp.json(); if (data.status success) { const expireDays rememberMe ? 10 : 0; setCookie(login_token, data.token, expireDays); location.href isAdmin ? menu.html : student/dashboard.html; } else { document.getElementById(loginMsg).textContent data.message || 账号或密码错误; } } catch (err) { document.getElementById(loginMsg).textContent 登录接口不可达; } });这个流程里 rememberMe 勾选与否决定 cookie 是十天后过期还是浏览器关闭即销毁。expireDays传 0 时setCookie 里计算出来的时间是当前时间浏览器会话结束便清理。跳转路径上管理员进入 menu.html 后台框架学生进入 dashboard 页面。顺带提醒一个安全问题如果后端有登录过期概念前端不能只凭 cookie 存在就认为登录有效。正式部署时服务器端应给同一个 cookie 加 HttpOnly 属性让 JavaScript 无法读取同时把 cookie 的 SameSite 设为 Lax降低 CSRF 风险。前端刻意做的“加密”在浏览器环境下任何人打开 DevTools 都能看到逻辑所以加密主体放在后端才是真正安全的做法。5. 本地联调与接口自测HBuilder 内置服务器、mock 数据与常见报错5.1 不要在 file:// 协议下调试 Ajax双击 index.html 打开页面Ajax 请求几乎没有成功的可能。浏览器在 file:// 协议下会拦截跨域请求jQuery 和 fetch 都会报错。正确方式是让项目跑在一个本地 HTTP 服务里。HBuilder 内置服务器直接解决这个问题右键项目目录选择运行到浏览器页面地址变成http://localhost:端口/项目目录/index.html相对路径请求自然可以发出。部署到服务器时浏览器访问公网 IP 指向的 nginx 站点根目录逻辑与本地一致只是接口地址里的 localhost 换成实际域名。5.2 用 json-server 临时充当注册登录接口后端没写好的时候可以用 json-server 起一个假接口。在项目外建 db.json{ students: [], loginResult: { status: success, token: mocked-token } }命令行执行npx json-server db.json --port 3000启动后把 ajax 里的 url 临时指向http://localhost:3000/loginResult登录流程就能通。测试学号重复时先向 students 数组手动塞一条记录再触发 blur 事件network 面板能直接看到响应 JSON 结构是否符合前端判断逻辑。5.3 高频报错定位表现象原因处理位置样式全部丢失css 路径错误或目录带中文检查 link 引用与 HBuilder 路径点击注册按钮无响应js 文件未加载或事件重复绑定打开 Console 看报错行号Ajax 返回 404接口路径与服务器根路径不一致看 Network 面板请求 URL注册成功却马上刷新未阻止 form 默认提交在事件中调用 preventDefault记住账号第二天失效cookie 未设置 expires确认写入时传入 10 天时间最后一类 cookie 失效问题最隐蔽前端调试时优先在 Application 面板里检查 cookie 的过期时间是否为十天后的日期而不是操作完立刻清掉浏览器会话。验证整个注册登录流程时按“格式错误提示 → 学号重复提示 → 登录成功跳转 → 刷新页面保持登录”四步走全部通过这节代码才能算真正闭环。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进