前言
本文介绍了一个基于Express搭建的静态文件服务器和小程序音乐播放器的实现方案。
主要内容包括:
-
使用Express、serve-static等模块搭建支持文件上传和浏览的静态服务器;
-
开发微信小程序音乐播放器,包含推荐页、播放器和播放列表三个标签页;
-
实现音乐播放控制功能,包括播放/暂停、进度条拖动、自动切歌等;
-
设计UI界面,包含轮播图、音乐推荐列表、播放控制面板等组件。
系统通过3000端口提供静态文件服务,小程序通过HTTP请求获取音频文件并进行播放管理。
结果展示



前提准备
1.1 搭建一个简单的静态文件服务器,并支持文件上传功能
1.2 创建index.js文件(注意:创建的文件夹一定是英文,中文cmd不能识别)

1.3 编辑index.js文件
javascript
// 搭建一个简单的静态文件服务器,并支持文件上传功能
// 静态文件服务:通过 serve-static 和 serve-index 提供 ./htdocs 目录下的文件浏览和下载。
// 文件上传:通过 multiparty 解析上传的文件,并保存到 ./htdocs/upfile 目录。
// 服务器监听:在 3000 端口启动 HTTP 服务器。
var express = require('express');
var serveIndex = require('serve-index');
var serveStatic = require('serve-static');
var multiparty = require('multiparty');
var path = require('path'); // 用于路径处理
var fs = require('fs'); // 用于检查目录
var LOCAL_BIND_PORT = 3000;
var app = express();
// 确保上传目录存在
const uploadDir = path.join(__dirname, 'htdocs', 'upfile');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
// 文件上传接口
app.post('/upload', function (req, res) {
var form = new multiparty.Form();
form.encoding = 'utf-8';
form.uploadDir = uploadDir;
form.maxFilesSize = 4 * 1024 * 1024; // 4MB
form.parse(req, function (err, fields, files) {
if (err) {
console.log('parse error: ' + err);
} else {
console.log('parse files: ' + JSON.stringify(files));
}
res.writeHead(200, { 'content-type': 'text/plain;charset=utf-8' });
res.write('received upload');
res.end();
});
});
// 静态文件服务
var serve = serveStatic('./htdocs');
app.use('/', serveIndex('./htdocs', { 'icons': true }));
app.use('/', serve); // ✅ 使用中间件自动处理所有静态请求
// 启动服务器
console.log(`✅ 静态文件服务器已启动:http://localhost:${LOCAL_BIND_PORT}`);
console.log(`📁 访问目录:./htdocs`);
console.log(`📥 上传目录:./htdocs/upfile`);
console.log(`🔧 按 Ctrl + C 停止服务器`);
app.listen(LOCAL_BIND_PORT);
1.4 在此目录(music_bg)路径输入cmd,运行以下命令
bash
npm init -y # 自动创建package,json配置文件
npm install express --save # 安装Express框架,用于快速搭建HTTP服务器
npm install nodemon -g # 安装nodemon监控文件修改
1.5 运行index.js文件
bash
node index.js

1.6 打开浏览器,运行http://127.0.0.1:3000

可以找些音频文件放进upfile文件夹



后期需要将如"http://127.0.0.1:3000/upfile/%5B%E5%85%8D%E8%B4%B9%E4%BC%B4%E5%A5%8F%5DAmbient.mp3
填入代码里
代码展示
整体结构

