First Assignment - Contacts App with Frontend/Backend Separation

Gasolinemoon 2025-11-04 22:00:51

EE308FZ Assignment 1: Building a Full-Stack Contacts App

Course: EE308FZ Software Engineering
Student ID: 832302225
Assignment: First Assignment - Contacts App with Frontend/Backend Separation

This post details my process, design choices, and the (many) lessons learned while building a full-stack, cloud-deployed contacts application from scratch.


Table of Contents

  1. Links: GitHub & Live Demos
  2. Personal Software Process (PSP) Table
  3. Final Product Showcase
  4. System Design & Architecture
  5. Key Codes
  6. The Journey: My Biggest Challenges & Takeaways
  7. Conclusion


2. Personal Software Process (PSP) Table

This table reflects my planning versus the actual time spent. The development phase, especially debugging the deployment and fixing API/library issues, took significantly longer than anticipated.

Personal Software Process StagesTime Planning (Estimate)Time Spending (Actual)
Planning1.0h1.0h
· Estimate1.0h1.0h
Development12.0h13.5h
· Analysis1.0h1.5h
· Design Spec1.0h1.0h
· Design Review0.5h0.5h
· Coding Standard0.5h0.5h
· Design1.5h2.5h
· Coding5.0h6.5h
· Code Review1.0h0.5h
· Test (Debug)1.5h3.5h
Reporting3.0h3.0h
· Test Report1.0h1.0h
· Size Measurement0.5h0.5h
· Post-mortem & Process Imp Plan1.5h1.5h
Total16.0h22.0h

3. Final Product Showcase

The application is fully deployed and functional. The final UI is clean, modern, and implements all core and advanced features.

Main Interface (Dark Mode)

The main interface is built with Pico.css and forced into a dark theme to resemble a native mobile application. It features the main title, an "Add" icon button, the dynamic search bar, and the main contact list.

Main Interface

Alphabetical Sorting & Grouping

Contacts are automatically sorted by pinyin using the pinyin-pro library. Each group is separated by a clear, stylized header (e.g., '#', 'G', 'M').

img

Dynamic Search Placeholder

The placeholder text in the search bar is dynamically updated on page load to reflect the total number of contacts (e.g., "在 15 位联系人中搜索...").

img

Real-time Filtering

The search bar filters the list in real-time. This works for name, phone, and the newly added email field.

img

List-to-Detail Interaction

The main list is simplified to show only the contact's name. To see more information, the user must click the name.When a contact is clicked, a new detail modal appears, featuring the requested two-column layout: Left Side (Avatar, Name) and Right Side (Info).Inspired by the exemplar, the "Edit" and "Delete" buttons in the detail modal feature clear icons above the text for an intuitive, app-like feel.

img

Add Contact (Modal Flow)

The "Add Contact" form is no longer static, but accessed by clicking the "+" icon in the header, which opens a modal.

img

Edit Contact

Clicking the "Edit" icon opens the "Edit" modal, which comes pre-filled with all the contact's information (including email).

img

Delete Confirmation

To prevent accidental deletions, clicking the "Delete" icon triggers a native browser confirm() dialog.

img

Pagination Controls

The pagination controls are correctly placed at the bottom and use Flexbox to ensure the "Previous" button, "Page X / Y" text, and "Next" button are perfectly aligned and balanced.

img


4. System Design & Architecture

This project was built from the ground up using a classic, decoupled frontend/backend architecture. This separation is crucial for modern web development, allowing each part to be developed, deployed, and scaled independently.

4.1 Architecture & Data Flow

  • Frontend (The "Client"): A static, single-page application (SPA) built with vanilla JavaScript, HTML5, and CSS. It contains all the UI logic and is responsible for rendering the interface the user sees. It was deployed on Vercel, which is optimized for high-performance static site hosting and provides seamless continuous deployment from a GitHub repository.

  • Backend (The "Server"): A Python-based RESTful API built with Flask. This server handles all business logic, data validation, and database interactions. It was deployed on Render, a platform well-suited for running web services like a Flask app.

  • Database (The "Persistence Layer"): A persistent SQLite database file. Unlike larger databases (like PostgreSQL or MySQL), SQLite is serverless and stores the entire database in a single file on the server's file system. This simplicity was perfect for this project, and Render's persistent disk service ensures the database file is not lost between deployments.

