Xây dựng Ứng dụng Web Single-Page (SPA) Truy vấn Dữ liệu với Flask và Bootstrap

1. Tổng quan về Công nghệ

  • Backend: Python & Flask
    • Flask: Một framework web nhẹ, dùng để xử lý yêu cầu HTTP, định tuyến và hiển thị template.
    • Thư viện kết nối cơ sở dữ liệu: Ví dụ: psycopg2 (cho PostgreSQL), mysql-connector-python (cho MySQL), hoặc sqlite3 (cho SQLite). Dùng để thực thi trực tiếp các câu lệnh SQL.
    • JSON: Định dạng dữ liệu được sử dụng để truyền kết quả truy vấn từ backend về frontend.
  • Frontend: HTML, CSS & JavaScript
    • Bootstrap: Cung cấp các thành phần giao diện, bố cục và style (ví dụ: form, bảng, thanh điều hướng).
    • Jinja2: Công cụ render template tích hợp trong Flask, dùng để tạo trang HTML.
    • JavaScript: Sử dụng để gửi các yêu cầu AJAX (Asynchronous JavaScript and XML) tới backend và cập nhật kết quả truy vấn trên trang một cách động.

2. Chi tiết Cách triển khai

2.1. Backend: Flask kết hợp SQL truyền thống

  1. Quản lý kết nối cơ sở dữ liệu:
    • Trong ứng dụng Flask, bạn có thể tạo một hàm tiện ích để quản lý kết nối. Ví dụ, mở kết nối khi bắt đầu mỗi yêu cầu và đóng lại khi kết thúc, hoặc sử dụng đối tượng g để lưu trữ kết nối, đảm bảo chỉ một kết nối được sử dụng trong suốt vòng đời của yêu cầu.
  2. Xử lý các tuyến (routes) cho yêu cầu truy vấn:
    • Tạo các tuyến (routes) riêng biệt cho từng loại truy vấn (ví dụ: danh sách người dùng, thông tin bán hàng). Ví dụ: /api/users, /api/sales.
    • Các tuyến này sẽ nhận tham số truy vấn từ frontend (ví dụ: khoảng ngày, tên người dùng).
  3. Thực thi truy vấn SQL:
    • Trong hàm xử lý của mỗi tuyến, dựa trên các tham số nhận được, xây dựng câu lệnh SQL tương ứng. Lưu ý: Để phòng tránh tấn công SQL Injection, hãy luôn sử dụng truy vấn có tham số (Parameterized Queries) thay vì ghép chuỗi trực tiếp.
    • Thực thi câu lệnh SQL và lấy kết quả.
    • Chuyển đổi kết quả truy vấn (thường là danh sách các tuple hoặc dictionary) sang định dạng JSON và trả về cho frontend.

Ví dụ mã Backend (Mã giả):


from flask import Flask, jsonify, request
import psycopg2 # Ví dụ với PostgreSQL
import os

app = Flask(__name__)

# Hàm quản lý kết nối đơn giản
def get_db_connection():
    conn = psycopg2.connect(os.environ['DATABASE_URL'])
    return conn

@app.route('/api/users')
def fetch_users():
    db_conn = get_db_connection()
    db_cursor = db_conn.cursor()

    # Lấy tham số từ request, ví dụ: username
    user_filter = request.args.get('username', '')

    # Sử dụng truy vấn có tham số để ngăn chặn SQL Injection
    sql_statement = "SELECT user_id, full_name, email_address FROM app_users WHERE full_name LIKE %s;"
    db_cursor.execute(sql_statement, (f"%{user_filter}%",))

    user_data = db_cursor.fetchall()
    db_conn.close()

    # Chuyển đổi kết quả thành danh sách các dictionary và trả về JSON
    user_list_formatted = [{"id": row[0], "name": row[1], "email": row[2]} for row in user_data]
    return jsonify(user_list_formatted)

# Các tuyến truy vấn khác (/api/sales, /api/products, v.v.) được triển khai tương tự
    