2.1 index.json
javascript
{
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "暮之沧蓝音乐",
"navigationBarTextStyle":"black"
}
2.2 index.wxml
html
<!-- 轮播图 -->
<view class="tab">
<!-- bindtap="changeItem",单击tab区域切换到对应的标签页 -->
<view class="tab-item {{ tab==0?'active:':''}}" bindtap="changeItem" data-item="0">音乐推荐</view>
<view class="tab-item {{ tab==1?'active:':''}}" bindtap="changeItem" data-item="1">播放器</view>
<view class="tab-item {{ tab==2?'active:':''}}" bindtap="changeItem" data-item="2">播放列表</view>
</view>
<!-- 主体内容 -->
<view class="content">
<swiper current="{{ item }}" bindchange="changeTab">
<swiper-item>
<include src="info.wxml" />
</swiper-item>
<swiper-item>
<include src="play.wxml" />
</swiper-item>
<swiper-item>
<include src="playlist.wxml" />
</swiper-item>
</swiper>
</view>
<!-- 底部播放器 -->
<view class="player">
<image class="player-cover" src="{{ play.coverImgUrl}}" />
<view class="player-info">
<view class="player-info-title">{{ play.title }}</view>
<view class="player-info-singer">{{ play.singer }}</view>
</view>
<view class="player-controls">
<!-- 切换到播放列表 -->
<image src="/images/B.png" bindtap="changePage" data-page="2"/>
<!-- 播放 -->
<image wx:if="{{ state=='paused'}}" src="/images/ZT.png" bindtap="play"/>
<image wx:else src="/images/BF.png" bindtap="pause"/>
<!-- 下一曲 -->
<image src="/images/XYQ.png" bindtap="next"/>
</view>
</view>
2.3 index.wxss
css
page {
display: flex;
flex-direction: column;
background: rgb(207, 236, 247);
color: rgb(70, 68, 68);
height: 100%;
}
.tab {
display: flex;
}
.tab-item {
flex: 1;
font-size: 10pt;
text-align: center;
line-height: 72rpx;
border-bottom: 6rpx solid white;
}
.content {
flex: 1;
}
.content > swiper {
height: 100%;
}
.player {
background:rgb(213, 238, 247);
border-top: 1rpx solid black;
height: 112rpx;
}
.tab-item.active {
color: red;
border-block-color: red;
}
.content-info {
height: 100%;
}
/* 隐藏滚动条 */
::-webkit-scrollbar {
width: 0;
height: 0;
color: transparent;
}
.content-info-slide {
height: 310rpx;
margin-bottom: 20rpx;
}
.content-info-slide image{
width: 100%;
height: 100%;
}
.content-info-portal {
display: flex;
margin-bottom: 15rpx;
}
.content-info-portal > view {
flex: 1;
font-size: 10pt;
text-align: center;
}
.content-info-portal image {
width: 90rpx;
height: 90rpx;
display: block;
margin: 20rpx auto;
}
.content-info-list {
font-size: 10pt;
margin-bottom: 20rpx;
}
.content-info-list > .list-title {
font-size: 15pt;
margin: 50rpx 35rpx;
color: brown;
}
.content-info-list > .list-inner {
display: flex;
flex-wrap: wrap;
margin: 0 20rpx;
}
.content-info-list > .list-inner > .list-item {
flex: 1;
}
.content-info-list > .list-inner > .list-item > image {
display: block;
width: 200rpx;
height: 200rpx;
margin: 0 auto;
border-radius: 10rpx;
border: 1rpx solid #555;
}
.content-info-list > .list-inner > .list-item > view {
width: 200rpx;
margin: 10rpx auto;
font-size: 10pt;
}
/* 播放器样式 */
.player {
display: flex;
align-items: center;
background: rgb(207, 236, 247);
border-top: 1rpx solid black;
height: 115rpx;
}
.player-cover {
width: 80rpx;
height: 80rpx;
margin-left: 15rpx;
border-radius: 8rpx;
border: 1rpx solid black;
}
.player-info {
flex: 1;
font-size: 10pt;
line-height: 50rpx;
margin-left: 20rpx;
padding-bottom: 10rpx;
}
.player-info-singer {
color: gray;
}
.player-controls image {
width: 50rpx;
height: 50rpx;
margin-right: 30rpx;
}
/* 播放器 */
.content-play {
display: flex;
justify-content: space-around;
flex-direction: column;
height: 100%;
text-align: center;
}
.content-play-info > view {
color: gray;
font-size: 12pt;
}
.content-play-cover image {
animation: rotateImage 10s linear infinite;
width: 400rpx;
height: 400rpx;
border-radius: 50%;
border: 1rpx solid gray;
}
@keyframes rotateImage {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.content-play-progress {
display: flex;
align-items: center;
margin: 0 35rpx;
font-size: 9pt;
text-align: center;
}
.content-play-progress > view {
flex: 1;
}
/* 播放列表 */
.playlist-item {
display: flex;
align-items: center;
border-bottom: 1rpx solid black;
height: 115rpx;
}
.playlist-cover {
width: 80rpx;
height: 80rpx;
margin-left: 15rpx;
border-radius: 8rpx;
border: 1rpx solid black;
}
.playlist-info {
flex: 1;
font-size: 10pt;
line-height: 40rpx;
margin-left: 20rpx;
padding-bottom: 5rpx;
}
.playlist-info-singer {
color: gray;
}
.playlist-controls {
font-size: 10pt;
margin-right: 20rpx;
color: red;
}
2.4 info.wxml
html
<scroll-view class="content-info" scroll-y>
<!-- 轮播图 -->
<view style="background: white; height: 1300rpx;">
<swiper class="content-info-slide" indicator-color="rgba(255,255,255,.5)" indicator-active-color="#fff" indicator-dots circular autoplay>
<swiper-item>
<image src="/images/banner.png"/>
</swiper-item>
<swiper-item>
<image src="/images/banner2.png"/>
</swiper-item>
<swiper-item>
<image src="/images/banner3.png"/>
</swiper-item>
</swiper>
<!-- 功能按钮 -->
<view class="content-info-portal">
<view>
<image src="/images/F1.png"/>
<text>私人漫游</text>
</view>
<view>
<image src="/images/F2.png"/>
<text>每日推荐</text>
</view>
<view>
<image src="/images/F3.png"/>
<text>新歌榜单</text>
</view>
</view>
<!-- 为你推荐 -->
<view class="content-info-list">
<view class="list-title">为你推荐</view>
<view class="list-inner">
<view class="list-item">
<image src="/images/T1.png"/>
<view>City Of Stars</view>
</view>
<view class="list-item">
<image src="/images/T2.png"/>
<view>那时雨</view>
</view>
<view class="list-item">
<image src="/images/T3.png"/>
<view>Show Me Love</view>
</view>
<view class="list-item">
<image src="/images/T4.png"/>
<view>AM</view>
</view>
<view class="list-item">
<image src="/images/T5.png"/>
<view>习惯失恋</view>
</view>
<view class="list-item">
<image src="/images/T6.png"/>
<view>幻想是痛的延续</view>
</view>
</view>
</view>
</view>
<!-- <view>已到达最底部</view> -->
</scroll-view>
2.5 play.wxml
html
<!-- 播放器页面 -->
<view class="content-play">
<!-- 显示音乐信息 -->
<view class="content-play-info">
<text>{{ play.title }}</text>
<view>--{{ play.singer }}--</view>
</view>
<!-- 显示专辑封面 -->
<view class="content-play-cover">
<image src="{{ play.coverImgUrl }}" style="animation-play-state: {{ state }} " />
</view>
<!-- 显示播放进度和时间 -->
<view class="content-play-progress">
<text>{{ play.currentTime }}</text>
<view>
<slider bindchange="sliderChange" activeColor="#d33a31" block-size="12" backgroundColor="#dadada" value="{{ play.percent }}" />
</view>
<text>{{ play.duration }}</text>
</view>
</view>
2.6 playlist.wxml
html
<!-- 播放列表 -->
<scroll-view class="content-playlist" scroll-y>
<view class="playlist-item" wx:for="{{ playlist }}" wx:key="id" bindtap="change" data-index="{{ index }}">
<image class="playlist-cover" src="{{ item.converImgUrl }}"/>
<view class="playlist-info">
<view class="playlist-info-title">{{ item.title }}</view>
<view class="playlist-info-singer">{{ item.singer }}</view>
</view>
<view class="playlist-controls">
<text wx:if="{{ index==playIndex }}">正在播放</text>
</view>
</view>
</scroll-view>
2.7 index.js
javascript
// index.js
Page({
data: {
// 切换标签页的值
item: 0,
// 标签页索引
tab: 0,
// 播放列表数据
playlist: [
{
id: 1,
title: "City Of Stars",
singer: "王OK",
src: "{此处填入前面服务器生成的网址}",
converImgUrl: "/images/T1.png"
},
{
id: 2,
title: "那时雨",
singer: "徐良",
src: "{此处填入前面服务器生成的网址}",
converImgUrl: "/images/T2.png"
},
{
id: 3,
title: "Show Me Love",
singer: "WizTheMC",
src: "{此处填入前面服务器生成的网址}",
converImgUrl: "/images/T3.png"
},
{
id: 4,
title: "AM",
singer: "T-Chenxi",
src: "{此处填入前面服务器生成的网址}",
converImgUrl: "/images/T4.png"
},
{
id: 5,
title: "习惯失恋",
singer: "容祖儿",
src: "{此处填入前面服务器生成的网址}",
converImgUrl: "/images/T5.png"
},
{
id: 6,
title: "幻想是痛的延续",
singer: "匿名",
src: "{此处填入前面服务器生成的网址}",
converImgUrl: "/images/T6.png"
},
],
// 播放状态,running表示正在播放
state: "paused",
// 当前播放的曲目在播放列表数组中的索引值
playIndex: 0,
play: {
// 播放时长
currentTime: "00:00",
duration: "00:00",
// 播放进度
percent: 0,
title: "",
singer: "",
converImgUrl: "/images/T1.png"
},
},
// 切换到对应的标签页
changeItem: function(e) {
this.setData({
item: e.target.dataset.item
})
},
// 更改当前标签页的索引
changeTab: function(e){
this.setData({
tab:e.detail.current
})
},
// 实现音乐播放功能
audioCtx: null,
onReady: function(){
// 控制播放器页面的进度条的进度与时间显示
this.audioCtx = wx.createInnerAudioContext()
var that = this
// 播放失败检测
this.audioCtx.onError(function(){
console.log("播放失败:" + that.audioCtx.src)
})
// 播放完成自动切换下一曲
this.audioCtx.onEnded(function(){
that.next()
})
// 自动更新播放进度
this.audioCtx.onPlay(function(){})
// 获取音乐状态信息
this.audioCtx.onTimeUpdate(function(){
that.setData({
"play.duration": formatTime(that.audioCtx.duration),
"play.currentTime": formatTime(that.audioCtx.currentTime),
"play.percent": that.audioCtx.currentTime / that.audioCtx.duration * 100
})
})
// 默认选择第一首歌
this.setMusic(0)
// 格式化时间
function formatTime(time) {
var minute = Math.floor(time / 60) % 60;
var second = Math.floor(time) % 60;
return (minute < 10 ? '0' + minute: minute) + ':' + (second < 10 ? '0' + second: second)
}
},
// 获取当前滚动条的进度
sliderChange: function(e) {
var second = e.detail.value * this.audioCtx.duration / 100
this.audioCtx.seek(second)
},
// 切换当前播放的歌曲
setMusic: function(index){
var music = this.data.playlist[index]
this.audioCtx.src = music.src
this.setData({
playIndex: index,
"play.title": music.title,
"play.singer": music.singer,
"play.coverImgUrl": music.converImgUrl,
"play.currentTime": "00:00",
"play.duration": "00:00",
"play.percent": 0
})
},
// 处理播放与暂停
play: function() {
this.audioCtx.play()
this.setData({
state: "running"
})
},
pause: function() {
this.audioCtx.pause()
this.setData({
state: "paused"
})
},
// 下一曲按钮
next: function() {
var index = this.data.playIndex >= this.data.playlist.length - 1 ? 0 : this.data.playIndex + 1
this.setMusic(index)
// 状态为暂停,不要立即播放
if (this.data.state === 'running') {
this.play()
}
},
// 点击列表歌曲并播放
change: function(e) {
this.setMusic(e.currentTarget.dataset.index)
this.play()
}
})