Xây Dựng Dự Án Music Player Bằng JavaScript – Dự Án Thực Tế Cho Lập Trình Viên Frontend

Xây Dựng Dự Án Music Player Bằng JavaScript – Dự Án Thực Tế Cho Lập Trình Viên Frontend
Jason Nguyen

Jason Nguyen

Ngày công bố: 22/11/2025

Tác giả

Ngày công bố: 22/11/2025


Xây Dựng Dự Án Music Player Bằng JavaScript – Dự Án Thực Tế Cho Lập Trình Viên Frontend

Âm nhạc là một phần không thể thiếu trong cuộc sống hiện đại, và việc xây dựng một trình phát nhạc (music player) là một trong những dự án thực tế và thú vị nhất dành cho lập trình viên frontend. Dự án này không chỉ giúp bạn rèn luyện các kỹ năng JavaScript cốt lõi mà còn là cơ hội để khám phá Web Audio API, xử lý file âm thanh, quản lý trạng thái phức tạp, và thiết kế giao diện tương tác mượt mà. Bài viết này sẽ hướng dẫn bạn xây dựng một trình phát nhạc hoàn chỉnh với các tính năng như phát/tạm dừng, chuyển bài, thanh tiến trình, điều chỉnh âm lượng, tạo playlist, và lưu trạng thái với localStorage.

1. Giới thiệu: Music Player – Dự án kết hợp giữa kỹ thuật và nghệ thuật

Xây dựng một Music Player là một trong những dự án frontend thực tế và thú vị nhất. Nó không chỉ đòi hỏi kiến thức JavaScript vững chắc mà còn cần sự tinh tế trong thiết kế giao diện và trải nghiệm người dùng. Các kỹ năng bạn sẽ học được bao gồm:

  • Web Audio API: Phát và điều khiển âm thanh trong trình duyệt.
  • Xử lý sự kiện: Quản lý các tương tác như click, kéo thanh tiến trình, volume.
  • Quản lý trạng thái (State Management): Lưu trạng thái phát nhạc, danh sách phát, vị trí bài hát.
  • LocalStorage: Lưu playlist và trạng thái để không bị mất khi tải lại trang.
  • UI/UX: Thiết kế giao diện đẹp mắt, responsive, có hiệu ứng chuyển động.

Hoàn thành dự án này sẽ là một bước tiến lớn trong hành trình trở thành một frontend developer chuyên nghiệp.

2. Thiết kế UI/UX – Giao diện trình phát nhạc hiện đại

Trước khi bắt tay vào code, hãy phác thảo giao diện của trình phát nhạc. Một trình phát nhạc tốt nên có:

  • Thông tin bài hát: Ảnh bìa, tên bài hát, tên ca sĩ.
  • Điều khiển phát: Nút Play/Pause, Next, Previous, Shuffle, Repeat.
  • Thanh tiến trình (Progress Bar): Hiển thị thời gian đã phát và cho phép kéo để tua.
  • Âm lượng: Thanh điều chỉnh âm lượng.
  • Thời gian: Hiển thị thời gian hiện tại và tổng thời gian.
  • Playlist: Danh sách các bài hát để dễ dàng chọn.
  • Responsive: Giao diện đẹp trên cả máy tính và thiết bị di động.

Màu sắc nên tối giản, tập trung vào trải nghiệm nghe nhạc. Gradient màu tối thường được sử dụng cho các ứng dụng nghe nhạc để tạo cảm giác tập trung và nghệ thuật.

3. Cấu trúc HTML

Chúng ta sẽ bắt đầu với cấu trúc HTML cho trình phát nhạc.