Data Flow Diagram

Here is a data flow diagram for the application, generated using Mermaid:

img

4.2 Tech Stack

  • Backend:

    • Python 3 / Flask: Chosen for its simplicity and speed in building the RESTful API endpoints.
    • SQLite: Used for persistent, file-based data storage. Its simplicity is a major advantage as it requires no separate database server setup.
    • Gunicorn: A production-grade WSGI (Web Server Gateway Interface) server. This is critical for deployment, as Flask's built-in server is only for development and cannot handle real traffic.
    • Flask-CORS: An essential Flask extension to handle Cross-Origin Resource Sharing. This was mandatory because our frontend (...vercel.app) and backend (...onrender.com) live on different domains.
  • Frontend:

    • Vanilla JavaScript (ES6+): No frameworks (like React or Vue) were used. All logic was handled with modern JavaScript features like fetch, async/await for API calls, and direct DOM manipulation for updating the UI.
    • Pico.css: A minimal, classless CSS framework. This allowed for clean, modern styling (including automatic dark mode) without writing extensive custom CSS.
    • pinyin-pro: A crucial third-party library used to correctly sort Chinese names by their pinyin, which is a key feature of this contacts app.
  • Deployment & DevOps:

    • Git & GitHub: Used for all version control.
    • Vercel: Provided continuous deployment (CI/CD) for the frontend. Every git push to the main branch automatically triggered a new build and deployment of the static site.
    • Render: Provided CI/CD for the backend. It was configured to run the gunicorn command on startup and provided a persistent disk to store the SQLite database file permanently.

5. Key Codes

5.1 Backend: Robust Database Initialization (app.py)

A critical part of the backend wasn't just the API routes, but ensuring the app could start reliably on a new server. The init_database function was key, especially the part that handles "schema migration" (adding the email column).

Using CREATE TABLE IF NOT EXISTS ensures the table is ready. More importantly, the try...except block for ALTER TABLE makes the app robust. If the app restarts or redeploys after the email column is added, it will simply catch the (expected) sqlite3.OperationalError and continue, instead of crashing the server. This was essential for solving the initial deployment failures.


def init_database():
    conn = sqlite3.connect('database.db')
    cursor = conn.cursor()
    cursor.execute("""
                   CREATE TABLE IF NOT EXISTS contacts
                   (
                       id
                       INTEGER
                       PRIMARY
                       KEY
                       AUTOINCREMENT, 
                       name
                       TEXT
                       NOT
                       NULL,         
                       phone
                       TEXT
                       NOT
                       NULL,                 
                       email
                       TEXT
                   );
                   """)
    
    try:
        cursor.execute("ALTER TABLE contacts ADD COLUMN email TEXT") 
    except sqlite3.OperationalError:
        pass
    conn.commit()
    conn.close()

5.2 Backend: The Search-Enabled API (app.py)

To support the search feature, I upgraded the get_contacts endpoint. It now checks for a URL query parameter q. If one is present, it uses a SQL LIKE query to filter by name, phone, and email.


# GET /api/contacts 
@app.route('/api/contacts', methods=['GET'])
def get_contacts():
    query = request.args.get('q', '')
    conn = get_db_connection()
    if query:
        search_term = f"%{query}%"
        contacts_rows = conn.execute(
            'SELECT * FROM contacts WHERE name LIKE ? OR phone LIKE ? OR email LIKE ?',
            (search_term, search_term, search_term)
        ).fetchall()
    else:
        contacts_rows = conn.execute('SELECT * FROM contacts').fetchall()
    conn.close()
    contacts = [dict(row) for row in contacts_rows]
    return jsonify(contacts)

5.3 Frontend: The All-in-One Render Function(script.js)

The renderPage function is the heart of the frontend. It's responsible for filtering, sorting, paginating, grouping, and finally rendering the contact list every time a change occurs. This single function ensures all state changes are correctly reflected in the UI.


