我们如何使用 TypeScript 开发在线音乐播放器

2025-06-10

我们如何使用 TypeScript 开发在线音乐播放器

在本文中,我们分享了使用 TypeScript 为社交音乐网络平台创建自定义音乐播放器的经验,并通过编程代码说明了每个步骤。


我们的客户是一家音乐科技公司,他们提出开发其社交音乐平台的网页版。他们希望拥有一个能够提供高级功能和响应式设计的在线音乐播放器。在与客户面谈并分析了需求和附带的文档后,我们开始寻找解决方案。我们发现大多数解决方案都过于复杂;因此,我们决定开发一款定制的音乐播放器。

为了根据需求提供所有播放器功能,我们基于 TypeScript 构建了音乐播放器的核心功能。在 UI 开发方面,我们使用了 React、Next.js、Redux-Toolkit 和 Material-UI。对于 Material-UI 组件的自定义样式,我们采用了 CSS-in-JS 方法。

免责声明:本文仅提供代码示例,并非实际执行!在这里,我们想分享使用 TypeScript 创建自定义音乐播放器的经验,并附上可用于类似项目的编程代码来说明每个步骤。

首先,我们将项目拆分成多个模块。其中一个模块负责曲目播放的逻辑:播放/暂停、曲目长度显示、下一曲/上一曲、音量控制等。另外,请注意,我们使用了发布-订阅模式,并编写了一个小型实现 - pubsub.ts。

import { createPubSub } from './pubsub';

type AudioState = {
  duration: number;
  playing: boolean;
  volume: number;
};

