Trong bài viết trước, chúng ta đã hoàn thiện ứng dụng “File management” phía backend sử dụng SpringBoot, JPA + test các API trong ứng dụng bằng Postman (các bạn có thể xem lại bài viết tại đây)

Trong bài viết này, chúng ta sẽ tiếp tục bổ sung thêm giao diện người dùng sử dụng thư viện React kết nối với các API đã thực hiện trước đó, để hoàn thành hoàn chỉnh ứng dụng

Setup CORS backend

CORS (Cross-origin resource sharing) là một cơ chế cho phép hạn chế việc chia sẻ tài nguyên của một trang web đối với các trang web khác.

Ví dụ : web A chỉ cho phép các request từ trang web A này yêu cầu tài nguyên, các trang web B và C sẽ bị hạn chế. Nếu cố truy cập sẽ báo lỗi CORS.

Đây là một cơ chế bảo mật của trình duyệt, khi không sử dụng CORS thì bất cứ trang web nào cũng có thể thực hiện requests tới trang web đó, từ đó gây ra lỗi tài nguyên hệ thống.

Để enable CORS trong SpringBoot chúng ta có thể thực hiện như sau

package vn.techmaster.image.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry
                .addMapping("/**")
                .allowedMethods("GET", "POST", "PUT", "DELETE");
    }
}
WebConfig.java

Tạo project

Để tạo project React chúng ta có thể sử dụng nhiều cách, ở đây mình sử dụng vite. Đây là công cụ build tool được giới thiệu là vô cùng mạnh mẽ, có tốc độ kinh hoàng. Các bạn có thể tham khảo document của nó ở đây : https://vitejs.dev/

Câu lệnh để tạo ứng dụng với vite như sau:

$ npm create vite@latest file-management-frontend

Sau khi chạy xong câu lệnh trên, nó sẽ hiển thị các gợi ý về framework và variant

Chúng ta sẽ chọn framework : Reactvariant : Javascript

Mở project vừa tạo, tiến hành download các dependence cần thiết cho ứng dụng react và chạy project

$ npm install // download các dependence
$ npm run dev // chạy project

Bổ sung thêm các dependence

Ngoài các dependence cần thiết cho ứng dụng React đã cài đặt ở trên, chúng ta bổ sung thêm các dependence sau:

  • bootstrap : framework css dùng để tạo UI
  • axios : thư viện dùng để gọi API
npm install bootstrap axios

Sau khi cài đặt xong bootstrap chúng ta tiến hành import vào trong project để có thể sử dụng

import "bootstrap/dist/css/bootstrap.min.css"
...
main.jsx

Định nghĩa các API

Trong file imageApi chúng ta định nghĩa các API để thao tác với backend sử dụng axios. Các API này bao gồm:

  • Lấy danh sách hình ảnh
  • Upload ảnh
  • Xóa ảnh
import axios from "axios"

export const ENDPOINT = "http://localhost:8080/api/v1/images"

const imageApi = {
   // Lấy danh sách ảnh
    getAllImage() {
        return axios.get(ENDPOINT)
    },

   // Upload ảnh
    uploadImage(file) {
        return axios.post(ENDPOINT, file, {
            headers: {
                "Content-Type": "form-data"
            }
        })
    },

   // Xóa ảnh
    deleteImage(id) {
        return axios.delete(`${ENDPOINT}/${id}`)
    }
}

export default imageApi;
imageApi.js

Lưu ý : Đối với API upload ảnh, vì sử dụng form-data để gửi dữ liệu lên trên server, vì vậy chúng ta cần set Content-Type : “form-data” trong header của request

Hiển thị danh sách hình ảnh ban đầu

Ban đầu chúng ta sẽ gọi API để lấy danh sách các ảnh, để hiển thị dữ liệu ban đầu

Phần gọi API sử dụng useEffect với empty dependence

Các bạn theo dõi đoạn code dưới đây để xem chi tiết

import { useEffect, useState } from "react";
import imageApi, { ENDPOINT } from "./apis/imageApi";