function renderPage() {
  const searchTerm = searchInput.value.toLowerCase();
  const filteredContacts = allContacts.filter(contact =>
    contact.name.toLowerCase().includes(searchTerm) ||
    contact.phone.includes(searchTerm) ||
    (contact.email && contact.email.toLowerCase().includes(searchTerm))
  );

  filteredContacts.sort((a, b) => {
    return pinyinPro.pinyin(a.name, { toneType: 'none' }).localeCompare(pinyinPro.pinyin(b.name, { toneType: 'none' }));
  });

  const startIndex = (currentPage - 1) * CONTACTS_PER_PAGE;
  const endIndex = startIndex + CONTACTS_PER_PAGE;
  const paginatedContacts = filteredContacts.slice(startIndex, endIndex);

  const groupedContacts = groupContacts(paginatedContacts);
  renderGroupedContacts(groupedContacts);
  updatePaginationControls(filteredContacts.length);
}

5.4 Frontend: Pinyin Grouping Logic (script.js)

While renderPage handles the sorting, the groupContacts function is what creates the alphabetical "A", "B", "C" headers. It uses pinyinPro a second time, but with the { pattern: 'first' } option to get just the first letter of the name's pinyin. It also includes important logic to group any non-alphabetical names (like those starting with numbers or symbols) under a single "#" group.


function groupContacts(contacts) {
    if (contacts.length === 0) return {};
    const groups = {};
    contacts.forEach(contact => {
        // Use pinyin-pro to get the first letter of the pinyin
        let firstLetter = pinyinPro.pinyin(contact.name, { pattern: 'first', toneType: 'none' }).toUpperCase();
        
        // Group non-alphabetical names under '#'
        if (!/^[A-Z]$/.test(firstLetter)) firstLetter = '#';
        
        if (!groups[firstLetter]) groups[firstLetter] = [];
        groups[firstLetter].push(contact);
    });
    return groups;
}

6. The Journey: My Biggest Challenges & Takeaways

This project was a fantastic lesson in the difference between "it works on my machine" and "it works in production." The real learning happened not during the initial coding, but during the deployment and debugging phase.

My first backend deployment on Render was the first major nightmare. The app failed immediately, greeting me with 500 and 404 errors. By reading the Render logs, I tracked the 500 error to a fatal sqlite3.OperationalError: no such table: contacts. My app was trying to read a database that simply didn't exist on the server yet. The fix was to make the app more robust. I added an init_database() function that runs on startup and executes CREATE TABLE IF NOT EXISTS..., which solved the crash. The 404 error, amusingly, was simpler: I was just visiting the root URL (/) instead of the correct /api/contacts endpoint.

Once the backend was live, the frontend presented its own mysteries. My sorting feature suddenly failed with a ReferenceError: pinyinPro is not defined. My first CDN link was unstable, and when I tried to download the library locally, I found the new version had a complex TypeScript structure I wasn't prepared for. The solution, it turned out, was to stop guessing and read the official README.md on GitHub. It provided the stable unpkg.com link, which solved the problem instantly. Always read the official docs.

Perhaps the most confusing bug was when my new "email" feature wasn't saving. The code was correct, but the data wouldn't persist. The issue was environmental: my local frontend was still sending data to my old, outdated Render backend (which didn't support email) instead of my new local backend. I had forgotten to change the API_BASE_URL in script.js back to http://127.0.0.1:5000/api for local testing. This taught me to be extremely careful about my environment variables and to always verify which API my frontend is actually talking to.


7. Conclusion

This project was a comprehensive journey from a blank editor to a fully deployed, full-stack application. Building this contacts app was about more than just writing code; it was a practical lesson in system architecture, frontend-backend separation, and the critical, often-overlooked challenges of deployment. Overcoming the bugs and configuration issues was the most valuable part of the experience, solidifying my understanding of how modern web applications are built and maintained in the real world.

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

164

社区成员

发帖
与我相关
我的任务
社区描述
2501_MU_SE_FZU
软件工程 高校
社区管理员
  • FZU_SE_LQF
  • 助教_林日臻
  • 朱仕君
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

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