微信小程序---暮之沧蓝音乐小程序
前言
本文介绍了一个基于Express搭建的静态文件服务器和小程序音乐播放器的实现方案。
主要内容包括:
1) 使用Express、serve-static等模块搭建支持文件上传和浏览的静态服务器;
2) 开发微信小程序音乐播放器,包含推荐页、播放器和播放列表三个标签页;
3) 实现音乐播放控制功能,包括播放/暂停、进度条拖动、自动切歌等;
4) 设计UI界面,包含轮播图、音乐推荐列表、播放控制面板等组件。
系统通过3000端口提供静态文件服务,小程序通过HTTP请求获取音频文件并进行播放管理。
结果展示
前提准备
1.1 搭建一个简单的静态文件服务器,并支持文件上传功能
1.2 创建index.js文件(注意:创建的文件夹一定是英文,中文cmd不能识别)
1.3 编辑index.js文件
// 搭建一个简单的静态文件服务器,并支持文件上传功能
// 静态文件服务:通过 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; // 4MBform.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,运行以下命令
npm init -y # 自动创建package,json配置文件npm install express --save # 安装Express框架,用于快速搭建HTTP服务器npm install nodemon -g # 安装nodemon监控文件修改
1.5 运行index.js文件
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
{"navigationBarBackgroundColor": "#fff","navigationBarTitleText": "暮之沧蓝音乐","navigationBarTextStyle":"black"
}
2.2 index.wxml
<!-- 轮播图 -->
<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
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
<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
<!-- 播放器页面 -->
<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
<!-- 播放列表 -->
<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
// index.jsPage({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 / 100this.audioCtx.seek(second)},// 切换当前播放的歌曲setMusic: function(index){var music = this.data.playlist[index]this.audioCtx.src = music.srcthis.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 + 1this.setMusic(index)// 状态为暂停,不要立即播放if (this.data.state === 'running') {this.play()}},// 点击列表歌曲并播放change: function(e) {this.setMusic(e.currentTarget.dataset.index)this.play()}})