function App() {
    // State chứa danh sách ảnh
    const [images, setImages] = useState([]);

    useEffect(() => {
        // Lấy danh sách ảnh ban đầu
        const fetchImages = async () => {
            try {
                // Gọi API lấy danh sách ảnh
                const rs = await imageApi.getAllImage();

                // Lưu kết quả vào trong state
                setImages(rs.data);
            } catch (error) {
                console.log(error);
            }
        };

        fetchImages();
    }, []);

    return (
        <>
            <div className="container py-5">
                <div className="mb-4">
                    <label htmlFor="input" className="btn btn-primary">
                        Upload image
                    </label>
                    <input
                        type="file"
                        className="d-none"
                        id="input"
                        onChange={handleUploadImage}
                    />
                </div>

                <h2 className="text-center">Image Management</h2>

                <div className="py-4">
                    <div className="row">
                        {images.map((image) => (
                            <div key={image.id} className="col-md-3">
                                <div className="text-center mb-5">
                                    <img
                                        src={`${ENDPOINT}/${image.id}`}
                                        alt={image.name}
                                    />
                                    <a
                                        href={`${ENDPOINT}/download/${image.id}`}
                                        download={image.name}
                                        className="btn btn-warning mt-2 me-2"
                                    >
                                        Download
                                    </a>
                                    <button
                                        className="btn btn-danger mt-2"
                                        onClick={() =>
                                            handleDeleteImage(image.id)
                                        }
                                    >
                                        Delete
                                    </button>
                                </div>
                            </div>
                        ))}
                    </div>
                </div>
            </div>
        </>
    );
}

export default App;
App.jsx

Kết quả ban đầu chúng ta có được như sau

hiển thị danh sách hình ảnh

Upload ảnh

Đối với chức năng upload ảnh, chúng ta cần lấy ra được file cần upload trong ô input

Sau đó tạo đối tượng FormData với key là “file” (key này cần chính xác với phần định nghĩa trong API ở backend để có thể đọc dữ liệu) và value chính là file trong ô input

Tiếp đến gọi API và gửi thông tin FormData vừa tạo lên server để xử lý, sau khi thành công, thêm đối tượng mới vào trong state để có thể re-render lại trên giao diện

// Xử lý upload ảnh
const handleUploadImage = async (e) => {
  // Lấy ra đối tượng file upload
  const file = e.target.files[0];

  // Tạo đối tượng formData chứa thông tin file upload
  const formData = new FormData();
  formData.append("file", file);

  try {
      // Gọi API upload image
      let rs = await imageApi.uploadImage(formData);

      // Lưu kết quả sau khi gọi API vào trong state
      setImages([rs.data, ...images]);
      alert("Upload ảnh thành công");
  } catch (error) {
      console.log(error);
  }
};
App.jsx

Xóa ảnh

Cuối cùng là chức năng xóa ảnh. Trước khi xóa cần người dùng confirm xem có xóa hay không?

Nếu người dùng yêu cầu xóa, lúc này chúng ta sẽ gọi API lên trên server để xóa, khi xóa trên server thành công, tiếp theo sẽ xóa đối tượng image tương ứng trong state để đồng bộ dữ liệu trên giao diện

// Xử lý xóa ảnh
const handleDeleteImage = async (id) => {
  // Xác nhận xem người dùng có muốn xóa không
  const isConfirm = window.confirm("Bạn có muốn xóa không");
  if (!isConfirm) return;
  try {
      // Gọi API xóa ảnh
      await imageApi.deleteImage(id);

      // Xóa ảnh trong state
      const newImages = images.filter((image) => image.id !== id);
      setImages(newImages);

      alert("Xóa ảnh thành công");
  } catch (error) {
      console.log(error);
  }
};
App.jsx

Vậy là chúng ta đã hoàn thành ứng dụng “File Management” bên phía FrontEnd. Hi vọng các bạn thấy bài viết này hữu ích và thú vị 😁😁😁

Phần sources code của bài viết này, các bạn có thể tham khảo tại đây: https://github.com/buihien0109/image-management/tree/main/image-management-frontend

Các bạn có thể tham khảo thêm khóa học này nhé: