Trang chủ/Blog/Tự tạo trình phát video & trình xem ảnh lightbox (Code from Scratch)
Lập trình

Tự tạo trình phát video & trình xem ảnh lightbox (Code from Scratch)

nam2508
nam2508 QTV
·9 phút đọc·8 lượt xem
Tự tạo trình phát video & trình xem ảnh lightbox (Code from Scratch)

Bài viết này hướng dẫn cách tự viết code để tạo ra một trình phát video và một trình xem ảnh lightbox mượt mà, tối ưu — như cách mình đã làm trên blog này. Đây là hướng dẫn lập trình (React + CSS), không phải cách sử dụng.

Kết quả cuối cùng bạn sẽ tạo ra được:

Trình phát video tự tạo

Trình xem ảnh lightbox tự tạo

1. Ý tưởng chính 💡

Hai tính năng "xịn" nhất đều dựa trên một nguyên tắc: trì hoãn tải tài nguyên nặng cho đến khi người dùng thực sự cần.

  • Video YouTube: đừng nhúng iframe ngay khi trang load (nặng, chậm). Thay vào đó hiện thumbnail + nút play, chỉ chèn iframe khi bấm.
  • Ảnh: đừng để ảnh tĩnh trong bài. Khi bấm, mở fullscreen overlay với animation.

Cả hai dùng event delegation ở cấp document vì nội dung bài viết được render qua dangerouslySetInnerHTML (không phải React component thuần).

2. Tạo trình phát video YouTube (click-to-load) 🎬

Bước 1 — Server render placeholder (không phải iframe)

Khi render bài viết, thay vì iframe, xuất ra một <div> placeholder có thumbnail + nút play:

tsx
// Trong quá trình xử lý nội dung (SSG)
const YT_EMBED = (id: string) =>
  `
`;

Lấy video ID từ link bằng regex (hỗ trợ watch?v=, youtu.be, shorts, embed):

ts
const ytId = (url: string) =>
  url.match(/(?:youtube\.com\/(?:watch\?(?:.*&)?v=|embed\/|shorts\/|live\/)|youtu\.be\/)([A-Za-z0-9_-]{6,})/)?.[1];

Bước 2 — Hydrator: chèn iframe khi bấm

Tạo một client component gắn listener ở cấp document:

tsx
"use client";
import { useEffect } from "react";

export default function StoryVideo() {
  useEffect(() => {
    const handleClick = (e: Event) => {
      const el = (e.target as HTMLElement).closest(".story-yt") as HTMLElement | null;
      if (!el || el.dataset.loaded) return;
      el.dataset.loaded = "1";
      const iframe = document.createElement("iframe");
      iframe.src = `https://www.youtube.com/embed/${el.dataset.yt}?autoplay=1&rel=0`;
      iframe.allowFullscreen = true;
      el.innerHTML = "";
      el.appendChild(iframe);
    };
    document.addEventListener("click", handleClick);
    return () => document.removeEventListener("click", handleClick);
  }, []);
  return null;
}

Bước 3 — Tối ưu hơn: scroll-reveal + loading shimmer

Dùng IntersectionObserver để chỉ hiện player khi cuộn tới (không hiện hết cùng lúc):

tsx
const io = new IntersectionObserver((entries) => {
  entries.forEach((e) => {
    if (e.isIntersecting) { e.target.classList.add("story-yt--visible"); io.unobserve(e.target); }
  });
}, { threshold: 0.15 });
document.querySelectorAll(".story-yt").forEach((p) => io.observe(p));

CSS: player mặc định opacity: 0; transform: translateY(28px), khi có class --visible thì chạy animation:

