MU_SE 第一次作业——前后端分离计算器系统
- 前后端分离计算器系统 —— 第一次作业
- 课程信息说明
- 目录
- 一、Git 仓库链接与代码规范链接
- 二、PSP 表格
- 三、成品展示
- 四、设计与实现过程
- 4.1 需求分析
- 4.2 系统架构
- 五、代码解释
- 5.1 后端核心:安全表达式解析(避免使用 eval)
- 5.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 + JavaScript(Fetch API)
后端技术栈:Python Flask + SQLite
部署地址:http://127.0.0.1:5000
二、PSP 表格
| 阶段 | 预估时间(分钟) | 实际时间(分钟) |
|---|---|---|
| 需求分析 | 30 | 40 |
| 系统设计 | 40 | 50 |
| 前端开发 | 60 | 80 |
| 后端开发 | 90 | 120 |
| 表达式计算模块 | 60 | 90 |
| 数据库设计 | 30 | 30 |
| 计算历史模块 | 40 | 50 |
| 前后端联调 | 50 | 70 |
| 测试 | 40 | 60 |
| 部署 | 30 | 40 |
| 博客撰写 | 60 | 90 |
| 合计 | 530 | 720 |
说明:实际时间比预估多,主要因为表达式解析和跨域问题调试花了较多时间。
三、成品展示
要求:10 张以上截图或 GIF,每张配一句说明。
- 主界面:展示输入框、等号按钮、历史记录区域。
2.加法:输入12+8,后端返回20。
减法:输入
50-23,返回27。乘法:输入
6*7,返回42。除法:输入
10/4,返回2.5。复合表达式:输入
(1+2)*3,返回9。一元负号:输入
3*-2,返回-6。无穷大:输入
5/0,前端显示“∞”。非法表达式:输入
1+*2,报错。历史记录持久化:刷新页面后历史仍在。
四、设计与实现过程
4.1 需求分析
系统需要实现:
- 基础四则运算
- 复合表达式(优先级、括号、一元正负、小数)
- 计算历史持久化到数据库
- 删除指定历史
- 前后端分离,通过 HTTP JSON 通信
- 禁止使用
eval等不安全方法
4.2 系统架构
五、代码解释
这一部分我挑选了几个最核心的代码片段,解释实现思路。
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:tree=ast.parse(expr,mode='eval')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_type=type(node.op)ifop_typenotinALLOWED_OPS:raiseValueError("Unsupported operator")left=_eval_node(node.left)right=_eval_node(node.right)ifop_typeisast.Divandright==0:raiseValueError("Division by zero")returnALLOWED_OPS[op_type](left,right)ifisinstance(node,ast.UnaryOp):op_type=type(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():data=request.get_json(silent=True)ifnotdataor'expression'notindata:returnjsonify(success=False,message='Missing expression'),400expr=normalize(str(data['expression']))try:result=safe_eval(expr)exceptValueErrorase:returnjsonify(success=False,message=str(e)),400created_at=datetime.now().strftime('%Y-%m-%d %H:%M:%S')conn=get_db()conn.execute('INSERT INTO history (expression, result, created_at) VALUES (?, ?, ?)',(expr,str(result),created_at))conn.commit()conn.close()returnjsonify(success=True,expression=expr,result=result)实现思路:这个接口接收前端传来的 JSON,先做非空校验,然后调用上面的 safe_eval 计算。如果计算成功,就写入数据库,并把结果返回给前端。关键点:核心计算逻辑在后端,前端不参与计算。
5.3 后端核心:数据库持久化
使用 SQLite 存储历史,每次成功计算后插入一条记录。
defget_db():conn=sqlite3.connect('calculator.db')conn.row_factory=sqlite3.Rowreturnconndefinit_db():conn=get_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(){constexpr=inputEl.value.trim();if(!expr)return;constresp=awaitfetch(API_BASE+'/api/calculate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({expression:expr})});constdata=awaitresp.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(){constresp=awaitfetch(API_BASE+'/api/history');constlist=awaitresp.json();historyEl.innerHTML='';list.forEach(item=>{constrow=document.createElement('div');row.innerHTML=`${item.expression}= <b>${item.result}</b> (${item.created_at})`;constbtn=document.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怎么算1+2*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,而是对“一个完整应用是怎么跑起来的”有了整体认识。
以前写代码都是单文件,这次前后端分离,前端发请求、后端处理、数据库存储、再把结果返回前端展示,这条链路我走通了一遍。过程中出的问题——跨域、持久化、状态同步——都不是语法错误,而是工程问题,靠查文档和调试一点点解决。
另外我也体会到写代码前先想清楚数据流有多重要。如果我一开始就决定历史记录存数据库,后面那个“刷新丢失”的坑根本不会踩。所以下次再做类似项目,我会先画一下数据流向图,而不是上来就敲代码。
最后,这个计算器虽然简单,但它是真正能跑起来、能用的东西。看到自己写的页面在浏览器里正常计算、历史记录一条条存下来,还是挺有成就感的。