Cùng tiếp tục với Bài 27 – Upload hình ảnh và xử lý form với Nuxt 3. Đây là kỹ năng quan trọng giúp bạn xây dựng tính năng như: đăng bài viết có ảnh, avatar người dùng, giao diện quản trị sản phẩm, hoặc kết nối cloud như Cloudinary, S3.
1. Mục tiêu bài học
- Tạo form có input ảnh (file upload)
- Preview ảnh trước khi gửi
- Gửi form kiểu
multipart/form-data
- Lưu tạm ảnh (local) hoặc upload lên dịch vụ như Cloudinary
2. Form đơn giản có upload ảnh
<template>
<form @submit.prevent="submitForm">
<input type="text" v-model="title" placeholder="Tên ảnh" />
<input type="file" accept="image/*" @change="onFileChange" />
<img v-if="preview" :src="preview" alt="preview" style="max-width:200px;" />
<button type="submit">Gửi</button>
</form>
</template>
<script setup>
const title = ref('')
const file = ref(null)
const preview = ref(null)
function onFileChange(e) {
const selected = e.target.files[0]
if (!selected) return
file.value = selected
preview.value = URL.createObjectURL(selected)
}
async function submitForm() {
const formData = new FormData()
formData.append('title', title.value)
formData.append('image', file.value)
try {
await $fetch('/api/upload', {
method: 'POST',
body: formData
})
alert('Upload thành công!')
} catch (err) {
console.error(err)
alert('Lỗi upload!')
}
}
</script>
3. API xử lý upload
Tạo file: server/api/upload.post.ts
import formidable from 'formidable'
import { writeFile } from 'fs/promises'
import path from 'path'
export default defineEventHandler(async (event) => {
const form = formidable({ multiples: false })
const [fields, files] = await form.parse(event.node.req)
const file = files.image?.[0]
if (!file) throw createError({ statusCode: 400, statusMessage: 'No file' })
const tempPath = file.filepath
const filename = file.originalFilename
const uploadDir = path.join(process.cwd(), 'public', 'uploads')
await writeFile(path.join(uploadDir, filename), await Bun.file(tempPath).arrayBuffer())
return { success: true, path: `/uploads/${filename}` }
})
Lưu ý:
- Tạo thư mục
/public/uploads
để chứa ảnh Bun.file()
nếu dùng Bun, hoặcfs.readFileSync()
nếu bạn chạy Node
4. Dùng Cloudinary thay vì lưu local (tuỳ chọn)
Cài gói:
npm install cloudinary
Tạo file .env
:
CLOUDINARY_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
Trong API:
import { v2 as cloudinary } from 'cloudinary'
import { readFile } from 'fs/promises'
cloudinary.config({
cloud_name: process.env.CLOUDINARY_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET
})
const result = await cloudinary.uploader.upload(tempPath, {
folder: 'nuxt-images'
})
→ Trả về result.secure_url
để hiển thị ảnh.
5. Gợi ý nâng cao UX
- Hiển thị loading khi đang upload
- Tự reset form sau khi upload thành công
- Tạo list ảnh đã upload
- Kết hợp
vee-validate
để kiểm tra ảnh
Bạn đã học được
- Tạo form upload ảnh đơn giản
- Preview ảnh trước khi gửi
- Gửi
FormData
đến server - Tạo API lưu ảnh local hoặc upload Cloudinary
- Xử lý form + trạng thái upload hiệu quả
Bài tập mở rộng
- Thêm upload nhiều ảnh cùng lúc
- Tạo giao diện quản lý ảnh (xem, xoá, sửa tên)
- Kết hợp upload + submit nội dung bài viết
- Tự tạo hook
useImageUploader()
để tái sử dụng
Thảo luận