<!DOCTYPE html>
<html lang="vi">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>🎵 Music Player</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <!-- Phần chính: thông tin và điều khiển -->
        <div class="player">
            <div class="song-image">
                <img id="coverImage" src="https://via.placeholder.com/300" alt="Album cover">
            </div>
            <div class="song-info">
                <h2 id="songTitle">Tên bài hát</h2>
                <p id="songArtist">Ca sĩ</p>
            </div>
            
            <!-- Thanh tiến trình -->
            <div class="progress-container">
                <span id="currentTime">0:00</span>
                <input type="range" id="progressBar" min="0" max="100" value="0">
                <span id="totalTime">0:00</span>
            </div>

            <!-- Điều khiển -->
            <div class="controls">
                <button id="shuffleBtn" title="Shuffle">🔀</button>
                <button id="prevBtn" title="Previous">⏮️</button>
                <button id="playBtn" title="Play">▶️</button>
                <button id="nextBtn" title="Next">⏭️</button>
                <button id="repeatBtn" title="Repeat">🔁</button>
            </div>

            <!-- Âm lượng -->
            <div class="volume-container">
                <span>🔊</span>
                <input type="range" id="volumeControl" min="0" max="100" value="80">
            </div>
        </div>

        <!-- Playlist -->
        <div class="playlist">
            <h3>📋 Playlist</h3>
            <ul id="playlist">
                <!-- Danh sách bài hát sẽ được render ở đây -->
            </ul>
        </div>
    </div>

    <audio id="audio" src=""></audio>
    <script src="songs.js"></script>
    <script src="script.js"></script>
</body>
</html>

4. CSS – Thiết kế giao diện trình phát nhạc đẹp mắt

CSS sẽ tạo nên diện mạo chuyên nghiệp cho trình phát nhạc của bạn.

/* Reset và biến CSS */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

:root {
    --bg: #0f0f1a;
    --card-bg: #1a1a2e;
    --text: #e0e0e0;
    --text-secondary: #a0a0b8;
    --primary: #6c63ff;
    --primary-hover: #5a52d5;
    --shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
    --radius: 16px;
    --transition: 0.3s ease;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background: var(--bg);
    color: var(--text);
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    padding: 20px;
}

.container {
    display: flex;
    gap: 30px;
    max-width: 900px;
    width: 100%;
    background: var(--card-bg);
    border-radius: var(--radius);
    padding: 30px;
    box-shadow: var(--shadow);
    backdrop-filter: blur(10px);
    flex-wrap: wrap;
}

/* Phần player chính */
.player {
    flex: 1;
    min-width: 280px;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
}

.song-image {
    width: 200px;
    height: 200px;
    border-radius: 50%;
    overflow: hidden;
    box-shadow: 0 8px 25px rgba(108, 99, 255, 0.3);
    margin-bottom: 20px;
    animation: rotate 10s linear infinite;
    animation-play-state: paused;
}

.song-image img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.song-info {
    margin-bottom: 16px;
}

.song-info h2 {
    font-size: 22px;
    font-weight: 600;
    margin-bottom: 4px;
}

.song-info p {
    font-size: 16px;
    color: var(--text-secondary);
}

/* Thanh tiến trình */
.progress-container {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    margin-bottom: 16px;
}

.progress-container span {
    font-size: 14px;
    color: var(--text-secondary);
    min-width: 40px;
}

#progressBar {
    flex: 1;
    height: 4px;
    -webkit-appearance: none;
    appearance: none;
    background: #2d2d44;
    border-radius: 4px;
    outline: none;
    cursor: pointer;
    transition: height 0.2s;
}

#progressBar::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--primary);
    cursor: pointer;
    transition: transform 0.2s;
}

#progressBar::-webkit-slider-thumb:hover {
    transform: scale(1.1);
}

#progressBar::-moz-range-thumb {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--primary);
    cursor: pointer;
    border: none;
}

/* Điều khiển */
.controls {
    display: flex;
    gap: 16px;
    margin-bottom: 12px;
    align-items: center;
}

.controls button {
    width: 48px;
    height: 48px;
    border: none;
    border-radius: 50%;
    background: rgba(108, 99, 255, 0.15);
    color: var(--text);
    font-size: 20px;
    cursor: pointer;
    transition: all var(--transition);
    display: flex;
    align-items: center;
    justify-content: center;
}

.controls button:hover {
    background: var(--primary);
    transform: scale(1.05);
}

#playBtn {
    width: 56px;
    height: 56px;
    background: var(--primary);
    font-size: 24px;
}

#playBtn:hover {
    background: var(--primary-hover);
}

/* Âm lượng */
.volume-container {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
}

.volume-container span {
    font-size: 18px;
}

#volumeControl {
    flex: 1;
    height: 4px;
    -webkit-appearance: none;
    appearance: none;
    background: #2d2d44;
    border-radius: 4px;
    outline: none;
    cursor: pointer;
}

#volumeControl::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--primary);
    cursor: pointer;
}

