开发基本准备
注册:产品定位及功能介绍 | 微信开放文档 (qq.com)
最新接口文档:https://github.com/Binaryify/NeteaseCloudMusicApi/releases
https://binaryify.github.io/NeteaseCloudMusicApi/#/?id=neteasecloudmusicapi
项目初始化
文件结构说明
小程序配置 | 微信开放文档 (qq.com)
小程序云音乐
配置项
项目的配置文件留着,其他都删掉
一开始会报错
app.json
完整内容查看官方文档即可,小程序配置 | 微信开放文档 (qq.com)
新建app.json
报错,因为app.json不能为空,需要添加内容,
小程序根目录下的 app.json
文件用来对微信小程序进行全局配置,决定页面文件的路径、窗口表现、设置网络超时时间、设置多 tab 等。
必填的两个字段是
pages,页面路径列表(必填)
sitemapLocation:
- 指明 sitemap.json 的位置;
- 默认为 ‘sitemap.json’ 即在 app.json 同级目录下名字的
sitemap.json
文件,可以不填了
创建页面
增加一个pages字段,保存后微信开发者工具会自动创建对应的4个文件,显示的页面在这里读取
1 2 3 4 5 6
| { "pages": [ "pages/index/index", "pages/logs/index" ] }
|
设置窗口
window字段,用于设置小程序的状态栏、导航条、标题、窗口背景色。全局配置 | 微信开放文档 (qq.com)
1 2 3 4 5 6 7 8 9 10 11
| { "pages": [ "pages/index/index", "pages/logs/index" ], "window": { "navigationBarBackgroundColor": "#000", "navigationBarTextStyle": "white", "navigationBarTitleText": "小程序" } }
|
app.js
除了app.json文件,全局还应该有一个app.js文件,注册小程序 | 微信开放文档 (qq.com)
每个小程序都需要在 app.js
中调用 App
方法注册小程序实例,绑定生命周期回调函数、错误监听和页面不存在监听函数等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| App({ onLaunch (options) { }, onShow (options) { }, onHide () { }, onError (msg) { console.log(msg) }, globalData: 'I am global data' })
|
app.wxss
全局公共样式,WXSS | 微信开放文档 (qq.com)
窗口配置
新建一个空项目
在全局设置窗口颜色
1 2 3 4 5 6
| "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#d43c33", "navigationBarTitleText": "云音乐", "navigationBarTextStyle": "white" },
|
编写页面
修改pages/index/index.wxml
view
标签相当于div
,text
标签相当于span
1 2 3 4 5 6 7 8
| <view> <image src="/static/images/nvsheng.jpg"></image> <text>北方汉子</text> <view> <text>Hello World</text> </view> </view>
|
微信开发者工具,不支持文件的复制,可以在本地文件夹复制下(静态资源:)
路径直接从根路径开始写,保存查看页面
添加类名开始写样式
1 2 3 4 5 6 7 8
| <view class="indexContainer"> <image class="avatarUrl" src="/static/images/nvsheng.jpg"></image> <text class="url">北方汉子</text> <view class="goStudy"> <text>Hello World</text> </view> </view>
|
index.wxss
不能用px作为单位,否则盒子大小不会随着机型分辨率的变化而变化,如果写100px,在任何机型上就都是100px,需要做一下适配(用微信提供的rpx单位)
在iphone6上是100px,随着屏幕变大,在6 plus上的盒子大小应该大一点
那么还能是100rpx吗?不能
- rpx(responsive pixel): 可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。
- WXSS | 微信开放文档 (qq.com)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
.indexContainer { display: flex; flex-direction: column; align-items: center; background-color: aliceblue; height: 100%; } .avatarUrl { width: 200rpx; height: 200rpx; border-radius: 50%; margin: 100rpx 0; } .userName { font-size: 32rpx; margin: 100rpx 0;
} .goStudy { width: 300rpx; height: 80rpx; line-height: 80rpx; text-align: center; font-size: 28rpx; border: 1px solid #333; border-radius: 10rpx; }
|
app.wxss
1 2 3 4
| page { height: 100%; }
|
效果:
优化:图片很明显尺寸不对
原生小程序的wxss不支持background-image的import导入,https://www.wzjm.cn/phper/90.html,
直接使用image的object-fit:cover
也没有生效,小程序不支持此属性,官方提供了自己的方法,mode属性。image | 微信开放文档 (qq.com)
╭(╯^╰)╮
1 2
| <image class="avatarUrl" src="/static/images/nvsheng.jpg" mode="aspectFill"></image>
|
1 2 3 4 5 6
| .avatarUrl { width: 200rpx; height: 200rpx; border-radius: 50%; margin: 100rpx 0; }
|
效果,头像不再是压缩的了:
开发工具
使用IDE开发微信小程序
vscode
微信小程序中使用字体图标
阿里图标库选好图标后,由于是css,需要手动复制到wxss文件中,和平时项目中使用iconfont一样,使用类名
如果图标没出来,重启下微信开发者工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <view class="navContainer"> <view class="navItem"> <text class="iconfont icon-tuijian"></text> <text class="desc">每日推荐</text> </view> <view class="navItem"> <text class="iconfont icon-gedan"></text> <text class="desc">歌单</text> </view> <view class="navItem"> <text class="iconfont icon-paixingbang"></text> <text class="desc">排行榜</text> </view> <view class="navItem"> <text class="iconfont icon-tuijian"></text> <text class="desc">电台</text> </view> <view class="navItem"> <text class="iconfont icon-zhibo"></text> <text class="desc">直播</text> </view> </view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| .navContainer { display: flex; }
.navItem { display: flex; flex-direction: column; align-items: center; width: 20%; }
.navItem .iconfont { width: 100rpx; height: 100rpx; margin: 20rpx 0; background-color: rgb(240, 19, 19); border-radius: 50%; font-size: 50rpx; line-height: 100rpx; text-align: center; color: #fff; }
.navItem .desc { font-size: 26rpx; }
|
效果:
使用vant框架
vant: 轻量、可靠的移动端 Vue 组件库 (gitee.com)
背景知识
使用 Vant Weapp 前,请确保你已经学习过微信官方的 小程序简易教程 和 自定义组件介绍。
配置注意:
2.27.0的版本(或以上),没有手动指定miniprogram_npm
路径,直接在app.json
中引入也可使用
使用Less预处理器
https://blog.csdn.net/qq_42592823/article/details/123603996
vscode下载插件并配置
1 2 3
| "less.compile": { "outExt": ".wxss" }
|
注意:只在小程序项目里配置
获取用户信息
前置知识:了解完小程序语法
这一小节
点击获取用户信息
button | 微信开放文档 (qq.com)
1 2 3
| <image class="avatarUrl" src="{{userInfo.avatarUrl}}" mode="aspectFill"></image> <button type="primary" open-type="getUserInfo" bindgetuserinfo="handleGetUserInfo">点击授权</button> <text class="userName">{{userInfo.nickName}}</text>
|
1 2 3 4 5 6 7 8 9 10 11 12
| data: { msg: '初始化数据', userInfo: {}, }, handleGetUserInfo(res) { console.log(res) if(res.detail.userInfo) { this.setData({ userInfo: res.detail.userInfo }) } },
|
详细参考官网,button按钮的open-type="getUserInfo"
是开放的api,通过bindgetuserinfo绑定自定义回调,参数是点击后的详细信息
目前上面的写法,每次重新进入小程序,已经授权的用户信息会丢失,见下一小节
onLoad时获取用户信息
什么时候获取用户信息?
使用条件渲染,优化代码:没授权时,头像和昵称的结构不应该显示
1 2 3
| <image wx:if="{{userInfo.avatarUrl}}" class="avatarUrl" src="{{userInfo.avatarUrl}}" mode="aspectFill"></image> <button wx:else type="primary" open-type="getUserInfo" bindgetuserinfo="handleGetUserInfo">点击授权</button> <text wx:if="{{userInfo.avatarUrl}}" class="userName">{{userInfo.nickName}}</text>
|
轮播图
使用swiper组件,swiper | 微信开放文档 (qq.com)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <view class="container"> <swiper class="banners" indicator-dots indicator-color="ivory" indicator-active-color="#d43c33" autoplay interval="5000" circular> <swiper-item> <image mode="aspectFill" src="/static/images/nvsheng.jpg"></image> </swiper-item> <swiper-item> <image mode="aspectFill" src="/static/images/nvsheng.jpg"></image> </swiper-item> <swiper-item> <image mode="aspectFill" src="/static/images/nvsheng.jpg"></image> </swiper-item> </swiper> </view>
|
推荐歌曲
scroll-view,可滚动的试图区域,scroll-view | 微信开放文档 (qq.com)
该组件需要设置enable-flex属性为true,设置flex才会生效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <view class="recommendContainer"> <view class="header"> <text class="title">123</text> <view class="item"> <text class="desc">为你精心推荐</text> <text class="more">查看更多</text> </view> </view> <scroll-view class="recommendScroll" enable-flex scroll-x> <view class="scrollItem"> <image mode="aspectFill" src="/static/images/nvsheng.jpg"></image> <text>推荐歌曲内容区</text> </view> <view class="scrollItem"> <image mode="aspectFill" src="/static/images/nvsheng.jpg"></image> <text>推荐歌曲内容区</text> </view> <view class="scrollItem"> <image mode="aspectFill" src="/static/images/nvsheng.jpg"></image> <text>推荐歌曲内容区推荐歌曲内容区</text> </view> <view class="scrollItem"> <image mode="aspectFill" src="/static/images/nvsheng.jpg"></image> <text>推荐歌曲内容区推荐歌曲内容区</text> </view> <view class="scrollItem"> <image mode="aspectFill" src="/static/images/nvsheng.jpg"></image> <text>推荐歌曲内容区推荐歌曲内容区</text> </view> <view class="scrollItem"> <image mode="aspectFill" src="/static/images/nvsheng.jpg"></image> <text>aaaa</text> </view> </scroll-view> </view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| .recommendContainer { padding: 20rpx; } .recommendContainer .header { margin-bottom: 20rpx; } .recommendContainer .header .title { font-size: 32rpx; line-height: 40rpx; color: #666; } .recommendContainer .header .item { height: 60rpx; } .recommendContainer .header .desc { font-size: 30rpx; line-height: 60rpx; } .recommendContainer .header .more { float: right; padding: 10rpx 20rpx; border: 1px solid #333; border-radius: 30rpx; font-size: 24rpx; }
.recommendScroll { display: flex; } .scrollItem { width: 200rpx; margin-right: 20rpx; } .scrollItem image { width: 200rpx; height: 200rpx; border-radius: 10rpx; } .scrollItem text { font-size: 26rpx;
overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
|
前后端交互
网络基础说明文档:网络 | 微信开放文档 (qq.com)
网易云音乐有自己的开放接口,我们用nodejs做一下中间层,分发路由
我们把服务部署在服务器上(也可以直接跑本地)
安装pm2管理
1 2
| npm i pm2 npx pm2 start app.js --name music
|
接口文档及后台,见项目资料
正式编写代码时,用测试工具测试一下接口
微信中发送网络请求:RequestTask | 微信开放文档 (qq.com)
语法:wx.request()
注意点:
- 协议必须是https协议
- 一个接口最多配置20个域名
- 并发限制上限是10个
启动后台测试服务
在小程序后台配置接口地址,配置会失败
只能配已备案过的域名,如果是在企业开发,后台应该提供的是https的接口
不过开发时,可以在开发者工具中,设置不校验合法域名
在onLoad或者onReady里面发送
pages/index/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| Page({ data: {
}, onLoad: function(options) { console.log('index加载了') wx.request({ url: 'http://localhost:3000/banner', data: { type: 2 }, success: (res) => { console.log('请求成功', res) }, fail: (err) => { console.log('请求失败', err) } }) } })
|
请求成功:
封装请求功能函数
发送ajax请求
1.封装功能函数
- 功能点明确
- 函数内部应该保留固定代码(静态的)
- 将动态的数据抽取成形参,由使用者根据自身的情况动态的传入实参
- 一个良好的功能函数应该设置形参的默认值(ES6的形参默认值)
2.封装功能组件
新建utils/request.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import config from './config.js' export default (url, data = {}, method = 'GET') => { return new Promise((resolve, reject) => { wx.request({ url: config.host + url, data, success: (res) => { console.log('请求成功', res) resolve(res.data) }, fail: (err) => { console.log('请求失败', err) reject(err) } }) }) }
|
新建配置文件
utils/config.js
1 2 3 4
| export default { host: 'http://localhost:3000' }
|
发送请求
1 2 3 4 5 6 7 8 9 10 11 12
| import request from '../../utils/request.js' Page({ data: {
}, onLoad: async function(options) { console.log('index加载了') let result = await request('/banner', {type: 2}) console.log(result) } })
|
渲染轮播图数据
1 2 3 4 5 6
| <swiper class="banners" indicator-dots indicator-color="ivory" indicator-active-color="#d43c33" autoplay interval="5000" circular> <swiper-item wx:for="{{bannerList}}" wx:key="bannerId"> <image mode="aspectFill" src="{{item.pic}}"></image> </swiper-item> </swiper>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import request from '../../utils/request.js' Page({ data: { bannerList: [] }, onLoad: async function(options) { console.log('index加载了') let bannerListData = await request('/banner', {type: 2}) console.log(bannerListData) this.setData({ bannerList: bannerListData.banners }) } })
|
效果
推荐歌曲动态实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <view class="recommendContainer"> <view class="header"> <text class="title">推荐歌曲</text> <view class="item"> <text class="desc">为你精心推荐</text> <text class="more">查看更多</text> </view> </view> <scroll-view class="recommendScroll" enable-flex scroll-x> <view class="scrollItem" wx:for="{{recommendList}}" wx:key="id"> <image mode="aspectFill" src="{{item.picUrl}}"></image> <text>{{item.name}}</text> </view> </scroll-view> </view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import request from '../../utils/request.js' Page({ data: { bannerList: [], recommendList: [], }, onLoad: async function(options) { console.log('index加载了')
let bannerListData = await request('/banner', {type: 2}) this.setData({ bannerList: bannerListData.banners })
let recommendListData = await request('/personalized', {limit: 10}) this.setData({ recommendList: recommendListData.result }) } })
|
效果:
自定义组件
Component(Object object) | 微信开放文档 (qq.com)
新建components/NavHeader
,及对应的4个文件
定义组件
1 2 3 4 5 6 7 8
| <view class="header"> <text class="title">推荐歌曲</text> <view class="item"> <text class="desc">为你精心推荐</text> <text class="more">查看更多</text> </view> </view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| .header { margin-bottom: 20rpx; } .header .title { font-size: 32rpx; line-height: 40rpx; color: #666; } .header .item { height: 60rpx; } .header .desc { font-size: 30rpx; line-height: 60rpx; } .header .more { float: right; padding: 10rpx 20rpx; border: 1px solid #333; border-radius: 30rpx; font-size: 24rpx; }
|
index.json
中注册组件
1 2 3 4 5
| { "usingComponents": { "NavHeader": "/components/NavHeader/NavHeader" } }
|
使用组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <view class="recommendContainer"> <NavHeader></NavHeader> <scroll-view class="recommendScroll" enable-flex scroll-x> <view class="scrollItem" wx:for="{{recommendList}}" wx:key="id"> <image mode="aspectFill" src="{{item.picUrl}}"></image> <text>{{item.name}}</text> </view> </scroll-view> </view>
<view> <NavHeader></NavHeader> </view>
|
调整scrollView的高度
1 2 3 4
| .recommendScroll { display: flex; height: 300rpx; }
|
排行榜
增加样式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| .topList { padding: 20rpx; padding-top: 0; } .topListSwiper { height: 400rpx; } .swiperItem { width: 96%; background-color: #fbfbfb; } .swiperItem .title { font-size: 30rpx; line-height: 80rpx; } .musicItem { display: flex; margin-bottom: 20rpx; }
.musicItem image { width: 100rpx; height: 100rpx; border-radius: 6rpx; }
.musicItem .count { width: 100rpx; height: 100rpx; text-align: center; line-height: 100rpx; }
.musicItem .musicName { height: 100rpx; line-height: 100rpx; max-width: 400rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
组件封装时,我们需要传入动态的数据
NavHeader.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| Component({
properties: { title: { type: String, value: '我是title默认值' }, nav: { type: String, value: '我是nav默认值' } },
data: {
},
methods: {
} })
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <view class="recommendContainer"> <NavHeader title="推荐歌曲" nav="为你精心推荐"></NavHeader> <scroll-view class="recommendScroll" enable-flex scroll-x> <view class="scrollItem" wx:for="{{recommendList}}" wx:key="id"> <image mode="aspectFill" src="{{item.picUrl}}"></image> <text>{{item.name}}</text> </view> </scroll-view> </view>
<view class="topList"> <NavHeader title="排行榜" nav="热歌风向标"></NavHeader> <swiper class="topListSwiper" circular next-margin="50rpx" previous-margin="50rpx"> <swiper-item wx:for="{{topList}}" wx:key="name"> <view class="swiperItem"> <view class="title"> {{item.name}} </view> <view class="musicItem" wx:for="{{item.tracks}}" wx:key="id" wx:for-item="musicItem"> <image src="{{musicItem.al.picUrl}}"></image> <text class="count">{{index + 1}}</text> <text class="musicName">{{musicItem.name}}</text> </view> </view> </swiper-item> </swiper> </view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| data: { topList: [], },
let index = 0, resultArr = [] while(index < 5) { let topListData = await request('/top/list', {idx: index++}) let topListItem = { name: topListData.playlist.name, tracks: topListData.playlist.tracks.slice(0, 3) } resultArr.push(topListItem) this.setData({ topList: resultArr }) }
|
效果:
更新:最新接口已不支持idx形式调用,根据最新接口文档自行调整
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| import request from '../../utils/request.js' Page({ data: { bannerList: [], recommendList: [], topList: [], }, onLoad: async function(options) { console.log('index加载了')
let bannerListData = await request('/banner', {type: 2}) this.setData({ bannerList: bannerListData.banners })
let recommendListData = await request('/personalized', {limit: 10}) this.setData({ recommendList: recommendListData.result })
let index = 0, resultArr = []
let topListData = await request('/toplist') let list = topListData.list
list.forEach(async (item, index) => { if(index < 5) { let {name, id} = item let listDetailData = await request('/playlist/detail', {id}) console.log('aa',listDetailData) let topListItem = { name, tracks: listDetailData.playlist.tracks.slice(0, 3) } resultArr.push(topListItem) } index++ this.setData({ topList: resultArr }) })
} })
|
内网穿透
真机调试时需要用到
但由于接口已经部署到服务器上了,可以公网访问,此小节跳过
tabBar使用
微信开发者工具中,新建perosnal和video两个页面
tabBar配置,全局配置 | 微信开放文档 (qq.com)
微信开发者工具中,在app.json全局配置中,新增tabBar配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| { "pages": [ "pages/index/index", "pages/personal/personal", "pages/video/video" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#d43c33", "navigationBarTitleText": "云音乐", "navigationBarTextStyle": "white" }, "style": "v2", "sitemapLocation": "sitemap.json", "tabBar": { "color": "#333", "selectedColor": "#d43c33", "backgroundColor": "#ffffff", "list": [ { "pagePath": "pages/index/index", "text": "主页", "iconPath": "/static/myImages/tabs/tab-home.png", "selectedIconPath": "/static/myImages/tabs/tab-home-current.png" }, { "pagePath": "pages/video/video", "text": "视频", "iconPath": "/static/myImages/tabs/video.png", "selectedIconPath": "/static/myImages/tabs/video-selected.png" }, { "pagePath": "pages/personal/personal", "text": "主页", "iconPath": "/static/myImages/tabs/tab-my.png", "selectedIconPath": "/static/myImages/tabs/tab-my-current.png" } ] } }
|
可以去阿里图标库里找一些好看的图标
个人中心静态页面
app.json中,先把个人中心页提到第一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| <view class="personalContainer"> <view class="user-section"> <image mode="aspectFill" class="bg" src="/static/images/personal/bgImg2.jpg"></image> <view class="user-info-box" bindtap="toLogin"> <view class="portrait-box"> <image class="portrait" src='{{userInfo.avatarUrl?userInfo.avatarUrl:"/static/images/personal/missing-face.png"}}'></image> </view> <view class="info-box"> <text class="username">{{userInfo.nickname?userInfo.nickname: '游客'}}</text> </view> </view>
<view class="vip-card-box"> <image class="card-bg" src="/static/images/personal/vip-card-bg.png" mode=""></image> <view class="b-btn"> 立即开通 </view> <view class="tit"> <text class="iconfont icon-huiyuan-"></text> 硅谷会员 </view> <text class="e-m">atguigu Union</text> <text class="e-b">开通会员听歌</text> </view> </view>
<view class="cover-container" bindtouchstart="handleTouchStart" bindtouchmove="handleTouchMove" bindtouchend="handleTouchEnd" style="transform: {{coverTransform}}; transition: {{coveTransition}}" > <image class="arc" src="/static/images/personal/arc.png"></image> <view class="nav-section"> <view class="nav-item" hover-class="common-hover" hover-stay-time="50"> <text class="iconfont icon-xiaoxi"></text> <text>我的消息</text> </view> <view class="nav-item" hover-class="common-hover" hover-stay-time="50"> <text class="iconfont icon-myRecommender"></text> <text>我的好友</text> </view> <view class="nav-item" hover-class="common-hover" hover-stay-time="50"> <text class="iconfont icon-gerenzhuye"></text> <text>个人主页</text> </view> <view class="nav-item" hover-class="common-hover" hover-stay-time="50"> <text class="iconfont icon-gexingzhuangban"></text> <text>个性装扮</text> </view> </view>
<view class="personalContent"> <view class="recentPlayContainer"> <text class="title">最近播放</text> <scroll-view wx:if="{{recentPlayList.length}}" scroll-x class="recentScroll" enable-flex> <view class="recentItem" wx:for="{{recentPlayList}}" wx:key="{{id}}"> <image src="{{item.song.al.picUrl}}"></image> </view> </scroll-view> <view wx:else>暂无播放记录</view> </view>
<view class="cardList"> <view class="card-item"> <text class="title">我的音乐</text> <text class="more"> > </text> </view> <view class="card-item"> <text class="title">我的收藏</text> <text class="more"> > </text> </view> <view class="card-item"> <text class="title">我的电台</text> <text class="more"> > </text> </view> </view> </view> </view>
</view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
| .personalContainer { width: 100%; height: 100%; }
.personalContainer .user-section { height: 520rpx; position: relative; padding: 100rpx 30rpx 0; } .user-section .bg { position: absolute; left: 0; top: 0; width: 100%; height: 100%; opacity: 0.95; filter: blur(1px);
}
.user-info-box{ height: 180rpx; display:flex; align-items:center; position:relative; z-index: 1;
}
.user-info-box .portrait{ width: 130rpx; height: 130rpx; border:5rpx solid #fff; border-radius: 50%; } .user-info-box .username{ font-size: 24; color: #303133; margin-left: 20rpx; }
.vip-card-box { position: relative; display: flex; flex-direction: column; background: linear-gradient(left, red, black); background: rgba(0, 0, 0, .7); height: 240rpx; color: #f7d680; border-radius: 16rpx 16rpx 0 0; padding: 20rpx 24rpx; }
.vip-card-box .card-bg{ position:absolute; top: 20rpx; right: 0; width: 380rpx; height: 260rpx; }
.vip-card-box .b-btn{ position: absolute; right: 20rpx; top: 16rpx; width: 132rpx; height: 40rpx; text-align: center; line-height: 40rpx; font-size: 22rpx; color: #36343c; border-radius: 20px; background: #f9e6af; z-index: 1; }
.vip-card-box .b-btn{ position: absolute; right: 20rpx; top: 16rpx; width: 132rpx; height: 40rpx; text-align: center; line-height: 40rpx; font-size: 22rpx; color: #36343c; border-radius: 20px; background: #f9e6af; z-index: 1; }
.vip-card-box .tit { font-size: 22rpx; color: #f7d680; margin-bottom: 28rpx; } .vip-card-box .tit .iconfont{ color: #f6e5a3; margin-right: 16rpx; }
.vip-card-box .e-m{ font-size: 34rpx; margin-top: 10rpx; } .vip-card-box .e-b{ font-size: 24rpx; color: #d8cba9; margin-top: 10rpx; }
.cover-container{ margin-top: -150rpx; padding: 0 30rpx; position:relative; background: #f5f5f5; padding-bottom: 20rpx; }
.cover-container .arc{ position:absolute; left: 0; top: -34rpx; width: 100%; height: 36rpx; }
.cover-container .nav-section { display: flex; background: #fff; padding: 20rpx 0; border-radius: 15rpx; }
.nav-section .nav-item { width: 25%; box-sizing: border-box; display: flex; flex-direction: column; align-items: center; }
.nav-section .nav-item .iconfont { font-size: 50rpx; color: #d43c33; line-height: 70rpx; }
.nav-section .nav-item text:last-child { font-size: 22rpx;
}
.personalContent { background: #fff; margin-top: 20rpx; }
.personalContent .scrollView { display: flex; height: 160rpx; } .personalContent .recentPlay { display: flex; }
.recentPlayContainer .title { padding-left: 20rpx; font-size: 26rpx; color: #333; line-height: 80rpx; }
.personalContent .recentPlay image { width: 160rpx; height: 160rpx; margin-left: 20rpx; border-radius: 20rpx; }
.cardList { margin-top: 20rpx;
} .cardList .card-item{ border-top: 1rpx solid #eee; height: 80rpx; line-height: 80rpx; padding: 10rpx; font-size: 26rpx; color: #333; } .cardList .card-item .more { float: right; }
.recentScroll { display: flex; height: 200rpx; } .recentItem { margin-right: 20rpx; } .recentItem image { width: 200rpx; height: 200rpx; border-radius: 10rpx; }
|
个人中心动画效果实现
不足:不够丝滑
- 二次touch的时候,会立即重置动画
- 下拉会有卡顿
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
| let startY = 0, moveY = 0, moveDistance = 0; Page({
data: { coverTransform: 'translateY(0)', coverTransition: '' },
onLoad(options) {
},
handleTouchStart(event) { console.log(event) this.setData({ coverTransition: '' }) startY = event.touches[0].clientY
}, handleTouchMove(event) { moveY = event.touches[0].clientY moveDistance = moveY - startY if(moveDistance <= 0) { return } if(moveDistance >= 80) { moveDistance = 80 } this.setData({ coverTransform: `translateY(${moveDistance}rpx)` }) }, handleTouchEnd() { this.setData({ coverTransform: 'translateY(0rpx)', coverTransition: 'transform 0.5s linear' }) },
onReady() {
},
onShow() {
},
onHide() {
},
onUnload() {
},
onPullDownRefresh() {
},
onReachBottom() {
},
onShareAppMessage() {
} })
|
登录界面及表单数据收集
新建login页,配置app.json
登录流程:
- 前端验证
- 验证用户信息(账号、密码)是否合法
- 前端验证不通过就提示用户,不需要发请求给后端
- 前端验证通过了,发请求(携带账号、密码(加密))给服务器
- 后端验证
- 查询数据库,验证用户是否存在
- 用户不存在,告诉前端密码或账户名错误
- 用户存在,解密验证密码是否正确
- 密码不正确,告诉前端密码或账户名错误
- 密码正确,签发token,提示前端登录成功
- 前端获取token,存储在本地,每次请求携带token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <view class="container"> <view class="wrapper"> <view class="left-top-sign">LOGIN</view> <view class="welcome"> 欢迎回来! </view> <view class="input-content"> <view class="input-item"> <text class="tit">手机号码</text> <input type="text" placeholder="请输入手机号码" data-test="abc" data-type="phone" id="phone" bindinput="handleInput"/> </view> <view class="input-item"> <text class="tit">密码</text> <input type="password" placeholder="请输入密码" data-test="abc" data-type="password" id="password" bindinput="handleInput"/> </view> </view> <button class="confirm-btn" bindtap="login">登录</button> <view class="forget-section"> 忘记密码? </view> </view> <view class="register-section"> 还没有账号? <text >马上注册</text> </view> </view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| .wrapper{ position:relative; z-index: 90; padding-bottom: 40rpx; }
.left-top-sign{ font-size: 120rpx; color: #f8f8f8; position:relative; left: -16rpx; letter-spacing: 2rpx; }
.welcome{ position:relative; left: 50rpx; top: -90rpx; font-size: 46rpx; color: #555; }
.input-content{ padding: 0 60rpx; } .input-item{ display:flex; flex-direction: column; align-items:flex-start; justify-content: center; padding: 0 30rpx; background:#f8f6fc; height: 120rpx; border-radius: 4px; margin-bottom: 50rpx;
}
.input-item:last-child{ margin-bottom: 0; } .input-item .tit{ height: 50rpx; line-height: 56rpx; font-size: 30rpx; color: #606266; } .input-item input{ height: 60rpx; font-size: 30rpx; color: #303133; width: 100%; } .confirm-btn{ width: 630rpx!important; height: 76rpx; line-height: 76rpx; border-radius: 50rpx; margin-top: 70rpx; background: #d43c33; color: #fff; font-size: 32rpx; padding: 0; } .confirm-btn2:after{ border-radius: 100px; }
.forget-section{ font-size: 28rpx; color: #4399fc; text-align: center; margin-top: 40rpx; }
.register-section{ position:absolute; left: 0; bottom: 50rpx; width: 100%; font-size: 28rpx; color: #606266; text-align: center;
} .register-section text{ color: #4399fc; margin-left: 10rpx; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| Page({
data: { phone: '', password: '', }, handleInput(event) { let type = event.currentTarget.dataset.type console.log(type, event.detail.value) this.setData({ [type]: event.detail.value }) },
onLoad(options) {
},
onReady() {
},
onShow() {
},
onHide() {
},
onUnload() {
},
onPullDownRefresh() {
},
onReachBottom() {
},
onShareAppMessage() {
} })
|
事件 | 微信开放文档 (qq.com)
当只传递唯一标识时,可以通过id来传递
当传递多个自定义属性,可以通过data-
的方式
登录字段验证
前端验证
界面交互API,wx.showToast(Object object) | 微信开放文档 (qq.com)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| Page({
data: { phone: '', password: '', }, handleInput(event) { let type = event.currentTarget.dataset.type console.log(type, event.detail.value) this.setData({ [type]: event.detail.value }) }, login() { let {phone , password} = this.data if(!phone) { wx.showToast({ title: '手机号不能为空', icon: 'none' }) return }
let phoneReg = /^1(3|4|5|6|7|8|9)\d{9}$/ if(!phoneReg.test(phone)) { wx.showToast({ title: '手机号格式错误', icon: 'none' }) return }
if(!password) { wx.showToast({ title: '密码不能为空', icon: 'none' }) return } },
})
|
后端验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| import request from '../../utils/request' Page({
data: { phone: '', password: '', }, handleInput(event) { let type = event.currentTarget.dataset.type console.log(type, event.detail.value) this.setData({ [type]: event.detail.value }) }, async login() { let {phone , password} = this.data if(!phone) { wx.showToast({ title: '手机号不能为空', icon: 'none' }) return }
let phoneReg = /^1(3|4|5|6|7|8|9)\d{9}$/ if(!phoneReg.test(phone)) { wx.showToast({ title: '手机号格式错误', icon: 'none' }) return }
if(!password) { wx.showToast({ title: '密码不能为空', icon: 'none' }) return }
let res = await request('/login/cellphone', {phone, password})
switch (res.code){ case 200: wx.showToast( { title: '登录成功' }) break case 400: wx.showToast({ title: '手机号或密码错误', icon: 'none' }) break case 502: wx.showToast({ title: '手机号或密码错误', icon: 'none' }) break default: wx.showToast({ title: res.message || '登录失败,请重新登录', icon: 'none' }) }
},
})
|
个人中心、登录页面交互(本地存储)
把个人中心页在app.json中提到第一页
数据本地存储,wx.setStorage(Object object) | 微信开放文档 (qq.com)
personal.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| let startY = 0, moveY = 0, moveDistance = 0; Page({
data: { coverTransform: 'translateY(0)', coverTransition: '', userInfo: {} },
onLoad(options) { let localUserInfo = wx.getStorageSync('userInfo') if(localUserInfo) { this.setData({ userInfo: JSON.parse(localUserInfo) }) } },
handleTouchStart(event) { console.log(event) this.setData({ coverTransition: '' }) startY = event.touches[0].clientY
}, handleTouchMove(event) { moveY = event.touches[0].clientY moveDistance = moveY - startY if(moveDistance <= 0) { return } if(moveDistance >= 80) { moveDistance = 80 } this.setData({ coverTransform: `translateY(${moveDistance}rpx)` }) }, handleTouchEnd() { this.setData({ coverTransform: 'translateY(0rpx)', coverTransition: 'transform 0.5s linear' }) }, toLogin() { wx.navigateTo({ url: '/pages/login/login' }) },
})
|
login.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| switch (res.code){ case 200: wx.showToast( { title: '登录成功' }) wx.wx.setStorage({ key: 'userinfo', data: JSON.stringify(res.profile) }) wx.reLaunch({ url: '/pages/personal/personal' }) break
|
personal.wxml
1 2 3 4 5 6
| <view class="portrait-box"> <image class="portrait" src='{{userInfo.avatarUrl ? userInfo.avatarUrl : "/static/images/personal/missing-face.png"}}'></image> </view> <view class="info-box"> <text class="username">{{userInfo.nickname ? userInfo.nickname : '游客'}}</text> </view>
|
获取用户播放记录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| onLoad(options) { try { let localUserInfo = wx.getStorageSync('userinfo') if(localUserInfo) { this.setData({ userInfo: JSON.parse(localUserInfo) }) }
this.getUserRecentPlayList(this.data.userInfo.userId) } catch(err) { console.log(err) } }, async getUserRecentPlayList(userId) { let recentPlayListData = await request('/user/record',{uid: userId, type: 0}) console.log(recentPlayListData) let index = 0 let recentPlayList= recentPlayListData.allData.splice(0, 10).map(item => { item.id = index++ return item }) this.setData({ recentPlayList }) },
|
video导航区域
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import request from '../../utils/request' Page({
data: { videoGroupList: [], navId: 0, },
onLoad(options) { this.getVideoGroupListData() }, async getVideoGroupListData() { let videoGroupListData = await request('/video/group/list') this.setData({ videoGroupList: videoGroupListData.data.slice(0, 14), navId: videoGroupListData.data[0].id })
this.getVideoList(this.data.navId) }, changeNav(event) { let navId = Number(event.currentTarget.id) this.setData({ navId }) }, async getVideoList(navId) { if(!navId) return let videoListData = await request('/video/group', {id: navId}) console.log(videoListData) },
})
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <view class="videoContainer"> <view class="header"> <image src="/static/images/video/video.jpg" mode="aspectFit" lazy-load="false" binderror="" bindload="" /> <view class="search">搜索商品</view> <image src="/static/images/logo.png" mode="aspectFit" lazy-load="false" binderror="" bindload="" /> </view>
<scroll-view scroll-x="true" class="navScroll" enable-flex> <view class="navItem" wx:for="{{videoGroupList}}" wx:key="id"> <view class="navContent {{navId === item.id ? 'active' : ''}}" bindtap="changeNav" id="{{item.id}}">{{item.name}}</view> </view> </scroll-view> </view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| .videoContainer .header { display: flex; padding: 10rpx; } .videoContainer .header image { width: 60rpx; height: 60rpx; } .videoContainer .header .search { border: 1px solid #eee; flex: 1; margin: 0 20rpx; font-size: 26rpx; text-align: center; line-height: 60rpx; color: #d43c33; }
.navScroll { display: flex; white-space: nowrap; }
.navScroll .navItem { height: 60rpx; line-height: 60rpx; padding: 0 30rpx; font-size: 28rpx; }
.navItem .active { border-bottom: 1px solid #d43c33; }
|
保存cookie
由于封装request时,返回的是res.data,但cookie在res中,所以需要在request.js中处理cookie
不直接判断请求是否包含login,而是通过传参控制
login.js
1 2
| let res = await request('/login/cellphone', {phone, password, isLogin: true})
|
request.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| wx.request({ url: config.host + url, data, success: (res) => { console.log('请求成功', res) if(data.isLogin) { console.log('hasCookie') wx.setStorage({ key: 'cookie', data: res.cookie }) } resolve(res.data) }, fail: (err) => { console.log('请求失败', err) reject(err) } })
|
但是,人家接口改了,cookie放到了res.data里,所以还是在登录成功的回调里保存cookie
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| case 200: wx.showToast( { title: '登录成功' }) wx.setStorage({ key: 'userinfo', data: JSON.stringify(res.profile) }) wx.setStorage({ key: 'cookie', data: res.cookie }) wx.reLaunch({ url: '/pages/personal/personal' }) break
|
在request.js中设置发送请求的请求头,获取cookie时使用同步获取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| wx.request({ url: config.host + url, data, header: { cookie: wx.getStorageSync('cookie') }, success: (res) => { console.log('请求成功', res) resolve(res.data) }, fail: (err) => { console.log('请求失败', err) reject(err) } })
|
获取标签下的视频,这个接口的offset
参数必需带上(文档上说是可选…)
视频列表动态显示
使用到了Promise.all
方法,map方法迭代时,需要再一次发送异步方法,相当于map的时候,发送了多个异步请求,用Promise.all
包裹一下,不然会报错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| async getVideoList(navId) { if(!navId) return
let videoListData = await request('/video/group', {id: navId, offset: 0}) let index = 0 let videoList = await Promise.all(videoListData.datas.map(async item => { item.id = index++ let urlInfo = await request('/video/url', {id: item.data.vid}) item.dealedUrl = urlInfo.urls[0].url return item }))
this.setData({ videoList }) },
|
但这种写法,真机调试时,会报videoListData.datas
是undefined
,要把拿出来
实际上发现,其实就是/video/group
前面的这个接口,报302没拿到数据!!估计跟NodeJS中间件的做了2分钟缓存有关,重新清除下数据,又可以拿到了
这个接口不知道咋回事,有时候会返回数据,有时候就是空,所以在做map的时候,是undefined.map
还是做下异常处理吧
最后在issue上和人讨论解决了:通过查询视频分类列表获取视频id,用此id作为参数查询视频列表没有数据 · Issue #1640 · Binaryify/NeteaseCloudMusicApi (github.com)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| async getVideoList(navId) { if(!navId) return
try { let videoListData = await request('/video/group', {id: navId, offset: 0})
console.log(videoListData) let index = 0
let videoList = await Promise.all(videoListData.datas.map(async item => { item.id = index++ let urlInfo = await request('/video/url', {id: item.data.vid}) item.dealedUrl = urlInfo.urls[0].url return item }))
this.setData({ videoList, }) } catch(err) { console.log(err) wx.showToast({ title: '资源转移,请稍后再试', icon: 'none' }) } },
|
login.js
1 2 3 4 5 6 7 8 9 10 11 12
| wx.setStorage({ key: 'userinfo', data: JSON.stringify(res.profile) }) let cookie = res.cookie.split(';;').find(item => item.indexOf('MUSIC_U') !== -1) console.log(cookie) wx.setStorage({ key: 'cookie', data: cookie, })
|
调整下样式
在点击菜单栏的回调中,调用上面的函数
1 2 3 4 5 6 7 8 9
| changeNav(event) { let navId = Number(event.currentTarget.id) this.setData({ navId }) this.getVideoList(navId) },
|
加下过渡,使用wx的api
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| wx.showLoading({ title: '加载中', }); this.getVideoList(navId)
....
let videoList = await Promise.all(videoListData.datas.map(async item => { item.id = index++ let urlInfo = await request('/video/url', {id: item.data.vid}) item.dealedUrl = urlInfo.urls[0].url return item })) wx.hideLoading()
|
在点击新的导航时,旧的视频数据不应该让它显示出来的,只要清空下videoList变量里的数据即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| changeNav(event) { let navId = Number(event.currentTarget.id) this.setData({ navId, videoList: [] }) wx.showLoading({ title: '加载中', });
this.getVideoList(navId) },
|
导航过渡效果实现
点击哪个导航,哪个导航移动到第一位
使用scroll-view
的scroll-into-view
属性,值和被包裹的元素的id保持一致,且只能以字母开头
1 2 3 4 5 6
| <scroll-view scroll-x="true" class="navScroll" enable-flex scroll-into-view="{{'scroll' + navId}}"> <view class="navItem" wx:for="{{videoGroupList}}" wx:key="id" id="{{'scroll'+item.id}}"> <view class="navContent {{navId === item.id ? 'active' : ''}}" bindtap="changeNav" id="{{item.id}}">{{item.name}}</view> </view> </scroll-view>
|
但现在过渡很生硬,在scroll-view
上加上过渡scroll-with-animation
1 2
| <scroll-view scroll-x="true" class="navScroll" enable-flex scroll-into-view="{{'scroll' + navId}}" scroll-with-animation>
|
解决多个视频同时播放问题
video | 微信开放文档 (qq.com)
VideoContext | 微信开放文档 (qq.com)
同时只能有一个视频播放,
当播放新的视频时,关闭播放的视频
video组件的bindplay事件
1 2
| <video src="{{item.dealedUrl}}" bindplay="handlePlay" id="{{item.data.vid}}"></video>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| handlePlay(event) { console.log('play')
let vid = event.currentTarget.id this.vid !== vid && this.videoContext && this.videoContext.stop() this.vid = vid this.videoContext = wx.createVideoContext(vid) },
|
使用image代替video
同一页面存在多个video时,video无法正常播放一直在加载转圈 | 微信开放社区 (qq.com)
使用poster属性,同时新增image标签
零和控制图片标签和video标签
- data中定义videoId
- 图片标签也用视频标签的回调,注意视频标签是
bindplay
,图片时bindtap
- 点击图片时,也给该回调传递当前视频的id,保存到
videoId
中,现在点击哪张图,就存了哪个视频的id,就可以通过此控制开启哪个视频标签
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| handlePlay(event) {
let vid = event.currentTarget.id
this.setData({ videoId: vid })
this.vid !== vid && this.videoContext && this.videoContext.stop() this.vid = vid this.videoContext = wx.createVideoContext(vid)
this.videoContext.play() },
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <video src="{{item.dealedUrl}}" bindplay="handlePlay" id="{{item.data.vid}}" poster="{{item.data.coverUrl}}" class="common" wx:if="{{videoId === item.data.vid}}" ></video> <image class="common" src="{{item.data.coverUrl}}" mode="aspectFit" bindtap="handlePlay" id="{{item.data.vid}}" wx:else />
|
1 2 3 4 5 6 7 8 9
| .videoItem .common { width: 100%; height: 360rpx; border-radius: 10rpx; } .videoItem image.common { background-color: #000000;
}
|
注意:由于我们用image替代了video,上一小节的优化代码就可以注释掉了,因为不存在多个视频同时存在的情况了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| handlePlay(event) {
let vid = event.currentTarget.id
this.setData({ videoId: vid })
this.videoContext = wx.createVideoContext(vid)
this.videoContext.play() },
|
那么此时里面的逻辑就比较清晰了,相当于每次就只针对一个视频在写逻辑了,因为页面同时只会出现一个视频
模拟器上有一个小问题:点击第一个播放,点击第二个播放,点击第三个播放,再回到第一个点击,没有自动播放
视频里说,这个是模拟器的问题,真机调试的时候是没有的
经过测试,真机上的问题比这个还严重,每一个点击都没有自动播放(等了一会,应该是视频资源加载的问题,但有时候又不行,等死了都没自动播放)
这一块应该可以优化,后面优化篇再说,先把整体流程过一遍
解决视频大小和图片大小不一致的问题
教程里是调整video标签的object-fit属性,但我们上一小节自己调整了图片的mode属性,
视频还是保持原宽高比最好,调整图片吧
视频列表滑动功能
1 2 3 4 5 6 7
| .videoScroll { margin-top: 20rpx; height: calc(100vh - 160rpx); }
|
数字左右需要加空格,计算才会生效
实现再次播放跳转到指定位置
问题描述:点击第二个视频,再点回第一个视频,视频又重新播放了
产生原因:我们用image标签做了性能优化,每次播放的时候,相当于重新加载了
解决问题:
wx提供了跳转到视频指定位置的api:VideoContext.seek(number position) | 微信开放文档 (qq.com)
需要知道视频播放的事件,以及视频id,所以数据结构设计成数组包对象的格式
- data中定义
videoUpdateTime: []
- 一个视频对象,只需要push一次
如何知道视频播放的时间: bindtimeupdate
属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| handleTimeUpdate(event) { console.log(event) let videoTimeObj = { vid: event.currentTarget.id, currentTime: event.detail.currentTime } let {videoUpdateTime} = this.data let videoItem = videoUpdateTime.find(item => item.vid === videoTimeObj.vid) if(videoItem) { videoItem.currentTime = videoTimeObj.currentTime } else { videoUpdateTime.push(videoTimeObj) } this.setData({ videoUpdateTime }) },
|
1 2 3 4 5 6 7 8 9
| <video src="{{item.dealedUrl}}" bindplay="handlePlay" id="{{item.data.vid}}" poster="{{item.data.coverUrl}}" class="common" wx:if="{{videoId === item.data.vid}}" bindtimeupdate="handleTimeUpdate" ></video>
|
在播放的回调中,正式播放时,再加一个判断,判读是否有播放记录,如果有,跳转至指定位置
1 2 3 4 5 6 7 8 9
| let {videoUpdateTime} = this.data let videoItem = videoUpdateTime.find(item => item.vid === vid) if(videoItem) { this.videoContext.seek(videoItem.currentTime) }
this.videoContext.play()
|
视频播放结束时,清空播放记录,使用bindended
事件
1 2 3 4 5 6 7 8 9 10 11
| handleEnded(event) { console.log('播放结束') let {videoUpdateTime} = this.data let index = videoUpdateTime.findIndex(item => item.vid === event.currentTarget.id) videoUpdateTime.splice(index, 1) this.setData({ videoUpdateTime }) },
|
1 2 3 4 5 6 7 8 9 10
| <video src="{{item.dealedUrl}}" bindplay="handlePlay" id="{{item.data.vid}}" poster="{{item.data.coverUrl}}" class="common" wx:if="{{videoId === item.data.vid}}" bindtimeupdate="handleTimeUpdate" bindended="handleEnded" ></video>
|
下拉刷新
使用bindrefresherrefresh
,自定义下拉被触发这个属性,需要使用refresher-enabled
开启
刷新时调用接口获取视频数据
添加refresher-triggered
属性,用一个变量标识下拉刷新是否被触发,默认给false,在获取视频数据接口中,添加控制,设置为true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| handleRefresher() { this.getVideoList(this.data.navId) },
async getVideoList(navId) { wx.hideLoading() this.setData({ videoList, isTrigger: false, }) },
|
1 2 3 4 5 6 7 8
| <scroll-view class="videoScroll" scroll-y="true" refresher-enabled bindrefresherrefresh="handleRefresher" refresher-triggered="{{isTrigger}}" >
|
上拉加载
使用bindscrolltolowder
属性
现在接口提供了offset,支持分页了,自己实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| async handleToLower() { console.log('下拉')
let newVideoList = await this.getVideoList(this.data.navId, this.data.offset+=8) let videoList = this.data.videoList console.log(videoList) videoList.push(...newVideoList) console.log(videoList) this.setData({ videoList }) },
|
获取视频数据的接口要修正一下,
data中初始化offset为0,上拉刷新时,offset每次加8
需要对setData的逻辑做下判断处理
同时需要对id的生成规则重新做下处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| async getVideoList(navId, offset = this.data.offset) { if(!navId) return
try { let videoListData = await request('/video/group', {id: navId, offset})
let index = 0 let videoList = await Promise.all(videoListData.datas.map(async item => {
let temp = offset - 1 index++ item.id = temp += index let urlInfo = await request('/video/url', {id: item.data.vid}) item.dealedUrl = urlInfo.urls[0].url return item })) if(offset === 0) { this.setData({ videoList, isTrigger: false, }) } else { this.setData({ isTrigger: false, }) return videoList } wx.hideLoading() } catch(err) { console.log(err) wx.showToast({ title: '资源转移,请稍后再试', icon: 'none' }) }
|
scrol-view
的上拉触底可能会触发多次,有时候到了底部,往上滑离开底部边界时,也会触发
这部分待优化
页面下拉刷新、上拉触底事件说明
上面我们处理的是scroll-view
的刷新事件
作为页面,也是有对应的事件的
在页面的生命周期函数里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
onPullDownRefresh() {
},
onReachBottom() {
},
onShareAppMessage() {
}
|
页面要有滚动条,才会触发
1 2 3 4 5 6 7 8
| .videoScroll { margin-top: 20rpx; height: calc(100vh - 100rpx); }
|
高度给高点,我们不需要做其他事情,上拉触底直接会在回调中被调用
而下拉刷新,需要设置一下Page(Object object) | 微信开放文档 (qq.com)的onPullDownRefresh
的属性
在video.json中配置,Page(Object object) | 微信开放文档 (qq.com)
1 2 3 4 5
| { "usingComponents": {}, "navigationBarTitleText": "视频页", "enablePullDownRefresh": true }
|
页面的下拉刷新,会自动回收的,但没有动画
实际开发时,到底用页面的事件,还是用scroll-veiw
的事件,看实际需求
转发分享功能实现
视频的button转发,设置open-type="share"
页面转发,在对应js最外层的回调中,Page(Object object) | 微信开放文档 (qq.com)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
onShareAppMessage({from}) { if(from === 'button') { return { title: '来自button的转发', page: '/pages/video/video', imageUrl: '/static/images/nvsheng.jpg' } } else { return { title: '来自menu的转发', page: '/pages/video/video', imageUrl: '/static/images/nvsheng.jpg' } } }
|
小程序后台,给好友添加体验权限
每日推荐静态页面搭建
1 2 3 4 5 6 7 8 9 10 11 12
| <view class="recommendSongContainer"> <view class="header"> <image class="image" src="/static/images/recommendSong/recommendSong.jpg" /> <view class="date"> <text class="day">17 /</text> <text class="month">10</text> </view> </view> </view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| .recommendSongContainer .header { position: relative; width: 100%; height: 300rpx; }
.recommendSongContainer .header .image { width: 100%; height: 100%; }
.recommendSongContainer .header .date { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 300rpx; height: 100rpx; text-align: center; line-height: 100rpx; color: #fff; }
.header .date .day { font-size: 38rpx; }
|
日期动态显示
1 2 3 4 5 6 7 8 9 10 11 12
| <view class="recommendSongContainer"> <view class="header"> <image class="image" src="/static/images/recommendSong/recommendSong.jpg" /> <view class="date"> <text class="day">{{day}} / </text> <text class="month">{{month}}</text> </view> </view> </view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
data: { day: '', month: '', },
onLoad(options) { this.setData({ day: new Date().getDate(), month: new Date().getMonth() + 1 }) },
|
内容区静态页面搭建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| <scroll-view class="listScroll" scroll-y> <view class="scrollItem"> <image src="/static/images/nvsheng.jpg" class="image" mode="aspectFill"/> <view class="musicInfo"> <text class="musicName text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> <text class="author text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> </view> <text class="iconfont icon-gengduo-shuxiang"></text> </view>
<view class="scrollItem"> <image src="/static/images/nvsheng.jpg" class="image" mode="aspectFill"/> <view class="musicInfo"> <text class="musicName text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> <text class="author text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> </view> <text class="iconfont icon-gengduo-shuxiang"></text> </view><view class="scrollItem"> <image src="/static/images/nvsheng.jpg" class="image" mode="aspectFill"/> <view class="musicInfo"> <text class="musicName text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> <text class="author text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> </view> <text class="iconfont icon-gengduo-shuxiang"></text> </view><view class="scrollItem"> <image src="/static/images/nvsheng.jpg" class="image" mode="aspectFill"/> <view class="musicInfo"> <text class="musicName text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> <text class="author text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> </view> <text class="iconfont icon-gengduo-shuxiang"></text> </view><view class="scrollItem"> <image src="/static/images/nvsheng.jpg" class="image" mode="aspectFill"/> <view class="musicInfo"> <text class="musicName text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> <text class="author text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> </view> <text class="iconfont icon-gengduo-shuxiang"></text> </view><view class="scrollItem"> <image src="/static/images/nvsheng.jpg" class="image" mode="aspectFill"/> <view class="musicInfo"> <text class="musicName text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> <text class="author text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> </view> <text class="iconfont icon-gengduo-shuxiang"></text> </view><view class="scrollItem"> <image src="/static/images/nvsheng.jpg" class="image" mode="aspectFill"/> <view class="musicInfo"> <text class="musicName text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> <text class="author text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> </view> <text class="iconfont icon-gengduo-shuxiang"></text> </view><view class="scrollItem"> <image src="/static/images/nvsheng.jpg" class="image" mode="aspectFill"/> <view class="musicInfo"> <text class="musicName text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> <text class="author text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> </view> <text class="iconfont icon-gengduo-shuxiang"></text> </view><view class="scrollItem"> <image src="/static/images/nvsheng.jpg" class="image" mode="aspectFill"/> <view class="musicInfo"> <text class="musicName text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> <text class="author text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> </view> <text class="iconfont icon-gengduo-shuxiang"></text> </view><view class="scrollItem"> <image src="/static/images/nvsheng.jpg" class="image" mode="aspectFill"/> <view class="musicInfo"> <text class="musicName text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> <text class="author text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> </view> <text class="iconfont icon-gengduo-shuxiang"></text> </view><view class="scrollItem"> <image src="/static/images/nvsheng.jpg" class="image" mode="aspectFill"/> <view class="musicInfo"> <text class="musicName text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> <text class="author text">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</text> </view> <text class="iconfont icon-gengduo-shuxiang"></text> </view> </scroll-view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| .recommendSongContainer .header { position: relative; width: 100%; height: 300rpx; }
.recommendSongContainer .header .image { width: 100%; height: 100%; }
.recommendSongContainer .header .date { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 300rpx; height: 100rpx; text-align: center; line-height: 100rpx; color: #fff; }
.header .date .day { font-size: 38rpx; }
.listContainer { position: relative; top: -20rpx; padding: 0 20rpx; border-radius: 30rpx; background: #fff; }
.listHeader { height: 80rpx; line-height: 80rpx; }
.listHeader .changeMore { float: right }
.listScroll { height: calc(100vh - 300rpx - 80rpx ); } .scrollItem { display: flex; padding: 0 20rpx; margin-bottom: 10rpx; } .scrollItem .image { width: 80rpx; height: 80rpx; border-radius: 8rpx; }
.musicInfo { display: flex; flex-direction: column; margin-left: 20rpx;
} .musicInfo .text{ height: 40rpx; line-height: 40rpx; font-size: 24rpx;
max-width: 500rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .scrollItem .iconfont { position: absolute; right: 20rpx;
width: 80rpx; height: 80rpx; line-height: 80rpx; text-align: right; }
|
内容区动态显示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| import request from '../../utils/request.js' Page({
data: { day: '', month: '', recommendList: [], },
onLoad(options) {
let userInfo = wx.getStorageSync('userinfo') if(!userInfo) { wx.showToast({ title: '请先登录', icon: 'none', success: function() { wx.reLaunch({ url: '/pages/login/login' }) } }) }
this.setData({ day: new Date().getDate(), month: new Date().getMonth() + 1 })
this.getRecommendList()
}, async getRecommendList() { let recommendListData = await request('/recommend/songs') this.setData({ recommendList: recommendListData.data.dailySongs }) },
})
|
1 2 3 4 5 6 7 8 9 10 11 12
| <scroll-view class="listScroll" scroll-y> <view class="scrollItem" wx:for="{{recommendList}}" wx:key="id"> <image src="{{item.al.picUrl}}" class="image" mode="aspectFill"/> <view class="musicInfo"> <text class="musicName text">{{item.name}}</text> <text class="author text">{{item.ar[0].name}}</text> </view> <text class="iconfont icon-gengduo-shuxiang"></text> </view>
</scroll-view>
|
歌曲详情页静态页面搭建
1 2 3 4 5 6 7 8 9 10 11 12
| <view class="songDetailContainer"> <view class="author">sai</view> <view class="circle"></view> <image class="needle" src="/static/images/song/needle.png" />
<view class="discContainer"> <image class="disc" src="/static/images/song/disc.png" /> <image class="musicImg" src="/static/images/nvsheng.jpg" mode="aspectFill" /> </view> </view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| .songDetailContainer { display: flex; flex-direction: column; align-items: center; height: 100%; background-color: rgba(0,0,0,.5);
}
.circle { position: relative; z-index: 100; width: 60rpx; height: 60rpx; background-color: #fff; border-radius: 50%; margin: 10rpx 0; }
.needle { position: relative; z-index: 99; top: -40rpx; left: 60rpx; width: 192rpx; height: 274rpx; }
.discContainer { position: relative; top: -170rpx; width: 598rpx; height: 598rpx; border: 1px solid red;
}
.disc { width: 100%; height: 100%; }
.musicImg { position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; width: 370rpx; height: 370rpx; border-radius: 50%;
}
|
摇杆动画实现
1 2 3 4 5 6 7 8 9 10 11 12
| <view class="songDetailContainer"> <view class="author">sai</view> <view class="circle"></view> <image class="needle {{isPlay ? 'needleRotate': ''}}" src="/static/images/song/needle.png" />
<view class="discContainer"> <image class="disc" src="/static/images/song/disc.png" /> <image class="musicImg" src="/static/images/nvsheng.jpg" mode="aspectFill" /> </view> </view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| .songDetailContainer { display: flex; flex-direction: column; align-items: center; height: 100%; background-color: rgba(0,0,0,.5);
}
.circle { position: relative; z-index: 100; width: 60rpx; height: 60rpx; background-color: #fff; border-radius: 50%; margin: 10rpx 0; }
.needle { position: relative; z-index: 99; top: -40rpx; left: 60rpx; width: 192rpx; height: 274rpx; border: 1px solid red;
transform-origin: 40rpx 0; transform: rotate(-20deg); transition: transform 1s; }
.needleRotate { transform: rotate(0); }
.discContainer { position: relative; top: -170rpx; width: 598rpx; height: 598rpx; border: 1px solid red;
}
.disc { width: 100%; height: 100%; }
.musicImg { position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; width: 370rpx; height: 370rpx; border-radius: 50%;
}
|
磁盘动画实现
1 2 3 4
| <view class="discContainer {{isPlay ? 'discAnimation': ''}}"> <image class="disc" src="/static/images/song/disc.png" /> <image class="musicImg" src="/static/images/nvsheng.jpg" mode="aspectFill" /> </view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| .discAnimation { animation: disc 4s linear infinite; animation-delay: 0.8s; } @keyframes disc { from { transform: rotate(0); } to { transform: rotate(360deg); } }
|
底部控制区域搭建
1 2 3 4 5
| handleMusicPlay() { this.setData({ isPlay: !this.data.isPlay }) },
|
1 2 3 4 5 6 7 8
| <view class="musicControl"> <text class="text iconfont icon-24gl-shuffle"></text> <text class="text iconfont icon-24gl-previousFrame"></text> <text class="text iconfont {{isPlay ? 'icon-bofang' : 'icon-bofang1'}}} big" bindtap="handleMusicPlay"></text> <text class="text iconfont icon-24gl-nextFrame"></text> <text class="text iconfont icon-24gl-playlistMusic3"></text> </view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| .musicControl { position: absolute; bottom: 40rpx; left: 0; display: flex;
width: 100%; border-top: 1px solid #fff; }
.musicControl .text { width: 20%; height: 120rpx; line-height: 120rpx; text-align: center; color: #fff; font-size: 40rpx; } .musicControl .text.big { font-size: 70rpx; }
|
路由跳转传参
onLoad
函数中的options
参数,专门用来接收路由跳转的query
参数,值会转为字符串
原生小程序中,路由传参对参数的长度是有限制的,它会截取
所以我们不传整个歌曲的item对象了,告诉详情页歌曲id,让它自己重新发请求
recommendSong.js
1 2 3 4 5 6 7 8
| toSongDetail(event) { let song = event.currentTarget.dataset.song wx.navigateTo({ url: `/pages/songDetail/songDetail?musicId=${song.id}` }) },
|
songDetail.js
1 2 3 4
| onLoad(options) { let musicId = options.musicId console.log(options.musicId) },
|
动态显示歌曲详情
1 2 3 4 5 6 7 8
| <view class="author">{{song.ar[0].name}}</view> <view class="circle"></view> <image class="needle {{isPlay ? 'needleRotate': ''}}" src="/static/images/song/needle.png" />
<view class="discContainer {{isPlay ? 'discAnimation': ''}}"> <image class="disc" src="/static/images/song/disc.png" /> <image class="musicImg" src="{{song.al.picUrl}}" mode="aspectFill" /> </view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| onLoad(options) { let musicId = options.musicId this.getMusicInfo(musicId) },
async getMusicInfo(musicId) { let song = await request('/song/detail', {ids: musicId}) this.setData({ song: song.songs[0] }) wx.setNavigationBarTitle({ title: this.data.song.name, }); },
|
音乐播放暂停的功能函数
BackgroundAudioManager | 微信开放文档 (qq.com)
如果不设置标题,音乐实际不会给你播放的
如果要实现后台播放,全局配置 | 微信开放文档 (qq.com)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| handleMusicPlay() { this.setData({ isPlay: !this.data.isPlay }) let {isPlay, musicId} = this.data this.musicControl(isPlay, musicId) },
async musicControl(isPlay, musicId) { let backgroundAudioManager = wx.getBackgroundAudioManager() if(isPlay) { let musicLinkData = await request('/song/url', {id: musicId}) let musicLink = musicLinkData.data[0].url
backgroundAudioManager.src = musicLink backgroundAudioManager.title = this.data.song.name } else { backgroundAudioManager.pause() } },
|
解决系统任务栏控制音乐播放状态不一致的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| onLoad(options) { let musicId = options.musicId this.getMusicInfo(musicId) this.setData({ musicId }) this.backgroundAudioManager = wx.getBackgroundAudioManager() this.backgroundAudioManager.onPlay(() => { this.changePlayState(true)
}) this.backgroundAudioManager.onPause(() => { this.changePlayState(false)
}) this.backgroundAudioManager.onStop(() => { this.changePlayState(false) }) }, changePlayState(isPlay) { this.setData({ isPlay }) },
handleMusicPlay() { let isPlay = !this.data.isPlay let { musicId} = this.data this.musicControl(isPlay, musicId) },
|
解决页面回退,歌曲详情页面销毁问题
如果某个音乐在播放,应该把这个状态存起来(不受页面销毁的影响:1.存本地;2.AppObject getApp(Object object) | 微信开放文档 (qq.com))
下次回到这个页面时,拿出这个状态
App.js
1 2 3 4 5 6 7 8
| App({ globalData: { isMusicPlay: false, musicId: '', }, })
|
songDetail.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import request from '../../utils/request.js'
const appInstance = getApp()
onLoad(options) { let musicId = options.musicId this.getMusicInfo(musicId) this.setData({ musicId }) console.log(appInstance) if(appInstance.globalData.isMusicPlay && appInstance.globalData.musicId == musicId) { this.setData({ isPlay: true }) }
this.backgroundAudioManager = wx.getBackgroundAudioManager() this.backgroundAudioManager.onPlay(() => { this.changePlayState(true) appInstance.globalData.musicId = musicId
}) this.backgroundAudioManager.onPause(() => { this.changePlayState(false)
}) this.backgroundAudioManager.onStop(() => { this.changePlayState(false) }) }, changePlayState(isPlay) { this.setData({ isPlay }) appInstance.globalData.isMusicPlay = isPlay },
|
页面通信需求分析,npm包使用准备
1 2 3
| <text class="text iconfont icon-24gl-previousFrame" id="prev" bindtap="handleSwitch"></text> <text class="text iconfont {{isPlay ? 'icon-bofang' : 'icon-bofang1'}}} big" bindtap="handleMusicPlay"></text> <text class="text iconfont icon-24gl-nextFrame" id="next" bindtap="handleSwitch"></text>
|
1 2 3 4 5 6
| handleSwitch(event) { let type = event.currentTarget.id },
|
项目根目录执行npm init -y
小程序开发者工具,设置允许使用npm,2.27.0版本的基础库没有这个设置了,猜测应该默认支持了
下载pubsub-js包:npm install pubsub-js
工具-构建npm,小程序会默认到构建后的miniprogram_npm
目录下找第三方的包
页面通信完整实现
songDetail.js
1 2 3 4 5 6 7 8 9 10 11 12 13
| handleSwitch(event) { let type = event.currentTarget.id PubSub.subscribe('musicId', (msg, musicId) => { console.log(msg, musicId) PubSub.unsubscribe() }) PubSub.publish('switchType', type) },
|
recommendSong.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| PubSub.subscribe('switchType', (msg, type) => { let {recommendList, index} = this.data console.log(msg, type) if(type === 'prev') { index -= 1 } else { index += 1 } let musicId = recommendList[index].id PubSub.publish('musicId', musicId) })
|
recommendSong.wxml
1 2
| <view class="scrollItem" wx:for="{{recommendList}}" wx:key="id" bindtap="toSongDetail" data-song="{{item}}" data-index="{{index}}">
|
切换歌曲功能实现
songDetail.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| handleSwitch(event) { let type = event.currentTarget.id this.backgroundAudioManager.stop()
PubSub.subscribe('musicId', (msg, musicId) => { console.log(musicId) this.setData({ musicId }) appInstance.globalData.musicId = musicId this.getMusicInfo(musicId) this.musicControl(true, musicId) PubSub.unsubscribe('musicId')
}) PubSub.publish('switchType', type) },
|
recommendSong.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
PubSub.subscribe('switchType', (msg, type) => { let {recommendList, index} = this.data if(type === 'prev') { (index === 0) && (index = recommendList.length) index -= 1 } else { (index === recommendList.length - 1) && (index = -1) index += 1 } this.setData({ index }) let musicId = recommendList[index].id PubSub.publish('musicId', musicId) })
|
播放歌曲性能优化
暂停歌曲后,每次点击播放,都会发送请求,没必要
利用形参来控制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| handleMusicPlay() { let isPlay = !this.data.isPlay let { musicId, musicLink} = this.data this.musicControl(isPlay, musicId, musicLink) }, async musicControl(isPlay, musicId, musicLink) { if(isPlay) { if(!musicLink) { let musicLinkData = await request('/song/url', {id: musicId}) musicLink = musicLinkData.data[0].url this.setData({ musicLink }) }
this.backgroundAudioManager.src = musicLink this.backgroundAudioManager.title = this.data.song.name } else { this.backgroundAudioManager.pause() } },
|
进度条区域静态页面搭建
1 2 3 4 5 6 7 8 9 10 11 12 13
| <view class="progressControl"> <text>00:00</text> <view class="progressControl__barControl"> <view class="progressControl__barControl__currtentTimeBar"> <view class="progressControl__barControl__currtentTimeBar__circle"></view> </view> </view> <text>03:10</text> </view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| .progressControl { display: flex; position: absolute; bottom: 200rpx; width: 640rpx; height: 80rpx; line-height: 80rpx;
}
.progressControl__barControl { position: relative; width: 450rpx; height: 4rpx; margin: auto; background-color: rgba(0,0,0,.4); } .progressControl__barControl__currtentTimeBar { position: absolute; top: 0; left: 0; width: 100rpx; height: 4rpx; background-color: red; z-index: 1; } .progressControl__barControl__currtentTimeBar__circle { position: absolute; top: -4rpx; right: -12rpx; width: 12rpx; height: 12rpx; border-radius: 50%; background-color: #fff; }
|
音乐总时长格式化显示
npm i moment
构建时的提示,不影响使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| async getMusicInfo(musicId) { let song = await request('/song/detail', {ids: musicId}) let durationTime = moment(song.songs[0].dt).format('mm:ss') this.setData({ song: song.songs[0], durationTime }) wx.setNavigationBarTitle({ title: this.data.song.name, }); },
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| <view class="progressControl"> <text>{{currentTime}}</text> <view class="progressControl__barControl"> <view class="progressControl__barControl__currtentTimeBar"> <view class="progressControl__barControl__currtentTimeBar__circle"></view> </view> </view> <text>{{durationTime}}</text> </view>
|
音乐实时播放时间格式化显示
BackgroundAudioManager.onTimeUpdate(function listener) | 微信开放文档 (qq.com)
监听背景音频播放进度更新事件,只有小程序在前台时会回调
1 2 3 4 5 6 7 8 9
| this.backgroundAudioManager.onTimeUpdate(() => { let currentTime = moment(this.backgroundAudioManager.currentTime * 1000).format('mm:ss') this.setData({ currentTime })
|
进度条动态展示
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| this.backgroundAudioManager.onTimeUpdate(() => { let currentTime = moment(this.backgroundAudioManager.currentTime * 1000).format('mm:ss') let currentWidth = this.backgroundAudioManager.currentTime/this.backgroundAudioManager.duration * 450
this.setData({ currentTime, currentWidth })
})
|
1 2
| <view class="progressControl__barControl__currtentTimeBar" style="width: {{currentWidth + 'rpx'}}">
|
音乐播放结束自动播放下一首
BackgroundAudioManager.onEnded(function listener) | 微信开放文档 (qq.com)
关联页面
关联主页和歌曲详情页
1 2 3 4 5 6
| toRecommendSong() { wx.navigateTo({ url: '/pages/recommendSong/recommendSong' }) }
|
index.wxml
1 2
| <view class="navItem" bindtap="toRecommendSong">
|
作业
单曲循环,随机播放
- 随机播放
- 要给到随机播放的标识,生成可选值范围内的随机下标,并且不能随机到当前歌曲
- 单曲循环
搜索页面
search头部及热搜榜静态页面搭建
新建page,并配置标题
1 2 3 4
| { "usingComponents": {}, "navigationBarTitleText": "发现音乐" }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <view class="searchContainer"> <view class="header"> <view class="header__searchInput"> <text class="iconfont icon-search header__searchInput__icon"></text> <input type="text" placeholder="搜索歌曲" class="header__searchInput__input" placeholder-class="placeholder" /> </view> <text class="header__cancel">取消</text> </view>
<view class="hotContainer"> <view class="hotContainer__title">热搜榜</view> <view class="hotContainer__hotList"> <view class="hotContainer__hotList__hotItem"> <text class="hotContainer__hotList__hotItem__order">1</text> <text class="hotContainer__hotList__hotItem__text">偏爱</text> </view>
<view class="hotContainer__hotList__hotItem"> <text class="hotContainer__hotList__hotItem__order">1</text> <text class="hotContainer__hotList__hotItem__text">偏爱</text> </view> <view class="hotContainer__hotList__hotItem"> <text class="hotContainer__hotList__hotItem__order">1</text> <text class="hotContainer__hotList__hotItem__text">偏爱</text> </view> <view class="hotContainer__hotList__hotItem"> <text class="hotContainer__hotList__hotItem__order">1</text> <text class="hotContainer__hotList__hotItem__text">偏爱</text> </view> <view class="hotContainer__hotList__hotItem"> <text class="hotContainer__hotList__hotItem__order">1</text> <text class="hotContainer__hotList__hotItem__text">偏爱</text> </view> <view class="hotContainer__hotList__hotItem"> <text class="hotContainer__hotList__hotItem__order">1</text> <text class="hotContainer__hotList__hotItem__text">偏爱</text> </view> </view> </view> </view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| .searchContainer { padding: 0 20rpx; } .header { display: flex; height: 60rpx; padding: 10rpx 0; line-height: 60rpx; } .header__searchInput { flex: 1; background-color: rgba(237,237,237,.3); border-radius: 30rpx; } .header__searchInput__icon { position: absolute; left: 15rpx; } .header__searchInput__input { margin-left: 50rpx; height: 60rpx; } .placeholder { font-size: 28rpx; } .header__cancel { width: 100rpx; text-align: center; }
.hotContainer__title { height: 80rpx; line-height: 80rpx; border-bottom: 1px solid #eee; font-size: 28rpx; }
.hotContainer__hotList { display: flex; flex-wrap: wrap; } .hotContainer__hotList__hotItem { width: 50%; height: 80rpx; line-height: 80rpx; font-size: 26rpx; } .hotContainer__hotList__hotItem__order { margin: 0 10rpx; }
|
效果:
热搜榜、placeholder数据动态展示
1 2 3 4 5 6 7 8 9
| async getInitData() { let placeHolderData = await request('/search/default') let hostListData = await request('/search/hot/detail') this.setData({ placeholderContent: placeHolderData.data.showKeyword, hotList: hostListData.data }) },
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <view class="searchContainer"> <view class="header"> <view class="header__searchInput"> <text class="iconfont icon-search header__searchInput__icon"></text> <input type="text" placeholder="{{placeholderContent}}" class="header__searchInput__input" placeholder-class="placeholder" /> </view> <text class="header__cancel">取消</text> </view>
<view class="hotContainer"> <view class="hotContainer__title">热搜榜</view> <view class="hotContainer__hotList"> <view class="hotContainer__hotList__hotItem" wx:for="{{hotList}}" wx:key="searchWord"> <text class="hotContainer__hotList__hotItem__order">{{index + 1}}</text> <text class="hotContainer__hotList__hotItem__text">{{item.searchWord}}</text> <image class="hotContainer__hotList__hotItem__iconImage" wx:if="{{item.iconUrl}}" src="{{item.iconUrl}}"></image> </view> </view> </view> </view>
|
1 2 3 4 5 6
| .hotContainer__hotList__hotItem__iconImage { width: 30rpx; height: 25rpx; margin-left: 10rpx; }
|
关键词模糊匹配搜索数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import request from '../../utils/request.js' let isSend = false
handleInputChange(event) { console.log(event) if(event.detail.value == '') return this.setData({ searchContent: event.detail.value.trim() }) if(isSend) return isSend = true this.getSearchList() setTimeout(() => { isSend = false }, 3000)
}, async getSearchList() { let searchListData = await request('/search', {keywords: this.data.searchContent, limit: 10}) this.setData({ searchList: searchListData.result.songs }) },
|
待优化:isSend放data配置项里不行,得放全局
删除输入时,最后剩一个空串也会发一次(已优化)
搜索列表动态显示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| handleInputChange(event) { console.log(event) if(!event.detail.value) { this.setData({ searchList: [] }) return } this.setData({ searchContent: event.detail.value.trim() }) if(isSend) return isSend = true this.getSearchList() setTimeout(() => { isSend = false }, 3000)
},
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <block wx:if='{{searchList.length}}'> <view class="showSearchContent"> <view class="showSearchContent__content">搜索内容:{{searchContent}}</view> <view class="showSearchContent__list"> <view class="showSearchContent__list__item" wx:for="{{searchList}}" wx:key="id"> <text class="iconfont icon-search"></text> <text class="showSearchContent__list__item__content">{{item.name}}</text> </view> </view> </view> </block> <block wx:else> <view class="hotContainer"> <view class="hotContainer__title">热搜榜</view> <view class="hotContainer__hotList"> <view class="hotContainer__hotList__hotItem" wx:for="{{hotList}}" wx:key="searchWord"> <text class="hotContainer__hotList__hotItem__order">{{index + 1}}</text> <text class="hotContainer__hotList__hotItem__text">{{item.searchWord}}</text> <image class="hotContainer__hotList__hotItem__iconImage" wx:if="{{item.iconUrl}}" src="{{item.iconUrl}}"></image> </view> </view> </view> </block>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| .showSearchContent__content { height: 80rpx; line-height: 80rpx; font-size: 24rpx; color: #d43c33; border-bottom: 1rpx solid #d43c33; } .showSearchContent__list__item { height: 80rpx; line-height: 80rpx; display: flex; } .showSearchContent__list__item__content { flex: 1; margin-left: 20rpx; border-bottom: 1px solid #eee; font-size: 26rpx; }
|
历史记录静态页面
1 2 3 4 5 6 7 8 9 10 11 12 13
| <view class="history"> <view class="history__title">历史记录</view> <view class="history__item">你好</view> <view class="history__item">你好</view> <view class="history__item">你好</view> <view class="history__item">你好</view> <view class="history__item">你好</view> <view class="history__item">你好</view> <view class="history__item">你好</view> <view class="history__icon iconfont icon-delete_light"></view> </view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| .history { position: relative; display: flex; flex-wrap: wrap; line-height: 50rpx; font-size: 28rpx; } .history__title { height: 50rpx; } .history__item { height: 50rpx; font-size: 26rpx; background-color: #ededed; margin: 0 0 10rpx 20rpx; padding: 0 30rpx; border-radius: 20rpx; } .history__icon { position: absolute; bottom: 10rpx; right: 15rpx; font-size: 36rpx; }
|
添加搜索历史记录
添加历史记录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| onLoad(options) { this.getInitData() this.getSearchHistory() }, getSearchHistory() { let historyList = wx.getStorageSync('searchHistory') if(historyList) { this.setData({ historyList }) } }, async getSearchList() { let {searchContent, historyList} = this.data let searchListData = await request('/search', {keywords: searchContent, limit: 10}) this.setData({ searchList: searchListData.result.songs }) if(historyList.indexOf(searchContent) !== -1) { historyList.splice(historyList.indexOf(searchContent), 1) } historyList.unshift(searchContent)
this.setData({ historyList }) wx.setStorageSync('searchHistory', historyList) },
|
1 2 3
| <view class="history__title">历史记录</view> <view class="history__item" wx:for="{{historyList}}" wx:key="item">{{item}}</view>
|
清除输入
1 2 3 4 5 6 7
| clearSearchContent() { this.setData({ searchContent: '', searchList: [] }) },
|
1 2
| input type="text" placeholder="{{placeholderContent}}" class="header__searchInput__input" placeholder-class="placeholder" bindinput="handleInputChange" value="{{searchContent}}" /> <text class="header__searchInput__clear iconfont icon-qingkong" bindtap="clearSearchContent"></text>
|
删除历史记录
弹出询问框,wx.showModal(Object object) | 微信开放文档 (qq.com)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| deleteHistory() { wx.showModal({ content: '确定清空全部历史记录?', success: (res) => { if(res.confirm) { this.setData({ historyList: [] }) wx.removeStorageSync('searchHistory') } } }) },
|
1 2
| <text class="header__searchInput__clear iconfont icon-qingkong" bindtap="clearSearchContent" hidden="{{!searchContent}}"></text>
|
关联搜索页面和视频页
1 2 3 4 5
| toSearch() { wx.navigateTo({ url: '/pages/search/search' }) },
|
1 2
| <view class="search" bindtap="toSearch">搜索音乐</view>
|
小程序语法
数据绑定
WXML | 微信开放文档 (qq.com)
页面.js文件的data配置项中初始化数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| Page({
data: { msg: '初始化数据' },
onLoad(options) {
},
onReady() {
},
onShow() {
},
onHide() {
},
onUnload() {
},
onPullDownRefresh() {
},
onReachBottom() {
},
onShareAppMessage() {
} })
|
控制台中AppData菜单可以看到,小程序页面中用到的数据都来自这里
使用模板语法使用数据
1
| <text class="userName">{{msg}}</text>
|
修改msg状态:this.setData
,this代表当前页面的实例对象
1 2 3 4 5 6
| onLoad(options) { console.log(this) this.setData({ msg: '修改之后的数据' }) },
|
在非自身的回调函数中,修改也是同步的
1 2 3 4 5 6 7 8 9
| onLoad(options) { console.log(this.msg) setTimeout(() => { this.setData({ msg: '修改之后的数据' }) },2000) },
|
小结:
小程序:
data中初始化数据
修改数据:this.setData()
数据流:
Vue:
- data中初始化数据
- 修改数据:this.key = value
- 数据流:
- Vue是单向数据流:Model => View
- Vue中实现了双向数据绑定
React:
- state中初始化数据状态
- 修改数据:
- 自身钩子函数中(componentDidMount)异步的
- 非自身的钩子函数中(定时器的回调)同步的
- 数据流:
小程序中的数据代理用的是Object.defineProperty()
事件绑定
事件 | 微信开放文档 (qq.com)
1.bind绑定:事件绑定不会阻止冒泡事件向上冒泡
1 2 3
| <view bindtap="handleTap"> <text></text> </view>
|
2.catch绑定:事件绑定可以阻止冒泡事件向上冒泡
1 2 3
| <view catchtap="handleTap"> <text></text> </view>
|
自定义事件写在*.js中,与data评级
1 2 3
| handleTap() { console.log('hello world') },
|
路由跳转
wx.navigateTo(Object object) | 微信开放文档 (qq.com)
页面配置
app.json
1 2 3 4 5 6 7 8 9 10 11
| { "pages": [ "pages/index/index", "pages/logs/index" ], "window": { "navigationBarBackgroundColor": "#fff", "navigationBarTextStyle": "black", "navigationBarTitleText": "小程序" } }
|
设置跳转
1 2 3 4 5 6
| handleTap() { console.log('hello world') wx.navigateTo({ url: '/pages/logs/index', }) },
|
wx.redirectTo(Object object)
的效果
1 2 3 4 5 6
| handleTap() { console.log('hello world') wx.redirectTo({ url: '/pages/logs/index', }) },
|
左上角不再是返回上一页,而是返回主页
跳转的各个方法的区别,在于关不关闭当前页面,和页面的生命周期有关,后面会讲到
如果需要单独配置某个页面的导航,可以在自己文件下的json文件中设置
pages/logs/index.json
1 2 3 4
| { "usingComponents": {}, "navigationBarTitleText": "日志" }
|
局部配置的优先级,高于全局
生命周期
生命周期 | 微信开放文档 (qq.com)
五个生命周期,如图:
- 第一次onLoad和OnShow时,逻辑层的数据还没来呢
- onLoad、onReady等执行次数,取决于路由跳转是选择的api
- 使用
wx.redirectTo
跳转路由时,会关闭当前页面,执行一次onUnload回调
- 如果使用的是
wx.navigateTo
跳转路由,页面不会销毁,切换页面时执行onHide回调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
onLoad(options) { console.log(this.msg) setTimeout(() => { this.setData({ msg: '修改之后的数据' }) },2000) },
onReady() {
},
onShow() {
},
onHide() {
},
onUnload() {
},
|
条件渲染
条件渲染 | 微信开放文档 (qq.com)
1 2 3
| <view wx:if="{{length > 5}}"> 1 </view> <view wx:elif="{{length > 2}}"> 2 </view> <view wx:else> 3 </view>
|
列表渲染
列表渲染 | 微信开放文档 (qq.com)
wx:for
在组件上使用 wx:for
控制属性绑定一个数组,即可使用数组中各项的数据重复渲染该组件。
默认数组的当前项的下标变量名默认为 index
,数组当前项的变量名默认为 item
wx:key
如果列表中项目的位置会动态改变或者有新的项目添加到列表中,并且希望列表中的项目保持自己的特征和状态(如 input 中的输入内容,switch 的选中状态),需要使用 wx:key
来指定列表中项目的唯一的标识符。
wx:key
的值以两种形式提供
- 字符串,代表在 for 循环的 array 中 item 的某个 property,该 property 的值需要是列表中唯一的字符串或数字,且不能动态改变。
- 保留关键字
*this
代表在 for 循环中的 item 本身,这种表示需要 item 本身是一个唯一的字符串或者数字。
可以自定义index和item变量名
使用 wx:for-item
可以指定数组当前元素的变量名,
使用 wx:for-index
可以指定数组当前下标的变量名;
模板使用
模板 | 微信开放文档 (qq.com)
定义模板
1 2 3 4 5 6 7
|
<template name="myTmp"> <view> <view class="title">自定义模板</view> </view> </template>
|
使用模板
1 2 3 4 5 6 7
| <import src="/template/template01/template01.wxml" /> <view class="testContainer"> <view>测试使用模板</view> <template is="myTmp"></template> </view>
|
样式也要在对应的样式文件中引入
定义样式
1 2 3 4
| .title { color: red; }
|
使用样式
1 2 3
|
@import '/template/template01/template01.wxss';
|
可以给模板传递数据
1 2 3 4 5 6 7 8 9 10 11
| <!--template/template01/template01.wxml--> <!-- 定义模板 --> <template name="myTmp"> <view> <view class="title">自定义模板</view> </view> <view class="userInfo"> <text class="userInfo__name">姓名:{{userName}}</text> <text class="userInfo__age">年龄:{{useAge}}</text> </view> </template>
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| Page({
data: { userInfo: { userName: 'sai', useAge: 18 } }, })
|
1 2 3 4 5 6 7
| <import src="/template/template01/template01.wxml" /> <view class="testContainer"> <view>测试使用模板</view> <template is="myTmp" data="{{...userInfo}}"></template> </view>
|
小程序API
小程序重点知识汇总
小程序本地存储
小程序前后端交互
小程序页面通信
小程序自定义组件
基本使用
如何创建自定义组件
(1)在项目根目录中,鼠标右键创建 components 文件夹
(2)右击components文件夹,创建item文件夹
(3)右击item文件夹,点击新建Component,输入item
(3)回车,自动生成四个小程序文件js json wxml wxss
自定义组件的使用(局部引入)
在页面xxx.json UsingComponent中注册,是以键值对的形式,前面的键就是我们创建的组件标签名,后面是url路径
插槽
单个插槽
在小程序中,默认每个自定义组件中只允许使用一个 进行占位,这种个数上的限制叫做单个插槽。
1 2 3 4 5 6 7 8 9 10 11 12
|
<view>
<view>这里是组件的内部结构</view>
<slot></slot>
</view>
|
1 2 3 4 5 6 7 8
| <my-test4>
<view>这是通过插槽填充的内容</view>
</my-test4>
|
多个插槽
在小程序的自定义组件中,需要使用多 插槽时,可以在组件的 .js 文件中,通过如下方式进行启用。
1 2 3 4 5 6 7 8 9
| Component({
options: {
multipleSlots: true
}
})
|
可以在组件的 .wxml 中使用多个 <slot>
标签,以不同的 name 来区分不同的插槽。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <view>
<slot name="before"></slot>
<view>这里是组件的内部结构</view>
<slot name="after"></slot>
</view>
|
使用
1 2 3 4 5 6 7 8
| <my-test4>
<view slot="before">这是插入到组件 slot name="before"中的内容</view>
<view slot="after">这是插入到组件 slot name="after"中的内容</view>
</my-test4>
|
参数传递
1.在使用组件时传递自定义属性,如下:
1
| <PubTitle myTitle="学生作品"></PubTitle>
|
2.在PubTitle自定义组件的js文件中,接收该自定义属性,如:
1 2 3 4 5 6 7 8
| properties: { myTitle:{ type:String, value:"" } },
|
3.在PubTitle.wxml中渲染从前端传递来的属性,如:
1 2 3 4 5
| <view class="pubTitle"> <view class="txt">{{myTitle}}</view> <navigator class="more">更多 ></navigator> </view>
|
小程序获取用户基本信息
小程序获取用户唯一标识openId
wx.login(Object object) | 微信开放文档 (qq.com)
小程序登录 | 微信开放文档 (qq.com)
说明
调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
1 2 3 4 5 6 7 8 9 10 11 12
| handleGetOpenId() { wx.login({ success: (res) => { console.log(res) let code = res.code } }) },
|
调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key。
之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
拿到用户openId后,一方面要存到数据库,另一方面要返回给客户端(使用jsonwebtoken
包,对返回的openId加密)
安装jsonwebtoken
:npm i jsonwebtoken
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| const jwt = require('jsonwebtoken')
app.use('/getOpenId', async (req, res, next) => { let code = req.query.code let appId = 'wxaff67bc5012b8d5b', appSecret = '8881533a8cb2de57b9b1f7135769b7b4'; let url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appId}&secret=${appSecret}&js_code=${code}&grant_type=authorization_code` let result = await fly.get(url) if(result.data) { let openId = JSON.parse(result.data).openid console.log(openId) let person = { name: 'sai', age: 18, openId } let token = jwt.sign(person, 'myMiniProgram') console.log(token) res.send(token) } })
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
const _token = JSON.stringify(token) wx.setStorageSync('jwt', _token)
const _jwt = wx.getStorageSync('jwt') const jwt = JSON.parse(_jwt)
wx.setJWT(response.data) wx.switchTab({ url: '/pages/index/index' })
|
注意事项
- 会话密钥
session_key
是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥。(和 openId
一起返回的字段)
- 临时登录凭证
code
只能使用一次
小程序分包流程
分包加载 | 微信开放文档 (qq.com)
微信客户端 6.6.0,基础库 1.7.3 及以上版本开始支持。开发者工具请使用 1.01.1712150 及以上版本,可点此下载。
某些情况下,开发者需要将小程序划分成不同的子包,在构建时打包成不同的分包,用户在使用时按需进行加载。
在构建小程序分包项目时,构建会输出一个或多个分包。每个使用分包小程序必定含有一个主包。所谓的主包,即放置默认启动页面/TabBar 页面,以及一些所有分包都需用到公共资源/JS 脚本;而分包则是根据开发者的配置进行划分。
在小程序启动时,默认会下载主包并启动主包内页面,当用户进入分包内某个页面时,客户端会把对应分包下载下来,下载完成后再进行展示。
目前小程序分包大小有以下限制:
- 整个小程序所有分包大小不超过 20M
- 单个分包/主包大小不能超过 2M
对小程序进行分包,可以优化小程序首次启动的下载时间,以及在多团队共同开发时可以更好的解耦协作。
具体使用方法请参考:
使用分包(常规分包)
使用分包 | 微信开放文档 (qq.com)
案例:
新建songPackage/pages
子包文件夹,把歌曲相关的两个文件夹放入其中(这两个页面不需要一开始加载),注意原来的引入路径需要修改
app.json
中添加subpackages
字段,并修改pages
字段,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| "pages": [ "pages/index/index", "pages/search/search", "pages/login/login", "pages/video/video", "pages/personal/personal", "template/template01/template01", "test/test" ], "subpackages": [ { "root": "songPackages", "pages": [ "pages/recommendSong/recommendSong", "pages/songDetail/songDetail" ] } ],
|
同理,我们把测试页面也单独放一个包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| "pages": [ "pages/index/index", "pages/search/search", "pages/login/login", "pages/video/video", "pages/personal/personal", "template/template01/template01" ], "subpackages": [ { "root": "songPackage", "pages": [ "pages/recommendSong/recommendSong", "pages/songDetail/songDetail" ] }, { "root": "testPackage", "pages": [ "test/test" ] } ],
|
分包之后,查看微信开发者工具的基本信息
分包过后,我们写在js
中的各种逻辑跳转的路径,需要修改一下,要加一层根目录文件夹(建议后期优化时,类似封装axios
额外再封装一层请求)
独立分包
独立分包 | 微信开放文档 (qq.com)
独立分包是小程序中一种特殊类型的分包,可以独立于主包和其他分包运行。从独立分包中页面进入小程序时,不需要下载主包。当用户进入普通分包或主包内页面时,主包才会被下载。
开发者可以按需将某些具有一定功能独立性的页面配置到独立分包中。当小程序从普通的分包页面启动时,需要首先下载主包;而独立分包不依赖主包即可运行,可以很大程度上提升分包页面的启动速度。
一个小程序中可以有多个独立分包。
开发者通过在app.json
的subpackages
字段中对应的分包配置项中定义independent
字段声明对应分包为独立分包。
特点:
- 独立分包可单独访问分包的内容,不需要下载主包
- 独立分包不能依赖主包或者其它包的内容,比如
iconfont
、全局的样式等,需要在自己包单独引入
使用场景:
- 通常某些页面和当前小程序的其它页面关联不大的时候,可进行独立分包
- 如:临时加的广告页或活动页
分包预下载
开发者可以通过配置,在进入小程序某个页面时,由框架自动预下载可能需要的分包,提升进入后续分包页面时的启动速度。对于独立分包,也可以预下载主包。
使用preloadRule
字段(和pages
字段同级)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| "subpackages": [ { "root": "songPackage", "pages": [ "pages/recommendSong/recommendSong", "pages/songDetail/songDetail" ] }, { "root": "testPackage", "pages": [ "test/test" ], "name": "test" } ], "preloadRule": { "pages/index/index": { "packages": [ "songPackage", "test" ] } },
|
预加载主包
应用场景:独立分包的时候
1 2 3 4
| "preloadRule": { "pages/independent/independent": { "packages": ["__APP__"] }
|
小程序转发分享
小程序支付流程
前提:必须是企业账号
流程要注册填写各种信息,法人要录视频声明,对公账号打钱(1毛钱也行),需要将小程序发布,成功发布后才能申请支付
(目前我们用的是网易云的数据,发布不了的)
流程
图示
用户在小程序客服端下单(包含用户及商品信息)
小程序客户端发送下单支付请求给商家服务器
商家服务器同微信服务器对接获取唯一标识 openID
商家服务器根据 openId 生成商户订单(包含商户信息)
商家服务器发送请求调用统一下单 API 获取预支付订单信息
a) 接口地址: https://api.mch.weixin.qq.com/pay/unifiedorder
- 商家对预支付信息签名加密后返回给小程序客户端
a) 签名方式: MD5
b) 签名字段:小程序 ID, 时间戳, 随机串,数据包,签名方式
c) 参考地址 :https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_7&index=3
- 用户确认支付(鉴权调起支付)
a) API: wx.requestPayment()
微信服务器返回支付结果给小程序客户端
微信服务器推送支付结果给商家服务器端
https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_3&index=1
小程序上线环境准备
域名
购买域名(开发版暂不需要备案)
配置@
记录和www
记录
部署https
腾讯云上部署免费HTTPS
SSL证书免费申请提醒
尊敬的腾讯云用户,您好!
您的腾讯云账号(账号 ID:10**8047
,昵称:**
) 购买的TrustAsia TLS RSA CA证书(年限:1年,证书ID:xGolouYI)现已创建成功。可访问 证书管理控制台 进行查看。域名验证方式可参考 域名验证指引 。证书颁发后需要安装至服务器,您可根据 最佳实践 中安装范例进行配置操作。
【腾讯云】尊敬的用户,您的域名 *****.cn
的 TrustAsia TLS RSA CA(1年)证书已审核通过并成功颁发。登录控制台查看证书:https://mc.tencent.com/g9algEWM 。证书部署可参考文档:https://mc.tencent.com/ZKGAc9go 。您的腾讯云账号(账号ID:10**8047
,昵称:**
)。
下载证书:
找到服务器nginx
位置,进行配置
SSL 证书 Nginx 服务器 SSL 证书安装部署-证书安装-文档中心-腾讯云 (tencent.com)
备案
国内需要备案,配置成功后,过一会儿如果https协议头访问不了,试试http协议头,然后会提示备案
需要在域名注册72小时后才可备案
配置nginx反向代理nodejs服务
前言
Node.js自身能作为web服务器用,但是如果要在一台机器上开启多个Node.js应用该如何做呢?有一种答案就是使用nginx做反向代理。反向代理在这里的作用就是,当代理服务器接收到请求,将请求转发到目的服务器,然后获取数据后返回。
步骤样例
一、正常使用node.js开启web服务
1 2 3 4 5 6 7
| var http = require('http'); http.createServer(function (request, response) { response.writeHead(200, {'Content-Type': 'text/plain'}); response.end('hello world '); }).listen(1337); console.log('Server running at http://127.0.0.1:1337/');
|
二、为域名配置nginx
1 2 3 4
| [root@iZ25lzba47vZ vhost] default.conf node.ruanwenwu.cn.conf test.ruanwenwu.conf www.tjzsyl.com.conf laravel.ruanwenwu.cn.conf wss.ruanwenwu.cn.conf www.tjzsyl.com.conf.bak [root@iZ25lzba47vZ vhost]
|
node.ruanwenwu.cn.conf:
1 2 3 4 5 6 7
| server{ listen 80; server_name node.ruanwenwu.cn; location / { proxy_pass http://127.0.0.1:1337; } }
|
实际配置
新增一个server
1 2 3 4 5 6 7 8 9 10
| server { listen 8090; server_name 1.***.78; location / { proxy_pass http://localhost:3000; } }
|
小程序中的config.js
1 2 3 4 5 6
| export default { host: 'http://1.***.78:8090', }
|