Chào bạn,
Ở bài học trước, chúng ta đã học cách sử dụng Camunda REST API để truy vấn danh sách các User Task. Bạn đã biết cách lọc task theo assignee, candidateUser, candidateGroup và thậm chí xây dựng các truy vấn phức tạp bằng POST /task. Về cơ bản, bạn đã có trong tay "nguyên liệu thô" - dữ liệu về các công việc cần làm.
Bài học hôm nay, "Xây dựng một giao diện web đơn giản để hiển thị danh sách task cho người dùng", sẽ giúp bạn "chế biến" nguyên liệu đó thành một sản phẩm cụ thể. Chúng ta sẽ cùng nhau tạo một trang web đơn giản có khả năng gọi đến API của Camunda và hiển thị danh sách các task một cách trực quan. Đây là một bước cực kỳ quan trọng, hiện thực hóa mục tiêu xây dựng một workflow engine tùy chỉnh bằng cách kết nối backend (Camunda) với frontend (giao diện người dùng).
Với kinh nghiệm làm developer của bạn, chúng ta sẽ tập trung vào các khái niệm cốt lõi và viết code thực tế để bạn có thể thấy ngay kết quả.
1. Kiến trúc tổng quan: Kết nối Frontend và Backend
Hãy hình dung kiến trúc hệ thống của chúng ta:
- Backend: Ứng dụng Spring Boot tích hợp Camunda engine đang chạy trên
localhost:8080. Nó cung cấp REST API tại endpoint/engine-rest. - Frontend: Một tệp
tasklist.htmlđơn giản chứa code HTML, CSS và JavaScript. Tệp này sẽ được mở trực tiếp trên trình duyệt của bạn.
Khi người dùng mở tasklist.html, mã JavaScript bên trong sẽ gửi một yêu cầu (request) đến http://localhost:8080/engine-rest/task để lấy dữ liệu, sau đó hiển thị dữ liệu này lên trang.