export const createAudio = () => {
  const pubsub = createPubSub();
  const element = document.createElement('video');
  let currentTime = 0;

  let state: AudioState = {
    duration: 0,
    playing: false,
    volume: 0,
  };

  const setState = (value: Partial<AudioState>) => {
    state = { ...state, ...value };

    pubsub.publish('change', state);
  };

  const setup = () => {
    element.addEventListener('durationchange', () =>
      setState({ duration: element.duration }),
    );

    element.addEventListener('playing', () => setState({ playing: true }));

    element.addEventListener('pause', () => setState({ playing: false }));

    element.addEventListener('timeupdate', () => {
      const newCurrentTime = Math.round(element.currentTime);

      if (currentTime !== newCurrentTime) {
        currentTime = newCurrentTime;

        pubsub.publish('change-current-time', currentTime);
      }
    });

    element.addEventListener('volumechange', () =>
      setState({ volume: element.volume }),
    );

    setState({ volume: element.volume });
  };

  setup();

  return {
    seek(seconds: number) {
      element.currentTime = seconds;
      currentTime = seconds;

      pubsub.publish('change-current-time', currentTime);
    },

    getElement() {
      return element;
    },

    getState() {
      return state;
    },

    getCurrentTime() {
      return currentTime;
    },

    play() {
      element.play();
    },

    pause() {
      element.pause();
    },

    volume(value: number) {
      element.volume = value;
    },

    setUrl(url: string) {
      element.setAttribute('src', url);
      setState({ playing: false });
    },

    subscribe(listener: (newState: AudioState) => void) {
      return pubsub.subscribe('change', listener);
    },

    onChangeCurrentTime(listener: (newCurrentTime: number) => void) {
      return pubsub.subscribe('change-current-time', listener);
    },

    onEnded(listener: () => void) {
      element.addEventListener('ended', listener);

      return () => element.removeEventListener('ended', listener);
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

另一个模块包含曲目播放和播放列表管理的功能。请参阅以下代码示例:

import { createPubSub } from './pubsub';
import { createAudio } from './audio';

type Track = {
  url: string;
  title: string;
};

type State = AudioState & {
  tracks: Track[];
  currentTrack: Track | null;
  currentTrackIndex: number | null;
};

const createPlayer = () => {
  const pubsub = createPubSub();
  const audio = createAudio();

  let state: State = {
    ...audio.getState(),
    tracks: [],
    currentTrackIndex: null,
    currentTrack: null,
  };

  const setState = (value: Partial<State>) => {
    state = { ...state, ...value };

    pubsub.publish('change', state);
  };

  audio.subscribe(setState);

  const changeTrack = () => {
    const track = state.currentTrack;

    if (track) {
      audio.setUrl(track.url);
      audio.play();
    }
  };

  const next = () => {
    if (state.currentTrackIndex === null) {
      return;
    }

    const lastIndex = state.tracks.length - 1;
    const newIndex = state.currentTrackIndex + 1;

    if (newIndex <= lastIndex) {
      setState({
        currentTrackIndex: newIndex,
        currentTrack: state.tracks[newIndex],
      });

      changeTrack();
    }
  };

  audio.onEnded(next);

  return {
    play: audio.play,
    pause: audio.pause,
    seek: audio.seek,
    volume: audio.volume,
    getCurrentTime: audio.getCurrentTime,
    getElement: audio.getElement,
    onChangeCurrentTime: audio.onChangeCurrentTime,

    getState() {
      return state;
    },

    setQueue(tracks: Track[]) {
      setState({ tracks });
    },

    playTrack(trackIndex: number) {
      setState({
        currentTrackIndex: trackIndex,
        currentTrack: state.tracks[trackIndex],
      });

      changeTrack();
    },

    next,

    prev() {
      if (state.currentTrackIndex === null) {
        return;
      }

      const newIndex = state.currentTrackIndex - 1;

      if (newIndex >= 0) {
        setState({
          currentTrack: state.tracks[newIndex],
          currentTrackIndex: newIndex,
        });

        changeTrack();
      }
    },

    subscribe(listener: (newState: State) => void) {
      return pubsub.subscribe('change', listener);
    },
  };
};

const player = createPlayer();

export default player;
Enter fullscreen mode Exit fullscreen mode

这里有几行代码说明了使用上面显示的播放器功能的 React 组件:

import React, { useState, useEffect, FC } from 'react';

import player from './player';

const usePlayerState = () => {
  const [state, setState] = useState(player.getState());

  useEffect(() => {
    const unsubscribe = player.subscribe(setState);

    return unsubscribe;
  }, []);

  return state;
};

const Player: FC = () => {
  const { currentTrack, playing } = usePlayerState();

  useEffect(() => {
    player.setQueue([
      {
        title: 'Tech House Vibes',
        url: '/music/mixkit-a-very-happy-christmas-897.mp3',
      },
      {
        title: 'Sample Video',
        url: '/videos/sample-video.mp4',
      },
    ]);
  }, []);

  const handlePlay = () => {
    if (playing) {
      player.pause();
    } else {
      player.play();
    }
  };

  return (
    <div>
      <button onClick={player.prev} disabled={!currentTrack}>
        Prev
      </button>

      {currentTrack ? (
        <button onClick={handlePlay}>{playing ? 'Pause' : 'Play'}</button>
      ) : (
        <button onClick={() => player.playTrack(0)}>Play All</button>
      )}

      <button onClick={player.next} disabled={!currentTrack}>
        Next
      </button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

下面是一个示例,说明如何编写一个专门用于视频文件的播放器的背景模式。在该模式下,我们可以从一个屏幕(或页面)移动到另一个屏幕(或页面),而视频会继续播放,不会受到任何干扰。

.visuallyhidden {
  position: absolute;
  overflow: hidden;
  clip: rect(0 0 0 0);
  height: 1;
  width: 1;
  margin: -1;
  padding: 0;
  border: 0;
}
Enter fullscreen mode Exit fullscreen mode
import React, { useRef, useEffect, FC } from 'react';

import player from './player';

const Video: FC = () => {
  const videoWrapperRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const videoWrapperElement = videoWrapperRef.current!;
    const video = player.getElement();

    video.remove();
    video.classList.remove('visuallyhidden');
    videoWrapperElement.append(video);

    return () => {
      videoWrapperElement.removeChild(video);
      video.classList.add('visuallyhidden');
      document.body.append(video);
    };
  }, []);

  return <div ref={videoWrapperRef} />;
};
Enter fullscreen mode Exit fullscreen mode

在这个例子中,您可以看到:当用户将播放器从后台模式切换到前台模式时,视频会扩展回来,而音轨会继续“无缝”播放而不会暂停。

CodeSandbox上,您可以更详细地探索代码示例。

完成上述步骤后,我们创建了一个音乐播放器,它进一步成为客户社交音乐网络平台的重要组成部分。


有问题吗?我们欢迎任何反馈,并乐意在评论区展开讨论。

鏂囩珷鏉ユ簮锛�https://dev.to/upsilon_it/how-we-development-an-online-music-player-with-typescript-2o9n
PREV
我迷茫了。能给一个有两年经验的普通程序员一些建议吗?
NEXT
每个开发人员必须知道的十大编码原则