css
.story-yt { opacity: 0; transform: translateY(28px); }
.story-yt--visible { animation: storyYtIn 0.7s cubic-bezier(0.16,1,0.3,1) forwards; }
@keyframes storyYtIn {
  from { opacity: 0; transform: translateY(28px) scale(0.98); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

Hiệu ứng pulse cho nút play và shimmer khi đang nạp:

css
.story-yt-btn { animation: playPulse 2.4s ease-in-out infinite; }
@keyframes playPulse {
  0%,100% { box-shadow: 0 8px 26px rgba(225,29,72,.5), 0 0 0 0 rgba(225,29,72,.5); }
  50%     { box-shadow: 0 8px 26px rgba(225,29,72,.5), 0 0 0 16px rgba(225,29,72,0); }
}

3. Tạo trình xem ảnh Lightbox 🖼️

Bước 1 — Bắt sự kiện bấm ảnh

Một client component gắn listener ở document, chỉ quan tâm ảnh trong vùng nội dung bài:

tsx
useEffect(() => {
  const h = (e: Event) => {
    const img = (e.target as HTMLElement).closest(".article-content img") as HTMLImageElement | null;
    if (!img) return;
    e.preventDefault();
    openGallery(img.src); // thu thập tất cả ảnh, mở modal
  };
  document.addEventListener("click", h);
  return () => document.removeEventListener("click", h);
}, []);

Bước 2 — State và điều hướng

tsx
const [open, setOpen] = useState(false);
const [index, setIndex] = useState(0);
const [images, setImages] = useState([]);
const [zoom, setZoom] = useState(1);

const next = () => setIndex((i) => (i + 1) % images.length);
const prev = () => setIndex((i) => (i - 1 + images.length) % images.length);

Hỗ trợ phím tắt: Esc đóng, ←→ chuyển ảnh, +/− zoom:

tsx
useEffect(() => {
  if (!open) return;
  const onKey = (e: KeyboardEvent) => {
    if (e.key === "Escape") close();
    else if (e.key === "ArrowLeft") prev();
    else if (e.key === "ArrowRight") next();
    else if (e.key === "+") zoomIn();
    else if (e.key === "-") zoomOut();
  };
  window.addEventListener("keydown", onKey);
  return () => window.removeEventListener("keydown", onKey);
}, [open, prev, next, close]);

Bước 3 — Giao diện + animation

Backdrop đen blur + ảnh zoom-in khi mở:

css
.story-gallery-backdrop {
  position: fixed; inset: 0;
  background: rgba(8,8,12,.92);
  backdrop-filter: blur(10px);
  animation: galleryFadeIn .3s ease forwards;
}
.story-gallery-img { animation: galleryZoomIn .35s cubic-bezier(.16,1,.3,1) forwards; }
@keyframes galleryZoomIn {
  from { opacity: 0; transform: scale(.85); }
  to   { opacity: 1; transform: scale(1); }
}

Vuốt trên mobile bằng touch events:

tsx
onTouchStart={(e) => (touchX.current = e.touches[0].clientX)}
onTouchEnd={(e) => {
  const dx = e.changedTouches[0].clientX - touchX.current!;
  if (Math.abs(dx) > 50) (dx < 0 ? next : prev)();
}}

Bước 4 — Khóa cuộn trang khi mở

tsx
useEffect(() => {
  if (!open) return;
  const prev = document.body.style.overflow;
  document.body.style.overflow = "hidden";
  return () => { document.body.style.overflow = prev; };
}, [open]);

4. Tổng kết 📌

  • Tối ưu = trì hoãn tải iframe/ảnh nặng đến khi người dùng cần
  • Mượt = dùng cubic-bezier(0.16,1,0.3,1) (ease-out mềm) + animation hợp lý
  • Bền vững = event delegation ở document (không phụ thuộc cấu trúc DOM bên trong)
  • Thân thiện = hỗ trợ phím tắt, vuốt, prefers-reduced-motion

Chỉ vài chục dòng code là bạn đã có trình phát video và xem ảnh đạt chuẩn "10/10". Chúc bạn code vui! 🚀

Chia sẻ:

Bình luận

Đăng nhập để tham gia bình luận

Đang tải bình luận...

Bài viết tương tự

Xem thêm →
Gộp 2, 3 hay nhiều đường truyền vào MikroTik OS (Load Balancing + Failover)
Mạng6 phút đọc

Gộp 2, 3 hay nhiều đường truyền vào MikroTik OS (Load Balancing + Failover)

40 lượt xem

Hướng dẫn chi tiết cách gộp 2, 3 hoặc nhiều đường truyền trên MikroTik OS bằng kỹ thuật PCC load balancing và Netwatch failover.

Engineering2 phút đọc

Test video YouTube trên KeoQuy

12 lượt xem

Bài viết test hệ thống phát video YouTube tự động nhận dạng link và hiển thị player riêng, mượt mà và tối ưu.

Kiến Trúc Anti-OCR Captcha: Từ Lỗ Hổng DOM Text Đến Vector Bézier Spline Transformation Trong Go
Security10 phút đọc

Kiến Trúc Anti-OCR Captcha: Từ Lỗ Hổng DOM Text Đến Vector Bézier Spline Transformation Trong Go

30 lượt xem1 bình luận

Phân tích chuyên sâu lỗ hổng DOM Scraping trên các hệ thống SVG Captcha phổ biến và giải pháp chuyển đổi sang kiến trúc Vector Bézier Spline kết hợp biến dạng sóng sin phi tuyến tính trong Go (Fiber), vô hiệu hóa Tesseract 5 và Deep Learning OCR.