2. Vấn đề thực tế: Cross-Origin Resource Sharing (CORS)
Khi bạn mở tệp tasklist.html từ hệ thống tệp (file:///...) hoặc từ một server khác, và nó cố gắng gọi API trên localhost:8080, trình duyệt sẽ chặn yêu cầu này vì lý do bảo mật. Đây là cơ chế Cross-Origin Resource Sharing (CORS).
Để giải quyết vấn đề này, chúng ta cần cấu hình cho backend (ứng dụng Spring Boot) để nó "nói" với trình duyệt rằng: "Tôi cho phép các yêu cầu đến từ các nguồn (origin) khác."
Đây là một bước cấu hình rất phổ biến khi phát triển ứng dụng web. Bạn có thể thêm một lớp cấu hình Spring Security vào project Camunda của mình để cho phép các yêu cầu CORS.
Tạo một file Java mới, ví dụ WebSecurityConfig.java, trong project Spring Boot của bạn:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // Kích hoạt CORS
.csrf(csrf -> csrf.disable()) // Tắt CSRF cho REST APIs
.authorizeHttpRequests(authz -> authz
.anyRequest().permitAll() // Cho phép tất cả các request trong môi trường dev
);
return http.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// Cho phép mọi nguồn, header, và method. Trong thực tế, bạn nên giới hạn lại.
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Sau khi thêm file này và khởi động lại ứng dụng Spring Boot, backend của bạn đã sẵn sàng nhận yêu cầu từ giao diện web.
3. Thực hành: Xây dựng trang Task List
Bây giờ, chúng ta sẽ tạo file tasklist.html.
Bước 1: Cấu trúc HTML
Tạo một file mới tên là tasklist.html và thêm đoạn mã sau. Nó định nghĩa một tiêu đề, một nút để làm mới danh sách và một bảng để hiển thị các task.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Custom Task List</title>
<style>
body { font-family: sans-serif; margin: 2em; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
button { margin-bottom: 1em; padding: 0.5em 1em; cursor: pointer; }
</style>
</head>
<body>
<h1>My Tasks</h1>
<button onclick="fetchTasks()">Refresh Tasks</button>
<table>
<thead>
<tr>
<th>Task ID</th>
<th>Task Name</th>
<th>Assignee</th>
<th>Created Date</th>
</tr>
</thead>
<tbody id="task-list-body">
<!-- Dữ liệu task sẽ được chèn vào đây bằng JavaScript -->
</tbody>
</table>
<script>
// JavaScript code sẽ được thêm vào đây
</script>
</body>
</html>
Bước 2: Logic JavaScript để lấy và hiển thị dữ liệu
Bây giờ, hãy thêm phần JavaScript vào bên trong thẻ <script>. Phần này sẽ thực hiện công việc chính.
const camundaApiUrl = 'http://localhost:8080/engine-rest';
const taskListBody = document.getElementById('task-list-body');
// Hàm để gọi API và lấy danh sách task
async function fetchTasks() {
// Xóa danh sách cũ trước khi tải mới
taskListBody.innerHTML = '<tr><td colspan="4">Loading...</td></tr>';
try {
// Thay đổi query parameter để lấy các task bạn muốn
// Ví dụ: ?assignee=demo, ?candidateUser=john, ?candidateGroup=management
const response = await fetch(`${camundaApiUrl}/task?candidateGroup=management`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const tasks = await response.json();
renderTasks(tasks);
} catch (error) {
console.error('Could not fetch tasks:', error);
taskListBody.innerHTML = `<tr><td colspan="4">Error loading tasks. See console for details.</td></tr>`;
}
}
// Hàm để hiển thị dữ liệu task lên bảng
function renderTasks(tasks) {
// Xóa nội dung "Loading..."
taskListBody.innerHTML = '';
if (tasks.length === 0) {
taskListBody.innerHTML = '<tr><td colspan="4">No tasks found.</td></tr>';
return;
}
tasks.forEach(task => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${task.id}</td>
<td>${task.name}</td>
<td>${task.assignee || 'Unassigned'}</td>
<td>${new Date(task.created).toLocaleString()}</td>
`;
taskListBody.appendChild(row);
});
}
// Tự động tải task khi trang được mở
document.addEventListener('DOMContentLoaded', fetchTasks);
Giải thích mã:
fetchTasks(): Hàm này sử dụngfetchAPI của trình duyệt để gửi một yêu cầuGETđến endpoint/taskcủa Camunda. Ở đây, chúng ta đang hardcode việc truy vấn các task cho nhómmanagement. Bạn có thể dễ dàng thay đổi nó thànhassignee=demohoặc bất kỳ tham số nào bạn đã học ở bài trước.renderTasks(tasks): Sau khi nhận được mảng JSON chứa các task, hàm này sẽ duyệt qua từng task, tạo một hàng mới (<tr>) trong bảng và điền thông tin của task vào các ô (<td>).document.addEventListener('DOMContentLoaded', fetchTasks): Dòng này đảm bảo hàmfetchTasksđược gọi ngay khi trang web được tải xong.
Bước 3: Chạy thử
- Đảm bảo ứng dụng Camunda Spring Boot của bạn (đã có cấu hình CORS) đang chạy.
- Tạo một vài instance của quy trình để có user task trong nhóm
management(hoặc bất kỳ user/group nào bạn định truy vấn). - Mở file
tasklist.htmltrong trình duyệt web của bạn.
Bạn sẽ thấy một bảng chứa danh sách các task đang chờ, giống như ví dụ dưới đây.

4. Hướng đi tiếp theo: Sử dụng các Framework hiện đại
Xây dựng giao diện với "vanilla" JavaScript như trên là cách tuyệt vời để hiểu các nguyên tắc cơ bản. Tuy nhiên, trong các dự án thực tế, các developer thường sử dụng các framework frontend như React, Angular, hoặc Vue.js để quản lý trạng thái, tái sử dụng component và tổ chức code tốt hơn.
Bài viết sau từ blog của Camunda sẽ cho bạn một cái nhìn tổng quan về cách xây dựng một tasklist tùy chỉnh bằng cả ba framework này.
Bài viết "Custom Tasklist examples" này sẽ cho bạn thấy cách các framework frontend phổ biến tích hợp với Camunda REST API. Nó củng cố những gì chúng ta vừa làm nhưng ở một quy mô chuyên nghiệp hơn.
Hãy dành khoảng 15 phút để đọc lướt qua bài viết. Đọc kỹ phần giới thiệu "The Showcase" và phần kết luận "Conclusion". Sau đó, hãy xem qua các đoạn code trong phần về framework mà bạn quan tâm nhất (React, Vue, hoặc Angular). Bạn sẽ nhận ra mẫu hình chung: tất cả chúng đều gọi REST API (giống như chúng ta đã làm) và sau đó dùng cú pháp của framework để hiển thị dữ liệu.
Bài viết này khẳng định một điều quan trọng: miễn là bạn có một REST API mạnh mẽ, bạn có thể sử dụng bất kỳ công nghệ frontend nào bạn muốn. Camunda không hề giới hạn lựa chọn của bạn. Nếu bạn muốn tìm hiểu sâu hơn, kho mã nguồn cộng đồng của Camunda cũng là một nguồn tài nguyên quý giá.
Tổng kết
Trong bài học này, bạn đã thực hiện thành công một bước tiến lớn:
- Bạn đã hiểu và giải quyết được vấn đề CORS khi kết nối một ứng dụng web frontend với backend Camunda.
- Bạn đã viết code HTML và JavaScript để tạo một trang web động, sử dụng
fetchAPI để gọi đến Camunda REST API. - Bạn đã xây dựng được một giao diện web đơn giản nhưng đầy đủ chức năng để hiển thị danh sách các task cho người dùng.
Bạn đã tự tay xây dựng được mảnh ghép đầu tiên của một ứng dụng workflow tùy chỉnh. Giờ đây, người dùng đã có thể thấy công việc của họ.
Xem trước bài học tiếp theo
Việc xem được danh sách task là rất tốt, nhưng người dùng cần phải có khả năng tương tác với chúng. Trong bài học tiếp theo, "Tích hợp chức năng claim và complete task vào giao diện web tùy chỉnh", chúng ta sẽ nâng cấp trang tasklist.html. Bạn sẽ học cách thêm các nút "Claim" và "Complete" và viết logic JavaScript để gọi đến các endpoint REST API tương ứng, cho phép người dùng thực sự nhận và hoàn thành công việc của họ ngay trên giao diện web của bạn.