Kiến Trúc Frontend JavaScript: AJAX, Xử Lý Bất Đồng Bộ, CORS Và Template Engine

1. Tổng Quan Về AJAX Và Mô Hình Client-Server

AJAX (Asynchronous JavaScript and XML) là kỹ thuật nền tảng giúp trang web giao tiếp với máy chủ ở chế độ nền mà không cần tải lại toàn bộ giao diện. Điều này mang lại trải nghiệm người dùng mượt mà, đồng thời giảm thiểu đáng kể băng thông và tải trọng cho server.

1.1. Triển khai API phía Server (Java Servlet)

Để minh họa, chúng ta sẽ xây dựng một RESTful API đơn giản quản lý danh sách Sách (Book) sử dụng Java Servlet.


// Book.java
public class Book {
    private String isbn;
    private String title;
    private String author;
    private int publishYear;
    
    // Constructors, Getters and Setters
}

// BookServlet.java
@WebServlet("/api/books")
public class BookServlet extends HttpServlet {
    private BookService bookService = new BookService();
    private ObjectMapper mapper = new ObjectMapper();

    protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
        res.setContentType("application/json;charset=UTF-8");
        String action = req.getParameter("action");
        
        if ("list".equals(action)) {
            res.getWriter().write(mapper.writeValueAsString(bookService.findAll()));
        } else if ("delete".equals(action)) {
            String isbn = req.getParameter("isbn");
            boolean success = bookService.remove(isbn);
            res.getWriter().write(mapper.writeValueAsString(success));
        }
    }
}

1.2. Tích hợp AJAX phía Client

Sử dụng thư viện jQuery để gọi API và cập nhật giao diện DOM một cách trực quan.


const LibraryApp = {
    baseUrl: 'http://localhost:8080/library/api/books',
    
    loadBooks: function() {
        $.ajax({
            url: this.baseUrl,
            data: { action: 'list' },
            method: 'GET',
            dataType: 'json',
            success: (books) => this.renderTable(books),
            error: (xhr) => console.error('Lỗi tải dữ liệu:', xhr.statusText)
        });
    },
    
    renderTable: function(books) {
        const tbody = $('#book-table tbody');
        tbody.empty();
        books.forEach(book => {
            const row = `<tr>
                <td>${book.isbn}</td>
                <td>${book.title}</td>
                <td>${book.author}</td>
                <td><button class="btn-delete" data-isbn="${book.isbn}">Xóa</button></td>
            </tr>`;
            tbody.append(row);
        });
    },
    
    init: function() {
        this.loadBooks();
        // Sử dụng Event Delegation để bắt sự kiện trên các phần tử được render động
        $('#book-table').on('click', '.btn-delete', (e) => {
            const isbn = $(e.target).data('isbn');
            this.deleteBook(isbn);
        });
    }
};

$(document).ready(() => LibraryApp.init());

2. Xử Lý Bất Đồng Bộ Với Deferred Và Promise

Trong các phiên bản cũ của JavaScript, việc xử lý nhiều request bất đồng bộ liên tiếp thường dẫn đến tình trạng "Callback Hell" (lồng ghép callback quá sâu). jQuery đã giới thiệu đối tượng Deferred (tiền thân của chuẩn Promise ES6) để giải quyết vấn đề này, giúp mã nguồn phẳng hơn và dễ bảo trì.

2.1. Sử dụng Deferred để chuỗi hóa xử lý

Các phương thức như .done(), .fail(), .always().then() cho phép bạn kiểm soát luồng thực thi một cách mạch lạc.


function fetchOrderDetails(orderId) {
    return $.getJSON(`/api/orders/${orderId}`);
}

function fetchShippingStatus(trackingNumber) {
    return $.getJSON(`/api/shipping/${trackingNumber}`);
}

// Chuỗi hóa các request bất đồng bộ
fetchOrderDetails(1001)
    .then(order => {
        console.log("Thông tin đơn hàng:", order);
        // Trả về một Promise mới cho bước tiếp theo
        return fetchShippingStatus(order.trackingId);
    })
    .then(shipping => {
        console.log("Trạng thái giao hàng:", shipping.status);
    })
    .fail((xhr, status, err) => {
        console.error("Có lỗi xảy ra trong quá trình xử lý:", err);
    })
    .always(() => {
        console.log("Hoàn tất quá trình kiểm tra đơn hàng.");
    });

2.2. Tự tạo Deferred Object cho tác vụ tùy chỉnh

Bạn có thể bọc các hàm bất đồng bộ (như setTimeout, WebSockets, hoặc FileReader) vào một Deferred object để thống nhất cách xử lý trên toàn bộ ứng dụng.


function simulateHeavyTask(duration) {
    const deferred = $.Deferred();
    
    setTimeout(() => {
        const isSuccess = Math.random() > 0.5;
        if (isSuccess) {
            deferred.resolve({ message: "Tác vụ hoàn thành", duration });
        } else {
            deferred.reject({ error: "Tác vụ thất bại", duration });
        }
    }, duration);
    
    // Trả về promise để tránh trạng thái bị thay đổi trái phép từ bên ngoài
    return deferred.promise(); 
}

$.when(simulateHeavyTask(2000))
    .done(result => console.log("Thành công:", result))
    .fail(err => console.log("Thất bại:", err));

3. Cơ Chế Cross-Origin (CORS Và JSONP)

