Frontend-Backend Separation Calculator System

832401220王梓涵 2026-10-03 20:10:29

Assignment Information

Item

Content

Course

Software Engineering

Assignment Name

Development of a Frontend-Backend Separation Calculator System

Assignment Goal

Master frontend-backend separation architecture, HTTP API communication, database persistence, exception handling, and project deployment workflow

Tech Stack

Frontend: HTML + CSS + Vanilla JavaScript Backend: Python Flask + Flask-SQLAlchemy Database: SQLite

Table of Contents

  1. GitHub Repository Links

  2. Code Style Guidelines

  3. PSP Personal Software Process Table

  4. Project Overview and Requirements Analysis

  5. System Overall Design

  6. Core Feature Implementation

  7. Key Code Analysis

  8. Feature Testing and Demo

  9. Deployment Instructions

  10. Development Issues, Solutions, and Reflections

GitHub Repository Links

Replace with your own repository links before submission.

Frontend Repository: https://github.com/xuntachuan/calculator-frontend

Backend Repository: https://github.com/xuntachuan/calculator-backend

Frontend Code Style: https://github.com/xuntachuan/calculator-frontend/blob/main/codestyle.md

Backend Code Style: https://github.com/xuntachuan/calculator-backend/blob/main/codestyle.md

Code Style Guidelines

The backend follows the Python PEP 8 coding standard, and the frontend follows the Airbnb JavaScript Style Guide.

Backend code uses snake_case naming with semantically clear function and variable names. Frontend JavaScript uses camelCase naming, HTML is structured in layers, and CSS styles are uniformly placed in the page head. Each repository contains a codestyle.md document that uniformly constrains indentation, comments, and naming to ensure code readability and facilitate future maintenance.

PSP Personal Software Process Table

Phase

Estimated (min)

Actual (min)

Description

Requirements Analysis

20

15

Clarified the hard requirements of frontend-backend separation: the frontend only handles UI interaction, all calculation logic is implemented on the backend; history persistence and exception handling are required

System Architecture Design

20

25

Designed REST-style API endpoints, SQLite table schema, Reverse Polish Notation calculation module, and CORS handling strategy

Backend Development

90

110

Implemented tokenization, infix-to-postfix conversion, postfix expression evaluation; wrote database models, three API endpoints, and exception handling

Frontend Development

60

55

Calculator UI layout, button click expression concatenation, fetch request encapsulation, history rendering and deletion interaction

Integration Testing

40

45

Tested arithmetic operations, nested parentheses, decimals, unary minus; tested division by zero, illegal characters, mismatched parentheses; tested history add/delete

Deployment

20

15

Installed dependencies, started Flask service, verified frontend-backend CORS communication, database auto-created tables

Blog Writing and Organization

60

70

Organized architecture, code, test screenshots, and development summary

Total

310

335

——

Project Overview and Requirements Analysis

Project Introduction

This project implements a frontend-backend separation online calculator based on Flask + vanilla frontend. The frontend performs no calculations — it only provides a visual interface, receives user input, and sends JSON requests to the backend. The backend receives expressions and uses the Reverse Polish Notation (postfix expression) algorithm to parse and evaluate expressions, without using eval, to avoid code injection risks. After a successful calculation, the expression, result, and timestamp are stored in a SQLite database for persistent history, supporting viewing and deleting history records.

Core Hard Requirements (All Implemented)

  1. Strict frontend-backend separation with HTTP + JSON API communication

  2. All mathematical calculation logic resides on the backend; the frontend only handles display and request sending

  3. Supports arithmetic operations, decimals, multi-level nested parentheses, and unary minus (-5, 3*-2)

  4. No dangerous eval function — manually implemented lexical analysis + Reverse Polish Notation calculation for security

  5. Calculation records are persistently stored; records survive page refresh and service restart

  6. Supports viewing all history records and deleting individual history records

  7. Complete exception handling: division by zero, illegal characters, mismatched parentheses, malformed expressions, network errors

  8. Backend enables CORS so the frontend webpage can directly access backend APIs

System Overall Design

Architecture Diagram


 