/* Playlist */
.playlist {
    flex: 1;
    min-width: 250px;
    border-left: 1px solid rgba(255, 255, 255, 0.06);
    padding-left: 20px;
}

.playlist h3 {
    margin-bottom: 16px;
    font-weight: 500;
    color: var(--text-secondary);
}

#playlist {
    list-style: none;
    max-height: 400px;
    overflow-y: auto;
}

#playlist::-webkit-scrollbar {
    width: 4px;
}

#playlist::-webkit-scrollbar-thumb {
    background: var(--primary);
    border-radius: 4px;
}

.playlist-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 14px;
    border-radius: 8px;
    cursor: pointer;
    transition: all var(--transition);
    margin-bottom: 4px;
}

.playlist-item:hover {
    background: rgba(108, 99, 255, 0.1);
}

.playlist-item.active {
    background: rgba(108, 99, 255, 0.2);
    border-left: 3px solid var(--primary);
}

.playlist-item .playlist-index {
    font-size: 14px;
    color: var(--text-secondary);
    min-width: 24px;
}

.playlist-item .playlist-info {
    flex: 1;
}

.playlist-item .playlist-info .playlist-title {
    font-size: 15px;
    font-weight: 500;
}

.playlist-item .playlist-info .playlist-artist {
    font-size: 13px;
    color: var(--text-secondary);
}

.playlist-item .playlist-duration {
    font-size: 13px;
    color: var(--text-secondary);
}

/* Trạng thái phát */
.playing .song-image {
    animation-play-state: running;
}

/* Animation xoay */
@keyframes rotate {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
}

/* Responsive */
@media (max-width: 700px) {
    .container {
        flex-direction: column;
        padding: 20px;
    }
    .playlist {
        border-left: none;
        padding-left: 0;
        border-top: 1px solid rgba(255, 255, 255, 0.06);
        padding-top: 20px;
    }
    .song-image {
        width: 160px;
        height: 160px;
    }
}

5. JavaScript – Trái tim của trình phát nhạc

JavaScript là phần quan trọng nhất, nơi xử lý logic phát nhạc, điều khiển, và tương tác.

5.1. Dữ liệu bài hát (songs.js)

Đầu tiên, hãy tạo một file chứa dữ liệu các bài hát. Trong thực tế, bạn có thể lấy từ API hoặc tải lên từ file.

// songs.js
const songs = [
    {
        id: 1,
        title: "Blinding Lights",
        artist: "The Weeknd",
        url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3",
        cover: "https://via.placeholder.com/300/1a1a2e/6c63ff?text=BL"
    },
    {
        id: 2,
        title: "Levitating",
        artist: "Dua Lipa",
        url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3",
        cover: "https://via.placeholder.com/300/1a1a2e/6c63ff?text=LV"
    },
    {
        id: 3,
        title: "Shape of You",
        artist: "Ed Sheeran",
        url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3",
        cover: "https://via.placeholder.com/300/1a1a2e/6c63ff?text=SY"
    },
    {
        id: 4,
        title: "Dance Monkey",
        artist: "Tones and I",
        url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3",
        cover: "https://via.placeholder.com/300/1a1a2e/6c63ff?text=DM"
    },
    {
        id: 5,
        title: "Watermelon Sugar",
        artist: "Harry Styles",
        url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-5.mp3",
        cover: "https://via.placeholder.com/300/1a1a2e/6c63ff?text=WS"
    }
];

5.2. Logic chính (script.js)

// script.js
// Lấy các element DOM
const audio = document.getElementById('audio');
const coverImage = document.getElementById('coverImage');
const songTitle = document.getElementById('songTitle');
const songArtist = document.getElementById('songArtist');
const playBtn = document.getElementById('playBtn');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const shuffleBtn = document.getElementById('shuffleBtn');
const repeatBtn = document.getElementById('repeatBtn');
const progressBar = document.getElementById('progressBar');
const volumeControl = document.getElementById('volumeControl');
const currentTimeEl = document.getElementById('currentTime');
const totalTimeEl = document.getElementById('totalTime');
const playlistEl = document.getElementById('playlist');
const playerContainer = document.querySelector('.container');

// State
let currentIndex = 0;
let isPlaying = false;
let isShuffle = false;
let isRepeat = false;
let currentPlaylist = [...songs];

