Đây là trái tim của máy bán nước tự động. Nếu trục xoắn hoạt động không chính xác, máy sẽ dễ bị kẹt hoặc nhả nhiều hơn một sản phẩm.
Sau bài này bạn sẽ:
Hiểu nguyên lý của trục xoắn.
Biết cách chọn kích thước phù hợp.
Biết cách tự chế một trục xoắn đơn giản.
Chuẩn bị lắp động cơ ở bài sau.
Trục xoắn (Spiral Dispenser) là một thanh thép được uốn thành hình lò xo.
Nó có nhiệm vụ:
Giữ sản phẩm.
Đẩy sản phẩm tiến về phía trước.
Chỉ nhả đúng 1 sản phẩm khi động cơ quay.
Ví dụ:
Nhìn từ trên xuống
┌────────────────────────────┐
🥤 🥤 🥤 🥤 🥤
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
└────────────────────────────┘
@ = Trục xoắn
Ban đầu:
🥤🥤🥤🥤🥤
@@@@@@@@@@@@
Sau khi động cơ quay:
🥤🥤🥤🥤
@@@@@@@@@@@
🥤
↓
Lon đầu tiên đi qua mép khay và rơi xuống.
Các lon phía sau chỉ tiến lên một khoảng ngắn.
Không phải lúc nào cũng quay một vòng (360°).
Có 3 cách phổ biến:
| Góc quay | Ứng dụng |
|---|---|
| 90° | Hộp nhỏ |
| 180° | Chai nhỏ |
| 360° | Lon nước, snack |
Ở phiên bản mini, chúng ta sẽ điều khiển quay đúng 1 vòng (360°) vì dễ lập trình và dễ hiệu chỉnh.
Ví dụ bán lon Coca 330ml:
Đường kính lon khoảng 66 mm.
Nên chọn:
| Thông số | Giá trị |
|---|---|
| Đường kính trục | 70–75 mm |
| Bước xoắn | 75–80 mm |
| Chiều dài | 30–40 cm |
Nguyên tắc:
Trục quá nhỏ → lon bị kẹt.
Trục quá lớn → lon dễ đổ.
Bước xoắn quá ngắn → khó di chuyển.
Bước xoắn quá dài → có thể nhả hai lon.
Có thể dùng:
Ưu điểm:
Rẻ.
Chịu lực tốt.
Nhược điểm:
Khó uốn.
Ưu điểm:
Chính xác.
Dễ thay đổi kích thước.
Nhược điểm:
Cần máy in 3D.
Hiện nay có thể mua:
Spiral Coil
Vending Machine Spiral
Nhiều kích thước khác nhau.
Giữa trục xoắn cần có một thanh thép.
Ví dụ:
====================
@@@@@@@@@@@@@@@@@@@@
====================
Thanh này giúp:
Không bị cong.
Truyền lực từ động cơ.
Đầu trục:
Motor
█████
│
Khớp nối
│
=================
@@@@@@@@@@@@@@@@@
Không nên gắn trực tiếp vào trục motor vì:
Dễ lệch tâm.
Dễ rung.
Mau hỏng bạc đạn.
Nên dùng khớp nối trục (shaft coupler).
Một đầu nối với motor.
Đầu còn lại nên có bạc đạn.
Motor ================= Bearing
Nếu không có bạc đạn:
Trục dễ võng.
Quay nặng.
Hao điện.
Máy thương mại thường có thêm vách chặn.
Ví dụ:
🥤🥤🥤🥤
@@@@@@@@@@@@
────────────
Vách chặn
Vách này giữ lon thứ hai lại khi lon đầu tiên rơi.
Ở bản mini, bạn có thể dùng một thanh nhựa hoặc mica nhỏ để thử nghiệm.
Làm một trục xoắn duy nhất.
Đặt:
5 lon.
Quay 10 lần.
Ghi lại:
| Lần | Kết quả |
|---|---|
| 1 | ✅ |
| 2 | ✅ |
| 3 | ❌ Kẹt |
| 4 | ✅ |
| 5 | ❌ Rơi 2 lon |
Nếu xuất hiện lỗi, hãy điều chỉnh:
Góc quay.
Bước xoắn.
Khoảng cách giữa lon và vách chặn.
Đừng vội lắp 4 ngăn cùng lúc. Một ngăn hoạt động ổn định trước sẽ giúp bạn tiết kiệm rất nhiều thời gian.
❌ Trục xoắn quá sát đáy → quay rất nặng.
❌ Trục xoắn quá cao → lon lọt xuống dưới.
❌ Động cơ quá yếu → không đủ mô-men xoắn.
❌ Khung bị lệch → trục cọ vào thành.
❌ Không có bạc đạn → trục rung mạnh.
Máy bán hàng thương mại không chỉ bán lon nước. Tùy loại sản phẩm sẽ dùng cơ cấu khác nhau:
| Sản phẩm | Cơ cấu |
|---|---|
| Lon nước | Trục xoắn |
| Bánh snack | Trục xoắn |
| Chai nước lớn | Trục xoắn cỡ lớn hoặc khay đẩy |
| Hộp cơm | Khay trượt |
| Thuốc | Ngăn kéo định vị |
| Điện thoại, tai nghe | Cánh tay gắp hoặc khay khóa điện |
Đó là lý do bạn thấy nhiều máy bán hàng có hình thức giống nhau nhưng bên trong lại rất khác.
Sau bài này, bạn đã nắm được bộ phận quan trọng nhất của máy bán nước tự động:
Hiểu nguyên lý hoạt động của trục xoắn.
Biết cách chọn kích thước theo sản phẩm.
Biết cách lắp trục với động cơ.
Biết các lỗi thường gặp và cách khắc phục.
🥤 Bài 4 — Chọn động cơ và điều khiển bằng ESP32
Chúng ta sẽ tìm hiểu:
Động cơ DC, động cơ bước hay servo phù hợp hơn?
Cách tính mô-men xoắn cần thiết.
Chọn driver điều khiển.
Viết chương trình đầu tiên để nhả đúng một lon nước chỉ bằng một nút bấm.
Cập nhật: 2026-08-06T07:39:00.211+07:00
Từ bài này chúng ta sẽ bắt đầu xây dựng dự án Blog CMS hoàn chỉnh.
Chức năng đầu tiên là quản lý danh mục bài viết (Categories) với đầy đủ các thao tác thêm – sửa – xóa – tìm kiếm – phân trang.
Tạo Resource Controller cho Category.
Xây dựng giao diện quản trị bằng Blade + Tailwind CSS.
Validate dữ liệu đầu vào.
Tìm kiếm theo tên danh mục.
Phân trang dữ liệu.
Hoàn thiện module CRUD chuẩn Laravel 12.
Trường hợp 01: các bài trước đã tạo category rồi nhưng thiếu column
php artisan make:migration add_slug_and_description_to_categories_table --table=categories
Trong file migration:
use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::table('categories', function (Blueprint $table) { $table->string('slug')->unique()->after('name'); $table->text('description')->nullable()->after('slug'); }); } public function down(): void { Schema::table('categories', function (Blueprint $table) { $table->dropColumn(['slug', 'description']); }); } };
Mở:
database/factories/CategoryFactory.php
Nếu đang là:
return [ 'name' => fake()->words(2, true), ];
đổi thành:
use Illuminate\Support\Str; return [ 'name' => $name = fake()->unique()->words(2, true), 'slug' => Str::slug($name), 'description' => fake()->sentence(), ];
File seeder:
use App\Models\Category; use Illuminate\Support\Str; public function run(): void { foreach (['Laravel', 'PHP', 'MySQL'] as $name) { Category::create([ 'name' => $name, 'slug' => Str::slug($name), 'description' => "Danh mục {$name}", ]); } }
php artisan migrate:fresh --seed
Trường hợp 2: nếu chưa có Category, tạo nhanh bằng Artisan:
Bash
php artisan make:model Category -mcr
Laravel sẽ tạo:
app/Models/Category.php
database/migrations/xxxx_create_categories_table.php
app/Http/Controllers/CategoryController.php
PHP
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->timestamps();
});
Chạy migration:
Bash
php artisan migrate
PHP
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Category extends Model
{
use HasFactory;
protected $fillable = [
'name',
'slug',
'description'
];
}
Mở routes/web.php:
PHP
use App\Http\Controllers\CategoryController;
Route::middleware(['auth'])->group(function () {
Route::resource('categories', CategoryController::class);
});
Laravel tự tạo 7 route chuẩn:
| Method | URL |
|---|---|
| GET | /categories |
| GET | /categories/create |
| POST | /categories |
| GET | /categories/{"{category}"} |
| GET | /categories/{"{category}"}/edit |
| PUT | /categories/{"{category}"} |
| DELETE | /categories/{"{category}"} |
PHP (pagination bằng Laravel)
use App\Models\Category;
use Illuminate\Http\Request;
public function index(Request $request)
{
$categories = Category::query()
->when($request->keyword, function ($query, $keyword) {
$query->where('name', 'like', "%{$keyword}%");
})
->latest()
->paginate(10)
->withQueryString();
return view('categories.index', compact('categories'));
}
PHP
<x-app-layout>
<div class="max-w-7xl mx-auto p-6">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">Quản lý Categories</h1>
<a href="{{ route('categories.create') }}"
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700">
+ Thêm Category
</a>
</div>
<form method="GET" class="mb-4">
<input type="text"
name="keyword"
value="{{ request('keyword') }}"
placeholder="Tìm kiếm category..."
class="border rounded-lg px-4 py-2 w-80">
<button class="bg-gray-800 text-white px-4 py-2 rounded-lg">
Tìm
</button>
</form>
<div class="bg-white rounded-xl shadow overflow-hidden">
<table class="min-w-full">
<thead class="bg-gray-100">
<tr>
<th class="px-4 py-3 text-left">ID</th>
<th class="px-4 py-3 text-left">Tên</th>
<th class="px-4 py-3 text-left">Slug</th>
<th class="px-4 py-3 text-left">Ngày tạo</th>
<th class="px-4 py-3 text-right">Thao tác</th>
</tr>
</thead>
<tbody class="divide-y">
@forelse($categories as $category)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3">{{ $category->id }}</td>
<td class="px-4 py-3 font-medium">
{{ $category->name }}
</td>
<td class="px-4 py-3 text-gray-600">
{{ $category->slug }}
</td>
<td class="px-4 py-3">
{{ $category->created_at->format('d/m/Y') }}
</td>
<td class="px-4 py-3 text-right space-x-2">
<a href="{{ route('categories.edit', $category) }}"
class="text-blue-600 hover:underline">
Sửa
</a>
<form action="{{ route('categories.destroy', $category) }}"
method="POST"
class="inline">
@csrf
@method('DELETE')
<button
onclick="return confirm('Xóa category này?')"
class="text-red-600 hover:underline">
Xóa
</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="text-center py-8 text-gray-500">
Chưa có dữ liệu
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-6">
{{ $categories->links() }}
</div>
</div>
</x-app-layout>
PHP
<form action="{{ route('categories.store') }}" method="POST"> @csrf <div class="mb-4"> <label class="block mb-1">Tên Category</label> <input type="text" name="name" value="{{ old('name') }}" class="w-full border rounded-lg px-4 py-2"> @error('name') <p class="text-red-500 text-sm mt-1">{{ $message }}</p> @enderror </div> <div class="mb-4"> <label class="block mb-1">Slug</label> <input type="text" name="slug" value="{{ old('slug') }}" class="w-full border rounded-lg px-4 py-2"> </div> <div class="mb-4"> <label class="block mb-1">Mô tả</label> <textarea name="description" class="w-full border rounded-lg px-4 py-2" rows="4">{{ old('description') }}</textarea> </div> <button class="bg-blue-600 text-white px-6 py-2 rounded-lg"> Lưu </button> </form>
PHP
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|min:2|max:100',
'slug' => 'required|unique:categories,slug',
'description' => 'nullable|max:1000',
]);
Category::create($validated);
return redirect()
->route('categories.index')
->with('success', 'Thêm category thành công!');
}
PHP
public function edit(Category $category)
{
return view('categories.edit', compact('category'));
}
PHP
public function update(Request $request, Category $category)
{
$validated = $request->validate([
'name' => 'required|min:2|max:100',
'slug' => 'required|unique:categories,slug,' . $category->id,
'description' => 'nullable|max:1000',
]);
$category->update($validated);
return redirect()
->route('categories.index')
->with('success', 'Cập nhật thành công!');
}
Điểm quan trọng là rule:
PHP
'slug' => 'required|unique:categories,slug,' . $category->id
Điều này cho phép giữ nguyên slug hiện tại mà không bị báo lỗi trùng.
PHP
public function destroy(Category $category)
{
$category->delete();
return redirect()
->route('categories.index')
->with('success', 'Đã xóa category!');
}
Trong bài 30 chúng ta sẽ nâng cấp thành Soft Delete.
Chức năng tìm kiếm đã được tích hợp trong phương thức index():
PHP
->when($request->keyword, function ($query, $keyword) {
$query->where('name', 'like', "%{$keyword}%");
})
Ví dụ:
Nhập laravel → hiển thị các category chứa từ “laravel”.
Nhập php → hiển thị các category liên quan PHP.
Để trống → hiển thị toàn bộ dữ liệu.
Nhờ withQueryString(), khi chuyển trang Laravel vẫn giữ lại từ khóa tìm kiếm.
PHP
->paginate(10)
Hiển thị:
PHP
{{ $categories->links() }}
Laravel 12 mặc định sử dụng giao diện phân trang tương thích Tailwind CSS.
Thêm vào đầu trang:
PHP
@if(session('success'))
<div class="mb-4 rounded-lg bg-green-100 border border-green-300
text-green-800 px-4 py-3">
{{ session('success') }}
</div>
@endif
Kết quả:
Thêm mới → “Thêm category thành công!”
Cập nhật → “Cập nhật thành công!”
Xóa → “Đã xóa category!”
app/
├── Http/
│ └── Controllers/
│ └── CategoryController.php
├── Models/
│ └── Category.php
resources/
└── views/
└── categories/
├── index.blade.php
├── create.blade.php
└── edit.blade.php
routes/
└── web.php
Sau bài học này, module Quản lý Categories của Blog CMS đã có đầy đủ:
CRUD
Quản lý dữ liệu
Danh sách Categories
Thêm mới Category
Cập nhật Category
Xóa Category
Validation
Kiểm tra dữ liệu
Validate tên danh mục
Validate slug duy nhất
Hiển thị lỗi ngay trên form
Search
Tìm kiếm
Lọc theo tên category
Giữ từ khóa khi chuyển trang
Pagination
Phân trang
10 bản ghi mỗi trang
Giao diện Tailwind mặc định của Laravel 12
Đây chính là mẫu CRUD chuẩn mà chúng ta sẽ tái sử dụng cho nhiều module khác trong dự án.
Sang Bài 27 — CRUD Posts, chúng ta sẽ kết hợp:
Relationship giữa Post và Category.
Gán bài viết cho User.
Upload hình ảnh.
Validate dữ liệu phức tạp hơn.
Hiển thị danh sách bài viết kèm tên danh mục và tác giả.
Từ bài 27 trở đi, dự án Blog CMS sẽ bắt đầu có cấu trúc giống một hệ thống quản trị nội dung thực tế.
Cập nhật: 2026-08-06T21:10:56.153+07:00
Sau bài này bạn sẽ:
Biết cách thiết kế khung máy.
Hiểu kích thước của từng bộ phận.
Chuẩn bị sẵn để lắp động cơ ở bài sau.
Lưu ý: Chúng ta không cố gắng làm giống máy thương mại ngay từ đầu. Mục tiêu là tạo một phiên bản đơn giản, chắc chắn và dễ nâng cấp.
Máy sẽ chia thành 3 tầng:
+---------------------------+
| Bảng điều khiển |
| (Màn hình + Nút bấm) |
+---------------------------+
| |
| Kho chứa sản phẩm |
| 4 ngăn |
| |
+---------------------------+
| Khay nhận sản phẩm |
+---------------------------+
Ví dụ:
| Bộ phận | Kích thước |
|---|---|
| Cao | 60 cm |
| Rộng | 35 cm |
| Sâu | 30 cm |
Đây là kích thước vừa đủ để chứa:
4 ngăn bán hàng
Mỗi ngăn 5–6 lon
ESP32
Adapter
Driver động cơ
Có nhiều lựa chọn:
| Vật liệu | Ưu điểm | Nhược điểm |
|---|---|---|
| MDF 9 mm | Rẻ, dễ cắt | Không chịu nước |
| Mica 5 mm | Đẹp, nhìn thấy bên trong | Giá cao hơn |
| Nhôm định hình | Rất chắc | Chi phí cao |
| In 3D | Chính xác | Chậm nếu kích thước lớn |
👉 Đối với người mới, MDF + mica là lựa chọn cân bằng giữa chi phí và tính thẩm mỹ.
Ví dụ 4 ngăn:
+-------------------------+
| A1 | A2 | A3 | A4 |
| | | | |
| | | | |
| | | | |
+-------------------------+
Mỗi ngăn sẽ có:
1 trục xoắn.
1 động cơ.
5–6 lon nước.
Toàn bộ phần điện sẽ nằm phía sau:
+----------------------+
ESP32
Motor Driver
Nguồn 12V
Dây điện
+----------------------+
Điều này giúp việc sửa chữa dễ dàng hơn.
Phía dưới cần một khay nghiêng nhẹ:
Lon rơi
↓
__________
/
/
___/____________
Góc nghiêng khoảng 10–15° là đủ để lon lăn ra phía trước.
Không cần dùng cửa tự động ở phiên bản đầu.
Chỉ cần:
____________
| |
| |
|____________|
Một cửa mica nhỏ hoặc cửa nhựa có bản lề là đủ.
Mỗi động cơ đặt ở cuối trục xoắn:
Motor
█████
======== Spiral ========
🥤🥤🥤🥤🥤
Motor chỉ quay khi khách chọn sản phẩm.
| Vật tư | Số lượng |
|---|---|
| Tấm MDF 9 mm | 1–2 tấm |
| Tấm mica trong | 1 tấm |
| Bản lề nhỏ | 2 |
| Tay nắm cửa | 1 |
| Ốc vít | 1 bộ |
| Ke góc chữ L | 8–12 cái |
| Keo gỗ | 1 chai |
Để mặt trước bằng mica trong để quan sát sản phẩm.
Chừa nắp phía sau có thể tháo ra khi cần sửa chữa.
Khoan sẵn các lỗ đi dây để việc lắp điện gọn gàng.
Chừa khoảng trống phía trên để sau này lắp màn hình hoặc đầu đọc QR.
Ở bài tiếp theo, chúng ta sẽ làm bộ phận quan trọng nhất của máy bán hàng: trục xoắn (spiral dispenser).
Bạn sẽ học:
Vì sao chỉ một lon được đẩy ra mỗi lần.
Cách chọn kích thước lò xo xoắn.
Cách gắn trục xoắn với động cơ.
Cách tự chế trục xoắn nếu không mua sẵn.
Đây là phần quyết định máy hoạt động mượt hay thường xuyên bị kẹt sản phẩm, nên chúng ta sẽ đi thật chi tiết với hình minh họa và bản vẽ đơn giản.
Cập nhật: 2026-08-04T19:42:01.243+07:00
Sau bài này bạn sẽ hiểu:
Máy bán nước hoạt động như thế nào.
Bên trong có những bộ phận gì.
Những gì chúng ta sẽ tự làm.
Không cần lập trình.
Không cần điện tử.
Chỉ cần hiểu nguyên lý.
Ví dụ khách muốn mua Coca.
Quy trình diễn ra như sau:
Khách chọn số A1
↓
Máy kiểm tra thanh toán
↓
Động cơ A1 quay
↓
Lon Coca rơi xuống
↓
Cảm biến phát hiện
↓
Mở cửa lấy hàng
↓
Kết thúc
Toàn bộ quá trình chỉ khoảng 2–4 giây.
Một máy bán nước có khoảng 6 hệ thống chính:
+---------------------------+
| Màn hình |
+---------------------------+
| Bộ điều khiển |
+---------------------------+
| Bộ nhận thanh toán |
+---------------------------+
| Động cơ các ngăn |
+---------------------------+
| Cảm biến rơi |
+---------------------------+
| Kho chứa nước |
+---------------------------+
Không cần làm máy lớn.
Phiên bản đầu tiên chỉ có 4 ngăn.
Ví dụ:
+--------------------+
🥤 Coca
🥤 Pepsi
🥤 7Up
🥤 Sting
+--------------------+
Mỗi ngăn có một động cơ riêng.
Giả sử khách bấm nút số 2.
ESP32 sẽ nhận tín hiệu.
Nút số 2
↓
ESP32
↓
Motor số 2 quay
↓
Lon rơi
↓
Hoàn thành
Đơn giản vậy thôi.
Đây là phần thông minh nhất.
Bên trong có một trục xoắn (spiral).
Ban đầu:
🥤🥤🥤🥤🥤
~~~~~~~~~~
Khi motor quay một vòng nhỏ:
🥤🥤🥤🥤
🥤↓
~~~~~~~~~~
Chỉ lon đầu tiên bị đẩy ra.
Các lon còn lại vẫn nằm trong hàng.
Đó là nguyên lý mà hầu hết máy bán hàng hiện nay sử dụng.
Không mua máy thật.
Chỉ cần:
ESP32
4 động cơ giảm tốc
4 lò xo xoắn (có thể tự uốn bằng thép hoặc in 3D)
Adapter 12V
Nút nhấn
Khung gỗ hoặc mica
Tổng chi phí vẫn khá thấp so với một máy thương mại.
Bài 1: Hiểu nguyên lý hoạt động. ✅
Bài 2: Thiết kế khung máy mini.
Bài 3: Làm cơ cấu trục xoắn để nhả từng lon.
Bài 4: Điều khiển động cơ bằng ESP32.
Bài 5: Thêm nút chọn sản phẩm.
Bài 6: Cảm biến xác nhận sản phẩm đã rơi.
Bài 7: Hiển thị màn hình OLED/LCD.
Bài 8: Thanh toán bằng mã QR.
Bài 9: Kết nối Wi-Fi và quản lý từ xa.
Bài 10: Hoàn thiện máy bán nước mini.
Hãy quan sát một máy bán nước tự động ở siêu thị hoặc trường học.
Ghi lại:
Có bao nhiêu hàng sản phẩm?
Có dùng tiền mặt, thẻ hay QR?
Sau khi chọn sản phẩm, máy mất bao lâu để nhả hàng?
Thử tưởng tượng nếu thay nước bằng bánh, mì hoặc đồ dùng học tập thì cơ cấu nào phải thay đổi, cơ cấu nào vẫn giữ nguyên?
💡 Mẹo: Từ bài 2 trở đi, chúng ta sẽ bắt đầu thiết kế theo hướng dễ gia công tại Việt Nam, ưu tiên dùng vật liệu như gỗ MDF, mica, nhôm định hình và các linh kiện phổ biến trên thị trường để bạn có thể tự lắp ráp mà không cần xưởng cơ khí chuyên nghiệp.
Cập nhật: 2026-08-03T11:10:00.190+07:00
Sau khi hoàn thành:
Bài 21 — Laravel Breeze
Bài 22 — Login
Bài 23 — Register
Bài 24 — Middleware
Chúng ta đã có hệ thống:
✅ Đăng ký tài khoản
✅ Đăng nhập
✅ Session Authentication
✅ Bảo vệ Route bằng Middleware
Nhưng hiện tại Laravel mới chỉ biết:
"Người này đã đăng nhập hay chưa?"
Ví dụ:
Route::middleware('auth')->group(function(){
Route::get('/dashboard', function(){
return view('dashboard');
});
});
Middleware auth chỉ kiểm tra:
User có đăng nhập?
|
|
Có
|
Cho phép truy cập
Nhưng trong thực tế CMS Blog cần nhiều hơn:
Ví dụ:
User A tạo bài viết:
ID: 10
Title: Laravel 12 Tutorial
User_id: 5
User B đăng nhập.
Nếu User B truy cập:
/posts/10/edit
thì sao?
Nếu chỉ dùng:
Route::middleware('auth')
Laravel sẽ cho phép.
Vấn đề:
User B
|
|
V
Sửa bài viết của User A
Đây là lỗi phân quyền.
Hai khái niệm rất dễ nhầm.
(Authentication = Xác thực)
Câu hỏi:
Bạn là ai?
Ví dụ:
Login:
Email:
admin@gmail.com
Password:
******
Laravel kiểm tra:
Database users
email
password
Kết quả:
OK
User ID = 1
(Authorization = Phân quyền)
Câu hỏi:
Bạn được phép làm gì?
Ví dụ:
User:
id: 5
name: Nam
role: user
Admin:
id: 1
name: Admin
role: admin
Quyền:
| Chức năng | Admin | User |
|---|---|---|
| Xem bài viết | ✅ | ✅ |
| Tạo bài viết | ✅ | ✅ |
| Sửa bài người khác | ✅ | ❌ |
| Xóa user | ✅ | ❌ |
| Quản lý hệ thống | ✅ | ❌ |
Laravel cung cấp 3 cơ chế chính:
Authorization
|
---------------------------------
| | |
Middleware Gate Policy
| | |
Chặn route Logic nhỏ Logic theo Model
Gate dùng cho các quyền đơn giản.
Ví dụ:
Chỉ Admin được vào trang quản trị.
Mở:
app/Providers/AppServiceProvider.php
Thêm:
use Illuminate\Support\Facades\Gate;
use App\Models\User;
public function boot(): void
{
$this->configureDefaults();//dòng mặc định 13x
Gate::define('admin', function(User $user){
return $user->role === 'Admin';//có phân biệt Hoa-thường
});
}
Ý nghĩa:
Khi gọi:
Gate::allows('admin')
Laravel chạy:
$user->role === 'admin'
Nếu:
admin
trả về:
true
Ví dụ:
Tạo AdminController:
php artisan make:controller Admin/AdminController
Trong routes/web.php:
use App\Http\Controllers\Admin\AdminController;
Route::get('/admin', [AdminController::class, 'index']);
tạo trang views/admin/index.blade.php
<h1>Trang quản trị</h1>
<p>Chúc mừng! Bạn đã vượt qua Gate Authorization.</p>
AdminController:
use Illuminate\Support\Facades\Gate;
public function index()
{
if(!Gate::allows('admin')){
abort(403);
}
return view('admin.index');
}
Nếu User thường truy cập:
Chú ý: trong các bài trước nếu đã seeder ra 01 đống users thì vào UserFactory để biết mật khẩu.
/admin
Laravel trả:
403 Forbidden
Laravel có sẵn middleware:
can
Ví dụ:
Route::get('/admin',
[
AdminController::class,
'index'
])
->middleware('can:admin');
Bây giờ:
Admin
|
OK
User
|
403
Gate phù hợp với quyền đơn giản.
Nhưng Blog CMS có nhiều quyền:
Ví dụ Post:
Post
create
read
update
delete
restore
forceDelete
Không nên viết:
if($user->role=='admin')
ở khắp Controller.
Code sẽ rất rối.
Giải pháp:
Policy gom toàn bộ quyền của một Model.
Ví dụ:
PostPolicy.php
Chạy:
php artisan make:policy PostPolicy --model=Post
Laravel tạo:
app
|
Policies
|
PostPolicy.php
Nội dung:
class PostPolicy
{
}
Laravel tạo sẵn:
viewAny()
view()
create()
update()
delete()
restore()
forceDelete()
Ví dụ:
User chỉ sửa bài của mình.
Post:
id
title
content
user_id
User:
id = 5
Post:
user_id = 5
Cho phép.
Code:
public function update(
User $user,
Post $post
){
return $user->id === $post->user_id;
}
Logic:
User đăng nhập
|
|
Post owner?
|
--------------
| |
Yes No
Cho sửa 403
Laravel 12 tự động phát hiện theo convention:
Model:
App\Models\Post
Policy:
App\Policies\PostPolicy
Laravel tự map:
Post
+
PostPolicy
Không cần khai báo.
Ví dụ:
PostController:
public function edit(Post $post)
{
$this->authorize(
'update',
$post
);
return view(
'posts.edit',
compact('post')
);
}
Nếu:
User_id = Post.user_id
cho phép.
Ngược lại:
403 Forbidden
Có thể viết trực tiếp:
Route::get(
'/posts/{post}/edit',
[
PostController::class,
'edit'
]
)
->middleware(
'can:update,post'
);
Laravel hiểu:
can:
update
Model:
post
Trong giao diện:
Ví dụ nút Edit:
@can('update',$post)
<a href="#">
Edit
</a>
@endcan
Nếu User không có quyền:
Nút không xuất hiện.
Chú ý: bài trước mình lỡ cấu hình kỹ quá chỉ cho hiện posts theo user nên k thực hành được theo ví dụ này.
Giờ chuyển sang dấu nút Mở trang Admin trong link dashboard nếu k phải role admin
AppServiceProvider Laravel 12/13.
use App\Models\User; use Illuminate\Support\Facades\Gate; public function boot(): void { $this->configureDefaults(); Gate::define('admin', function (User $user) { return $user->role === 'Admin'; }); }
Trong Blade:
@can('admin') <a href="{{ route('admin.index') }}" class="inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"> Mở </a> @endcan
Migration:
php artisan make:migration add_role_to_users_table
Migration:
Schema::table('users', function(Blueprint $table){
$table->string('role')
->default('user');
});
Chạy:
php artisan migrate
Database:
users
| id | name | role |
|---|---|---|
| 1 | Admin | admin |
| 2 | Nam | user |
| 3 | Lan | user |
Trong Model User:
public function isAdmin()
{
return $this->role === 'admin';
//nên chuyển sang ký tự thường cho an toàn
//return strtolower($this->role) === 'admin';
}
Sử dụng:
if(auth()->user()->isAdmin()){
echo "Admin";
}
Dashboard
|
|
CRUD Categories
|
|
CRUD Users
|
|
Delete Post
Dashboard
|
|
Create Post
|
|
Edit Post của mình
|
|
Delete Post của mình
User Login
|
|
Authentication
|
|
Middleware auth
|
|
Authorization
|
|
Gate / Policy
|
|
Allow / Deny
app
├── Models
│ ├── User.php
│ └── Post.php
├── Policies
│ └── PostPolicy.php
├── Http
│ ├── Controllers
│ │
│ └── Middleware
routes
└── web.php
Sau bài 25, người học hiểu:
✅ Authentication khác Authorization
✅ Gate trong Laravel
✅ Policy trong Laravel
✅ $this->authorize()
✅ can Middleware
✅ Blade @can
✅ Phân quyền Admin/User
✅ Bảo vệ dữ liệu theo User sở hữu
Chúng ta sẽ nâng cấp từ:
role = admin/user
thành hệ thống thực tế:
Users
|
Roles
|
Permissions
Admin
├── create_post
├── edit_post
├── delete_user
Editor
├── edit_post
User
├── create_post
Kết hợp:
Middleware Admin
Permission table
Role table
Pivot relationship
Phân quyền CRUD hoàn chỉnh cho Blog CMS Laravel 12.
Có 2 lỗi độc lập:
npm run build thất bại ✅ (nguyên nhân gốc)Bạn đang dùng:
postcss.config.js của Tailwind v3
Bạn có:
export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, };
Trong khi với Tailwind v4 phải là:
export default { plugins: { "@tailwindcss/postcss": {}, }, };
Đây là nguyên nhân gây lỗi:
Cannot find module 'autoprefixer'
Sau khi sửa, npm run build chạy bình thường.
Đây không liên quan đến build.
about.blade.php chỉ có:
<h1>Trang About</h1> <div class="bg-blue-600"> Hello Tailwind </div>
nên Laravel trả về đúng đoạn HTML đó, không qua layout chứa:
@vite(['resources/css/app.css', 'resources/js/app.js'])
Khi bọc bằng:
<x-app-layout> ... </x-app-layout>
thì Tailwind mới được nạp và các class mới có tác dụng.
Chú ý: trang about cho mọi users truy cập nên k dùng x-app-layout (dính navbar)
Đây là quy trình đầy đủ để tạo <x-public-layout> bằng Artisan.
php artisan make:component PublicLayout
Laravel sẽ tạo:
app/
└── View/
└── Components/
└── PublicLayout.php
resources/
└── views/
└── components/
└── public-layout.blade.php
resources/views/components/public-layout.blade.phpThay toàn bộ nội dung mặc định:
<div>
<!-- An unexamined life is not worth living. - Socrates -->
</div>
thành:
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ config('app.name') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-gray-100">
<main class="max-w-7xl mx-auto px-6 py-8">
{{ $slot }}
</main>
</body>
</html>
{{ $slot }} là nơi nội dung của từng trang sẽ được chèn vào.
Ví dụ resources/views/about.blade.php:
<x-public-layout>
<h1 class="text-3xl font-bold mb-6">About</h1>
<div class="bg-orange-600 text-white p-4 rounded-lg">
Hello Tailwind
</div>
</x-public-layout>
Route::view('/about', 'about');
hoặc
Route::get('/about', function () {
return view('about');
});
app/
└── View/
└── Components/
└── PublicLayout.php
resources/
└── views/
├── about.blade.php
└── components/
└── public-layout.blade.php
Đây là cách làm chuẩn khi dùng Blade Components trong Laravel 12, tương tự như các component sẵn có như <x-app-layout>, <x-guest-layout> và <x-input-label>.
x1
Cập nhật: 2026-08-05T21:21:04.361+07:00
Ở các bài trước, chúng ta đã cài đặt Laravel Breeze, đăng ký tài khoản và đăng nhập vào hệ thống.
Tuy nhiên, hiện tại nếu người dùng biết đường dẫn trực tiếp như:
/posts/create
hoặc:
/dashboard
thì liệu họ có thể truy cập được hay không?
Đó chính là nhiệm vụ của Middleware.
Middleware đóng vai trò như một lớp bảo vệ, kiểm tra mọi yêu cầu trước khi cho phép truy cập vào Controller hoặc Route.
Middleware là đoạn mã nằm giữa Request và Controller.
Mọi yêu cầu gửi đến Laravel đều có thể đi qua một hoặc nhiều Middleware.
Sơ đồ:
Người dùng
│
▼
HTTP Request
│
▼
Middleware
│
▼
Controller
│
▼
Response
Nếu Middleware từ chối yêu cầu, Controller sẽ không được thực thi.
Giả sử Blog có các chức năng:
Xem bài viết.
Thêm bài viết.
Chỉnh sửa bài viết.
Xóa bài viết.
Khách chưa đăng nhập:
✅ Được xem bài viết.
❌ Không được tạo bài viết.
❌ Không được sửa bài viết.
❌ Không được xóa bài viết.
Middleware sẽ đảm nhận việc kiểm tra này.
Laravel đã có sẵn Middleware:
auth
Nó sẽ kiểm tra:
Người dùng đã đăng nhập?
│
Có Không
│ │
▼ ▼
Cho phép Chuyển Login
Ví dụ:
use App\Http\Controllers\PostController;
Route::middleware('auth')->group(function () {
Route::resource(
'posts',
PostController::class
);
});
Toàn bộ các Route của PostController đều yêu cầu đăng nhập.
Ví dụ:
Route::get(
'/dashboard',
function () {
return view('dashboard');
}
)->middleware('auth');
Nếu chưa đăng nhập, Laravel sẽ chuyển người dùng đến trang Login.
Đăng xuất khỏi hệ thống.
Sau đó truy cập:
/posts/create
Kết quả:
302 Redirect
↓
/login
Đây là hành vi mặc định của Middleware auth.
Sau khi cài Laravel Breeze, Route dashboard thường được bảo vệ như sau:
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth']);
Điều này đảm bảo chỉ người dùng đã xác thực mới truy cập được.
Laravel cũng có Middleware:
guest
Ý nghĩa:
Chưa đăng nhập → được truy cập.
Đã đăng nhập → chuyển hướng.
Ví dụ:
Route::middleware('guest')->group(function () {
Route::get(
'/login',
...
);
Route::get(
'/register',
...
);
});
Người đã đăng nhập( k phải guest) sẽ không thể mở lại trang Login hoặc Register.
Một Route có thể sử dụng nhiều Middleware.
Ví dụ:
Route::middleware([
'auth',
'verified',
])->group(function () {
//
});
Laravel sẽ kiểm tra lần lượt:
Đã đăng nhập.
Email đã xác minh.
Nếu một bước không đạt, yêu cầu sẽ bị chặn.
Tạo bằng Artisan:
php artisan make:middleware CheckPostOwner
Laravel sẽ tạo:
app/
└── Http/
└── Middleware/
CheckPostOwner.php
public function handle(
Request $request,
Closure $next
)
{
if (!auth()->check()) {
return redirect()
->route('login');
}
return $next($request);
}
$next($request) nghĩa là cho phép Request tiếp tục đi tới Controller.
Giả sử mỗi Post có:
user_id
Middleware:
$post = $request->route('post');
if (
$post->user_id != auth()->id()
) {
abort(403);
}
return $next($request);
Nếu người dùng không phải là tác giả:
403 Forbidden
Đây là cách bảo vệ dữ liệu rất phổ biến trong Blog CMS.
Chú ý: ví dụ này chỉ minh họa vì Breeze thiết kế để mỗi user chỉ nhìn thấy profile của chính mình nên k cần middleware.
trước khi chạy link /profile nên dùng lệnh tạo CategoryController trước (do đã khai báo trong route nhưng chưa tạo dẫn đến lỗi trong route:list)
/profile/5/edit
Middleware kiểm tra:
$profile = $request
->route('profile');
if (
$profile->user_id != auth()->id()
) {
abort(403);
}
Người dùng chỉ được sửa Profile của chính mình.
Một số Middleware thường gặp:
| Middleware | Chức năng |
|---|---|
| auth | Kiểm tra đăng nhập |
| guest | Chỉ cho khách truy cập |
| verified | Email đã xác minh |
| throttle | Giới hạn số lần gửi Request |
| signed | Kiểm tra URL có chữ ký |
| custom | Middleware tự tạo |
Quy trình xử lý:
Request
│
▼
Middleware
│
▼
Validation
│
▼
Controller
│
▼
Model
│
▼
Database
Middleware luôn hoạt động trước Controller.
Trong bài này chúng ta đã học:
Middleware là gì.
Middleware auth.
Middleware guest.
Bảo vệ Route.
Route Group.
Tạo Middleware bằng Artisan.
$next($request).
abort(403).
Kiểm tra chủ sở hữu của Post và Profile.
Middleware là tuyến phòng thủ đầu tiên của ứng dụng Laravel, giúp bảo vệ các chức năng quan trọng trước khi dữ liệu được xử lý.
Bảo vệ toàn bộ Route posts bằng Middleware auth.
Kiểm tra truy cập /dashboard khi chưa đăng nhập.
Tạo Middleware CheckPostOwner.
Chỉ cho phép tác giả sửa hoặc xóa bài viết của mình.
Áp dụng Middleware cho chức năng chỉnh sửa Profile.
Thử truy cập bằng tài khoản khác và quan sát kết quả 403 Forbidden.
Kết hợp auth và verified cho một Route bất kỳ.
Ở Bài 25, chúng ta sẽ tìm hiểu Authorization, cơ chế phân quyền trong Laravel bằng Gate và Policy, giúp xác định ai được phép thực hiện hành động nào trên từng đối tượng dữ liệu như Post, User hoặc Category.
Phần view posts (hiện tại mới xây dựng có search theo category)
Có thể dùng CDN.
<link rel="stylesheet"
href="https://cdn.datatables.net/2.3.4/css/dataTables.dataTables.min.css">
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/2.3.4/js/dataTables.min.js"></script>
<table id="postsTable" class="min-w-full">
<thead>
<tr>
<th>ID</th>
<th>Tiêu đề</th>
<th>User</th>
<th>Ngày đăng</th>
</tr>
</thead>
<tbody>
@foreach($posts as $post)
<tr>
<td>{{ $post->id }}</td>
<td>{{ $post->title }}</td>
<td>{{ $post->user->name }}</td>
<td>{{ $post->created_at->format('d/m/Y') }}</td>
</tr>
@endforeach
</tbody>
</table>
new DataTable('#postsTable', {
pageLength: 10,
responsive: true,
ordering: true,
searching: true,
info: true,
lengthChange: true
});
Là có ngay
new DataTable('#postsTable', {
language: {
search: "🔍 Tìm kiếm:",
lengthMenu: "Hiển thị _MENU_ dòng",
info: "Hiển thị _START_ đến _END_ / _TOTAL_",
paginate: {
first: "Đầu",
last: "Cuối",
next: "Sau",
previous: "Trước"
},
zeroRecords: "Không tìm thấy dữ liệu",
emptyTable: "Chưa có dữ liệu"
}
});
public function index()
{
$posts = Post::with('user')
->latest()
->get();
return view('posts.index', compact('posts'));
}
Không cần code search nữa vì DataTables search ngay trên trình duyệt.
Khi số lượng bài viết lớn (ví dụ 10.000–100.000 bài), không nên tải tất cả về trình duyệt. Khi đó nên dùng Server-side Processing của DataTables:
return DataTables::eloquent(
Post::with('user')
)->toJson();
Kết hợp với gói Yajra DataTables trong Laravel để:
Mục 4 nên hiển thị all posts cho admin only...
Trong Laravel nên xử lý ở Controller bằng cách kiểm tra role/admin trước khi query.
Ví dụ bạn đang có cột role trong bảng users (admin, editor, user) như các bài trước, có thể làm:
use Illuminate\Support\Facades\Auth;
public function index()
{
$user = Auth::user();
if ($user->isAdmin()) {
// Admin xem tất cả bài viết
$posts = Post::with('user')
->latest()
->get();
} else {
// User thường chỉ xem bài của mình
$posts = Post::with('user')
->where('user_id', $user->id)
->latest()
->get();
}
return view('posts.index', compact('posts'));
}
Sau đó thêm function isAdmin() trong User Model:
app/Models/User.php
public function isAdmin()
{
return strtolower($this->role) === 'admin';
}
Khi đó logic sẽ là:
| User | Query |
|---|---|
| Admin | Post::all() → thấy tất cả posts |
| User | where('user_id', Auth::id()) → chỉ thấy posts của mình |
Có thể viết gọn hơn bằng toán tử 3 ngôi:
public function index()
{
$user = Auth::user();
$posts = Post::with('user')
->when(!$user->isAdmin(), function ($query) use ($user) {
$query->where('user_id', $user->id);
})
->latest()
->get();
return view('posts.index', compact('posts'));
}
Cách này Laravel style hơn vì chỉ có một query chain.
Sau này mở rộng thêm:
Admin → CRUD tất cả
Editor → sửa bài được giao
User → chỉ CRUD bài của mình
thì nên chuyển sang Laravel Policy + Gate thay vì kiểm tra trực tiếp trong Controller.
x1
Cập nhật: 2026-08-02T17:14:52.404+07:00
Một phiên bản mini sẽ giúp bạn học gần như toàn bộ nguyên lý của máy bán hàng tự động mà chi phí thấp hơn rất nhiều.
Làm một máy cao khoảng 40–60 cm, chứa được:
6–12 lon nước ngọt mini hoặc chai nhỏ
4–6 ô bán hàng
Thanh toán bằng nút bấm hoặc QR
Tự nhả đúng 1 sản phẩm
| Thiết bị | Giá tham khảo |
|---|---|
| ESP32 | 180.000đ |
| Driver động cơ | 60.000đ |
| Motor giảm tốc 12V | 120.000đ × 4 |
| Adapter 12V | 150.000đ |
| Màn hình LCD OLED | 120.000đ |
| Nút bấm | 20.000đ |
| Cảm biến hồng ngoại | 50.000đ |
| Khung mica hoặc gỗ | 300.000–800.000đ |
Tổng chi phí khoảng 1,5–2,5 triệu đồng nếu chưa tích hợp thanh toán.
Khách bấm nút
↓
ESP32 kiểm tra
↓
Motor quay khoảng 180–360°
↓
Lon rơi xuống
↓
Cảm biến xác nhận
↓
Hiển thị "Cảm ơn"
Phiên bản 1:
✅ Bấm nút để lấy nước.
Phiên bản 2:
✅ Thêm màn hình hiển thị.
Phiên bản 3:
✅ Quét mã QR để thanh toán.
Phiên bản 4:
✅ Wi-Fi, quản lý từ xa bằng Node.js.
Phiên bản 5:
✅ Thêm hệ thống làm lạnh.
Dự án này không chỉ là "máy bán nước". Sau khi hoàn thành, bạn sẽ học được:
Lập trình vi điều khiển (ESP32).
Điều khiển động cơ DC.
Đọc cảm biến.
Giao tiếp Wi-Fi.
Thiết kế cơ khí đơn giản.
Kết nối phần mềm quản lý với phần cứng.
Đây là 01 dự án rất phù hợp để mở rộng nếu sau này bạn muốn học về IoT hoặc tự động hóa.
Theo mình, chỉ cần khoảng 10–12 bài học, mỗi bài thực hiện một phần nhỏ, là bạn có thể hoàn thành một máy bán nước mini hoạt động thật.
Sau đó chỉ việc mở rộng số ngăn và bổ sung các tính năng để tiến gần đến một máy thương mại.
Cập nhật: 2026-08-02T19:42:13.160+07:00
Ở bài trước, chúng ta đã tìm hiểu cách đăng nhập (Login) hoạt động trong Laravel.
Trong bài này, chúng ta sẽ tìm hiểu Register – chức năng tạo tài khoản mới cho người dùng.
Laravel Breeze đã tạo sẵn toàn bộ quy trình đăng ký, nhưng hiểu cách nó hoạt động sẽ giúp bạn dễ dàng tùy biến theo yêu cầu của từng dự án.
Register là quá trình tạo một tài khoản mới.
Thông thường người dùng sẽ nhập:
Họ tên
Mật khẩu
Xác nhận mật khẩu
Sau khi đăng ký thành công, Laravel sẽ:
Kiểm tra dữ liệu.
Mã hóa mật khẩu.
Lưu User vào Database.
Đăng nhập tự động.
Chuyển tới Dashboard.
Người dùng
│
▼
Điền Form Register
│
▼
Validation
│
▼
Kiểm tra Email
│
▼
Hash Password
│
▼
Lưu User
│
▼
Đăng nhập tự động
│
▼
Dashboard
Đây là quy trình mặc định của Laravel Breeze.
Laravel Breeze đã tạo sẵn Form:
//resources/views/auth/register.blade.php
<form
method="POST"
action="{{ route('register') }}">
@csrf
<input
type="text"
name="name">
<input
type="email"
name="email">
<input
type="password"
name="password">
<input
type="password"
name="password_confirmation">
<button>
Đăng ký
</button>
</form>
Laravel kiểm tra dữ liệu trước khi tạo User.
Ví dụ:
//app/Http/Controllers/Auth/RegisteredUserController.php
$request->validate([
'name' =>
'required|string|max:255',
'email' =>
'required|email|unique:users,email',
'password' =>
'required|confirmed|min:8',
]);
Ý nghĩa:
required: bắt buộc nhập.
unique: Email không được trùng.
confirmed: phải có trường password_confirmation và hai giá trị phải giống nhau.
min:8: mật khẩu tối thiểu 8 ký tự.
Sau khi Validation thành công:
//app/Http/Controllers/Auth/RegisteredUserController.php
//store function
use Illuminate\Support\Facades\Hash;
$user = User::create([
'name' =>
$request->name,
'email' =>
$request->email,
'country_id' =>
$request->country_id,
'password' =>
Hash::make(
$request->password
),
]);
Hash::make() sẽ băm mật khẩu trước khi lưu vào cơ sở dữ liệu.
Ngay sau khi tạo User:
Auth::login($user);
Người dùng không cần nhập lại Email và Password.
Laravel sẽ tạo Session và chuyển sang trang tiếp theo.
Ở các bài trước, chúng ta đã tạo bảng profiles và quan hệ One To One với users.
Cho phép ghi 02 trường phone và address trong bảng profiles
class Profile extends Model
{
protected $fillable = [
'phone',
'address',
];
...
Sau khi tạo User, có thể tạo luôn Profile:
//đặt trước dòng event(new Registered($user));
$user->profile()->create([
'phone' => $request->phone,
'address' => $request->address,
]);
Khi đó, mỗi User mới đều có sẵn một Profile để cập nhật sau này.
Chèn thêm Phone và Address sau phần Email (hoặc trước Password). Mã theo đúng style của Breeze:
<!-- Phone --> <div class="mt-4"> <x-input-label for="phone" :value="__('Phone')" /> <x-text-input id="phone" class="block mt-1 w-full" type="text" name="phone" :value="old('phone')" required autocomplete="tel" /> <x-input-error :messages="$errors->get('phone')" class="mt-2" /> </div> <!-- Address --> <div class="mt-4"> <x-input-label for="address" :value="__('Address')" /> <x-text-input id="address" class="block mt-1 w-full" type="text" name="address" :value="old('address')" required autocomplete="street-address" /> <x-input-error :messages="$errors->get('address')" class="mt-2" /> </div>
Validation:
'phone' => ['required', 'string', 'max:20'],
'address' => ['required', 'string', 'max:255'],Giả sử bảng countries đã có dữ liệu từ Seeder.
Có thể gán quốc gia mặc định:
$user->update([
'country_id' => 1,
]);
Hoặc lấy theo dữ liệu người dùng chọn từ Form:
'country_id' =>
$request->country_id
Việc này giúp mở rộng chức năng đăng ký cho các dự án đa quốc gia.
Blade:
<!-- Country -->
<div class="mt-4">
<x-input-label for="country_id" :value="__('Country')" />
<select
id="country_id"
name="country_id"
class="block mt-1 w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
required>
<option value="">-- Select Country --</option>
@foreach($countries as $country)
<option
value="{{ $country->id }}"
{{ old('country_id') == $country->id ? 'selected' : '' }}>
{{ $country->name }}
</option>
@endforeach
</select>
<x-input-error :messages="$errors->get('country_id')" class="mt-2" />
</div>
Validation:
'country_id' =>
'required|exists:countries,id'
Nhờ đó người dùng chỉ có thể chọn Country hợp lệ.
trong RegisteredUserController cần truyền biến.
Ví dụ:
use App\Models\Country; public function create(): View { $countries = Country::all(); return view('auth.register', compact('countries')); }
Laravel sẽ tự động báo lỗi nếu:
Email đã tồn tại.
Do Rule:
unique:users,email
giúp ngăn tạo nhiều tài khoản với cùng một Email.
Blade:
<input
type="email"
name="email"
value="{{ old('email') }}">
Thông báo:
@error('email')
<div class="text-danger">
{{ $message }}
</div>
@enderror
Nếu Validation thất bại, dữ liệu đã nhập vẫn được giữ lại nhờ old().
Người dùng có thể:
Tạo Post.
Cập nhật Profile.
Upload ảnh.
Quản lý bài viết của mình.
Nhờ auth()->id(), mọi dữ liệu sẽ gắn đúng với tài khoản vừa đăng ký.
Trong dự án thực tế, sau khi Register có thể tự động:
Tạo User
│
▼
Tạo Profile
│
▼
Gán Country
│
▼
Đăng nhập
│
▼
Dashboard
Đây là cách triển khai phổ biến trong các hệ thống quản lý hiện đại.
Trong bài này chúng ta đã học:
Register là gì.
Quy trình tạo tài khoản.
Validation.
Hash::make().
Auth::login().
Tạo Profile tự động.
Gán Country.
Kiểm tra Email trùng.
Hiển thị lỗi Validation.
Đăng ký tài khoản là bước khởi đầu để người dùng tham gia và sử dụng đầy đủ các chức năng của ứng dụng Laravel.
Đăng ký một tài khoản mới.
Kiểm tra Email không được trùng.
Kiểm tra Password tối thiểu 8 ký tự.
Tạo Profile tự động sau khi Register.
Thêm trường chọn Country vào Form Register.
Gán country_id cho User.
Kiểm tra User và Profile đã được tạo trong cơ sở dữ liệu.
Ở Bài 24, chúng ta sẽ tìm hiểu Middleware, cơ chế giúp bảo vệ các Route và chỉ cho phép người dùng đã đăng nhập truy cập vào những khu vực như Dashboard, quản lý bài viết hoặc trang quản trị.
x1
Cập nhật: 2026-08-02T11:05:51.310+07:00
Thuộc phần 3: Asynchronous Programming
Sau bài này, bạn sẽ hiểu:
Promise là gì.
Vì sao Promise ra đời.
Ba trạng thái của Promise.
Cách tạo Promise.
.then()
.catch()
.finally()
Promise Chaining.
Promise trong Node.js thực tế.
Ở bài trước chúng ta đã học Callback.
Ví dụ:
login(function(user){
getProfile(user, function(profile){
getPosts(profile, function(posts){
console.log(posts);
});
});
});
Khi chương trình lớn lên, callback lồng callback sẽ khiến code:
Khó đọc
Khó bảo trì
Khó bắt lỗi
Đây gọi là:
Callback Hell
Hay còn gọi vui là:
Pyramid of Doom
Ví dụ:
step1(function(){
step2(function(){
step3(function(){
step4(function(){
step5(function(){
});
});
});
});
});
Nhìn giống một kim tự tháp.
Đây chính là lý do Promise ra đời.
Promise là một đối tượng đại diện cho kết quả của một tác vụ bất đồng bộ, có thể thành công hoặc thất bại trong tương lai.
Hiểu đơn giản:
Promise giống như một "tờ giấy hứa".
Ví dụ:
Bạn đặt hàng online.
Ngay lúc đặt:
Chưa có hàng
Chưa biết khi nào tới
Shop chỉ hứa:
"Tôi sẽ giao."
Sau vài ngày:
giao thành công
hoặc giao thất bại
Đó chính là Promise.
Đang chờ xử lý.
Ví dụ:
Đang tải dữ liệu...
Thành công.
Ví dụ:
Đã lấy dữ liệu thành công.
Thất bại.
Ví dụ:
Mất kết nối.
Minh họa:
Pending
/ \
/ \
Fulfilled Rejected
const promise = new Promise(function(resolve, reject){
});
Hoặc dùng Arrow Function:
const promise = new Promise((resolve, reject) => {
});
Trong đó:
resolve() → thành công
reject() → thất bại
Ví dụ:
const promise = new Promise((resolve, reject) => {
resolve("Xin chào Node.js");
});
Lấy kết quả:
promise.then(function(data){
console.log(data);
});
Kết quả:
Xin chào Node.js
const promise = new Promise((resolve, reject) => {
reject("Có lỗi xảy ra");
});
Xử lý lỗi:
promise.catch(function(error){
console.log(error);
});
Kết quả:
Có lỗi xảy ra
.then() chạy khi Promise thành công.
Ví dụ:
const promise = new Promise((resolve) => {
resolve(100);
});
promise.then((number) => {
console.log(number);
});
Kết quả:
100
.catch() chạy khi Promise bị lỗi.
Ví dụ:
const promise = new Promise((resolve, reject) => {
reject("Server Error");
});
promise.catch((error)=>{
console.log(error);
});
Kết quả:
Server Error
.finally() luôn chạy dù thành công hay thất bại.
Ví dụ:
promise
.then(data=>{
console.log(data);
})
.catch(err=>{
console.log(err);
})
.finally(()=>{
console.log("Hoàn thành.");
});
Kết quả:
Hoàn thành.
Rất hữu ích khi:
Tắt Loading
Đóng Database
Giải phóng tài nguyên
Ghi log
Ví dụ:
const promise = new Promise((resolve)=>{
setTimeout(()=>{
resolve("Đã xong.");
},3000);
});
Lấy dữ liệu:
promise.then((data)=>{
console.log(data);
});
Sau 3 giây:
Đã xong.
Promise có thể nối tiếp nhau.
Ví dụ:
Promise.resolve(5)
.then(number=>{
return number * 2;
})
.then(number=>{
return number + 10;
})
.then(number=>{
console.log(number);
});
Kết quả:
20
Mỗi .then() nhận kết quả của .then() trước đó.
Ví dụ:
Promise.resolve(10)
.then(number=>{
return new Promise((resolve)=>{
resolve(number * 3);
});
})
.then(result=>{
console.log(result);
});
Kết quả:
30
Promise sẽ tự động chờ Promise mới hoàn thành trước khi chuyển sang .then() tiếp theo.
Ví dụ:
function sleep(ms){
return new Promise((resolve)=>{
setTimeout(resolve, ms);
});
}
sleep(2000)
.then(()=>{
console.log("2 giây đã trôi qua.");
});
Đây là một mẫu rất phổ biến trong lập trình Node.js.
Từ Node.js hiện đại, nhiều API đã hỗ trợ Promise.
Ví dụ:
import { readFile } from "node:fs/promises";
readFile("hello.txt", "utf8")
.then(data=>{
console.log(data);
})
.catch(err=>{
console.log(err);
});
Không còn phải truyền callback như trước.
Node.js hiện đại đã tích hợp sẵn fetch().
Ví dụ:
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => {
console.log(error);
});
Quy trình xử lý:
fetch()
↓
Response
↓
response.json()
↓
Object JavaScript
Đây là cách gọi API rất phổ biến trong các ứng dụng Node.js và trình duyệt.
Chạy nhiều Promise song song.
Ví dụ:
Promise.all([
Promise.resolve("Node.js"),
Promise.resolve("Express"),
Promise.resolve("Prisma")
])
.then(data=>{
console.log(data);
});
Kết quả:
[
'Node.js',
'Express',
'Prisma'
]
Nếu một Promise thất bại thì toàn bộ Promise.all() sẽ bị từ chối (reject).
Trả về Promise hoàn thành đầu tiên.
Ví dụ:
Promise.race([
new Promise(resolve => setTimeout(() => resolve("A"), 3000)),
new Promise(resolve => setTimeout(() => resolve("B"), 1000))
])
.then(data => {
console.log(data);
});
Kết quả:
B
Promise.race() hữu ích khi cần giới hạn thời gian chờ hoặc chọn kết quả phản hồi nhanh nhất.
Trả về Promise thành công đầu tiên.
Ví dụ:
Promise.any([
Promise.reject("Lỗi 1"),
Promise.resolve("Đã thành công"),
Promise.resolve("Dự phòng")
])
.then(result => {
console.log(result);
});
Kết quả:
Đã thành công
Nếu tất cả Promise đều thất bại thì Promise.any() mới phát sinh lỗi.
Chờ tất cả Promise kết thúc, bất kể thành công hay thất bại.
Ví dụ:
Promise.allSettled([
Promise.resolve("OK"),
Promise.reject("Lỗi")
])
.then(results => {
console.log(results);
});
Kết quả:
[
{ status: "fulfilled", value: "OK" },
{ status: "rejected", reason: "Lỗi" }
]
Rất phù hợp khi cần tổng hợp kết quả của nhiều tác vụ độc lập.
Nên dùng Promise khi:
Đọc/Ghi file.
Gọi REST API.
Truy vấn cơ sở dữ liệu.
Upload file.
Xử lý nhiều tác vụ bất đồng bộ.
Làm việc với các thư viện hiện đại của Node.js.
❌ Quên xử lý lỗi bằng .catch().
promise.then(data => {
console.log(data);
});
Nếu Promise bị từ chối (reject), lỗi có thể không được xử lý.
❌ Không return giá trị trong .then().
Promise.resolve(5)
.then(number => {
number * 2;
})
.then(result => {
console.log(result);
});
Kết quả:
undefined
Cần viết:
return number * 2;
❌ Tạo Promise không cần thiết.
Ví dụ:
return new Promise((resolve) => {
resolve(data);
});
Có thể rút gọn:
return Promise.resolve(data);
Hoặc đơn giản:
return data;
Trong bài học này, bạn đã nắm được cách Promise giúp giải quyết hạn chế của Callback bằng cách tổ chức mã nguồn rõ ràng và dễ bảo trì hơn. Bạn đã biết ba trạng thái của Promise, cách sử dụng .then(), .catch(), .finally(), kết hợp nhiều Promise với Promise.all(), Promise.race(), Promise.any(), Promise.allSettled(), cũng như ứng dụng Promise trong đọc file và gọi API.
Ở bài tiếp theo, chúng ta sẽ học Bài 13 — Async / Await, cú pháp hiện đại giúp viết mã bất đồng bộ theo phong cách gần giống mã đồng bộ, ngắn gọn và dễ đọc hơn.
Cập nhật: 2026-08-02T06:59:00.186+07:00
Ở bài trước, chúng ta đã cài đặt Laravel Breeze và có thể đăng ký, đăng nhập vào hệ thống.
Tuy nhiên, khi người dùng nhập Email và Password rồi nhấn Login, phía sau Laravel đã thực hiện những công việc gì?
Trong bài này, chúng ta sẽ tìm hiểu quy trình đăng nhập, Session và cách lấy thông tin người dùng đang đăng nhập để sử dụng trong ứng dụng.
Login (Đăng nhập) là quá trình xác thực người dùng.
Người dùng cung cấp:
Password
Laravel sẽ kiểm tra thông tin này với dữ liệu trong cơ sở dữ liệu.
Nếu hợp lệ:
Tạo Session
Ghi nhớ người dùng.
Cho phép truy cập các trang được bảo vệ.
Nếu không hợp lệ:
Trả về trang đăng nhập.
Hiển thị thông báo lỗi.
Người dùng
│
▼
Nhập Email + Password
│
▼
Request
│
▼
Validation
│
▼
Kiểm tra Database
│
▼
Password Hash
│
▼
Đúng?
│ │
Có Không
│ │
▼ ▼
Tạo Session Báo lỗi
│
▼
Chuyển đến Dashboard
Đây là quy trình xác thực cơ bản trong Laravel.
Laravel Breeze đã tạo sẵn Form views/auth/login.blade.php:
<form method="POST"
action="{{ route('login') }}">
@csrf
<input
type="email"
name="email">
<input
type="password"
name="password">
<button type="submit">
Đăng nhập
</button>
</form>
Người dùng không cần tự viết từ đầu.
Trước khi xác thực, Laravel kiểm tra dữ liệu:
app/Http/Requests/Auth/LoginRequest.php
$request->validate([
'email' =>
['required', 'email'],
'password' =>
['required'],
]);
Điều này giúp loại bỏ những dữ liệu không hợp lệ ngay từ đầu.
Sau khi Validation thành công, Laravel sẽ:
Tìm User theo Email.
Lấy mật khẩu đã được băm (Hash).
So sánh với mật khẩu người dùng nhập.
Nếu khớp:
Đăng nhập thành công
Nếu không:
Email hoặc mật khẩu không đúng.
Laravel không lưu mật khẩu gốc trong cơ sở dữ liệu.
Khi đăng nhập thành công, Laravel tạo một Session.
Session lưu thông tin người dùng trong suốt thời gian họ sử dụng ứng dụng.
Ví dụ:
Người dùng
↓
Login
↓
Session
↓
Truy cập các trang bảo vệ
Khi đóng phiên làm việc hoặc đăng xuất, Session sẽ bị hủy.
Dữ liệu session thực sự nằm trong database/sessions
F12
└── Application
└── Cookies
└── http://127.0.0.1:8000
Laravel cung cấp:
auth()->check()
Ví dụ:
if (auth()->check()) {
echo 'Đã đăng nhập';
}
Nếu chưa đăng nhập:
echo 'Khách';
$user = auth()->user();
Hoặc:
echo auth()->user()->name;
Email:
echo auth()->user()->email;
ID:
echo auth()->id();
Đây là các phương thức được sử dụng rất thường xuyên trong Laravel.
Ở các bài trước chúng ta dùng:
'user_id' => 1
Bây giờ:
'user_id' => auth()->id()
Ví dụ:
Post::create([
'title' =>
$request->title,
'content' =>
$request->content,
'category_id' =>
$request->category_id,
'image' =>
$image ?? null,
'user_id' =>
auth()->id(),
]);
Bài viết sẽ thuộc về đúng người đang đăng nhập.
Nhờ Relationship đã học:
$posts = auth()
->user()
->posts;
Hoặc:
$posts = Post::where(
'user_id',
auth()->id()
)->get();
Đây là chức năng phổ biến trong Dashboard cá nhân.
Giả sử đã tạo quan hệ One To One:
$profile = auth()
->user()
->profile;
Ví dụ:
echo $profile->phone;
echo $profile->address;
Laravel hỗ trợ:
@auth
Xin chào
{{ auth()->user()->name }}
@endauth
Nếu chưa đăng nhập:
@guest
<a href="{{ route('login') }}">
Login
</a>
@endguest
Nhờ vậy giao diện có thể thay đổi theo trạng thái người dùng.
Laravel Breeze đã tạo sẵn chức năng:
<form method="POST"
action="{{ route('logout') }}">
@csrf
<button>
Logout
</button>
</form>
Sau khi Logout:
Session bị hủy.
Người dùng trở về trạng thái khách.
| Hàm | Ý nghĩa |
|---|---|
| auth()->check() | Kiểm tra đã đăng nhập |
| auth()->user() | Lấy User hiện tại |
| auth()->id() | Lấy ID User |
| @auth | Chỉ hiển thị khi đã đăng nhập |
| @guest | Chỉ hiển thị khi chưa đăng nhập |
Trong bài này chúng ta đã học:
Quy trình Login.
Validation.
Session.
auth()->check().
auth()->user().
auth()->id().
@auth.
@guest.
Tạo Post theo người dùng đang đăng nhập.
Đăng nhập là nền tảng để xây dựng các chức năng như Dashboard, quản lý bài viết cá nhân, phân quyền và bảo vệ dữ liệu.
Đăng nhập bằng tài khoản đã tạo ở bài trước.
Hiển thị tên người dùng trên thanh điều hướng.
Thay toàn bộ user_id = 1 bằng auth()->id().
Hiển thị danh sách bài viết của người đang đăng nhập.
Hiển thị thông tin Profile của người dùng.
Thêm nút Logout vào giao diện.
Dùng @auth và @guest để thay đổi giao diện theo trạng thái đăng nhập.
Ở Bài 23, chúng ta sẽ tìm hiểu Register, cách tạo tài khoản mới, kiểm tra dữ liệu đầu vào, lưu người dùng vào cơ sở dữ liệu và mở rộng quy trình đăng ký để tự động tạo Profile hoặc gán Country mặc định sau khi người dùng đăng ký thành công.
x1
Cập nhật: 2026-07-31T21:00:53.851+07:00
Errors log:
Sau khi cài Breeze file route bị ghi đè (mất dữ liệu)
-> sửa lại thêm middleware (để Breeze xác thực mới cho vào link)
Ví dụ:
use App\Http\Controllers\PostController;
use App\Http\Controllers\CategoryController;
Route::middleware('auth')->group(function () {
Route::resource('posts', PostController::class);
Route::resource('categories', CategoryController::class);
});
và giữ lại:
require __DIR__.'/auth.php';
ở cuối file.
Đến thời điểm này, chúng ta đã có thể xây dựng một Blog CMS với các chức năng như:
Quản lý Post
Category
Tag
Relationship
Form
Validation
Upload File
Tuy nhiên, ứng dụng vẫn còn thiếu một thành phần rất quan trọng:
Hệ thống đăng nhập và quản lý người dùng.
Laravel cung cấp một giải pháp chính thức để xây dựng Authentication hiện đại mang tên Laravel Breeze.
Laravel Breeze là bộ khung Authentication chính thức do đội ngũ Laravel phát triển.
Chỉ với vài lệnh cài đặt, Breeze sẽ tạo sẵn:
Đăng nhập (Login)
Đăng ký (Register)
Quên mật khẩu
Đặt lại mật khẩu
Xác minh Email (tùy chọn)
Giao diện đăng nhập
Middleware xác thực
Controller
Route
View
Người lập trình không cần tự xây dựng từ đầu.
Nếu tự viết Authentication, bạn sẽ phải xử lý:
Mã hóa mật khẩu.
Kiểm tra đăng nhập.
Lưu Session.
Đăng xuất.
CSRF.
Remember Me.
Bảo mật mật khẩu.
Laravel Breeze đã xử lý sẵn toàn bộ các công việc này theo chuẩn của Laravel.
Trước khi cài Breeze, dự án cần:
Laravel đã được cài đặt.
Database đã cấu hình.
Chạy Migration thành công.
Ví dụ:
php artisan migrate
Cài package:
composer require laravel/breeze --dev
Sau khi hoàn tất:
php artisan breeze:install
Laravel sẽ hỏi một số tùy chọn.
Ví dụ:
Which starter kit?
● Blade
○ React
○ Vue
○ API
Trong khóa học này chúng ta chọn:
Blade
để tập trung vào Laravel trước khi học các công nghệ Front-end nâng cao.
Sau khi Breeze tạo mã nguồn:
npm install
Tiếp theo:
npm run build
Trong quá trình phát triển, có thể dùng:
npm run dev
để tự động biên dịch tài nguyên khi có thay đổi.
Breeze sử dụng bảng users có sẵn.
Chạy:
php artisan migrate
Nếu dự án của bạn đã chạy Migration từ các bài trước thì Laravel sẽ chỉ tạo những bảng còn thiếu.
php artisan serve
Mở trình duyệt:
http://127.0.0.1:8000
Trang chủ sẽ xuất hiện thêm:
Login
Register
ở góc trên bên phải.
Nhấn:
Register
Điền:
Name
Password
Confirm Password
Sau khi gửi Form, Laravel sẽ:
Validation.
Mã hóa mật khẩu.
Lưu User vào Database.
Đăng nhập tự động.
Trong Database:
password
$2y$12$...
Laravel sử dụng thuật toán băm để lưu mật khẩu, không lưu mật khẩu gốc.
Điều này giúp tăng tính an toàn nếu cơ sở dữ liệu bị lộ.
Sau khi có tài khoản:
Login
Nhập:
Password
Nếu đúng:
Laravel tạo Session và xác định người dùng đã đăng nhập.
Thay vì:
'user_id' => 1
Từ bây giờ:
'user_id' => auth()->id()
Hoặc:
auth()->user()->id
Đây là cách làm chuẩn trong các ứng dụng Laravel.
Post::create([
'title' =>
$request->title,
'content' =>
$request->content,
'image' =>
$image ?? null,
'category_id' =>
$request->category_id,
'user_id' =>
auth()->id(),
]);
Mỗi bài viết sẽ tự động gắn với người đang đăng nhập.
Blade:
{{ auth()->user()->name }}
Email:
{{ auth()->user()->email }}
Điều này rất hữu ích cho Dashboard hoặc thanh điều hướng views/layous/navigation.blade.php.
Breeze đã tạo sẵn chức năng:
Logout
Sau khi đăng xuất:
Session bị xóa.
Người dùng phải đăng nhập lại để tiếp tục.
Sau khi cài đặt, dự án sẽ có thêm:
app/
Http/
Controllers/
Auth/
resources/
views/
auth/
routes/
auth.php
Ngoài ra còn có:
Middleware.
Validation.
Email Verification (nếu bật).
Các Route xác thực.
Chính thức từ Laravel.
Dễ cài đặt.
Dễ tùy biến.
Mã nguồn rõ ràng.
Phù hợp cho các dự án nhỏ và vừa.
Là nền tảng để học Laravel Authentication.
Trong bài này chúng ta đã học:
Laravel Breeze là gì.
Cài đặt Breeze.
Blade Starter Kit.
Build giao diện.
Đăng ký tài khoản.
Đăng nhập.
auth()->id().
auth()->user().
Đăng xuất.
Từ bài này trở đi, mọi thao tác tạo Post, cập nhật Profile hay quản lý dữ liệu đều sẽ gắn với người dùng đang đăng nhập, thay vì sử dụng giá trị user_id cố định như ở các bài trước.
Cài đặt Laravel Breeze.
Tạo một tài khoản mới.
Đăng nhập vào hệ thống.
Thay user_id = 1 bằng auth()->id().
Hiển thị tên người dùng trên giao diện.
Tạo một Post và kiểm tra user_id trong cơ sở dữ liệu.
Đăng xuất khỏi hệ thống.
Ở Bài 22, chúng ta sẽ tìm hiểu chi tiết cách Login hoạt động trong Laravel, bao gồm Session, Guard, Middleware và quy trình xác thực người dùng sau khi đăng nhập.
x1
Cập nhật: 2026-07-31T20:38:13.239+07:00
"Trước khi có Promise và Async/Await, Callback chính là cách Node.js xử lý các tác vụ bất đồng bộ. Hiểu Callback sẽ giúp bạn hiểu vì sao JavaScript hiện đại được thiết kế như ngày nay."
Sau bài này bạn sẽ:
Hiểu Callback là gì.
Phân biệt Callback đồng bộ và bất đồng bộ.
Biết cách truyền Callback vào hàm.
Hiểu Callback Hell là gì.
Biết khi nào nên dùng Callback.
Hiểu vì sao Promise và Async/Await ra đời.
Callback là một hàm được truyền vào làm tham số của một hàm khác để được gọi lại (call back) khi cần.
Ví dụ đơn giản:
function hello(callback) {
console.log("Xin chào");
callback();
}
hello(() => {
console.log("Node.js");
});
Kết quả
Xin chào
Node.js
Ở đây:
hello() là hàm chính.
Hàm mũi tên được truyền vào chính là Callback.
Giả sử bạn cần:
Đọc file.
Sau khi đọc xong mới xử lý dữ liệu.
Không thể:
readFile();
xuLy();
vì chưa chắc file đã đọc xong.
Thay vào đó:
readFile(() => {
xuLy();
});
Chỉ khi hoàn thành, Callback mới được gọi.
Ví dụ
function tinhTong(a, b, callback) {
const tong = a + b;
callback(tong);
}
console.log("Bắt đầu");
tinhTong(5, 8, (kq) => {
console.log(kq);
});
console.log("Kết thúc");
↓
13
Ở đây Callback được gọi ngay lập tức, nên đây là Callback đồng bộ (Synchronous Callback).
Ví dụ
function tinhTong(a, b, callback) {
const tong = a + b;
setTimeout(() => {
callback(tong);
}, 2000);
}
console.log("Bắt đầu");
tinhTong(5, 8, (kq) => {
console.log(kq);
});
console.log("Kết thúc");
↓
Đang chờ...
(sau 2 giây)
Đã xong
Callback chỉ chạy sau khi hết thời gian chờ.
Ví dụ
import fs from "node:fs";
fs.readFile(
"data.txt",
"utf8",
(err, data) => {
if (err) {
console.log(err);
return;
}
console.log(data);
}
);
↓
Xin chào Node.js
Đây là kiểu Callback rất phổ biến trong Node.js.
Phần lớn API của Node.js sử dụng quy ước:
(err, data)
Ví dụ
fs.readFile(
"abc.txt",
"utf8",
(err, data) => {
if (err) {
console.log(err);
return;
}
console.log(data);
}
);
Nếu lỗi:
ENOENT
Nếu thành công:
Nội dung file
Tham số đầu tiên luôn là Error.
Đây được gọi là:
Error-first Callback
Ví dụ
function login(callback) {
console.log("Đang đăng nhập...");
setTimeout(() => {
console.log("Đăng nhập thành công");
callback();
}, 2000);
}
function loadProfile(callback) {
console.log("Đang tải thông tin...");
setTimeout(() => {
console.log("Đã tải Profile");
callback();
}, 1500);
}
function loadOrders(callback) {
console.log("Đang tải đơn hàng...");
setTimeout(() => {
console.log("Đã tải đơn hàng");
callback();
}, 1000);
}
// Callback nhiều cấp
login(() => {
loadProfile(() => {
loadOrders(() => {
console.log("Hoàn thành");
});
});
});
Nhìn vẫn còn đơn giản.
Nhưng nếu dự án lớn:
login(() => {
profile(() => {
avatar(() => {
order(() => {
payment(() => {
history(() => {
message(() => {
notify(() => {
});
});
});
});
});
});
});
});
Code bắt đầu lệch sang bên phải rất nhiều.
Đây là hiện tượng nhiều Callback lồng nhau.
login()
↓
profile()
↓
order()
↓
payment()
↓
email()
↓
log()
↓
notification()
Hay còn được gọi vui là:
Pyramid of Doom (Kim tự tháp địa ngục).
Khó đọc.
Khó bảo trì.
Debug khó.
Dễ quên xử lý lỗi.
Khó tái sử dụng.
Đó là lý do Promise và Async/Await trở thành lựa chọn phổ biến trong JavaScript hiện đại.
Không.
Nhiều API của Node.js vẫn dùng Callback vì:
Đơn giản.
Hiệu năng tốt.
Không cần tạo Promise.
Ví dụ
fs.readFile(
"data.txt",
"utf8",
callback
);
Hoặc
import http from "node:http";
const server = http.createServer((req, res) => {
res.end("Hello Node.js");
});
server.listen(3000, () => {
console.log("Server running");
});
Hai khái niệm này thường bị nhầm.
Callback
function download(callback) {
console.log("Đang tải...");
setTimeout(() => {
console.log("Tải xong");
callback();
}, 3000);
}
download(() => {
console.log("Xong");
});
Một Callback.
Event
import EventEmitter from "node:events";
const event = new EventEmitter();
event.on("message", () => {
console.log("Có tin nhắn");
});
event.emit("message");
Có thể có nhiều Listener.
Callback
readFile((err, data) => {
});
Promise
import fs from "node:fs/promises";
fs.readFile("data.txt", "utf8")
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log(err);
});
Promise giúp code phẳng hơn và dễ kết hợp nhiều tác vụ bất đồng bộ.
Promise
await readFile();
↓
Code nhìn gần giống:
const data =
docFile();
Dễ đọc hơn rất nhiều.
Ví dụ:
import fs from "node:fs/promises"; fs.readFile("data.txt", "utf8") .then((data) => { console.log(data); }) .catch((err) => { console.log(err); });
import fs from "node:fs/promises"; async function docFile() { try { const data = await fs.readFile("data.txt", "utf8"); return data; } catch (err) { console.log(err); } } async function main() { const data = await docFile(); console.log(data); } main();
Giả sử data.txt có nội dung:
Giả sử tải thông tin người dùng.
function getUser(callback){
setTimeout(()=>{
callback({
name:"An"
});
},1000);
}
getUser((user)=>{
console.log(user.name);
});
↓
An
Mặc dù mất 1 giây để lấy dữ liệu, chương trình vẫn không bị chặn.
Gọi hàm
↓
Thực hiện công việc
↓
Hoàn thành
↓
Gọi Callback
Đây chính là cơ chế nền tảng của rất nhiều API bất đồng bộ trong Node.js.
Sai
fs.readFile(
"data.txt",
(err,data)=>{
console.log(data);
}
);
Nên
if(err){
return;
}
trước khi sử dụng data.
A(()=>{
B(()=>{
C(()=>{
D(()=>{
});
});
});
});
Nên cân nhắc Promise hoặc Async/Await khi chuỗi xử lý trở nên phức tạp.
callback();
callback();
Có thể gây lỗi logic hoặc khiến kết quả bị xử lý lặp lại ngoài ý muốn.
function test(cb){
}
Nếu không gọi:
cb();
phần mã đang chờ Callback sẽ không bao giờ được thực thi.
Phù hợp với:
API cũ.
Event.
Timer.
Stream.
HTTP Server.
Một số module lõi của Node.js.
Trong các dự án mới, Callback vẫn xuất hiện nhưng thường được thay thế hoặc bọc bởi Promise khi chuỗi xử lý trở nên phức tạp.
| Khái niệm | Ý nghĩa |
|---|---|
| Callback | Hàm truyền vào hàm khác |
| Sync Callback | Chạy ngay |
| Async Callback | Chạy sau |
| Error-first Callback | (err, data) |
| Callback Hell | Callback lồng nhau |
| Promise | Giải quyết Callback Hell |
| Async/Await | Cú pháp hiện đại cho Promise |
Callback là nền tảng của lập trình bất đồng bộ trong Node.js. Mặc dù ngày nay Promise và Async/Await được sử dụng phổ biến hơn, việc hiểu Callback vẫn rất quan trọng vì nhiều API cốt lõi của Node.js và không ít thư viện vẫn hỗ trợ hoặc sử dụng mô hình này.
Nắm vững Callback sẽ giúp bạn hiểu rõ cách hoạt động của Event Loop, Promise và Async/Await trong các bài học tiếp theo.
👉 Bài 12 — Promise
Bạn sẽ học:
Promise là gì.
Ba trạng thái của Promise.
resolve() và reject().
then(), catch() và finally().
Vì sao Promise giúp mã nguồn dễ đọc, dễ bảo trì và giải quyết hiệu quả Callback Hell.
x1
quay về MỤC LỤC
Cập nhật: 2026-08-01T20:13:20.672+07:00
Sau khi hoàn thành phần HTML5 Enterprise, chúng ta đã có thể xây dựng cấu trúc website chuẩn. Tuy nhiên, nếu mở cùng một trang trên Chrome, Firefox, Edge hoặc Safari, bạn sẽ thấy giao diện có thể không giống nhau.
Nguyên nhân là mỗi trình duyệt đều có một bộ CSS mặc định (User Agent Stylesheet) riêng.
Để giải quyết vấn đề này, lập trình viên thường bắt đầu dự án bằng CSS Reset.
Sau bài này bạn sẽ:
Hiểu CSS Reset là gì.
Biết vì sao cần Reset CSS.
Phân biệt CSS Reset và Normalize CSS.
Tự xây dựng một bộ Reset hiện đại.
Chuẩn bị nền tảng cho Responsive Design và Design System.
CSS Reset là tập hợp các quy tắc CSS giúp đưa tất cả các trình duyệt về một điểm xuất phát giống nhau.
Ví dụ:
Chrome có thể đặt:
body {
margin: 8px;
}
Trong khi trình duyệt khác có thể xử lý khoảng cách hoặc kích thước mặc định khác nhau.
Điều này khiến cùng một mã HTML hiển thị không đồng nhất.
Nếu không Reset CSS:
Margin khác nhau.
Padding khác nhau.
Font mặc định khác nhau.
Danh sách (ul, ol) hiển thị khác nhau.
Heading có kích thước khác nhau.
Form hiển thị không đồng nhất.
Điều này làm quá trình thiết kế giao diện trở nên khó kiểm soát.
Không Reset:
<h1>Enterprise Web</h1>
<p>Hello World</p>
Bạn sẽ thấy khoảng cách giữa các phần tử phụ thuộc vào trình duyệt.
Sau khi Reset, khoảng cách sẽ do chính bạn quyết định.
Trước đây nhiều lập trình viên sử dụng:
*{
margin:0;
padding:0;
}
Đây là cách đơn giản nhất.
Tuy nhiên, nó chưa đủ cho các dự án hiện đại.
*{
margin:0;
padding:0;
}
Dấu * nghĩa là:
Áp dụng cho tất cả phần tử HTML.
Ví dụ:
div
h1
p
ul
img
button
đều bị Reset.
Đây là dòng gần như luôn có trong mọi dự án.
*{
box-sizing:border-box;
}
width = content
+
padding
+
border
Tổng chiều rộng sẽ lớn hơn giá trị bạn khai báo.
width
=
content
+
padding
+
border
Chiều rộng luôn đúng bằng giá trị khai báo.
Đây là cách tính được sử dụng phổ biến trong các dự án hiện đại.
body{
margin:0;
}
Loại bỏ khoảng trắng mặc định của trình duyệt.
img{
display:block;
max-width:100%;
}
Ý nghĩa:
display:block
→ loại bỏ khoảng trắng dưới ảnh.
max-width:100%
→ ảnh không vượt quá phần tử cha.
Đây là nền tảng cho Responsive Images.
ul,
ol{
list-style:none;
}
Loại bỏ:
dấu chấm
số thứ tự
Bạn có thể tự thiết kế giao diện danh sách sau này.
a{
text-decoration:none;
color:inherit;
}
Giúp:
bỏ gạch chân
kế thừa màu chữ từ phần tử cha
Sau đó bạn có thể tùy chỉnh màu sắc theo Design System.
button{
border:none;
background:none;
cursor:pointer;
}
Giúp nút có giao diện thống nhất hơn giữa các trình duyệt.
input,
textarea,
select,
button{
font:inherit;
}
Giúp các thành phần Form sử dụng cùng font với website.
table{
border-collapse:collapse;
}
Loại bỏ khoảng cách giữa các ô trong bảng.
Đây là bộ Reset đơn giản nhưng phù hợp với hầu hết dự án.
*,
*::before,
*::after{
box-sizing:border-box;
}
*{
margin:0;
padding:0;
}
body{
line-height:1.5;
}
img{
display:block;
max-width:100%;
}
ul,
ol{
list-style:none;
}
a{
color:inherit;
text-decoration:none;
}
input,
button,
textarea,
select{
font:inherit;
}
table{
border-collapse:collapse;
}
Đây là một điểm khởi đầu tốt cho các dự án doanh nghiệp.
Mục tiêu:
Xóa gần như toàn bộ CSS mặc định của trình duyệt.
Ưu điểm:
Đồng nhất.
Dễ kiểm soát.
Phù hợp Design System.
Nhược điểm:
Phải tự định nghĩa lại nhiều kiểu hiển thị.
Mục tiêu:
Giữ lại những thiết lập mặc định hữu ích và chỉ sửa những điểm khác biệt giữa các trình duyệt.
Ưu điểm:
Ít phải viết lại CSS.
Phù hợp nhiều website nội dung.
Nhược điểm:
Một số kiểu mặc định vẫn còn tồn tại.
Nên dùng khi xây dựng:
Landing Page
Website doanh nghiệp
Dashboard
CRM
ERP
SaaS
E-Commerce
Design System
Component Library
Đây cũng là lựa chọn phổ biến trong các dự án Front-End hiện đại.
❌ Chỉ Reset bằng:
*{
margin:0;
padding:0;
}
mà quên box-sizing.
❌ Không Reset ảnh khiến hình vượt khỏi khung.
❌ Quên font: inherit cho Form.
❌ Reset quá mạnh rồi không định nghĩa lại kiểu hiển thị cần thiết.
❌ Xóa toàn bộ Focus của trình duyệt mà không thay thế bằng kiểu Focus mới, làm giảm khả năng Accessibility.
Tạo tệp:
reset.css
Bao gồm:
Reset Margin
Reset Padding
Border Box
Reset Image
Reset List
Reset Link
Reset Button
Reset Form
Reset Table
Sau đó nhúng vào một trang HTML và kiểm tra sự khác biệt trước và sau khi áp dụng.
CSS Reset là bước đầu tiên trong hầu hết các dự án Front-End chuyên nghiệp. Nó giúp loại bỏ sự khác biệt giữa các trình duyệt, tạo nền tảng ổn định để xây dựng giao diện hiện đại và dễ bảo trì.
Trong bài tiếp theo, chúng ta sẽ tìm hiểu CSS Variables, một tính năng mạnh mẽ giúp quản lý màu sắc, khoảng cách và toàn bộ Design System một cách linh hoạt.
✅ Hiểu CSS Reset là gì.
✅ Biết nguyên nhân khiến giao diện khác nhau giữa các trình duyệt.
✅ Sử dụng box-sizing: border-box.
✅ Reset hình ảnh, danh sách, liên kết, Form và bảng.
✅ Phân biệt CSS Reset và Normalize CSS.
✅ Xây dựng bộ CSS Reset hiện đại cho dự án Enterprise.
➡️ Bài tiếp theo: Bài 06 — CSS Variables: Xây dựng Design System linh hoạt với Custom Properties.
Cập nhật: 2026-07-27T07:20:00.189+07:00
Một website chuyên nghiệp không chỉ đẹp và nhanh, mà còn phải dễ sử dụng đối với tất cả mọi người, bao gồm:
Người khiếm thị.
Người khiếm thính.
Người lớn tuổi.
Người gặp khó khăn về vận động.
Người chỉ sử dụng bàn phím.
Người dùng thiết bị di động.
Đó chính là mục tiêu của Accessibility (A11y).
HTML5 đã hỗ trợ rất nhiều tính năng Accessibility, và khi kết hợp với ARIA (Accessible Rich Internet Applications), chúng ta có thể xây dựng các website thân thiện với mọi người dùng.
Sau bài này bạn sẽ:
Hiểu Accessibility là gì.
Biết vai trò của ARIA.
Viết HTML thân thiện với Screen Reader.
Hỗ trợ điều hướng bằng bàn phím.
Xây dựng Form dễ sử dụng hơn.
Tránh các lỗi Accessibility phổ biến.
Accessibility (A11y) là việc thiết kế website để mọi người đều có thể sử dụng, kể cả người khuyết tật hoặc người có nhu cầu hỗ trợ đặc biệt.
Một website có Accessibility tốt sẽ:
Dễ đọc.
Dễ điều hướng.
Dễ tương tác.
Hoạt động tốt với các công nghệ hỗ trợ.
Bạn thường thấy ký hiệu:
A11y
Nó là cách viết tắt của:
Accessibility
Có:
A
11 ký tự ở giữa
y
→ A11y
Không phải ai cũng sử dụng chuột.
Nhiều người chỉ dùng:
bàn phím
Screen Reader
thiết bị hỗ trợ
điều khiển bằng giọng nói
Website cần hỗ trợ tất cả các trường hợp này.
Google cũng thích:
Semantic HTML
Heading rõ ràng
Alt hình ảnh
Label đầy đủ
Accessibility và SEO có rất nhiều điểm chung.
Rất nhiều doanh nghiệp lớn yêu cầu website phải đạt tiêu chuẩn WCAG (Web Content Accessibility Guidelines).
Đây là một kỹ năng quan trọng khi làm việc trong các dự án quốc tế.
Accessibility bắt đầu từ việc sử dụng đúng thẻ HTML.
Ví dụ:
<header>
<nav>
<main>
<section>
<footer>
Thay vì:
<div>
<div>
<div>
Screen Reader sẽ hiểu cấu trúc của trang tốt hơn khi sử dụng các thẻ Semantic.
Sai:
<input
type="email"
placeholder="Email">
Đúng:
<label for="email">
Email
</label>
<input
id="email"
type="email">
Screen Reader sẽ đọc đúng tên trường nhập liệu.
Sai:
<img src="logo.png">
Đúng:
<img
src="logo.png"
alt="Logo Lớp Học Vi Tính">
Nếu hình ảnh mang tính trang trí và không chứa thông tin quan trọng, bạn có thể sử dụng:
<img
src="line.png"
alt="">
Điều này giúp Screen Reader bỏ qua hình ảnh không cần thiết.
Người dùng cần có thể sử dụng phím:
Tab
Shift + Tab
Enter
Space
để thao tác với website.
Các phần tử như:
<button>
<a>
<input>
<select>
<textarea>
đã hỗ trợ điều hướng bằng bàn phím theo mặc định.
Khi sử dụng phím Tab, trình duyệt sẽ hiển thị vùng được chọn (Focus).
Không nên xóa Focus bằng CSS:
button:focus {
outline: none;
}
Điều này khiến người dùng bàn phím khó xác định vị trí hiện tại.
Nếu muốn tùy chỉnh, hãy thay bằng một kiểu hiển thị khác rõ ràng hơn.
ARIA (Accessible Rich Internet Applications) là tập hợp các thuộc tính giúp tăng khả năng truy cập cho các thành phần giao diện phức tạp.
Ví dụ:
<button
aria-label="Đóng cửa sổ">
×
</button>
Screen Reader sẽ đọc:
Đóng cửa sổ
thay vì chỉ đọc ký tự "×".
<input
type="search"
aria-label="Tìm kiếm khóa học">
Rất hữu ích khi không có label hiển thị trên giao diện.
Nếu một biểu tượng chỉ mang tính trang trí:
<i
class="bi bi-star"
aria-hidden="true">
</i>
Screen Reader sẽ bỏ qua biểu tượng này.
Dùng cho menu hoặc Accordion.
<button
aria-expanded="false">
Menu
</button>
Khi menu mở:
<button
aria-expanded="true">
Menu
</button>
Đánh dấu trang hiện tại.
<a
href="/blog"
aria-current="page">
Blog
</a>
Screen Reader sẽ biết đây là trang đang được mở.
Đôi khi cần bổ sung vai trò cho các thành phần tùy chỉnh.
Ví dụ:
<div
role="button">
Đăng nhập
</div>
Tuy nhiên, nếu có thể, hãy sử dụng:
<button>
Đăng nhập
</button>
Thẻ HTML gốc luôn được ưu tiên hơn.
Website lớn thường có liên kết:
<a
href="#main">
Bỏ qua menu
</a>
Giúp người dùng bàn phím đi thẳng đến nội dung chính.
Trong trang:
<main id="main">
...
</main>
Không nên sử dụng:
chữ xám nhạt trên nền trắng
chữ vàng trên nền trắng
chữ xanh nhạt trên nền xanh
Nội dung cần có độ tương phản đủ cao để dễ đọc.
<header>
<h1>
Enterprise Web Layout Master
</h1>
</header>
<nav>
<a href="#main">
Bỏ qua menu
</a>
</nav>
<main id="main">
<form>
<label for="email">
Email
</label>
<input
id="email"
type="email"
required>
<button>
Đăng nhập
</button>
</form>
</main>
Đây là một ví dụ đơn giản nhưng đã áp dụng nhiều nguyên tắc Accessibility.
❌ Chỉ dùng placeholder mà không có label.
❌ Thiếu alt cho hình ảnh.
❌ Sử dụng <div> để làm nút bấm thay vì <button>.
❌ Xóa hiệu ứng Focus bằng CSS.
❌ Không hỗ trợ điều hướng bằng bàn phím.
❌ Dùng màu chữ và màu nền có độ tương phản thấp.
❌ Lạm dụng ARIA khi HTML gốc đã đủ đáp ứng.
Tạo một trang Đăng nhập gồm:
Tiêu đề <h1>.
Form đăng nhập.
Label cho Email và Password.
Nút Đăng nhập.
Liên kết "Bỏ qua menu".
Một hình ảnh logo có thuộc tính alt.
Một nút đóng sử dụng aria-label.
Yêu cầu:
Sử dụng Semantic HTML.
Hỗ trợ điều hướng bằng bàn phím.
Không dùng div thay cho button.
Chỉ sử dụng ARIA khi thực sự cần.
Accessibility không phải là tính năng dành riêng cho người khuyết tật mà là cách xây dựng website để mọi người đều có thể sử dụng dễ dàng. Khi kết hợp Semantic HTML, Form chuẩn, ARIA và khả năng điều hướng bằng bàn phím, bạn sẽ tạo ra những website chuyên nghiệp, thân thiện với người dùng và đáp ứng các tiêu chuẩn hiện đại.
Đây cũng là nền tảng quan trọng cho các dự án doanh nghiệp, thương mại điện tử và hệ thống quản trị trong tương lai.
✅ Hiểu khái niệm Accessibility (A11y).
✅ Biết vai trò của ARIA trong HTML.
✅ Sử dụng label, alt và Semantic HTML đúng cách.
✅ Hỗ trợ điều hướng bằng bàn phím và Focus.
✅ Áp dụng các thuộc tính aria-label, aria-hidden, aria-expanded, aria-current.
✅ Tránh các lỗi Accessibility phổ biến.
✅ Sẵn sàng xây dựng giao diện đạt tiêu chuẩn doanh nghiệp.
Sau 4 bài học đầu tiên, bạn đã xây dựng được nền móng vững chắc về HTML5 theo tiêu chuẩn Enterprise:
✅ Semantic HTML
✅ Form HTML5
✅ SEO Friendly HTML
✅ Accessibility & ARIA
Đây là những kiến thức cốt lõi sẽ được sử dụng xuyên suốt các phần tiếp theo như CSS Foundation, Responsive Design, Bootstrap 5, Tailwind CSS và Dashboard Enterprise.
➡️ Bài tiếp theo: Bài 05 — CSS Reset: Đồng bộ giao diện trên mọi trình duyệt.
Cập nhật: 2026-07-26T11:14:36.274+07:00
"Nếu phải đọc một file 20 GB, bạn sẽ tải toàn bộ vào RAM hay đọc từng phần nhỏ? Câu trả lời chính là Buffer và Stream - hai nền tảng giúp Node.js xử lý dữ liệu cực kỳ hiệu quả."
Sau bài này bạn sẽ:
Hiểu Buffer là gì.
Hiểu Stream là gì.
Biết các loại Stream trong Node.js.
Thành thạo Readable Stream và Writable Stream.
Biết sử dụng pipe().
Hiểu vì sao Stream giúp Node.js xử lý file lớn với hiệu năng cao.
Máy tính chỉ hiểu dữ liệu dưới dạng byte.
Node.js sử dụng Buffer để lưu trữ dữ liệu nhị phân (Binary Data).
Ví dụ:
File ảnh
Video
Âm thanh
File ZIP
Dữ liệu truyền qua mạng
đều được xử lý bằng Buffer.
const buf =
Buffer.from("Hello");
In ra
console.log(buf);
↓
<Buffer 48 65 6c 6c 6f>
Đây chính là mã ASCII của chuỗi.
console.log(
buf.toString()
);
↓
Hello
const buf =
Buffer.from([
65,
66,
67
]);
console.log(
buf.toString()
);
↓
ABC
Tạo Buffer có kích thước cố định.
const buf =
Buffer.alloc(8);
console.log(buf);
↓
<Buffer 00 00 00 00 00 00 00 00>
Đếm số byte.
console.log(
Buffer.byteLength(
"Node.js"
)
);
↓
7
Ví dụ với Unicode
console.log(
Buffer.byteLength(
"Xin chào"
)
);
Kết quả sẽ lớn hơn số ký tự vì một số ký tự Unicode chiếm nhiều hơn 1 byte.
Thay vì:
Đọc toàn bộ file
↓
Đưa vào RAM
↓
Xử lý
Node.js có thể:
Đọc từng phần nhỏ
↓
Xử lý
↓
Đọc tiếp
Đó chính là Stream.
File
movie.mp4
Dung lượng
20 GB
Nếu đọc bằng:
readFile()
↓
RAM phải chứa toàn bộ file.
Nếu dùng Stream
64 KB
↓
64 KB
↓
64 KB
↓
...
Node chỉ giữ một phần dữ liệu trong bộ nhớ tại mỗi thời điểm.
Tiết kiệm RAM.
Tăng tốc xử lý.
Phù hợp file lớn.
Có thể xử lý dữ liệu ngay khi nhận được.
Được sử dụng trong HTTP, Upload và Download.
Node.js có bốn loại Stream.
| Loại | Ý nghĩa |
|---|---|
| Readable | Đọc dữ liệu |
| Writable | Ghi dữ liệu |
| Duplex | Đọc và ghi |
| Transform | Đọc, biến đổi rồi ghi |
Đọc file.
import fs from "node:fs";
const stream =
fs.createReadStream(
"data.txt", {
highWaterMark: 5
}
);
stream.on(
"data",
(chunk)=>{
console.log(chunk);
});
↓
<Buffer ...>
Mỗi lần sẽ nhận một chunk (khối dữ liệu).
stream.setEncoding(
"utf8"
);
stream.on(
"data",
(chunk)=>{
console.log(chunk);
});
↓
Xin
chào
Node.js
Tùy kích thước file, dữ liệu có thể được chia thành nhiều chunk.
stream.on(
"end",
()=>{
console.log(
"Hoàn thành"
);
});
↓
Hoàn thành
stream.on(
"error",
(err)=>{
console.log(err);
});
Ví dụ:
ENOENT
Tạo Stream ghi.
import fs from "node:fs";
const stream =
fs.createWriteStream(
"log.txt"
);
Ghi
stream.write(
"Hello\n"
);
stream.write(
"Node.js\n"
);
stream.end();
Nội dung file
Hello
Node.js
Kết thúc Stream.
stream.end();
Sau khi gọi end(), bạn không nên tiếp tục ghi thêm dữ liệu vào Stream đó.
Đây là phương pháp phổ biến nhất.
Ví dụ sao chép file (phải có file video.mp4 trong thư mục gốc).
import fs from "node:fs";
const read =
fs.createReadStream(
"video.mp4"
);
const write =
fs.createWriteStream(
"copy.mp4"
);
read.pipe(write);
Sơ đồ
video.mp4
↓
Readable
↓
pipe()
↓
Writable
↓
copy.mp4
Không cần đọc toàn bộ file vào RAM.
Xác định kích thước mỗi chunk.
const stream =
fs.createReadStream(
"data.txt",
{
highWaterMark:
1024
}
);
↓
1 KB/chunk
Nếu không cấu hình, Node.js sẽ sử dụng giá trị mặc định tùy theo loại Stream.
Đếm số byte đã đọc.
let total = 0;
stream.on(
"data",
(chunk)=>{
total +=
chunk.length;
});
stream.on(
"end",
()=>{
console.log(
total
);
});
↓
102400
Bạn sẽ gặp Stream ở rất nhiều nơi.
HTTP
Express
Upload
Download
Video
Audio
ZIP
Cloud Storage
Database Export
| Buffer | Stream |
|---|---|
| Lưu dữ liệu trong RAM | Đọc/Ghi từng phần |
| Phù hợp dữ liệu nhỏ | Phù hợp dữ liệu lớn |
| Truy cập toàn bộ dữ liệu ngay | Xử lý theo từng chunk |
| Có thể tốn nhiều RAM | Tiết kiệm bộ nhớ |
Buffer phù hợp khi:
Chuỗi nhỏ.
Mã hóa dữ liệu.
Xử lý Binary.
Chuyển đổi Encoding.
Làm việc với các gói dữ liệu nhỏ.
Nên dùng:
Upload ảnh.
Upload video.
Download file.
HTTP Server.
Sao chép file lớn.
Xử lý log.
Truyền dữ liệu thời gian thực.
await fs.readFile(
"movie.mp4"
);
Có thể tiêu tốn rất nhiều RAM hoặc làm ứng dụng phản hồi chậm.
Hãy cân nhắc sử dụng createReadStream() với các tệp lớn.
stream.on(
"error",
(err)=>{
console.log(err);
});
Nếu không lắng nghe sự kiện error, chương trình có thể dừng khi gặp lỗi đọc hoặc ghi file.
stream.write(
"Hello"
);
Nếu không gọi:
stream.end();
Stream ghi có thể chưa đóng đúng cách và dữ liệu có thể chưa được ghi hoàn tất.
console.log(
chunk
);
↓
<Buffer ...>
Muốn đọc dạng văn bản:
chunk.toString();
hoặc:
stream.setEncoding(
"utf8"
);
| API | Ý nghĩa |
|---|---|
Buffer.from() | Tạo Buffer |
Buffer.alloc() | Tạo Buffer rỗng |
Buffer.byteLength() | Đếm số byte |
createReadStream() | Đọc Stream |
createWriteStream() | Ghi Stream |
pipe() | Nối hai Stream |
write() | Ghi dữ liệu |
end() | Kết thúc Stream |
Trong bài học này, bạn đã tìm hiểu hai khái niệm rất quan trọng của Node.js là Buffer và Stream. Buffer dùng để lưu trữ dữ liệu nhị phân trong bộ nhớ, còn Stream cho phép đọc và ghi dữ liệu theo từng phần nhỏ, giúp tiết kiệm RAM và xử lý hiệu quả các tệp có dung lượng lớn.
Đây là nền tảng cho rất nhiều tính năng trong Node.js như HTTP Server, Upload/Download, xử lý video, âm thanh, lưu trữ đám mây và truyền dữ liệu thời gian thực.
👉 Bài 11 — Callback
Bạn sẽ học:
Callback là gì.
Callback hoạt động như thế nào.
Callback đồng bộ và bất đồng bộ.
Callback Hell là gì.
Vì sao Promise và Async/Await ra đời để giải quyết những hạn chế của Callback.
x1
quay về MỤC LỤC
Cập nhật: 2026-07-26T11:14:45.801+07:00
Ở bài trước, chúng ta đã học cách Validation dữ liệu trước khi lưu vào cơ sở dữ liệu.
Tuy nhiên, một Blog hoặc CMS không chỉ có văn bản mà còn cần hình ảnh minh họa cho bài viết, ảnh đại diện người dùng hoặc các tệp đính kèm.
Laravel cung cấp hệ thống Storage giúp việc tải lên (Upload) và quản lý tệp trở nên đơn giản, an toàn và dễ mở rộng.
Trong bài này, chúng ta sẽ xây dựng chức năng tải ảnh đại diện cho Post.
Upload File là quá trình người dùng gửi tệp từ máy tính lên máy chủ.
Ví dụ:
Ảnh bài viết.
Avatar người dùng.
File PDF.
Video.
Tài liệu Word.
Laravel sẽ tiếp nhận tệp thông qua đối tượng Request.
Trong bảng posts, thêm cột lưu đường dẫn ảnh.
Tạo Migration:
php artisan make:migration add_image_to_posts_table
Migration:
Schema::table('posts', function (Blueprint $table) {
$table->string('image')
->nullable()
->after('content');
});
Chạy Migration:
php artisan migrate
class Post extends Model
{
protected $fillable = [
'title',
'content',
'category_id',
'user_id',
'image'
];
...
create Blade:
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"
rel="stylesheet">
<form
action="{{ route('posts.store') }}"
method="post"
enctype="multipart/form-data">
@csrf
<input
type="text"
name="title"
value="{{ old('title') }}"
placeholder="Tiêu đề"
id="">
@error('title')
<div class="text-danger">
{{ $message }}
</div>
@enderror
<br><br>
<textarea
name="content"
placeholder="Nội dung" id="" rows="6">{{ old('content') }}
</textarea>
@error('content')
<div class="text-danger">
{{ $message }}
</div>
@enderror
<br><br>
<select name="category_id" >
@foreach($categories as $category)
<option value="{{ $category->id }}">
{{ $category->name }}
</option>
@endforeach
</select>
<br><br>
<select
name="tags[]"
multiple>
@foreach($tags as $tag)
<option value="{{ $tag->id }}">
{{ $tag->name }}
</option>
@endforeach
</select>
<br><br>
<input
type="file"
name="image"
id="image">
<br><br>
<img
id="preview"
src=""
style="display:none;max-width:300px;">
<br><br>
<button type="submit">
Lưu bài viết
</button>
</form>
<script>
const input = document.getElementById('image');
const preview = document.getElementById('preview');
input.addEventListener('change', function () {
const file = this.files[0];
if (!file) {
preview.style.display = 'none';
preview.src = '';
return;
}
preview.src = URL.createObjectURL(file);
preview.style.display = 'block';
});
</script>
Thuộc tính:
enctype="multipart/form-data"
là bắt buộc khi gửi tệp.
Kiểm tra hình ảnh:
$request->validate([
'title' =>
'required|max:255',
'content' =>
'required|min:20',
'image' =>
'nullable|image|mimes:jpg,jpeg,png,webp|max:2048',
]);
Ý nghĩa:
image: phải là ảnh.
mimes: chỉ cho phép các định dạng.
max:2048: tối đa 2 MB.
//dd($request->hasFile('image'));
//dd($request->file('image'));
if ($request->hasFile('image')) {
//
}
Nếu người dùng chọn ảnh thì mới xử lý.
$image = $request
->file('image')
->store(
'posts',
'public'
);
Laravel sẽ lưu ảnh vào:
storage/app/public/posts
Biến $image sẽ chứa đường dẫn, ví dụ:
posts/aBcD12345.jpg
$post = Post::create([
'title' => $request->title,
'content' => $request->content,
'image' => $image ?? null,
'category_id' => $request->category_id,
'user_id' => 1,
]);
$post->tags()->sync(
$request->tags ?? []
);
return redirect()->route('posts.show', $post);
Đường dẫn ảnh sẽ được lưu vào cột image.
show.blade.php
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"
rel="stylesheet">
<div class="container py-4">
<h1 class="mb-4">
Chi tiết bài viết
</h1>
<div class="card">
<div class="card-body">
<h2 class="card-title">
{{ $post->title }}
</h2>
<hr>
<p>
<strong>Danh mục:</strong>
{{ $post->category->name ?? 'Chưa có' }}
</p>
<p>
<strong>Nội dung:</strong>
</p>
<p>
{!! nl2br(e($post->content)) !!}
</p>
@if($post->image)
<hr>
<h5>Hình ảnh</h5>
<img
src="{{ asset('storage/' . $post->image) }}"
alt="{{ $post->title }}"
class="img-fluid rounded">
@endif
@if($post->tags->count())
<hr>
<h5>Tags</h5>
@foreach($post->tags as $tag)
<span class="badge bg-primary">
{{ $tag->name }}
</span>
@endforeach
@endif
<hr>
<p class="text-muted mb-1">
Ngày tạo:
{{ $post->created_at }}
</p>
<p class="text-muted">
Cập nhật:
{{ $post->updated_at }}
</p>
<a
href="{{ route('posts.index') }}"
class="btn btn-secondary">
Quay lại
</a>
<a
href="{{ route('posts.edit', $post) }}"
class="btn btn-warning">
Sửa
</a>
</div>
</div>
</div>Laravel không hiển thị trực tiếp thư mục storage.
Chạy:
php artisan storage:link
Laravel sẽ tạo:
public/storage
trỏ tới:
storage/app/public
Chỉ cần chạy một lần cho mỗi dự án.
Blade:
@if($post->image)
<img
src="{{ asset('storage/' . $post->image) }}"
alt="{{ $post->title }}"
width="300">
@endif
Laravel sẽ hiển thị đúng đường dẫn ảnh đã lưu.
Khi chỉnh sửa bài viết:
if ($request->hasFile('image')) {
$image = $request
->file('image')
->store(
'posts',
'public'
);
$post->image = $image;
}
$post->save();
Ảnh mới sẽ thay thế đường dẫn cũ trong cơ sở dữ liệu.
Nếu thay ảnh mới, nên xóa ảnh cũ để tránh tệp không còn sử dụng.
use Illuminate\Support\Facades\Storage;
if (
$post->image &&
Storage::disk('public')
->exists($post->image)
) {
Storage::disk('public')
->delete($post->image);
}
Sau đó mới lưu ảnh mới.
Điều này giúp tiết kiệm dung lượng lưu trữ.
Có thể lấy tên gốc:
$request
->file('image')
->getClientOriginalName();
Hoặc:
$request
->file('image')
->extension();
Tuy nhiên, trong hầu hết trường hợp, nên để Laravel tự tạo tên tệp để tránh trùng lặp.
Ví dụ:
<input
type="file"
name="images[]"
multiple>
Controller:
foreach (
$request->file('images')
as $image
) {
$image->store(
'posts',
'public'
);
}
Kỹ thuật này thường dùng cho thư viện ảnh hoặc nhiều tệp đính kèm.
Người dùng
│
▼
Chọn ảnh
│
▼
Validation
│
▼
Lưu vào Storage
│
▼
Lưu đường dẫn vào Database
│
▼
Hiển thị bằng asset('storage/...')
Đây là quy trình chuẩn trong hầu hết các ứng dụng Laravel.
Trong bài này chúng ta đã học:
Upload File.
multipart/form-data.
hasFile().
store().
storage:link.
Hiển thị ảnh.
Cập nhật ảnh.
Xóa ảnh cũ.
Upload nhiều ảnh.
Upload File là chức năng không thể thiếu trong Blog, CMS, hệ thống quản lý tài liệu và nhiều ứng dụng web hiện đại.
Thêm cột image vào bảng posts.
Tạo Form cho phép tải ảnh.
Validation chỉ cho phép ảnh JPG, PNG và WebP.
Lưu ảnh vào thư mục storage/app/public/posts.
Hiển thị ảnh trong danh sách bài viết.
Khi cập nhật bài viết, thay thế ảnh cũ bằng ảnh mới.
Xóa ảnh cũ khỏi Storage khi không còn sử dụng.
Đến đây, bạn đã hoàn thành các kiến thức nền tảng về xử lý biểu mẫu trong Laravel:
Request
Validation
Upload File
Đây là ba thành phần cốt lõi được sử dụng gần như trong mọi chức năng nhập liệu của một ứng dụng Laravel.
Ở Bài 21, chúng ta sẽ bắt đầu PHẦN 6 — AUTHENTICATION với Laravel Breeze, giải pháp xác thực chính thức của Laravel giúp nhanh chóng xây dựng hệ thống đăng nhập, đăng ký và quản lý người dùng theo chuẩn hiện đại.
x1
Cập nhật: 2026-07-29T20:29:35.681+07:00
"Node.js hoạt động dựa trên mô hình hướng sự kiện (Event-Driven). Hiểu EventEmitter là bước quan trọng để xây dựng các ứng dụng bất đồng bộ hiệu quả."
Sau bài này bạn sẽ:
Hiểu Event là gì.
Biết EventEmitter hoạt động như thế nào.
Thành thạo các hàm:
on()
once()
emit()
off()
Truyền dữ liệu qua sự kiện.
Biết cách xây dựng hệ thống Publisher–Subscriber (Pub/Sub) đơn giản.
Event (Sự kiện) là một hành động xảy ra trong chương trình.
Ví dụ:
Người dùng bấm nút.
File được đọc xong.
Server nhận request.
Timer kết thúc.
Có người đăng nhập.
Khi một sự kiện xảy ra, chương trình có thể thực hiện một hoặc nhiều hành động tương ứng.
Node.js cung cấp lớp EventEmitter để:
Phát sự kiện (emit)
Lắng nghe sự kiện (listen)
Import:
import EventEmitter from "node:events";
Tạo đối tượng:
const event = new EventEmitter();
Phát một sự kiện.
import EventEmitter from "node:events";
const event = new EventEmitter();
event.emit("hello");
Ở ví dụ trên sẽ không có gì xảy ra, vì chưa có ai lắng nghe sự kiện.
Đăng ký lắng nghe sự kiện.
import EventEmitter from "node:events";
const event = new EventEmitter();
event.on("hello", () => {
console.log("Xin chào Node.js");
});
event.emit("hello");
Kết quả
Xin chào Node.js
event.emit("hello");
event.emit("hello");
event.emit("hello");
↓
Xin chào Node.js
Xin chào Node.js
Xin chào Node.js
Mỗi lần emit() được gọi, tất cả các listener của sự kiện sẽ được thực thi.
Chỉ chạy một lần.
event.once("login", () => {
console.log("Đăng nhập");
});
event.emit("login");
event.emit("login");
event.emit("login");
↓
Đăng nhập
Các lần sau sẽ không chạy nữa.
Có thể gửi dữ liệu khi phát sự kiện.
event.on("welcome", (name) => {
console.log(
"Xin chào",
name
);
});
event.emit(
"welcome",
"Nguyễn Văn A"
);
↓
Xin chào Nguyễn Văn A
Nhiều tham số
event.on(
"order",
(
id,
total
) => {
console.log(id);
console.log(total);
}
);
event.emit(
"order",
101,
500000
);
↓
101
500000
Một sự kiện có thể có nhiều listener.
event.on("save", () => {
console.log("Lưu Database");
});
event.on("save", () => {
console.log("Ghi Log");
});
event.on("save", () => {
console.log("Gửi Email");
});
event.emit("save");
↓
Lưu Database
Ghi Log
Gửi Email
Đây là ưu điểm lớn của lập trình hướng sự kiện: một sự kiện có thể kích hoạt nhiều hành động độc lập.
Xóa listener.
function hello(){
console.log("Hello");
}
event.on(
"test",
hello
);
event.off(
"test",
hello
);
event.emit("test");
↓
Không có kết quả.
Xóa toàn bộ listener.
event.removeAllListeners(
"save"
);
Đếm listener.
console.log(
event.listenerCount(
"save"
)
);
↓
3
Danh sách các sự kiện.
console.log(
event.eventNames()
);
↓
[
'login',
'save'
]
Giả sử người dùng đăng ký tài khoản.
event.on("register", () => {
console.log(
"Lưu Database"
);
});
event.on("register", () => {
console.log(
"Gửi Email"
);
});
event.on("register", () => {
console.log(
"Tạo Log"
);
});
event.emit("register");
↓
Lưu Database
Gửi Email
Tạo Log
Một sự kiện nhưng thực hiện nhiều công việc khác nhau mà các phần của chương trình không cần biết chi tiết về nhau.
emit()
Publisher
│
▼
EventEmitter
├────────────┐
▼ ▼
Subscriber Subscriber
(Database) (Email)
Publisher chỉ phát sự kiện.
Subscriber chỉ quan tâm đến sự kiện mình đăng ký.
Hai bên không phụ thuộc trực tiếp vào nhau.
Rất nhiều module sử dụng EventEmitter.
Ví dụ
HTTP Server
Streams
File System
Socket.IO
WebSocket
Process
Bạn sẽ gặp EventEmitter rất nhiều khi làm việc với Express, WebSocket và các ứng dụng Realtime.
import EventEmitter from "node:events";
const event =
new EventEmitter();
event.on(
"message",
(text)=>{
console.log(text);
});
event.emit(
"message",
"Hello Node.js"
);
↓
Hello Node.js
Sai
event.emit("hello");
event.on(
"hello",
()=>{
console.log("Hello");
});
↓
Không có kết quả.
Hãy đăng ký listener trước khi phát sự kiện.
event.on("save",()=>{});
event.emit("Save");
↓
Không chạy.
Tên sự kiện phân biệt chữ hoa và chữ thường.
Nếu liên tục thêm listener mà không gỡ bỏ khi không còn cần thiết, ứng dụng có thể tiêu tốn thêm bộ nhớ và Node.js sẽ cảnh báo khi số lượng listener vượt quá ngưỡng mặc định.
MaxListenersExceededWarning
Có thể xảy ra khi bạn đăng ký quá nhiều listener cho cùng một sự kiện mà không quản lý vòng đời của chúng.
Rất phù hợp cho:
Ghi log.
Gửi email.
Thông báo.
Upload file.
Chat Realtime.
WebSocket.
Plugin.
Hệ thống mở rộng.
| Hàm | Ý nghĩa |
|---|---|
emit() | Phát sự kiện |
on() | Lắng nghe sự kiện |
once() | Chỉ chạy một lần |
off() | Gỡ listener |
removeAllListeners() | Xóa tất cả listener |
listenerCount() | Đếm listener |
eventNames() | Danh sách sự kiện |
Module events là nền tảng của mô hình lập trình hướng sự kiện trong Node.js.
Thông qua EventEmitter, bạn có thể phát và lắng nghe các sự kiện, truyền dữ liệu giữa các thành phần và xây dựng các hệ thống linh hoạt theo mô hình Publisher–Subscriber.
Đây là cơ chế được sử dụng rộng rãi trong nhiều module cốt lõi của Node.js và sẽ còn xuất hiện trong các phần Express, WebSocket và Realtime ở các bài học sau.
👉 Bài 10 — Streams & Buffer
Bạn sẽ học:
Buffer là gì.
Stream là gì.
Readable Stream và Writable Stream.
Pipe dữ liệu giữa các Stream.
Vì sao Stream giúp Node.js xử lý các tệp lớn và dữ liệu truyền tải hiệu quả hơn mà không cần tải toàn bộ vào bộ nhớ.
x1
quay về MỤC LỤC
Cập nhật: 2026-07-26T11:10:35.537+07:00
Dưới đây là Bài 19, tiếp nối trực tiếp sau Request.
Bài này sử dụng luôn các bảng và Model đã có trong khóa học (users, posts, categories, tags, profiles, countries), không tạo thêm cấu trúc dữ liệu mới.
Ở bài trước, chúng ta đã học cách lấy dữ liệu từ người dùng thông qua Request.
Tuy nhiên, dữ liệu người dùng nhập vào không phải lúc nào cũng hợp lệ. Họ có thể để trống tiêu đề, nhập email sai định dạng hoặc chọn một Category không tồn tại.
Laravel cung cấp Validation để kiểm tra dữ liệu trước khi lưu vào cơ sở dữ liệu.
Validation là một trong những tính năng được sử dụng nhiều nhất trong mọi dự án Laravel.
Validation là quá trình kiểm tra dữ liệu đầu vào.
Ví dụ:
Tiêu đề không được để trống.
Nội dung phải có ít nhất 20 ký tự.
Email phải đúng định dạng.
Category phải tồn tại.
Số điện thoại không được vượt quá số ký tự quy định.
Nếu dữ liệu không hợp lệ, Laravel sẽ tự động quay lại Form và hiển thị lỗi.
Trong Controller:
use Illuminate\Http\Request;
Ví dụ:
public function store(Request $request)
{
$request->validate([
'title' => 'required',
'content' => 'required',
]);
}
Nếu người dùng bỏ trống một trong hai trường, Laravel sẽ tự động trả về lỗi.
Bắt buộc phải nhập.
'title' => 'required'
Độ dài tối thiểu.
'content' => 'required|min:20'
Người dùng phải nhập ít nhất 20 ký tự.
Giới hạn tối đa.
'title' => 'required|max:255'
Đây cũng là giới hạn phổ biến của kiểu dữ liệu string trong cơ sở dữ liệu.
Kiểm tra định dạng Email.
Ví dụ cập nhật User:
$request->validate([
'email' => 'required|email',
]);
Laravel sẽ kiểm tra địa chỉ email có đúng định dạng hay không.
Chỉ cho phép số.
Ví dụ với số điện thoại trong bảng profiles:
$request->validate([
'phone' => 'required|numeric',
]);
Kiểm tra dữ liệu có tồn tại trong cơ sở dữ liệu.
Ví dụ khi thêm Post:
$request->validate([
'category_id' =>
'required|exists:categories,id',
]);
Nếu Category không tồn tại, Validation sẽ thất bại.
Người dùng chọn nhiều Tag.
$request->validate([
'tags' => 'array',
]);
Kiểm tra từng Tag:
$request->validate([
'tags.*' =>
'exists:tags,id',
]);
Laravel sẽ kiểm tra từng ID trong mảng.
Kiểm tra dữ liệu chưa tồn tại.
Ví dụ:
$request->validate([
'email' =>
'required|email|unique:users,email',
]);
Không cho phép trùng Email.
Ví dụ:
$request->validate([
'title' =>
'required|max:255',
'content' =>
'required|min:20',
'category_id' =>
'required|exists:categories,id',
]);
Sau khi Validation thành công:
$post = Post::create([
'title' => $request->title,
'content' => $request->content,
'category_id' => $request->category_id,
'user_id' => 1,
]);
Tiếp theo:
$post->tags()->sync(
$request->tags
);
Validation giúp đảm bảo dữ liệu trước khi lưu.
Ví dụ:
$request->validate([
'phone' =>
'required|numeric',
'address' =>
'required|max:255',
]);
Sau đó:
$profile->update([
'phone' => $request->phone,
'address' => $request->address,
]);
Blade:
<input
type="text"
name="title"
value="{{ old('title') }}">
Thông báo lỗi:
@error('title')
<div class="text-danger">
{{ $message }}
</div>
@enderror
old() giúp giữ lại dữ liệu người dùng đã nhập sau khi Validation thất bại.
Chèn thêm Bootstrap css trong create blade.
| Rule | Ý nghĩa |
|---|---|
| required | Bắt buộc nhập |
| nullable | Có thể để trống |
| string | Chuỗi ký tự |
| numeric | Chỉ gồm số |
| integer | Số nguyên |
| boolean | true / false |
| Định dạng Email | |
| min | Giá trị tối thiểu |
| max | Giá trị tối đa |
| unique | Không được trùng |
| exists | Phải tồn tại |
| array | Kiểu mảng |
Có thể truyền thêm mảng thông báo:
$request->validate(
[
'title' => 'required',
],
[
'title.required' =>
'Vui lòng nhập tiêu đề.',
]
);
Laravel sẽ hiển thị thông báo theo nội dung bạn định nghĩa.
Trong bài này chúng ta đã học:
Validation là gì.
validate().
Các Rule phổ biến.
Validation với Post.
Validation với Profile.
Validation mảng Tag.
Hiển thị lỗi bằng @error.
Giữ dữ liệu bằng old().
Validation giúp bảo vệ dữ liệu, giảm lỗi và mang lại trải nghiệm tốt hơn cho người dùng.
Thêm Validation khi tạo Post.
Kiểm tra category_id bằng exists.
Kiểm tra title không vượt quá 255 ký tự.
Kiểm tra content có ít nhất 20 ký tự.
Kiểm tra tất cả Tag được chọn đều tồn tại.
Hiển thị lỗi bằng @error.
Giữ lại dữ liệu bằng old() khi Validation thất bại.
Ở Bài 20, chúng ta sẽ tìm hiểu Upload File, cách tải hình ảnh lên máy chủ, lưu đường dẫn vào cơ sở dữ liệu và hiển thị ảnh trong ứng dụng Laravel.
Bài 19 hoàn thiện quy trình xử lý Form cơ bản trong Laravel: Request → Validation → Lưu dữ liệu. Ở bài tiếp theo, người học sẽ bổ sung khả năng Upload File, một chức năng rất phổ biến trong các hệ thống Blog, CMS và ứng dụng quản lý nội dung.
x1
Cập nhật: 2026-07-28T05:58:48.318+07:00
"Để xây dựng ứng dụng thông minh, đôi khi bạn cần biết nó đang chạy trên hệ điều hành nào, có bao nhiêu CPU, bao nhiêu RAM và thư mục Home ở đâu. Module
osgiúp Node.js làm được điều đó."
Sau bài này bạn sẽ:
Hiểu module os là gì.
Lấy thông tin hệ điều hành.
Kiểm tra CPU, RAM và kiến trúc máy.
Biết thư mục Home, Temp.
Phân biệt các hàm thường dùng trong module os.
Ứng dụng os trong các dự án thực tế.
os (Operating System) là module có sẵn của Node.js.
Không cần cài đặt.
Import:
import os from "node:os";
Hoặc theo từng hàm (lúc gọi k có chữ os)
import {
cpus,
totalmem
} from "node:os";
Cho biết hệ điều hành đang chạy.
import os from "node:os";
console.log(
os.platform()
);
Ví dụ
Windows
win32
Linux
linux
macOS
darwin
Lưu ý: Giá trị
win32vẫn được sử dụng cho Windows 64-bit để đảm bảo tính tương thích ngược.
Kiến trúc CPU.
import os from "node:os";
console.log(
os.arch()
);
Ví dụ
x64
Hoặc
arm64
Tên máy tính.
console.log(
os.hostname()
);
Ví dụ
DESKTOP-ABC123
Loại hệ điều hành.
console.log(
os.type()
);
Ví dụ
Windows
Windows_NT
Linux
Linux
macOS
Darwin
Phiên bản hệ điều hành.
console.log(
os.release()
);
Ví dụ
10.0.26100
Thông tin phiên bản chi tiết hơn.
console.log(
os.version()
);
Ví dụ
Windows 11 Pro
(Chuỗi trả về phụ thuộc vào hệ điều hành.)
Thông tin CPU.
import os from "node:os";
console.log(
os.cpus()
);
Ví dụ
[
{
model: "...",
speed: 3200
}
]
Đếm số lõi CPU
console.log(
os.cpus().length
);
Ví dụ
8
Đây là cách hiện đại để biết số luồng xử lý khả dụng.
console.log(
os.availableParallelism()
);
Ví dụ
8
Trong các phiên bản Node.js mới,
os.availableParallelism()được khuyến nghị thay cho việc dùngos.cpus().lengthkhi cần xác định mức độ song song phù hợp.
Dung lượng RAM.
console.log(
os.totalmem()
);
Ví dụ
17071734784
Đổi sang GB
const ram =
os.totalmem() /
1024 /
1024 /
1024;
console.log(
ram.toFixed(2)
);
↓
15.90 GB
RAM còn trống.
console.log(
os.freemem()
);
Ví dụ
8478232576
Thư mục Home.
console.log(
os.homedir()
);
Windows
C:\Users\Admin
Linux
/home/admin
Thư mục tạm.
console.log(
os.tmpdir()
);
Ví dụ
Windows
C:\Users\Admin\AppData\Local\Temp
Thời gian máy đã hoạt động.
console.log(
os.uptime()
);
↓
86400
Nghĩa là
86400 giây
Đổi sang giờ
const hours =
os.uptime() /
60 /
60;
console.log(
hours.toFixed(1)
);
↓
24.0 giờ
Thông tin người dùng.
console.log(JSON.stringify(os.userInfo(), null, 2));
Ví dụ
{
username:'admin',
shell:'cmd'
}
Thông tin mạng.
console.log(
JSON.stringify(os.networkInterfaces(), null, 2)
);
Ví dụ
Ethernet
Wi-Fi
IPv4
IPv6
Thông tin này thường được dùng trong các công cụ quản trị hoặc chẩn đoán hệ thống.
Ký tự xuống dòng của hệ điều hành.
console.log(
os.EOL
);
Windows
\r\n
Linux/macOS
\n
Ví dụ
import fs from "node:fs";
import os from "node:os";
fs.writeFileSync(
"log.txt",
"Dong 1" +
os.EOL +
"Dong 2"
);
Hiển thị thông tin hệ thống
import os from "node:os";
console.log("Platform:", os.platform());
console.log("CPU:", os.availableParallelism());
console.log(
"RAM:",
(
os.totalmem()/1024/1024/1024
).toFixed(2),
"GB"
);
Kết quả
Platform: win32
CPU: 8
RAM: 15.90 GB
Rất hữu ích trong:
Kiểm tra môi trường chạy.
Hiển thị thông tin máy chủ.
Công cụ CLI.
Dashboard quản trị.
Monitoring.
Docker.
VPS.
DevOps.
if(os.platform()=="windows")
Sai.
Đúng là
if(os.platform()=="win32")
console.log(
os.totalmem()
);
Kết quả
17071734784
Đây là byte, không phải GB.
os.cpus().length cho biết số lõi logic, nhưng không phản ánh đầy đủ khả năng xử lý của hệ thống. Trong các ứng dụng hiện đại, nên ưu tiên os.availableParallelism() khi cần xác định số luồng làm việc phù hợp.
| Hàm | Ý nghĩa |
|---|---|
platform() | Hệ điều hành |
arch() | Kiến trúc CPU |
hostname() | Tên máy |
type() | Loại hệ điều hành |
release() | Phiên bản kernel/HĐH |
version() | Phiên bản chi tiết |
cpus() | Thông tin CPU |
availableParallelism() | Số luồng xử lý khả dụng |
totalmem() | Tổng RAM |
freemem() | RAM còn trống |
homedir() | Thư mục Home |
tmpdir() | Thư mục Temp |
uptime() | Thời gian hoạt động |
userInfo() | Người dùng hiện tại |
networkInterfaces() | Thông tin mạng |
EOL | Ký tự xuống dòng |
Module os giúp Node.js truy cập thông tin về hệ điều hành và phần cứng của máy đang chạy ứng dụng. Với các API như platform(), availableParallelism(), totalmem() hay homedir(), bạn có thể xây dựng các chương trình thích ứng với nhiều môi trường khác nhau, đồng thời hỗ trợ giám sát và quản trị hệ thống hiệu quả.
Đây là một module nền tảng thường được sử dụng trong các công cụ CLI, ứng dụng máy chủ, Docker, VPS và các hệ thống giám sát.
import os from "node:os";
const user = os.userInfo();
console.log(`
Platform : ${os.platform()}
Arch : ${os.arch()}
Hostname : ${os.hostname()}
Type : ${os.type()}
Release : ${os.release()}
Version : ${os.version()}
CPU Model: ${os.cpus()[0].model}
CPU Speed: ${os.cpus()[0].speed} MHz
CPUs : ${os.cpus().length}
Số luồng : ${os.availableParallelism()}
RAM : ${(os.totalmem() / 1024 ** 3).toFixed(2)} GB
RAM trống: ${(os.freemem() / 1024 ** 3).toFixed(2)} GB
Home Dir : ${os.homedir()}
Temp Dir : ${os.tmpdir()}
Hoạt động: ${(os.uptime() / 3600).toFixed(2)} giờ
Username : ${user.username}
UID : ${user.uid}
GID : ${user.gid}
Home : ${user.homedir}
Shell : ${user.shell}
Network : ${JSON.stringify(os.networkInterfaces(), null, 2)}
`);
👉 Bài 09 — Events
Bạn sẽ học:
Event là gì.
EventEmitter.
on(), once(), emit().
Truyền dữ liệu qua sự kiện.
Xây dựng hệ thống xử lý sự kiện theo mô hình Publisher–Subscriber (Pub/Sub) trong Node.js.
Cập nhật: 2026-07-24T20:32:28.107+07:00
Sau khi đã học xong Eloquent ORM và các Relationship, chúng ta sẽ chuyển sang phần Form.
Trong mọi ứng dụng web, người dùng luôn gửi dữ liệu lên máy chủ thông qua biểu mẫu (Form), URL hoặc API.
Laravel cung cấp lớp Request để tiếp nhận và xử lý những dữ liệu này một cách đơn giản và an toàn.
Ở bài này, chúng ta sẽ tìm hiểu cách lấy dữ liệu từ Request trước khi chuyển sang Validation ở bài tiếp theo.
Request là đối tượng đại diện cho một yêu cầu HTTP gửi đến Laravel.
Ví dụ người dùng:
Mở một trang web.
Gửi biểu mẫu thêm bài viết.
Nhấn nút tìm kiếm.
Cập nhật thông tin cá nhân.
Tất cả dữ liệu đều được Laravel đóng gói trong đối tượng Request.
Trong Controller:
use Illuminate\Http\Request;
Ví dụ:
use Illuminate\Http\Request;
class PostController extends Controller
{
public function store(Request $request)
{
}
}
Laravel sẽ tự động truyền đối tượng Request vào phương thức.
Ví dụ trong Blade resources/views/posts/create.blade.php :
<form action="{{ route('posts.store') }}" method="POST">
@csrf
<input
type="text"
name="title"
placeholder="Tiêu đề">
<textarea
name="content"
placeholder="Nội dung"></textarea>
<select name="category_id">
@foreach($categories as $category)
<option value="{{ $category->id }}">
{{ $category->name }}
</option>
@endforeach
</select>
<button type="submit">
Lưu bài viết
</button>
</form>
Danh sách Category chính là dữ liệu đã được tạo bằng Seeder ở các bài trước.
Trong Controller:
use App\Models\Category;
/**
* Show the form for creating a new resource.
*/
public function create()
{
//Tạo form nhập thông tin post
$categories = Category::all();
return view(
'posts.create',
compact('categories')
);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//xảy ra sau khi bấm nút Save
dd($request->all());
}
Và route:
use App\Http\Controllers\PostController;
Route::resource(
'posts',
PostController::class
);
//kiểm tra route php artisan route:list
Chạy:
php artisan route:list
Nếu thấy dòng tương tự:
GET|HEAD posts/create posts.create
thì bạn chỉ cần mở:
http://127.0.0.1:8000/posts/create
Ví dụ kết quả :http://127.0.0.1:8000/posts/create
[
"title" => "Laravel 12",
"content" => "...",
"category_id" => 2,
]
public function store(Request $request)
{
$title = $request->input('title');
dd($title);
}
Hoặc:
$title = $request->title;
Hai cách đều hợp lệ.
Nếu trường không tồn tại:
$status = $request->input(
'status',
0
);
Nếu không gửi status, Laravel sẽ trả về 0.
if ($request->has('title')) {
echo "Có tiêu đề";
}
Kiểm tra nhiều trường:
$request->has([
'title',
'content'
]);
$data = $request->only([
'title',
'content'
]);
Hoặc:
$data = $request->except([
'_token'
]);
Điều này rất hữu ích khi lưu dữ liệu vào Model.
Ví dụ:
use App\Models\Post;
use Illuminate\Http\Request;
public function store(Request $request)
{
Post::create([
'title' => $request->title,
'content' => $request->content,
'category_id' => $request->category_id,
'user_id' => 1,
]);
return 'Tạo Post thành công!';//return redirect()->route('posts.index');
} Post phải cho phép gán dữ liệu// app/Models/Post.php protected $fillable = [ 'title', 'content', 'category_id', 'user_id', ];
Nếu không sẽ báo lỗi.
Trong dự án có đăng nhập, user_id thường lấy từ tài khoản đang đăng nhập.
Ở giai đoạn hiện tại của khóa học, chúng ta tạm gán một giá trị cố định để tập trung vào cách xử lý Request.
Request không chỉ dùng cho Form.
Ví dụ URL:
/posts?keyword=Laravel Route::resource('posts', PostController::class);
Trong resources/views/posts/index.blade.php
<form action="{{ route('posts.index') }}" method="GET">
<input
type="text"
name="keyword"
placeholder="Nhập tiêu đề">
<button type="submit">
Tìm kiếm
</button>
</form>
use Illuminate\Http\Request;
use App\Models\Post;
public function index(Request $request)
{
$keyword = $request->keyword;
$posts = Post::where(
'title',
'like',
"%{$keyword}%"
)->get();
return view(
'posts.index',
compact('posts')
);
}
@foreach($posts as $post)
<h3>{{ $post->title }}</h3>
@endforeach
Người dùng nhập từ khóa, Laravel sẽ lấy dữ liệu từ URL và tìm kiếm bài viết.
Ví dụ:
/posts?category=2
Post Controller:
public function index(Request $request)
{
$category = $request->category;
$posts = Post::where(
'category_id',
$category
)->get();
return view(
'posts.index',
compact('posts')
);
}
Nhờ vậy có thể lọc bài viết theo danh mục đã tạo bằng Seeder.
create()Truyền danh sách Tag sang View:
use App\Models\Tag; public function create() { $categories = Category::all(); $tags = Tag::all(); return view( 'posts.create', compact('categories', 'tags') ); }
<select name="tags[]" multiple> @foreach($tags as $tag) <option value="{{ $tag->id }}"> {{ $tag->name }} </option> @endforeach </select>
Người dùng chọn nhiều Tag rồi bấm Lưu bài viết.
store()Phải tạo Post trước.
$post = Post::create([ 'title' => $request->title, 'content' => $request->content, 'category_id' => $request->category_id, 'user_id' => 1, ]);
$tagIds = $request->tags; $post->tags()->sync($tagIds);
Hoặc gọn hơn:
$post->tags()->sync( $request->tags );
Đây chính là cách kết hợp Request với Relationship Many To Many đã học ở bài trước.
Ví dụ cập nhật Profile:
use App\Models\Profile; use Illuminate\Http\Request; public function update( Request $request, Profile $profile ) { $profile->update([ 'phone' => $request->phone, 'address' => $request->address, ]); return 'Cập nhật thành công!'; }
Hoặc nếu chưa học Route Model Binding, có thể dùng ví dụ đơn giản hơn:
$profile = Profile::find(1); $profile->update([ 'phone' => $request->phone, 'address' => $request->address, ]);
<form action="{{ route('profiles.update', 1) }}" method="POST"> @csrf @method('PUT') <input type="text" name="phone"> <input type="text" name="address"> <button> Cập nhật </button> </form>
Các trường phone và address thuộc bảng profiles mà chúng ta đã tạo trong phần Relationship.
Thực ra ví dụ này không nên có trong Bài 18 (Request). Nó kéo theo quá nhiều kiến thức chưa học.
Để chạy được, bạn phải có:
✅ ProfileController
✅ Route::resource('profiles', ProfileController::class)
✅ edit.blade.php
✅ Route profiles.update
✅ Form PUT
✅ Có bản ghi profiles.id = 1
✅ Đã học Route Model Binding (hoặc find())
Trong khi bài này chỉ đang dạy Request.
Route::resource(
'profiles',
ProfileController::class
);
http://127.0.0.1:8000/profiles/1/edit
Laravel gọi
edit(Profile $profile)
<form
action="{{ route('profiles.update', $profile) }}"
method="POST">
@csrf
@method('PUT')
<input
type="text"
name="phone"
value="{{ $profile->phone }}">
<input
type="text"
name="address"
value="{{ $profile->address }}">
<button>
Cập nhật
</button>
</form>
Laravel gửi
PUT /profiles/1
và mới gọi
public function update(
Request $request,
Profile $profile
)
{
$profile->update([
'phone' => $request->phone,
'address' => $request->address,
]);
return 'Cập nhật thành công!';
}
Lý do là người học sẽ gặp hàng loạt câu hỏi:
profiles.update ở đâu?
ProfileController ở đâu?
Sao lại PUT?
Sao URL là /profiles/1?
Sao không phải /profiles/update?
$profile lấy ở đâu?
Trong khi bài học chỉ muốn giới thiệu:
$request->phone
$request->address
Đến bài CRUD Update hoặc Resource Controller, lúc đó mới dùng:
$profile->update([
'phone' => $request->phone,
'address' => $request->address,
]);
Còn trong Bài 18 – Request, các ví dụ về:
store() (tạo Post)
GET /posts?keyword=Laravel
GET /posts?category=2
tags[] và $request->tags
đã đủ để minh họa hầu hết các cách lấy dữ liệu từ Request.
Ví dụ Profile không bổ sung thêm kiến thức mới về Request, nhưng lại đòi hỏi nhiều kiến thức khác chưa học, nên dễ làm người đọc rối hơn là giúp họ hiểu.
✅ Đưa sang Bài 26 – CRUD Posts (hoặc nếu sau này có một bài CRUD Profile thì đưa vào đó).
| Phương thức | Ý nghĩa |
|---|---|
| all() | Lấy toàn bộ dữ liệu |
| input() | Lấy một trường |
| has() | Kiểm tra trường tồn tại |
| only() | Chỉ lấy các trường được chỉ định |
| except() | Loại bỏ một số trường |
| method() | Lấy phương thức HTTP |
| isMethod() | Kiểm tra phương thức HTTP |
Ví dụ:
if ($request->isMethod('post')) {
echo 'Đây là POST';
}
Trong bài này chúng ta đã học:
Request là gì.
Lấy dữ liệu từ Form.
Lấy dữ liệu từ URL.
all().
input().
has().
only().
except().
Kết hợp Request với Eloquent và Relationship.
Request là bước đầu tiên trong quá trình xử lý dữ liệu người dùng trước khi lưu vào cơ sở dữ liệu.
Tạo Form thêm Post gồm tiêu đề, nội dung và Category.
Hiển thị toàn bộ dữ liệu bằng dd($request->all()).
Lưu Post mới vào cơ sở dữ liệu.
Viết chức năng tìm kiếm Post theo tiêu đề bằng Request.
Lọc danh sách Post theo Category.
Cho phép chọn nhiều Tag và liên kết với Post bằng sync().
Ở bài sau, chúng ta sẽ tìm hiểu Validation, cách kiểm tra dữ liệu đầu vào để đảm bảo người dùng nhập đúng định dạng trước khi lưu vào cơ sở dữ liệu.
x1
Cập nhật: 2026-07-26T20:06:44.220+07:00
Một website đẹp nhưng không ai tìm thấy trên Google thì cũng khó phát huy giá trị. Vì vậy, ngay từ khi viết HTML, chúng ta cần xây dựng cấu trúc thân thiện với công cụ tìm kiếm (SEO - Search Engine Optimization).
SEO không chỉ là thêm từ khóa hay cài plugin. Nền tảng của SEO bắt đầu từ HTML chuẩn, Semantic HTML, nội dung rõ ràng và cấu trúc hợp lý.
Trong bài học này, chúng ta sẽ tìm hiểu cách xây dựng một trang HTML dễ hiểu đối với Google và các công cụ tìm kiếm khác.
Sau bài này bạn sẽ:
Hiểu SEO Friendly HTML là gì.
Biết cách sử dụng đúng các thẻ Heading.
Viết Title và Meta Description hiệu quả.
Tối ưu hình ảnh và liên kết.
Xây dựng cấu trúc HTML giúp Google dễ thu thập dữ liệu.
Tránh những lỗi SEO HTML phổ biến.
SEO Friendly HTML là việc viết mã HTML theo các tiêu chuẩn giúp công cụ tìm kiếm:
Hiểu nội dung trang.
Thu thập dữ liệu nhanh hơn.
Xác định chủ đề chính.
Hiển thị kết quả tìm kiếm chính xác hơn.
Một HTML được tổ chức tốt cũng giúp cải thiện trải nghiệm người dùng, từ đó gián tiếp hỗ trợ SEO.
Google không "nhìn" website giống con người.
Thay vào đó, Google sẽ phân tích:
Tiêu đề trang.
Các thẻ Heading.
Nội dung văn bản.
Hình ảnh.
Liên kết.
Cấu trúc HTML.
Nếu HTML lộn xộn, thiếu ý nghĩa hoặc sai cấu trúc, công cụ tìm kiếm sẽ khó hiểu nội dung của trang.
Đây là một trong những yếu tố SEO quan trọng nhất.
Ví dụ:
<head>
<title>
Học HTML5 từ cơ bản đến nâng cao | Lớp Học Vi Tính
</title>
</head>
Title nên:
mô tả đúng nội dung
ngắn gọn
chứa từ khóa chính
không quá dài
Đây là đoạn mô tả thường xuất hiện dưới tiêu đề trên Google.
<meta
name="description"
content="Khóa học HTML5 Enterprise giúp bạn xây dựng website chuẩn SEO, Responsive và hiện đại.">
Meta Description không trực tiếp tăng thứ hạng, nhưng có thể giúp tăng tỷ lệ người dùng nhấp vào kết quả tìm kiếm.
HTML có sáu cấp Heading.
<h1>
<h2>
<h3>
<h4>
<h5>
<h6>
Mỗi trang chỉ nên có một thẻ <h1>.
Ví dụ:
<h1>
Khóa học HTML5 Enterprise
</h1>
Đây là tiêu đề chính của trang.
Dùng cho các mục lớn.
Ví dụ:
<h2>
Nội dung khóa học
</h2>
<h2>
Giảng viên
</h2>
<h2>
Học phí
</h2>
Dùng cho các mục con.
<h3>
HTML5
</h3>
<h3>
CSS3
</h3>
Sai:
<h2>
Khuyến mãi
</h2>
chỉ vì muốn chữ to.
Heading phải phản ánh cấu trúc nội dung, không phải để trang trí. Nếu muốn thay đổi kích thước chữ, hãy sử dụng CSS.
Google hiểu rất tốt các thẻ:
<header>
<nav>
<main>
<section>
<article>
<aside>
<footer>
Đây cũng là lý do Semantic HTML được học đầu tiên trong khóa học.
Ví dụ:
<img
src="html5.jpg"
alt="Khóa học HTML5 Enterprise">
Nếu ảnh không hiển thị, nội dung alt sẽ được dùng thay thế.
Đối với công cụ tìm kiếm, alt giúp hiểu nội dung của hình ảnh.
Tốt:
html5-course.jpg
Không tốt:
IMG001.jpg
Tên tệp nên phản ánh nội dung của hình ảnh.
Ví dụ:
<a href="/khoa-hoc-html">
Khóa học HTML
</a>
Nội dung liên kết nên mô tả rõ điểm đến.
Không nên:
<a href="/html">
Click tại đây
</a>
Thay vào đó:
<a href="/html">
Xem khóa học HTML5
</a>
Khi liệt kê nội dung, hãy sử dụng:
<ul>
<li>HTML</li>
<li>CSS</li>
<li>JavaScript</li>
</ul>
Hoặc:
<ol>
<li>Bài 1</li>
<li>Bài 2</li>
</ol>
Điều này giúp cấu trúc nội dung rõ ràng hơn.
Nên chia bài viết thành nhiều đoạn ngắn.
Ví dụ:
<p>
HTML là ngôn ngữ đánh dấu.
</p>
<p>
CSS dùng để tạo giao diện.
</p>
Không nên dồn toàn bộ nội dung vào một đoạn quá dài.
Tốt:
/khoa-hoc-html5
Không tốt:
/?id=123
URL ngắn gọn, dễ hiểu sẽ thân thiện hơn với người dùng và công cụ tìm kiếm.
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>
Học HTML5 Enterprise
</title>
<meta
name="description"
content="Khóa học HTML5 dành cho lập trình viên PHP.">
</head>
<body>
<header>
<h1>
HTML5 Enterprise
</h1>
</header>
<main>
<section>
<h2>
Giới thiệu
</h2>
<p>
Nội dung...
</p>
</section>
</main>
<footer>
© 2026
</footer>
</body>
</html>
Đây là cấu trúc cơ bản nhưng đã đáp ứng nhiều yêu cầu SEO.
❌ Không có thẻ <title>.
❌ Thiếu meta description.
❌ Có nhiều thẻ <h1> trên cùng một trang.
❌ Bỏ trống thuộc tính alt cho hình ảnh quan trọng.
❌ Đặt tên tệp hình ảnh không có ý nghĩa.
❌ Sử dụng "Click here" hoặc "Xem thêm" làm nội dung liên kết mà không có ngữ cảnh.
❌ Dùng <div> thay cho các thẻ Semantic HTML.
❌ Không khai báo ngôn ngữ của tài liệu bằng lang.
Tạo một trang giới thiệu khóa học với:
Title
Meta Description
Một thẻ <h1>
Ba thẻ <h2>
Một hình ảnh có alt
Menu điều hướng bằng <nav>
Nội dung chính trong <main>
Chân trang <footer>
Yêu cầu:
Sử dụng Semantic HTML.
Không dùng nhiều hơn một thẻ <h1>.
Đặt tiêu đề và mô tả phù hợp với nội dung.
SEO bắt đầu từ những điều rất cơ bản. Một cấu trúc HTML rõ ràng, sử dụng đúng Heading, kết hợp với Semantic HTML và các thẻ Meta sẽ giúp công cụ tìm kiếm hiểu website tốt hơn.
Đây là nền tảng quan trọng trước khi chúng ta tối ưu CSS, JavaScript hay xây dựng các hệ thống lớn bằng PHP, Laravel hoặc các framework khác.
✅ Hiểu SEO Friendly HTML là gì.
✅ Biết cách sử dụng title và meta description.
✅ Phân biệt vai trò của các thẻ Heading (h1 đến h6).
✅ Tối ưu hình ảnh với thuộc tính alt.
✅ Viết liên kết và URL thân thiện với SEO.
✅ Xây dựng cấu trúc HTML chuẩn cho công cụ tìm kiếm.
✅ Nhận biết các lỗi SEO HTML thường gặp.
➡️ Bài tiếp theo: Bài 04 — Accessibility & ARIA: Xây dựng Website thân thiện với mọi người dùng.
Cập nhật: 2026-07-24T07:22:00.186+07:00
Một website doanh nghiệp có thể thiếu hiệu ứng đẹp, nhưng gần như không thể thiếu Form.
Từ trang liên hệ, đăng nhập, đăng ký, tìm kiếm, đặt hàng, gửi yêu cầu báo giá đến hệ thống CRM hay ERP, tất cả đều hoạt động thông qua biểu mẫu.
HTML5 đã bổ sung rất nhiều tính năng giúp Form trở nên thông minh hơn, giảm lượng JavaScript phải viết và mang lại trải nghiệm tốt hơn cho người dùng.
Sau bài này bạn sẽ:
Hiểu cấu trúc chuẩn của một Form HTML5.
Biết sử dụng các loại input hiện đại.
Áp dụng các thuộc tính kiểm tra dữ liệu.
Tạo Form thân thiện với người dùng.
Chuẩn bị nền tảng để tích hợp PHP, Laravel hoặc Node.js.
Form là nơi người dùng nhập dữ liệu và gửi về máy chủ.
Ví dụ:
Đăng nhập
Đăng ký
Liên hệ
Thanh toán
Đặt lịch
Gửi CV
Tìm kiếm
<form action="/login" method="post">
<label>Email</label>
<input type="email">
<label>Password</label>
<input type="password">
<button type="submit">
Đăng nhập
</button>
</form>
Form luôn bắt đầu bằng thẻ:
<form>
và kết thúc bằng:
</form>
Là địa chỉ sẽ nhận dữ liệu.
Ví dụ:
<form action="/contact">
hoặc
<form action="save.php">
Có hai giá trị phổ biến.
<form method="get">
Dữ liệu xuất hiện trên URL.
Ví dụ
/search?q=html
Phù hợp cho:
tìm kiếm
lọc dữ liệu
phân trang
<form method="post">
Dữ liệu gửi trong phần thân của request.
Phù hợp cho:
đăng nhập
đăng ký
thanh toán
cập nhật dữ liệu
upload file
Luôn nên sử dụng label.
Ví dụ:
<label>Email</label>
<input type="email">
Hoặc tốt hơn:
<label for="email">
Email
</label>
<input
id="email"
type="email">
Khi nhấn vào chữ Email, con trỏ sẽ tự động chuyển vào ô nhập liệu.
<input type="text">
Dùng nhập:
họ tên
địa chỉ
tên công ty
<input type="email">
HTML5 tự kiểm tra định dạng email.
<input type="password">
Ẩn ký tự khi nhập.
<input type="number">
Chỉ cho phép nhập số.
<input type="date">
Hiển thị lịch chọn ngày.
<input type="time">
<input type="tel">
Nhập số điện thoại.
<input type="url">
Kiểm tra định dạng website.
<input type="search">
Dùng cho ô tìm kiếm.
<input type="file">
Cho phép tải tệp lên.
<input
type="text"
placeholder="Nhập họ tên">
Placeholder chỉ mang tính gợi ý.
Không thay thế Label.
Bắt buộc nhập.
<input
type="email"
required>
Nếu bỏ trống, trình duyệt sẽ không cho gửi Form.
<input
type="text"
minlength="6">
Ít nhất 6 ký tự.
<input
maxlength="50">
Giới hạn số ký tự.
<input
type="number"
min="18"
max="65">
Cho phép kiểm tra bằng biểu thức chính quy.
Ví dụ:
<input
pattern="[A-Za-z]{5,20}">
Chỉ chấp nhận chữ cái.
<input
readonly>
Có thể xem nhưng không sửa.
<input
disabled>
Không thể thao tác.
Giá trị cũng sẽ không được gửi lên máy chủ.
<input
autofocus>
Con trỏ tự động đứng tại ô này khi mở trang.
<input
autocomplete="email">
Giúp trình duyệt gợi ý dữ liệu đã lưu.
<input type="checkbox">
Có thể chọn nhiều.
Ví dụ:
HTML
CSS
JavaScript
<input
type="radio"
name="gender">
Chỉ chọn một.
Ví dụ:
Nam
Nữ
Khác
<select>
<option>Việt Nam</option>
<option>Hoa Kỳ</option>
<option>Nhật Bản</option>
</select>
Danh sách lựa chọn.
<textarea
rows="5">
</textarea>
Dùng nhập nội dung dài.
<button
type="submit">
Gửi
</button>
<button
type="reset">
Làm lại
</button>
Xóa dữ liệu đã nhập.
<form action="/contact" method="post">
<label for="name">Họ và tên</label>
<input
id="name"
type="text"
required>
<label for="email">Email</label>
<input
id="email"
type="email"
required>
<label for="phone">Số điện thoại</label>
<input
id="phone"
type="tel">
<label for="message">Nội dung</label>
<textarea
id="message"
rows="5">
</textarea>
<button type="submit">
Gửi liên hệ
</button>
</form>
Đây là cấu trúc phù hợp cho hầu hết website doanh nghiệp.
❌ Không dùng label.
❌ Chỉ dùng placeholder thay cho label.
❌ Dùng method="get" cho đăng nhập.
❌ Không kiểm tra dữ liệu bằng required.
❌ Không đặt type phù hợp cho từng trường.
❌ Không liên kết label với input bằng thuộc tính for và id.
❌ Quên thêm name cho các trường, khiến dữ liệu không được gửi lên máy chủ.
Tạo một Form Đăng ký khóa học gồm:
Họ và tên
Số điện thoại
Ngày sinh
Giới tính
Khóa học muốn đăng ký
Nội dung ghi chú
Nút Đăng ký
Nút Làm lại
Yêu cầu:
Sử dụng Semantic HTML.
Mỗi trường đều có label.
Áp dụng required cho các trường quan trọng.
Chọn đúng type cho từng ô nhập liệu.
Form là cầu nối giữa người dùng và hệ thống. Một biểu mẫu được xây dựng đúng chuẩn HTML5 không chỉ giúp nhập liệu nhanh hơn mà còn giảm lỗi, cải thiện trải nghiệm người dùng và tạo nền tảng vững chắc để tích hợp với PHP, Laravel, Node.js hay bất kỳ công nghệ backend nào.
Trong các bài tiếp theo, chúng ta sẽ tiếp tục tối ưu cấu trúc HTML để website thân thiện hơn với công cụ tìm kiếm và sẵn sàng cho các dự án thực tế.
✅ Hiểu cấu trúc của thẻ form.
✅ Phân biệt GET và POST.
✅ Sử dụng các loại input phổ biến trong HTML5.
✅ Áp dụng các thuộc tính kiểm tra dữ liệu như required, minlength, maxlength, pattern.
✅ Biết cách tạo Form theo chuẩn Enterprise.
✅ Nắm được các lỗi phổ biến khi xây dựng biểu mẫu.
➡️ Bài tiếp theo: Bài 03 — SEO Friendly HTML: Xây dựng HTML thân thiện với công cụ tìm kiếm.
Cập nhật: 2026-07-23T18:54:21.016+07:00