Module Breakdown

  1. Frontend Modules

  • UI Module: calculator buttons, input field, result display area, history panel

  • Interaction Module: button expression concatenation, clear function

  • Network Request Module: send calculation requests, fetch history list, delete history records

  • Rendering Module: render history data returned by the backend to the page, auto-refresh list after deletion

  1. Backend Modules

  • Lexical Tokenization Module tokenize: splits expressions into tokens, identifies numbers, operators, unary minus, and filters illegal characters

  • Infix-to-Postfix Module infix_to_postfix: uses a stack to convert infix math expressions to Reverse Polish Notation, handling operator precedence and parentheses

  • Postfix Evaluation Module calc_postfix: evaluates postfix expressions with a stack, catching division by zero and format errors

  • Database Model Module: defines the history table using Flask-SQLAlchemy

  • API Module: calculation endpoint, history query endpoint, history deletion endpoint

  • Exception Handling Module: uniformly catches various calculation exceptions and returns standardized JSON error codes

Database Design

Table: CalcRecord

Field

Type

Description

id

Integer (primary key, auto-increment)

Unique record ID

expression

String(256)

Original expression entered by the user

result

String(256)

Calculation result

create_time

DateTime

Record creation time, defaults to current UTC time

Core Feature Implementation

Feature 1: Basic Arithmetic and Compound Expressions

The backend manually implements the Reverse Polish Notation algorithm, following mathematical operator precedence (multiplication and division before addition and subtraction). Supports multi-level nested parentheses, decimal operations, and unary minus.

Examples: (1+2.5)*(-3+7), 10/2 both calculate correctly.

Feature 2: Safe Expression Parsing (No eval)

Does not call Python's built-in eval to execute user strings. Manually tokenizes and parses syntax, intercepts illegal characters, and prevents code injection security risks.

Feature 3: History Persistence

On every successful calculation, the backend automatically stores the expression, result, and timestamp in the SQLite database. The frontend automatically requests the backend API to read history on page load; data survives page refresh because it is stored in the backend database, not dependent on frontend localStorage caching.

Feature 4: Delete Individual History Record

The frontend clicks the delete button and sends a DELETE request with the record ID to the backend. The backend queries the corresponding record; if it exists, it deletes the database record and commits the transaction. After successful deletion, the frontend auto-refreshes the history list. If the record does not exist, a 404 error is returned.

Feature 5: Complete Exception Handling

  • Division by zero: catches ZeroDivisionError when the divisor is near zero and returns a prompt

  • Illegal characters: identifies non-digit/non-operator characters during tokenization and raises an exception

  • Mismatched parentheses: detects inconsistent parenthesis counts during infix-to-postfix conversion

  • Malformed expressions: e.g., consecutive operators, missing operands

  • Network errors: the frontend catches failed fetch requests and prompts to check the backend service

Key Code Analysis

Backend: Lexical Tokenization and Reverse Polish Notation Core (No eval)

# Operator precedence
priority = {'+': 1, '-': 1, '*': 2, '/': 2}

def tokenize(expr):
    """Tokenize: split numbers and operators, handle unary minus"""
    tokens = []
    i = 0
    n = len(expr)
    while i < n:
        c = expr[i]
        if c.isspace():
            i += 1
            continue
        if c in '()+ -*/':
            # Unary minus detection
            if c == '-' and (i == 0 or expr[i-1] in '(*+-/'):
                tokens.append('u-')
            else:
                tokens.append(c)
            i += 1
        elif c.isdigit() or c == '.':
            # Read number (supports decimals)
            j = i
            while j < n and (expr[j].isdigit() or expr[j] == '.'):
                j += 1
            tokens.append(expr[i:j])
            i = j
        else:
            raise ValueError("Contains illegal characters")
    return tokens

def infix_to_postfix(tokens):
    """Convert infix to postfix (Reverse Polish Notation)"""
    postfix = []
    op_stack = []
    for t in tokens:
        if t.replace('.', '').isdigit():
            postfix.append(t)
        elif t == '(':
            op_stack.append(t)
        elif t == ')':
            while op_stack and op_stack[-1] != '(':
                postfix.append(op_stack.pop())
            if not op_stack:
                raise ValueError("Mismatched parentheses")
            op_stack.pop()
        else:  # operators + - * / u-
            while op_stack and op_stack[-1] != '(' and priority.get(op_stack[-1], 3) >= priority.get(t, 3):
                postfix.append(op_stack.pop())
            op_stack.append(t)
    while op_stack:
        top = op_stack.pop()
        if top == '(':
            raise ValueError("Mismatched parentheses")
        postfix.append(top)
    return postfix