// Hàm định dạng thời gian (mm:ss)
function formatTime(seconds) {
    if (isNaN(seconds) || !isFinite(seconds)) return '0:00';
    const mins = Math.floor(seconds / 60);
    const secs = Math.floor(seconds % 60);
    return `${mins}:${secs.toString().padStart(2, '0')}`;
}

// Tải bài hát
function loadSong(index) {
    const song = currentPlaylist[index];
    if (!song) return;
    
    audio.src = song.url;
    songTitle.textContent = song.title;
    songArtist.textContent = song.artist;
    coverImage.src = song.cover;
    
    // Cập nhật active trong playlist
    document.querySelectorAll('.playlist-item').forEach((item, i) => {
        item.classList.toggle('active', i === index);
    });
    
    // Cập nhật trạng thái
    currentIndex = index;
    audio.load();
    
    // Nếu đang phát, phát bài mới
    if (isPlaying) {
        audio.play();
    }
}

// Phát hoặc tạm dừng
function togglePlay() {
    if (isPlaying) {
        audio.pause();
        playBtn.textContent = '▶️';
        playerContainer.classList.remove('playing');
    } else {
        audio.play();
        playBtn.textContent = '⏸️';
        playerContainer.classList.add('playing');
    }
    isPlaying = !isPlaying;
}

// Bài tiếp theo
function nextSong() {
    if (isShuffle) {
        // Chọn bài ngẫu nhiên trong playlist
        let randomIndex;
        do {
            randomIndex = Math.floor(Math.random() * currentPlaylist.length);
        } while (randomIndex === currentIndex && currentPlaylist.length > 1);
        currentIndex = randomIndex;
    } else {
        currentIndex = (currentIndex + 1) % currentPlaylist.length;
    }
    loadSong(currentIndex);
    if (isPlaying) audio.play();
}

// Bài trước
function prevSong() {
    // Nếu bài đang ở giây > 3 thì tua về đầu
    if (audio.currentTime > 3) {
        audio.currentTime = 0;
        return;
    }
    currentIndex = (currentIndex - 1 + currentPlaylist.length) % currentPlaylist.length;
    loadSong(currentIndex);
    if (isPlaying) audio.play();
}

// Render playlist
function renderPlaylist() {
    playlistEl.innerHTML = currentPlaylist.map((song, index) => `
        <li class="playlist-item ${index === currentIndex ? 'active' : ''}" data-index="${index}">
            <span class="playlist-index">${index + 1}</span>
            <div class="playlist-info">
                <div class="playlist-title">${escapeHTML(song.title)}</div>
                <div class="playlist-artist">${escapeHTML(song.artist)}</div>
            </div>
            <span class="playlist-duration">${song.duration || '3:30'}</span>
        </li>
    `).join('');
}