Trình duyệt áp dụng chính sách Same-Origin Policy (SOP) để ngăn chặn mã JavaScript từ domain này truy cập tài nguyên của domain khác nhằm bảo mật thông tin. Để vượt qua giới hạn này, chúng ta có các phương pháp sau:

3.1. JSONP (JSON with Padding)

JSONP lợi dụng thực tế là thẻ <script> không bị chặn bởi SOP. Server sẽ trả về một đoạn mã JavaScript gọi đến một hàm callback do client chỉ định.


// Client định nghĩa hàm callback
function handleExchangeRate(data) {
    console.log("Tỷ giá USD/VND:", data.rate);
}

// Thẻ script sẽ tải mã từ server và tự động thực thi hàm callback
// <script src="http://api.finance.com/rate?callback=handleExchangeRate"></script>

Hạn chế: JSONP chỉ hỗ trợ phương thức GET và tiềm ẩn rủi ro bảo mật (XSS) nếu server không được tin cậy tuyệt đối.

3.2. CORS (Cross-Origin Resource Sharing)

CORS là chuẩn W3C hiện đại, cho phép server khai báo các domain được phép truy cập thông qua HTTP Headers. CORS hỗ trợ tất cả các phương thức HTTP (GET, POST, PUT, DELETE) và cung cấp cơ chế xử lý lỗi tốt hơn.

Thiết lập CORS trong Spring Boot (Java)


@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://frontend-domain.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

Thiết lập CORS trong ASP.NET Core (C#)


public void ConfigureServices(IServiceCollection services) {
    services.AddCors(options => {
        options.AddPolicy("AllowFrontend", builder => {
            builder.WithOrigins("https://frontend-domain.com")
                   .AllowAnyHeader()
                   .AllowAnyMethod()
                   .AllowCredentials();
        });
    });
}

public void Configure(IApplicationBuilder app) {
    app.UseCors("AllowFrontend");
}

Phân biệt Simple Request và Preflight Request

  • Simple Request: Sử dụng các phương thức GET, HEAD, POST với các Content-Type cơ bản (application/x-www-form-urlencoded, multipart/form-data, text/plain). Trình duyệt gửi request trực tiếp.
  • Preflight Request (OPTIONS): Áp dụng cho các phương thức như PUT, DELETE hoặc khi request chứa custom headers. Trình duyệt sẽ tự động gửi một request OPTIONS trước để "xin phép" server trước khi gửi request thực tế.

4. Tối Ưu Giao Diện Với Modal Và Template Engine

4.1. Sử dụng Modal Để Cải Thiện UX

Thay vì điều hướng sang trang mới hoặc sử dụng alert() mặc định của trình duyệt, việc sử dụng Modal giúp người dùng tập trung vào tác vụ và giữ nguyên ngữ cảnh trang. Dưới đây là cách tích hợp thư viện SweetAlert2 để tạo popup xác nhận hiện đại.


function confirmDeletion(bookIsbn) {
    Swal.fire({
        title: 'Xác nhận xóa?',
        text: "Bạn không thể hoàn tác hành động này!",
        icon: 'warning',
        showCancelButton: true,
        confirmButtonColor: '#d33',
        cancelButtonColor: '#3085d6',
        confirmButtonText: 'Xóa ngay',
        cancelButtonText: 'Hủy bỏ'
    }).then((result) => {
        if (result.isConfirmed) {
            LibraryApp.deleteBook(bookIsbn);
            Swal.fire('Đã xóa!', 'Quyển sách đã được xóa khỏi hệ thống.', 'success');
        }
    });
}

4.2. Tách Biệt Dữ Liệu Và Giao Diện Với Template Engine

Việc nối chuỗi HTML trong JavaScript rất dễ gây lỗi, khó đọc và vi phạm nguyên tắc Separation of Concerns. Template Engine (như Handlebars, EJS, hay Pug) giúp tách biệt hoàn toàn logic xử lý dữ liệu và cấu trúc giao diện.

Ví dụ sử dụng Handlebars.js để render danh sách chuyến bay:


<!-- Định nghĩa Template -->
<script id="flight-template" type="text/x-handlebars-template">
    <ul class="flight-list">
        {{#each flights}}
        <li class="flight-item {{#if isDelayed}}delayed{{/if}}">
            <span class="flight-code">{{flightCode}}</span>
            <span class="destination">Đến: {{destination}}</span>
            <span class="time">Khởi hành: {{departureTime}}</span>
        </li>
        {{else}}
        <li class="empty-state">Không có chuyến bay nào khả dụng.</li>
        {{/each}}
    </ul>
</script>

// Dữ liệu nhận được từ API
const flightData = {
    flights: [
        { flightCode: 'VN123', destination: 'Hà Nội', departureTime: '08:00', isDelayed: false },
        { flightCode: 'VJ456', destination: 'Đà Nẵng', departureTime: '10:30', isDelayed: true }
    ]
};

// Compile template và render dữ liệu
const templateSource = document.getElementById('flight-template').innerHTML;
const template = Handlebars.compile(templateSource);
const htmlResult = template(flightData);

// Chèn vào DOM
document.getElementById('flight-container').innerHTML = htmlResult;

Thẻ: JavaScript ajax CORS jsonp jQuery

Đăng vào ngày 7 tháng 9 lúc 15:25