def calc_postfix(postfix):
    """Evaluate postfix expression"""
    stk = []
    for token in postfix:
        if token == 'u-':
            if not stk:
                raise ValueError("Expression error")
            val = float(stk.pop())
            stk.append(-val)
        elif token in '+-*/':
            if len(stk) < 2:
                raise ValueError("Expression error")
            b = float(stk.pop())
            a = float(stk.pop())
            if token == '+':
                res = a + b
            elif token == '-':
                res = a - b
            elif token == '*':
                res = a * b
            elif token == '/':
                if abs(b) < 1e-10:
                    raise ZeroDivisionError("Division by zero")
                res = a / b
            stk.append(res)
        else:
            stk.append(float(token))
    if len(stk) != 1:
        raise ValueError("Malformed expression")
    return stk[0]

def calculate_expression(expr):
    tokens = tokenize(expr)
    post = infix_to_postfix(tokens)
    res = calc_postfix(post)
    return res

Backend API Endpoint Code

@app.post("/api/calc")
def api_calc():
    data = request.get_json()
    if not data or "expression" not in data:
        return jsonify({"code": 400, "msg": "Missing expression parameter"}), 400
    expr = data["expression"].strip()
    try:
        result = calculate_expression(expr)
        res_str = f"{result:.6f}".rstrip('0').rstrip('.') if '.' in f"{result:.6f}" else f"{result:.6f}"
        # Save to database
        rec = CalcRecord(expression=expr, result=res_str)
        db.session.add(rec)
        db.session.commit()
        return jsonify({"code": 200, "data": {"result": res_str}})
    except ZeroDivisionError:
        return jsonify({"code": 400, "msg": "Division by zero"}), 400
    except ValueError as e:
        return jsonify({"code": 400, "msg": f"Illegal expression: {str(e)}"}), 400
    except Exception as e:
        return jsonify({"code": 400, "msg": f"Calculation error: {str(e)}"}), 400

# Query history endpoint
@app.get("/api/history")
def api_get_history():
    records = CalcRecord.query.order_by(CalcRecord.create_time.desc()).all()
    return jsonify({"code": 200, "data": [r.to_dict() for r in records]})

# Delete history endpoint
@app.delete("/api/history/<int:rid>")
def api_del_history(rid):
    rec = CalcRecord.query.get(rid)
    if not rec:
        return jsonify({"code": 404, "msg": "Record not found"}), 404
    db.session.delete(rec)
    db.session.commit()
    return jsonify({"code": 200, "msg": "Deleted successfully"})

Frontend Core JavaScript Code

// Backend URL, modify according to actual situation
const BACKEND_URL = "http://127.0.0.1:5000";
const exprInput = document.getElementById("input-exp");
const resultDiv = document.getElementById("result");
const historyWrap = document.getElementById("history-list");

window.onload = () => {
    loadHistory();
}

function appendChar(c) {
    exprInput.value += c;
}

function clearAll() {
    exprInput.value = "";
    resultDiv.innerText = "Result";
}

// Click equals, request backend to calculate
async function calcEqual() {
    const expr = exprInput.value.trim();
    if (!expr) {
        resultDiv.innerText = "Please enter an expression";
        return;
    }
    try {
        const resp = await fetch(`${BACKEND_URL}/api/calc`, {
            method: "POST",
            headers: {"Content-Type": "application/json"},
            body: JSON.stringify({expression: expr})
        })
        const json = await resp.json();
        if (json.code === 200) {
            resultDiv.innerText = json.data.result;
            loadHistory(); // Refresh history after successful calculation
        } else {
            resultDiv.innerText = json.msg;
        }
    } catch (err) {
        resultDiv.innerText = "Network request failed, please check the backend";
        console.error(err);
    }
}

// Fetch history list
async function loadHistory() {
    try {
        const resp = await fetch(`${BACKEND_URL}/api/history`);
        const json = await resp.json();
        if (json.code === 200) {
            renderHistory(json.data);
        }
    } catch (e) {
        historyWrap.innerHTML = "<p>Failed to load history</p>";
    }
}

// Delete a single record
async function delRecord(id) {
    if (!confirm("Are you sure you want to delete this record?")) return;
    try {
        await fetch(`${BACKEND_URL}/api/history/${id}`, {method: "DELETE"});
        loadHistory();
    } catch (e) {
        alert("Delete failed");
    }
}