2.2. Frontend: Bootstrap & AJAX

  1. Bố cục trang chính:
    • Sử dụng các thành phần của Bootstrap để thiết kế giao diện trên trang HTML chính.
    • Dùng khu vực Form (<form>) để chứa các điều kiện truy vấn khác nhau.
    • Sử dụng Thanh điều hướng (Navs) hoặc Tab của Bootstrap để chuyển đổi giữa các loại truy vấn.
    • Sử dụng một phần tử <table> trống hoặc một container <div> làm nơi hiển thị kết quả truy vấn.
  2. Logic JavaScript:
    • Thêm trình lắng nghe sự kiện (event listener) cho từng form hoặc nút truy vấn.
    • Khi người dùng gửi form hoặc nhấp vào nút truy vấn, ngăn chặn hành vi gửi form mặc định.
    • Thu thập các tham số truy vấn từ form.
    • Sử dụng Fetch API hoặc jQuery.ajax() để gửi yêu cầu GET không đồng bộ tới tuyến API tương ứng của backend, đính kèm các tham số truy vấn vào URL.
    • Khi nhận được dữ liệu JSON từ backend, JavaScript sẽ phân tích cú pháp dữ liệu đó.
    • Tạo động các dòng bảng (<tr>), điền dữ liệu vào bảng và thay thế kết quả cũ.

Ví dụ mã Frontend (Mã giả):


<div class="container mt-4">
    <h2>Trung tâm Truy vấn Dữ liệu</h2>

    <ul class="nav nav-tabs" id="dataQueryTabs">
      <li class="nav-item">
        <a class="nav-link active" data-bs-toggle="tab" href="#user-query-pane">Truy vấn Người dùng</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" data-bs-toggle="tab" href="#sales-query-pane">Truy vấn Bán hàng</a>
      </li>
    </ul>

    <div class="tab-content">
      <div class="tab-pane fade show active" id="user-query-pane">
        <form id="userSearchForm" class="mt-3">
          <div class="mb-3">
            <input type="text" class="form-control" id="userNameInput" placeholder="Nhập tên người dùng">
          </div>
          <button type="submit" class="btn btn-primary">Tìm kiếm Người dùng</button>
        </form>
        <table class="table table-striped table-hover mt-4">
          <thead>
            <tr>
              <th scope="col">Mã</th>
              <th scope="col">Tên</th>
              <th scope="col">Email</th>
            </tr>
          </thead>
          <tbody id="userResultTableBody">
            <!-- Dữ liệu người dùng sẽ được tải vào đây -->
          </tbody>
        </table>
      </div>
      
      <!-- Pane cho Truy vấn Bán hàng sẽ được thêm vào đây -->
    </div>
</div>

<script>
document.getElementById('userSearchForm').addEventListener('submit', function(event) {
    event.preventDefault(); // Ngăn chặn hành vi submit mặc định của form
    const userNameToFind = document.getElementById('userNameInput').value.trim();

    // Xây dựng URL với tham số
    const apiUrl = `/api/users?username=${encodeURIComponent(userNameToFind)}`;

    fetch(apiUrl)
        .then(response => {
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            return response.json(); // Chuyển đổi response thành JSON
        })
        .then(userDataList => {
            const tableBodyElement = document.getElementById('userResultTableBody');
            tableBodyElement.innerHTML = ''; // Xóa dữ liệu cũ trước khi thêm mới

            if (userDataList.length === 0) {
                tableBodyElement.innerHTML = '<tr><td colspan="3">Không tìm thấy người dùng nào.</td></tr>';
                return;
            }

            // Duyệt qua danh sách dữ liệu và tạo các dòng bảng
            userDataList.forEach(user => {
                const tableRow = `
                    <tr>
                        <td>${user.id}</td>
                        <td>${user.name}</td>
                        <td>${user.email}</td>
                    </tr>
                `;
                tableBodyElement.innerHTML += tableRow; // Thêm dòng mới vào bảng
            });
        })
        .catch(error => {
            console.error('Lỗi khi truy vấn dữ liệu người dùng:', error);
            const tableBodyElement = document.getElementById('userResultTableBody');
            tableBodyElement.innerHTML = `<tr><td colspan="3">Đã xảy ra lỗi khi tải dữ liệu.</td></tr>`;
        });
});
</script>
    

Mô hình phát triển tách biệt giữa frontend và backend, trao đổi dữ liệu qua API này, đáp ứng tốt yêu cầu về một ứng dụng Single-Page (SPA) và tận dụng tối đa sự gọn nhẹ của Flask cùng tính dễ sử dụng của Bootstrap.

Thẻ: Flask python Bootstrap ajax JavaScript

Đăng vào ngày 18 tháng 9 lúc 20:46