// Escape HTML
function escapeHTML(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

// Cập nhật progress bar và thời gian
function updateProgress() {
    if (!audio.duration || isNaN(audio.duration)) return;
    const progress = (audio.currentTime / audio.duration) * 100;
    progressBar.value = progress;
    currentTimeEl.textContent = formatTime(audio.currentTime);
    totalTimeEl.textContent = formatTime(audio.duration);
}

// Kéo thanh tiến trình
function setProgress(e) {
    const percent = parseFloat(e.target.value);
    if (!audio.duration || isNaN(audio.duration)) return;
    audio.currentTime = (percent / 100) * audio.duration;
}

// Điều chỉnh âm lượng
function setVolume(e) {
    const volume = parseFloat(e.target.value) / 100;
    audio.volume = volume;
}

// Xử lý sự kiện khi bài hát kết thúc
function handleSongEnd() {
    if (isRepeat) {
        audio.currentTime = 0;
        audio.play();
    } else {
        nextSong();
    }
}

// Lưu trạng thái vào localStorage
function saveState() {
    const state = {
        currentIndex,
        isPlaying,
        currentPlaylist: currentPlaylist.map(s => s.id),
        volume: audio.volume
    };
    localStorage.setItem('musicPlayerState', JSON.stringify(state));
}

// Tải trạng thái từ localStorage
function loadState() {
    const stored = localStorage.getItem('musicPlayerState');
    if (stored) {
        try {
            const state = JSON.parse(stored);
            // Khôi phục playlist
            if (state.currentPlaylist) {
                const playlistIds = state.currentPlaylist;
                currentPlaylist = playlistIds.map(id => songs.find(s => s.id === id)).filter(Boolean);
                if (currentPlaylist.length === 0) currentPlaylist = [...songs];
            }
            // Khôi phục chỉ số bài hát
            if (state.currentIndex && state.currentIndex < currentPlaylist.length) {
                currentIndex = state.currentIndex;
            }
            // Khôi phục âm lượng
            if (state.volume !== undefined) {
                audio.volume = state.volume;
                volumeControl.value = state.volume * 100;
            }
            loadSong(currentIndex);
            if (state.isPlaying) {
                togglePlay();
            }
        } catch (e) {
            console.warn('Cannot load saved state');
        }
    }
}

// ====== Sự kiện ======

// Play/Pause
playBtn.addEventListener('click', togglePlay);

// Next/Previous
nextBtn.addEventListener('click', nextSong);
prevBtn.addEventListener('click', prevSong);

// Shuffle
shuffleBtn.addEventListener('click', () => {
    isShuffle = !isShuffle;
    shuffleBtn.style.color = isShuffle ? '#6c63ff' : 'inherit';
});

// Repeat
repeatBtn.addEventListener('click', () => {
    isRepeat = !isRepeat;
    repeatBtn.style.color = isRepeat ? '#6c63ff' : 'inherit';
});

// Progress bar
progressBar.addEventListener('input', setProgress);

// Volume
volumeControl.addEventListener('input', setVolume);

// Audio events
audio.addEventListener('timeupdate', updateProgress);
audio.addEventListener('ended', handleSongEnd);
audio.addEventListener('play', () => {
    isPlaying = true;
    playBtn.textContent = '⏸️';
    playerContainer.classList.add('playing');
    saveState();
});
audio.addEventListener('pause', () => {
    isPlaying = false;
    playBtn.textContent = '▶️';
    playerContainer.classList.remove('playing');
    saveState();
});

// Click trên playlist để chọn bài
playlistEl.addEventListener('click', (e) => {
    const item = e.target.closest('.playlist-item');
    if (item) {
        const index = parseInt(item.dataset.index);
        if (!isNaN(index) && index !== currentIndex) {
            currentIndex = index;
            loadSong(currentIndex);
            if (isPlaying) audio.play();
            saveState();
        }
    }
});

// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
    if (e.target.tagName === 'INPUT') return;
    switch(e.key) {
        case ' ': e.preventDefault(); togglePlay(); break;
        case 'ArrowRight': nextSong(); break;
        case 'ArrowLeft': prevSong(); break;
    }
});

// Khởi tạo
renderPlaylist();
loadState();

6. Các tính năng nâng cao (Bonus)

Nếu bạn muốn nâng cấp trình phát nhạc của mình, hãy thử thêm các tính năng sau:

  • Upload nhạc: Cho phép người dùng tải lên file nhạc của riêng họ.
  • Hiệu ứng sóng âm (Visualizer): Sử dụng Web Audio API để hiển thị sóng âm thanh trực quan.
  • Playlist từ API: Kết nối với Spotify API hoặc SoundCloud API để lấy playlist thực tế.
  • Tìm kiếm: Tìm kiếm bài hát trong playlist.
  • Drag & Drop: Sắp xếp lại playlist bằng kéo thả.
  • Chế độ tối (Dark Mode): Cho phép chuyển đổi theme.

Kết luận: Từ trình phát nhạc đến kỹ năng frontend đỉnh cao

Dự án Music Player là một trong những dự án thực tế và thú vị nhất mà một lập trình viên frontend có thể thực hiện. Nó kết hợp hầu hết các kỹ năng quan trọng: JavaScript logic, Web Audio API, xử lý sự kiện, quản lý trạng thái, localStorage, và thiết kế UI/UX tinh tế. Việc hoàn thành dự án này sẽ giúp bạn tự tin hơn khi bắt tay vào các dự án lớn hơn như ứng dụng streaming nhạc hoặc video. Hãy bắt tay vào code ngay hôm nay, thêm những tính năng của riêng bạn, và tạo ra một trình phát nhạc độc đáo để đưa vào portfolio của mình. Chúc bạn thành công!

Bình luận (0)

No comments yet. Be the first to share your thoughts!