function renderHistory(list) {
    if (list.length === 0) {
        historyWrap.innerHTML = "<p>No records yet</p>";
        return;
    }
    let html = "";
    list.forEach(item => {
        html += `
        <div class="history-item">
            <div>${item.expression} = ${item.result}<br><small>${item.create_time}</small></div>
            <button class="del-btn" onclick="delRecord(${item.id})">Delete</button>
        </div>
        `
    })
    historyWrap.innerHTML = html;
}

Feature Testing and Demo

 

 

 

 

 

 

 

 

  1. Calculator main interface: left-right layout, calculator button panel on the left, history panel on the right; supports clicking buttons to input expressions and manual input.

  2. Basic arithmetic test: 1+2, 10-3.5, 4*5, 9/2 all produce correct results.

  3. Compound expression and parentheses test: (1+2)*3, 1+2*3 — precedence follows mathematical rules; supports multi-level parentheses.

  4. Unary minus test: -5+9, 4*-2 — minus sign is correctly recognized.

  5. Division by zero test: entering 10/0, the backend catches ZeroDivisionError and the frontend displays a division by zero error prompt.

  6. Illegal expression test: mismatched parentheses, illegal characters — the backend returns corresponding error messages.

  7. History persistence test: calculations are automatically saved to the database; after page refresh, history records remain.

  8. Delete record test: clicking the delete button, after confirmation the database record is deleted and the page history list auto-updates; deleting a non-existent ID returns 404.

  9. Exception scenario test: when the backend is not running, the frontend prompts network request failure.

Deployment Instructions

  1. Install backend dependencies

pip install flask flask-sqlalchemy flask-cors
  1. Run the backend program: python main.py. The backend starts at http://127.0.0.1:5000 and automatically creates the calc_history.db database file.

  2. Open the frontend HTML file directly in a browser. Ensure BACKEND_URL in the frontend JavaScript matches the backend address.

  3. The backend enables CORS, so local webpages can directly access the API without additional proxy configuration.

Development Issues, Solutions, and Reflections

Issues Encountered

  1. Difficulty in recognizing unary minus

Initially, the tokenization module could not distinguish between the subtraction operator and the negative sign, e.g., 3*-2 was parsed incorrectly. Solution: added detection logic — when - appears at the beginning of the expression or after (, *, +, it is judged as a unary minus, marked as u-, and handled separately during postfix evaluation.

  1. Frontend-backend CORS request error

When the frontend opened the HTML and accessed the backend API, the browser reported a CORS restriction. Solution: the backend introduced flask-cors and executed CORS(app) to enable global CORS, allowing the frontend webpage to access the API.

  1. Handling mismatched parentheses in Reverse Polish Notation

When a user entered (1+2 (missing closing parenthesis), the program crashed directly. Solution: during the infix-to-postfix stack operation, detect leftover opening parentheses and actively raise a ValueError with "Mismatched parentheses", which is uniformly caught and returned to the frontend.

  1. Database time formatting

The database stores UTC time. The to_dict function formats the time, converting datetime to a readable string that the frontend directly displays in YYYY-MM-DD HH:MM:SS format.

  1. Trailing zeros in floating-point results

Calculation results like 4.0 were desired to be simplified to 4. Solution: format the result string to automatically strip trailing zeros and decimal points for better display.

ReflectionsThis assignment used Flask to build the backend and manually implemented a Reverse Polish Notation calculator, fully experiencing the frontend-backend separation architecture. The frontend is only responsible for view display, while all business logic, security validation, and data persistence are handed to the backend, achieving separation of concerns.

Compared to directly using eval, manually implementing lexical analysis and stack-based expression evaluation helped me understand the underlying principles of expression parsing while avoiding the security risk of code injection. Through REST-style APIs and JSON data interaction, I became familiar with the engineering practices of interface design, CORS, database ORM operations, and exception handling.

During the integration process, I also learned frontend-backend joint debugging and troubleshooting, distinguishing between frontend page errors, network errors, and backend business exceptions. The entire project followed the software engineering process — from requirements analysis, architecture design, coding, testing to documentation writing — with standardized development that enhanced my engineering development mindset.

...全文
24 回复 打赏 收藏 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

173

社区成员

发帖
与我相关
我的任务
社区描述
梅努斯-软件工程
软件工程 高校 福建省·福州市
社区管理员
  • FZU_SE_LQF
  • zjcTander
  • 木村修
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