ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

【MU_SE 第一次作业——前后端分离计算器系统】

2026/10/4 3:25:10 拓冰建站 浏览量
【MU_SE 第一次作业——前后端分离计算器系统】 MU_SE 第一次作业——前后端分离计算器系统前后端分离计算器系统 —— 第一次作业课程信息说明目录一、Git 仓库链接与代码规范链接二、PSP 表格三、成品展示四、设计与实现过程4.1 需求分析4.2 系统架构五、代码解释5.1 后端核心安全表达式解析避免使用 eval5.2 后端核心API 接口设计5.3 后端核心数据库持久化5.4 前端核心Fetch API 交互5.5 前端核心历史记录的渲染与删除六、个人历程与收获6.1 选题与初期规划6.2 踩的第一个坑eval 禁用后怎么办6.3 踩的第二个坑前端跨域请求失败6.4 收获与反思前后端分离计算器系统 —— 第一次作业课程信息说明项目内容本次作业所属课程软件开发综合实践本次作业要求开发前后端分离计算器并撰写博客本次作业目标掌握前后端分离架构、API 设计、数据库持久化其他参考资料Flask 官方文档、MDN Fetch API、PEP 8目录一、Git 仓库链接与代码规范链接二、PSP 表格三、成品展示四、设计与实现过程五、代码解释六、个人历程与收获一、Git 仓库链接与代码规范链接前端仓库https://github.com/Anson-Kang/832401115_calculator_frontend后端仓库https://github.com/Anson-Kang/832401115_calculator_backend前端代码规范https://github.com/Anson-Kang/832401115_calculator_frontend/blob/main/codestyle.md后端代码规范https://github.com/Anson-Kang/832401115_calculator_backend/blob/main/codestyle.md前端技术栈HTML CSS JavaScriptFetch API后端技术栈Python Flask SQLite部署地址http://127.0.0.1:5000二、PSP 表格阶段预估时间分钟实际时间分钟需求分析3040系统设计4050前端开发6080后端开发90120表达式计算模块6090数据库设计3030计算历史模块4050前后端联调5070测试4060部署3040博客撰写6090合计530720说明实际时间比预估多主要因为表达式解析和跨域问题调试花了较多时间。三、成品展示要求10 张以上截图或 GIF每张配一句说明。主界面展示输入框、等号按钮、历史记录区域。2.加法输入128后端返回20。减法输入50-23返回27。乘法输入6*7返回42。除法输入10/4返回2.5。复合表达式输入(12)*3返回9。一元负号输入3*-2返回-6。无穷大输入5/0前端显示“∞”。非法表达式输入1*2报错。历史记录持久化刷新页面后历史仍在。四、设计与实现过程4.1 需求分析系统需要实现基础四则运算复合表达式优先级、括号、一元正负、小数计算历史持久化到数据库删除指定历史前后端分离通过 HTTP JSON 通信禁止使用eval等不安全方法4.2 系统架构HTTP JSONSQL前端 HTML/JS后端 Flask APISQLite 数据库五、代码解释这一部分我挑选了几个最核心的代码片段解释实现思路。5.1 后端核心安全表达式解析避免使用 eval作业明确禁止使用eval所以我使用了 Python 内置的ast抽象语法树模块自己实现了一个安全的解析器。importast,operatorasop ALLOWED_OPS{ast.Add:op.add,ast.Sub:op.sub,ast.Mult:op.mul,ast.Div:op.truediv,ast.USub:op.neg,ast.UAdd:op.pos,}defsafe_eval(expr):try:treeast.parse(expr,modeeval)exceptSyntaxError:raiseValueError(Invalid expression)return_eval_node(tree.body)def_eval_node(node):ifisinstance(node,ast.Constant):ifisinstance(node.value,(int,float))andnotisinstance(node.value,bool):returnnode.valueraiseValueError(Only numbers are allowed)ifisinstance(node,ast.BinOp):op_typetype(node.op)ifop_typenotinALLOWED_OPS:raiseValueError(Unsupported operator)left_eval_node(node.left)right_eval_node(node.right)ifop_typeisast.Divandright0:raiseValueError(Division by zero)returnALLOWED_OPS[op_type](left,right)ifisinstance(node,ast.UnaryOp):op_typetype(node.op)ifop_typenotinALLOWED_OPS:raiseValueError(Unsupported unary operator)returnALLOWED_OPS[op_type](_eval_node(node.operand))raiseValueError(Unsupported expression)实现思路ast.parse 会把表达式解析成一棵树然后我递归遍历这棵树只允许 数字、 - * /、一元正负号 这几种节点其他一律报错。这样就防止了用户输入import(‘os’).system(…) 这种危险代码。5.2 后端核心API 接口设计后端只提供 API返回 JSON 数据不处理页面渲染app.route(/api/calculate,methods[POST])defapi_calculate():datarequest.get_json(silentTrue)ifnotdataorexpressionnotindata:returnjsonify(successFalse,messageMissing expression),400exprnormalize(str(data[expression]))try:resultsafe_eval(expr)exceptValueErrorase:returnjsonify(successFalse,messagestr(e)),400created_atdatetime.now().strftime(%Y-%m-%d %H:%M:%S)connget_db()conn.execute(INSERT INTO history (expression, result, created_at) VALUES (?, ?, ?),(expr,str(result),created_at))conn.commit()conn.close()returnjsonify(successTrue,expressionexpr,resultresult)实现思路这个接口接收前端传来的 JSON先做非空校验然后调用上面的 safe_eval 计算。如果计算成功就写入数据库并把结果返回给前端。关键点核心计算逻辑在后端前端不参与计算。5.3 后端核心数据库持久化使用 SQLite 存储历史每次成功计算后插入一条记录。defget_db():connsqlite3.connect(calculator.db)conn.row_factorysqlite3.Rowreturnconndefinit_db():connget_db()conn.execute( CREATE TABLE IF NOT EXISTS history ( id INTEGER PRIMARY KEY AUTOINCREMENT, expression TEXT NOT NULL, result TEXT NOT NULL, created_at TEXT NOT NULL ) )conn.commit()conn.close()实现思路每次插入、查询、删除都新建一个数据库连接简单够用使用参数化查询? 占位符防止 SQL 注入。历史记录包含表达式、结果和时间满足作业要求。5.4 前端核心Fetch API 交互前端不计算只负责发送请求和展示结果。asyncfunctioncalculate(){constexprinputEl.value.trim();if(!expr)return;constrespawaitfetch(API_BASE/api/calculate,{method:POST,headers:{Content-Type:application/json},body:JSON.stringify({expression:expr})});constdataawaitresp.json();if(data.success){resultEl.textContent${data.expression}${data.result};loadHistory();// 计算成功后刷新历史记录}else{resultEl.textContent错误data.message;}}实现思路用户点击“”或回车时把输入框里的表达式用 fetch 发送给后端的 /api/calculate。拿到后端的 JSON 后如果 success 为 true就在页面上显示结果并调用 loadHistory() 重新拉取历史记录。这样即使前端刷新或重启历史数据依然存在因为真正的数据在数据库里。5.5 前端核心历史记录的渲染与删除asyncfunctionloadHistory(){constrespawaitfetch(API_BASE/api/history);constlistawaitresp.json();historyEl.innerHTML;list.forEach(item{constrowdocument.createElement(div);row.innerHTML${item.expression} b${item.result}/b (${item.created_at});constbtndocument.createElement(button);btn.textContent删除;btn.onclick()deleteHistory(item.id);row.appendChild(btn);historyEl.appendChild(row);});}asyncfunctiondeleteHistory(id){awaitfetch(API_BASE/api/history/id,{method:DELETE});loadHistory();}实现思路loadHistory 发送 GET 请求获取历史列表然后循环生成 HTML 元素。每一条记录旁边有一个“删除”按钮点击后发送 DELETE 请求后端删除数据库记录前端再次调用 loadHistory 刷新页面确保显示的数据和后端数据库完全一致。六、个人历程与收获6.1 选题与初期规划刚拿到这个作业的时候我其实有点纠结做什么。题目给了几个方向我最后选了 Web 计算器原因有两个一是平时用得多对功能比较熟二是它能把前端、后端、数据库串起来正好把这学期学的东西都练一遍。确定题目后我先花了一个晚上理需求。没急着写代码而是在纸上把功能拆成三块前端界面、后端 API、数据库存储。当时列的需求大概是这样加减乘除支持小数和负数有历史记录刷新页面不丢历史记录能删除后端不能用eval得自己解析表达式现在回头看这个“先理需求再动手”的习惯挺重要的后面写代码时思路基本没乱。6.2 踩的第一个坑eval 禁用后怎么办一开始我想偷懒直接用 Python 的eval()算表达式两行就搞定。但仔细一看作业要求明确写了禁止使用eval说是安全风险。我当时有点懵不用eval怎么算12*3后来查资料才知道可以用ast模块把表达式解析成语法树再自己递归计算。第一次写的时候没考虑除零测试1/0直接崩了报了个ZeroDivisionError。后来在除法那里加了个判断如果除数是 0 就抛ValueError前端再统一显示“除数不能为零”。这个坑让我第一次真正理解了为什么不能用eval——用户如果输入__import__(os).system(rm -rf /)用eval就直接执行了后果不堪设想。自己解析虽然麻烦但安全可控。6.3 踩的第二个坑前端跨域请求失败后端写好后我本地用http://127.0.0.1:5000跑 Flask前端用http://localhost:5500开 Live Server。结果前端发请求时浏览器控制台一片红报 CORS 错误。一开始我以为是自己代码写错了反复检查fetch的写法后来才意识到是跨域问题域名虽然都是本机但端口不一样浏览器就认为是不同源。解决办法是在 Flask 里加 CORS 支持我是直接手动加响应头也可以用flask-cors库。加完之后请求就通了。这个问题让我对“同源策略”有了实际体会以前只在书上看过真踩一次印象深得多。6.4 收获与反思做完这个项目我最大的收获不是学会了某个具体 API而是对“一个完整应用是怎么跑起来的”有了整体认识。以前写代码都是单文件这次前后端分离前端发请求、后端处理、数据库存储、再把结果返回前端展示这条链路我走通了一遍。过程中出的问题——跨域、持久化、状态同步——都不是语法错误而是工程问题靠查文档和调试一点点解决。另外我也体会到写代码前先想清楚数据流有多重要。如果我一开始就决定历史记录存数据库后面那个“刷新丢失”的坑根本不会踩。所以下次再做类似项目我会先画一下数据流向图而不是上来就敲代码。最后这个计算器虽然简单但它是真正能跑起来、能用的东西。看到自己写的页面在浏览器里正常计算、历史记录一条条存下来还是挺有成就感的。