微信小程序云开发之网易云音乐
这是跟着慕课网谢成老师敲得代码注意 知识点 疑惑 思路 妙 优化其他:第一章课程介绍云开发(音乐播放器项目)第二章云开发介绍以及构建项目2.1传统开发模式什么是serverless(微服务):他可以打破物理隔离(前端与后端,手机端与服务器)函数即服务比较早的是html css后来向dom延伸(jquary)从dom又发展到mvc这种模式 再到现在的mvvm这种模式 同时推出很多框架 (vue re
这是跟着慕课网谢成老师敲得代码
注意 知识点 疑惑 思路 妙 优化
一个要注意的地方:如果获取歌词的时候报错(才发现解决办法的图片没传上来 大概就是改变一下请求头数据)
第一章课程介绍
云开发(音乐播放器项目)
第二章云开发介绍以及构建项目
2.1传统开发模式
什么是serverless(微服务):他可以打破物理隔离(前端与后端,手机端与服务器)
函数即服务
比较早的是html css后来向dom延伸(jquary)从dom又发展到mvc这种模式 再到现在的mvvm这种模式 同时推出很多框架 (vue react……)
2.2云开发与serverless
2.3 云开发优势
快速上线
专注核心业务 不需要考虑联调 运维等问题(为什么不用考虑运维呢 因为云开发会弹性伸缩性能)
独立开发出一个小程序
数据安全
其实云函数就相当于nodejs
2.4开通
默认情况时有两个环境的 一个是正式环境 就是上线以后的环境 另一个是测试环境也就是开发时候的环境
建议测试环境取名test,正式环境取名release,系统会自动分配环境后缀。
然后过一会文件就变成这样了
关于费用 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/billing/adjust-payment.html
2.5初始化
云函数部分:
前端部分:
注意填入的是环境id
那么问题来了,环境id在哪儿找呢
我们就可以把它填入这里
到时候正式上线就可以改成正式上线的id
下面的traceUser为true就表示是否把访问的用户记录在控制台中,而且是以倒序的方式显示的
还有这个 this.globalData = {}
可以设置全局的属性 或者方法
项目配置文件:
建三个页面:playList,blog,profile
这里我实在不知道哪里错了
一定要记住是这样写的
有一个要注意的:.json文件中不能写注释
2.6代码规范
知识点:看一下github开源项目:Airbnb https://github.com/airbnb/javascript
把习惯性的var改成let (作用域的问题) 合理使用const
像这种的
const obj=new Object()
const arr=new Array()
就变成 const obj={} const arr=[]
还有建议这样
onReady(){ ……} 而不是 onReady:function(){……}
然后还有简写 比如说 username:username 可以简写成username 还有简写的最好写在上面 下面写非简写的
还有箭头函数比较方便
第三章播放列表
3.1轮播图组件
swiper组件用起来很简单 这里有个知识点:block
老师是这样写的 我也不知道为什么要加一个block
<swiper>
<block wx:for="{{imageUrls}}" wx:key="{{item}}">
<swiper-item>
<image src="{{item.url}}"></image>
</swiper-item>
</block>
</swiper>
-
image的几个mode属性(没有列全):
-
scaleToFill :缩放 不保持横纵比例 能让图片填满容器
-
aspectFit 图片按比例 全都显示出来 不覆
+ widthFix 高度自动变化 宽度自己设 宽高比不变(符合当前需求)
-
3.2组件化开发
很多前端框架都在使用组件化开发
它不是前端特有的
他是在用户界面开发领域,独立的可复用的交互元素的封装
-
组件化开发的意义
。组件化是对实现的分层,是更有效地代码组合方式
。组件化是对资源的重组和优化,从而使项目资源管理更合理。组件化有利于单元测试
。组件化对重构较友好
原则:高内聚 低耦合 单一职责 避免过多参数(一段代码尽量解决一个需求,组件要相对独立,尽量少去依赖其他的组件, )
我们在项目中用到了哪些组件呢
歌单
歌曲列表组件
进度条组件
歌词组件
博客卡片组件
博客控制组件
底部弹窗组件
登录组件(因为很多情况都要判断用户是否登录了)
搜索组件
3.3自定义歌单组件
先举一个例子怎么使用 1.components下的playlist文件:这是一个组件 2.pages下面的playlist的json文件引入 ,要给他起个名字 他的规范一般是XX-XXX然后冒号后面是路径 3.在playlist页面引入名字
<view class="playListContainer" wx:for="{{playList}}">
<s-playList playlist={{item}}>
</s-playList>
</view>
这个时候s-playList的属性值playlist如何接收item呢 我们就要学习一下components里面的x-playList的结构
/**
* 组件的属性列表
*/
properties: {
},
/**
* 组件的初始数据
*/
data: {
},
/**
* 组件的方法列表
*/
methods: {
//比如说当点击歌单的时候 点击跳转到歌曲列表 就要把处理函数放到这里面
}
我们重新梳理一下
这里我们如何把一条条数据传入到组件的playlist属性里面呢
<view class="playListContainer" wx:for="{{playList}}">
<s-playList playlist="{{item}}">
</s-playList>
</view>
这样就可以接收到了
properties: {
playlist:{
type:Object
}
},
然后呢 在页面上显示出来
<view>
<image src="{{playlist.picUrl}}"></image>
</view>
我们小程序的背景图片只能用本地的或者是用base64格式
我试过了,把阿里巴巴库的代码复制到app.wxss里面 组件里面是无法使用的 所以我们也是只能用本地图片或者base6格式了 更新,可以直接组件里建一个文件,放组件里
超过两行超出的替换成省略号
.playlist-name{
width: 220rpx;
font-size: 26rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
3.4播放数量细节处理
这里用到了observers
注意写法 监听对象下面的属性的话 要写成[ ]
当属性一变化 就自动执行下面的方法
observers:{
['playlist.playCount'](val){
console.log(val);
}
},
method里面:
// 6位以下 6-8位 8位以上
// 12345.444 1251232.12 123654789
// 1 直接取整
// 2 小数部分等于字符串的长度-4到长度-4+要保存的长度 然后整数部分就等于数字/10000取整 再拼接一下返回就ok
//3 小数:长度-8到长度-8+decimal 整数:/100000000
_tranNumber(num, point) {
let numStr = num.toString().split('.')[0]
if (numStr.length < 6) {
return numStr
} else if (numStr.length >= 6 && numStr.length <= 8) {
let decimal = numStr.substring(numStr.length - 4, numStr.length - 4 + point)
return parseFloat(parseInt(num / 10000) + '.' + decimal) +
'万'
} else if (numStr.length > 8) {
let decimal = numStr.substring(numStr.length - 8, numStr.length - 8 + point)
return parseFloat(parseInt(num / 100000000) + '.' + decimal) + '亿'
}
}
注意看toString substring parseInt parseFloat
现在我们可以打印出来了 但是如何把它显示到页面上去呢
我们知道有一个setdata可以修改playlist里面的内容,那么可以这样写吗
答案是不可以 他会形成死循环 开发者工具会卡死的
来解释一下为什么 ,obervers是一个监听器 用来监听playlist这个对象下面的playcount这个属性,当属性发生变化的时候,就会执行对应的方法,当给playcount赋值的时候 第一个playcount的值发生变化,当发生了变化,又触发了函数,一直循环。
那怎么办呢 我们不能给playlist里的playcount赋值了 但是可以给其他的赋值啊 我们可以在data里定义一个count 然后wxml里显示count的值
注意下划线的意思(一种规范)
3.5wx:key
<view class="list" wx:for="{{list}}">
<view><checkbox></checkbox> {{item}}</view>
</view>
<button bindtap="change">点击交换</button>
change(){
let length=this.data.list.length
for(let i=0;i<3;i++){
let x=Math.floor(Math.random()*length)
let y=Math.floor(Math.random()*length)
let temp=this.data.list[x]
this.data.list[x]=this.data.list[y]
this.data.list[y]=temp
}
this.setData({
list:this.data.list
})
}
这时候发现不行 √没有随着我们选中的一起动 这时候我们就可以加一个wx:key=“*this”
如果是对象的话 就可以选一个唯一的属性 比如id 给wx:key 而且注意不用加{{}}
3.6 ,3.7js异步操作管理
promise
举一个例子
//event loop
onLoad: function (options) {
setTimeout(() => {
console.log(1);
}, 1000);
console.log("2");
}, 会先打印出2再打印1
event loop
当执行到settimeout的时候他发现是个异步的任务 他就会把这个放到任务队列里面 然后执行主线程的任务( 输出2)。当发现主线程空闲并且settimeout的时间达到1s的时候,就会执行任务队列
当然不止这一个例子 后面会发请求 ,有的时候网好,有的时候网差,还有服务器处理速度,数据包的大小,都会影响效率,这时候他是异步的,但是后面要用到请求的数据怎么办呢
这里插一个例子:等1s输出1 再等2s输出2 再等3s输出3
setTimeout(() => {
console.log(1);
setTimeout(() => {
console.log(2);
setTimeout(() => {
console.log(3);
}, 3000);
}, 2000);
}, 1000);
这样写就陷入了回调地狱
然后就出现了异步解决方案:promise 他有三种状态:pending fulfilled rejected pending:一个人过生日了,我要送他礼物,他不知道我是会送礼物呢还是不会送呢
fulfilled:如果我送了 那么他的状态就会变成fulfilled
rejected:我太忙了 没有送
一旦状态变成了其中一个 那么不能改变
https://www.jianshu.com/p/1b63a13c2701
new Promise((resolve,reject)=>{
setTimeout(() => {
console.log("1");
resolve()
}, 100);
}).then(()=>{
setTimeout(() => {
console.log("2");
}, 2000);
})
知识点:promise.all([ ])
let p1=new Promise((resolve,reject)=>{
setTimeout(() => {
console.log("p1");
resolve()
}, 2000);
})
let p2=new Promise((resolve,reject)=>{
setTimeout(() => {
console.log("p2");
resolve()
}, 1000);
})
let p3=new Promise((resolve,reject)=>{
setTimeout(() => {
console.log("p3");
resolve()
}, 3000);
})
//等三个promise任务都完成 再进行操作
Promise.all([p1,p2,p3]).then((res)=>{
console.log("全部完成");
}).catch((err)=>{
console.log("失败");
})
//输出p2 p1 p3 全部完成
有个疑问 如果其中一个失败了会怎么显示呢
这就表示一个任务失败了 整个任务都失败了 但是他不会阻止下面任务继续执行
还有一个方法叫promise.race
可以看到,有一个任务完成就算完成了 不影响后面执行
那么这两个方法在什么场景用到呢
比如发博客等照片都上传成功了再执行下面的操作
还有怎么判断超时呢 就可以用到race 开个定时器,如果定时器先执行成功了,还没请求成功的话就可以知道请求超时了
async和await(ES7)
调用foo()
如果没有async awiat的话就会先打印res 但是res还没取到值
有的话会先等待57行执行完
ES6 generator
https://www.jianshu.com/p/83da0901166f
3.8读取歌单数据并插入云数据库
为什么要放到云数据库呢
我们设一个触发器,每天定时取数据,来保证每天取的都是最新的数据,还要与旧的数据进行比较,把没有的数据插入到数据库中
知识点:定时触发器 云函数 如何向数据库插入数据
发送请求:request request-promise、 axios等第三方库
一个注意事项,如果确定node已经安装过了 但是微信开发者工具安装包的时候显示不是内部命令 那么可以右键管理员打开微信开发者工具
request-promise库的官方例子
rp('http://www.google.com')
.then(function (htmlString) {
// Process html...
})
.catch(function (err) {
// Crawling failed...
});
我们写的是什么意思呢?
如果请求成功就把res返回给playlist 然后我们打印出playlist
那可以打印到我们平时用调试器吗 是不可以的,云函数代码属于后端的代码,后端的日志不会打印到我们的调试器中,他会打印到云函数日志里面
然后要右键文件夹 上传并部署
我们发现传入的是字符串类型的,我们需要的是json格式的
然后我们就可以学习如何放到数据库中了
先新建一个集合
然后向数据库插入数据 注意只能单条插入 ,所以我们要遍历数组 然后一条一条插入
- 补充微信官方参考代码:
- https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/Database.html
- https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/Database.collection.html
- https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/collection/Collection.add.html
- https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/Database.serverDate.html
- 三点运算符
- https://blog.csdn.net/wilie_C/article/details/109133238
完成
// 云函数入口文件
const cloud = require('wx-server-sdk')
cloud.init()
const db=cloud.database() //云数据库初始化
const rp=require('request-promise')
const URL = 'http://musicapi.leanapp.cn/top/playlist/highquality/%E5%8D%8E%E8%AF%AD'
// 云函数入口函数
exports.main = async (event, context) => {
const playlist=await rp(URL).then((res)=>{
return JSON.parse(res).playlists.splice(0,6) //因为一共有20条 我们只需要6条(写错了 需要不止六条 所以不应该加splice的)
})
// console.log(playlist);
for(let i=0;i<playlist.length;i++){
await db.collection('playlist').add({ //获取playlist的集合 //注意异步操作
data:{
...playlist[i], //注意三点运算符的使用
creatTime:db.serverDate() //获取服务器时间
}
}).then((res)=>{
console.log("插入成功");
}).catch((err)=>{
console.log("插入失败");
})
}
}
部署完以后就去数据库看一下 发现有数据了 (不过不是6条 )不知道为啥 疑惑 更新:确实是6条 可能因为我存太多次了,所以一直在累加
3.9歌单数据去重
如果我们再去读取歌单信息,他会又把这些信息放到数据库,这样数据库东西就会越来越多而且是重复的
这时候我们在传入数据库之前要判断之前存不存在 (根据id判断)
我们来取到之前数据库里面的数据
const list=db.collection("playlist").get()
但是呢 取到的数据是有限制的 最多只能获取100条; 从小程序端获取数据,最多获取20条 如何突破限制呢 我们下节课学(我去看了 更新了现在是1000条,不过还是学习一下)
思路:首先拿到数据库里面的歌单(list)和发请求得到的歌单(playlist) 并且定义一个数组newData来存放新的数据
云函数入口之前:const playlistCollection = db.collection('playlist')
const list = await playlistCollection.get() //得到数据库歌单已有数据 用来去重
const playlist = await rp(URL).then((res) => { //得到请求数据
return JSON.parse(res).playlists.splice(0, 6) //因为一共有20条 我们只需要6条
})
// console.log(playlist);
const newData = [] //存放新的数据
for (let i = 0; i < playlist.length; i++) {
let flag=true //判断是否重复的标志 true表示不重复
for (let j = 0; j <list.data.length;j++ ){ //注意这里是data
if(playlist[i].id===list.data[j].id){
flag=false
break
}
}
if(flag){
newData.push(playlist[i])
}
}
所以插入数据库的时候要把 playlist改成newData,…playlist[i]改成…newData[i]
3.10突破获取数据条数的限制
-
微信官方文档
- limit:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/collection/Collection.limit.html
- skip:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/collection/Collection.skip.html
- 定时触发器 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/triggers.html
- 取到数据库条数 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/collection/Collection.count.html
-
reduce:
-
https://www.cnblogs.com/amujoe/p/11376940.html
-
https://www.liaoxuefeng.com/wiki/1022910821149312/1024322552460832
一个例子
var arr = [1, 3, 5, 7, 9]; arr.reduce(function (x, y) { return x + y; }); // 25
-
-
concat
我们最多获取云数据库100条数据 如何优化呢
const list = await playlistCollection.get() 需要更改
- 获取总数据条数 不要忘记是异步操作 因为后面需要这个值进行计算
- playlistConllection.count()得到的是个对象 取到条数还要.total
https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/collection/Collection.count.html - 然后要判断要取几次 先在上面定义一个常量 MAX_LIMIT=100 如果total是221条的话 需要取3次 我们定义一个batchTimes=total/MAX_LIMIT 然后向上取整 得到取的次数
- 然后push到一个数组里 这样tasks里面就有多个promise对象
for(let i=0;i<batchTimes;i++){
//从第0取到100 从第100取到200 从第200取到221
let promise= playlistCollection.skip(i*MAX_LIMIT).limit(MAX_LIMIT).get()
tasks.push(promise)
}
-
然后定义一个list 里面有个数组data[] 注意为什么要定义一个data数组呢 因为上一节课我们也看到了,其实返回的是data里面的东西( if(playlist[i].id===list.data[j].id)) 一开始我们就是传入数据库的data data下面有请求网络的数据还有一个系统时间
if(task.length>0){
//都执行完了再执行下面的
list=(await Promise.all(task)).reduce((acc,cur)=>{
return {
data:acc.data.concat(cur.data)
}
})
}
- 回顾:首先取的集合里总的条数,但是返回的是一个对象,所以通过对象.total的方式取到当前数据的总的条数,然后总条数/最大次数向上取整,得到分次取的次数,然后定义一个任务,放的是每个promise的集合,当取的时候,通过i*最大数据的方式每次跳过相应的数据,分次去取,当所有任务都执行完成以后,迭代数据,把数据给list;后面读取链接获取数据,然后对比进行去重操作,然后将相应数据插入到数据库中
条数限制代码完成
// 云函数入口文件
const cloud = require('wx-server-sdk')
cloud.init()
const db = cloud.database() //云数据库初始化
const rp = require('request-promise')
const URL = 'http://musicapi.leanapp.cn/top/playlist/highquality/%E5%8D%8E%E8%AF%AD'
const playlistCollection = db.collection('playlist')
const MAX_LIMIT = 10//最大获取信息数量
// 云函数入口函数
exports.main = async (event, context) => {
// const list = await playlistCollection.get() //得到数据库歌单已有数据 用来去重
// 因为获取数据库最多100条数据的限制 我们需要优化
const countResult = await playlistCollection.count() //这是一个对象
const total = countResult.total //这样才是数字 得到了数据库信息个数
const batchTimes = Math.ceil(total / MAX_LIMIT) //取数据的次数
const task = [] //放promise对象的一个数组
for(let i=0;i<batchTimes;i++){
let promise=playlistCollection.skip(i*MAX_LIMIT).limit(MAX_LIMIT).get()
task.push(promise)
}
//注意这里为什么这样写
let list={
data:[]
}
if(task.length>0){
//都执行完了再执行下面的
list=(await Promise.all(task)).reduce((acc,cur)=>{
return {
data:acc.data.concat(cur.data) //还是不懂这里 这个意思是list=data的值呢还是list下面有个data呢
}
})
}
const playlist = await rp(URL).then((res) => { //得到请求数据
// return JSON.parse(res).playlists.splice(0, 6) //因为一共有20条 我们只需要6条(收回这句话 还是要数据多一点)
return JSON.parse(res).playlists
})
// console.log(playlist);
const newData = [] //存放新的数据
for (let i = 0; i < playlist.length; i++) {
let flag = true //判断是否重复的标志 true表示不重复
for (let j = 0; j < list.data.length; j++) { //注意这里是data
if (playlist[i].id === list.data[j].id) {
flag = false
break
}
}
if (flag) {
newData.push(playlist[i])
}
}
for (let i = 0; i < newData.length; i++) {
await playlistCollection.add({
data: {
...newData[i], //注意三点运算符的使用
creatTime: db.serverDate() //获取服务器时间
}
}).then((res) => {
console.log("插入成功");
}).catch((err) => {
console.log("插入失败");
})
}
return newData.length
}
定时触发云函数
官方文档:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/triggers.html
在config.json里写
{
"triggers":[
{
"name":"myTrigger",
"type":"timer",
"config":"0 0 10,14,16 * * * *"
}
]
}
一定注意要上传触发器
可以把云函数超时时间设长一点
3.11上拉加载下拉刷新
我们在cloudfunction里面新建一个music文件夹 用来专门供取数据用
我们要知道,新的歌单要排在前面,旧的歌单排在后面,
那么应该用什么排序呢?orderBy(“对应的字段”,“desc”) (desc逆序,默认是正序)
我们要先看懂这个
exports.main = async (event, context) => {
cloud.database().collection('playlist')
.skip(event.start).limit(event.count) //从第几条开始取 取到第几条
.orderBy("creatTime",'desc')
.get()
.then((res)=>{
return res
})
}
要知道,这是异步操作 所以cloud.database()……前面还要加个await 然后应该是return await cloud.database().collection(‘playlist’)……
这时候我们要去miniprogram里面的playlist文件夹了 来取出这里面的数据
onLoad: function (options) {
wx.cloud.callFunction({
name:'music',
data:{
start:this.data.playList.length, //这里的用法很妙
count:MAX_LIMIT //这里上面已经定义好了 值为15
}
}).then((res)=>{
console.log(res);
})
},
完善
onLoad: function (options) {
wx.showLoading({
title: '加载中',
})
wx.cloud.callFunction({
name:'music',
data:{
start:this.data.playList.length,
count:MAX_LIMIT
}
}).then((res)=>{
console.log(res);
this.setData({
playList:res.result.data
})
wx.hideLoading()
})
},
滑动到底部加载:
写到这个钩子函数里面onReachBottom: function () {},
我们会发现还需要请求,但是呢 需要复制粘贴一份请求的代码,我们需要优化一下把发请求封装成一个函数
但是我们又发现触底函数里面发请求的话 有问题 因为我们请求的是当前长度的后15条数据 我们需要拼接上前15条数据
_getPlaylist(){
wx.showLoading({
title: '加载中',
})
wx.cloud.callFunction({
name:'music',
data:{
start:this.data.playList.length,
count:MAX_LIMIT
}
}).then((res)=>{
console.log(res);
this.setData({
playList:this.data.playList.concat(res.result.data)
})
wx.hideLoading()
})
},
那下拉刷新呢 ? 先把playlist置空 再发请求 还有个问题 下拉刷新有三个点 ,数据请求完成了 点还在 所以请求数据那里应该再加一句置空的句子
3.12路由优化tcb-router
-
一个用户在一个云环境中只能创建50个云函数
-
相似的请求归类到同一个云函数处理
- 举个例子music里面用来获取歌单信息,这是一个功能,后面的课里面我们还要获取当前歌单里面有哪些歌曲,以及歌曲对应的url地址,我们还要获取歌曲的歌词信息,这些功能我们难道都要独立创建一个文件吗?不是的,类似的可以放在一个文件里面 这些我们都可以放在music文件里面,博客我们就创建一个文件夹专门放博客
- 这时候就需要tcb-router
- 相关链接:
- https://blog.csdn.net/hongxue8888/article/details/104599794/
- https://github.com/TencentCloudBase/tcb-router
- https://www.v2ex.com/t/491897
-
tbc-router是一个koa风格(node的一个框架)的云函数路由库
第一层执行完了执行第二层 第二层执行完了执行第三层 当最后一个中间件执行完以后才会往回执行
现在我们来举个例子 demo里面写两个按钮 当点击的时候,调用同一个云函数 但是一个是获取音乐信息,一个获取movie信息,就是说在同一个云函数里接收两个不同的路由请求, getMusicInfo是获取个人信息,音乐名称和音乐类型,getMovieInfo也是个人信息,电影名称和电影类型
exports.main = async (event, context) => {
const app=new TcbRouter({event})
//这个是适用所有路由的
app.use(async(ctx,next)=>{
ctx.data={},
ctx.data.userInfo=event //我们要知道 云函数入口函数里面的event里面有个人信息
await next()
})
//这是只适用于music的
app.router('music',async(ctx,next)=>{
ctx.data.musicName="数鸭子",
await next()
},async(ctx,next)=>{ //musicName得到数据以后执行下面的
ctx.data.musicType="儿歌"
//这时候可以返回数据了
// ctx.body 返回数据到小程序端
ctx.body={
data:ctx.data
}
})
//这是只适用于movie的
app.router('movie',async(ctx,next)=>{
ctx.data.movieName="千与千寻",
await next()
},async(ctx,next)=>{ //movieName得到数据以后执行下面的
ctx.data.movieType="儿歌"
//这时候可以返回数据了
ctx.body={
data:ctx.data
}
})
//最后千万别忘了 return app.serve();
return app.serve();
}
//获取音乐信息
getMusicInfo(){
wx.cloud.callFunction({
name:'tcbRouter', //云函数文件名叫tcbRouter
data:{
$url:'music'
}
}).then((res)=>{
console.log(res);
})
},
//获取电影信息同理
所以我们更改一下music下的代码:
// 云函数入口文件
const cloud = require('wx-server-sdk')
const TcbRouter=require('tcb-router')
cloud.init()
//与音乐相关的数据库都放这里面
// 云函数入口函数
exports.main = async (event, context) => {
const app=new TcbRouter({event})
app.router('playlist',async(ctx,next)=>{
ctx.body=await cloud.database().collection('playlist') //ctx.body后面就是要返回的
.skip(event.start).limit(event.count)
.orderBy("creatTime",'desc')
.get()
.then((res)=>{
return res
})
})
return app.serve()
}
这里data里也要增加一个参数$url因为当时只写了在哪个云函数里,现在我们用了tcbrouter也要指定里面的路由名称
3.13 3.14自定义列表组件
在我们的playlist组件里面,还应该绑定事件:goToMusiclist
然后跳转到musiclist(新建的一个页面,用于点击歌单,跳转到音乐列表页面) 这时候需要传参
url: ‘…/…/pages/musiclist/musiclist?playlistId=’+this.properties.playlist.id,
然后在musiclist的onload里面接收参数
然后我们就要获取playlistid对应的音乐列表
api接口:https://music.163.com/api/playlist/detail?id=歌单里的id 注意不登录没有cookie的话不能拿到全部数据
music云函数里
//根据id 获取歌单对应的歌曲
url='https://music.163.com/api/playlist/detail?id='+parseInt(event.playlistId)
let options = {
method:"POST",
url,
headers: {
cookie:'MUSIC_U=XXXXXX' //因安全问题不能放出来后续会根据登录获取cookie放到这里面
},
}
app.router('musiclist',async(ctx,next)=>{
ctx.body=await rp(options)
.then((res)=>{
return JSON.parse(res)
})
})
return app.serve()
//musiclist
onLoad: function (options) {
console.log(options); //得到id
wx.cloud.callFunction({
name:'music',
data:{
playlistId:options.playlistId, //把playlistId传入云函数里的event
$url:'musiclist'
}
}).then((res)=>{
console.log(res);
})
},
这样就可以拿到数据了
然后就可以渲染到页面上
这个地方用到了 透明度 filter 层级 还有背景图等知识点
背景是一个高斯模糊的效果,我们把照片平铺,然后做出来模糊效果,然后再盖一层黑色的遮罩
然后下面的歌曲 也应该像上次歌单那样写个组件 不过为了展示不止有一种方法,这次用了另一种方法
(上次是什么样呢 是原来的页面进行循环,然后组件页面就相当于是拿到了一条数据,来写一条数据的样式,然后循环到最后,一条条数据最终都去过了组件页面,最后展示出了所有数据都是组件写的样式
这次呢,是我们把所有数据都传到了组件页面,在组件页面循环)
这里有一个坑:flex布局以后 flex:1 但是还是超出了盒子 这时候怎么办呢
https://blog.csdn.net/qq_41075132/article/details/82865248
有一个很妙的地方:如果一个页面是前一个页面跳转过来的,而我们又专注于写后面的那个页面,我们知道可以改编译模式,但是没有参数是不行的 我们可以在下面添加启动参数(不知道为什么后面那个字符串不用加“” 而且注意用的是等号,不是:)
点击变红 基本操作了 不贴代码了 思路就是点击传参,把参数写data里,然后页面里面判断当前id与data里的id相不相等,相等添加样式 用三元运算符
有一个知识点以前一直没有注意过: 以前只知道currentTarget比较精确 为什么这里接收参数currentTarget有值而target没有呢?因为事件是绑定在容器上的 当点击某一条歌曲的时候,由于js里面有一条事件机制(冒泡)由于js是从事件源开始逐层向父元素传播,当点击name的时候,就相当于点击了container,触发事件处理函数,但是真正点击的是name
事件三要素:事件源(谁触发) 事件处理函数 事件对象(就像是event) 事件类型(tap),我们要区分当前的事件源是谁,如果点击了name真正的事件源其实是name target指的就是真正的事件源,但是name上面没有我们的data-XX ,currentTarget指的是绑定事件的元素 就是我们的container元素
3.15总结
第四章播放页面
4.1
4.2播放页布局
我们的musiclist做完后是这样的,然后我们发现 播放页要的数据在musiclist里面都有了(这是老师的那种情况,他的api和我的不一样,所以我是没有音乐的url的),当然我们可以用调用api根据id获取信息(因为比前面那个方法多发了请求,所以用户体验会不好)
我们先看看老师的方法,他这种方法可以用本地存储 然后到播放页再取出来本地缓存,这里不用担心数据会越积越多的情况,因为key相同的话,value值会覆盖掉
知识点,如果页面之间要传递多个参数,可以用&符号连接
这时候我们要把缓存取出来,注意要放在一个全局变量里面因为不需要在界面显示,还有正在播放的index也不需要在页面显示
// pages/player/player.js
let musiclist = []
// 正在播放歌曲的index
let nowPlayingIndex = 0
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function(options) {
console.log(options)
musiclist = wx.getStorageSync('musiclist')
nowPlayingIndex:options.options.index
},
然后就可以专门定义一个方法来取当前的信息,写完了放onload里面
看到这里发现没有api实在是不好弄 我找不到可以播放音乐的接口了 我想自己买一个服务器学着怎么搭建,然后就可以畅通无阻了
插播:服务器
注册完以后
重置实例密码
安全组规则:
老师加了这三个
安装ssh客户端软件
客户端:filezilla: 终端:xshell
我又在网上找到api了 …… 先不学建服务器了
4.2接着上面的4.2
一些东西我喜欢往data里面放,一些关于界面显示的可以往data里面放,有些东西直接放在最外面也可以在任意函数里面用,没必要放data
我们在列表页点击的时候要传到player页面两个参数,一个index一个id id的话是为了得到歌曲播放的url和歌词,index的话是为了判断取缓存里的哪一条数据
我的疑问,为什么要放缓存呢,难道不能跳转的时候直接传参(歌曲名,歌手,封面)吗?
学习一下图片撑满整个屏幕
我们的模糊效果:
.container{
/* background: url(http://p4.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg); */
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
filter: blur(40rpx);
opacity: 0.7;
z-index: -1;
}
.mask{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #222;
z-index: -2;
}
这里的唱臂是用::after添加上去的
4.3iconfont字体图标
注意
4.4音乐控制面板功能实现
注意使用BASE_URL来简化代码
获取全局唯一的背景音乐播放器(关键词:唯一):相当于js里面的单例模式
本来是在player.js里请求数据的,后来看老师的是在云函数里请求的,我觉得很有道理,然后再云函数同时请求了url和lyric,都成功了才返回,但是我觉得下次再遇到这样的不能这样写了,因为这两个有的返回时长慢有的返回时长快,url如果已经请求到数据了就没必要等歌词了,不能为了减少代码量而增加等待时间 然后还有个问题,官方有时候歌词是请求不到的,我们要在这时候判断一下有没有歌词,要不然返回的时候回出错,如果没有歌词就返回空就行了。
我以前都没有注意过这个,
注意单引号:
<text class="iconfont {{isplaying?'icon-bofang':'icon-bofang1'}}" bind:tap="togglePlaying"></text>
4.5音乐控制面板功能实现(2)
有一个地方很好,我们上个视频是因为当前页面点击上一首下一首,还要用到发布订阅,这个的话因为我们用到了缓存,从缓存里取出来了,所以上一首下一首实现是很方便的
还有细节要注意,比如说一开始是暂停的,获取到数据以后播放,还有播放到最后一首回到第一首,播放第一首点上一曲回到最后一首
4.6进度条组件
一定要记住常用的居中方法
left: 50%;
transform: translateX(-50%);
我们的进度条是要自己做的,为什么不用官方给的呢?我们要联系一种案例,比如说左滑,数据的右边显示删除,点击删除,数据就删除掉了(slider适用范围比较少)
这次学了滑动组件和可移动组件
关于可移动动画:
https://developers.weixin.qq.com/miniprogram/dev/component/movable-view.html
direction damping x
我们如何在js获取元素的信息呢
_getMovableDis(){
//现在是在组件当中,所以用this而不是wx
const query=this.createSelectorQuery()
query.select('.movable-area').boundingClientRect()
query.select('.movable-view').boundingClientRect()
query.exec((rect)=>{
console.log(rect);
movableAreaWidth=rect[0].width
movableViewWidth=rect[1].width
console.log(movableAreaWidth,movableViewWidth);
})
},
知识点:如何判断是不是undefined?可不可以XXX==undefined? 不可以,因为null也等于undefined 不严格
三等号可以 或者是typeof XXX==‘undefined’
然后是格式化时间
_dateFormat(second){
const minute=Math.floor(second/60)
second= Math.floor(second%60)
return {
"min":minute,
"sec":second
}
}
//格式化时间
_dateFormat(second){
const minute=Math.floor(second/60)
second= Math.floor(second%60)
return {
"min":this.parse0(minute),
"sec":this.parse0(second)
}
},
//补零操作
parse0(sec){
return sec<10?'0'+sec:sec
}
//进度条的时间
_setTime(){
const duration=backgroundAudioManager.duration
console.log(duration);
const durationFMT=this._dateFormat(duration)
console.log(durationFMT);
this.setData({
"showTime.totalTime":`${durationFMT.min}:${durationFMT.sec}`
//这里老师说外面要加中括号,我没加也赋上值了
})
console.log(this.data.showTime.totalTime);
},
//格式化时间
_dateFormat(second){
const minute=Math.floor(second/60)
second= Math.floor(second%60)
return {
"min":this.parse0(minute),
"sec":this.parse0(second)
}
},
//补零操作
parse0(sec){
return sec<10?'0'+sec:sec
}
4.8进度条与播放时间联动
在这里我们前面获取的宽度就派上用场了
但是呢,
我们不用setdata那么多次的,一秒触发一次就行了,那么如何变成一秒只触发一次呢 我们可以取到前面的秒数,如果秒数已经设置过,那么就不再对他设置了
backgroundAudioManager.onTimeUpdate(() => {
const currentTime=backgroundAudioManager.currentTime
const duration=backgroundAudioManager.duration
//变成字符串根据点分割,如果与currentSec不相等,就setdata
if(currentTime.toString().split('.')[0]!=currentSec){
console.log(currentTime);//测试setdata的次数是不是少了
const currentTimeFmt=this._dateFormat(currentTime)
this.setData({
//正在播放的时间,滑块距离,进度
"showTime.currentTime":`${currentTimeFmt.min}:${currentTimeFmt.sec}`,
movableDis: (movableAreaWidth - movableViewWidth) * currentTime / duration,
progress: currentTime / duration * 100,
})
currentSec=currentTime.toString().split('.')[0]
}
4.9拖拽进度条(小程序组件)
这时候就要在滑块上加上事件处理函数
bindtouchend是在触摸完成的时候触发
<movable-view direction="horizontal" class="movable-view"
damping="1000" x="{{movableDis}}" bindchange="onChange" bindtouchend="onTouchEnd">
</movable-view>
我们发现,onChange事件在没有动他的时候,他也会被触发,他有一个source属性,当没动它自己就触发的时候属性是空的,但是拖动触发的时候,source为touch 所以我们就要判断souce是不是touch
onChange(e){
// console.log(e);
//判断是不是拖动触发的
if(e.detail.source==="touch"){
console.log(e);
//注意我们没有setdata 为什么呢 因为如果这样的话,他会频繁setdata给progress和movableDis赋值,小程序带不动,我们应该在用户松手的时候赋值就好了 这里把值保存起来待会给onTouchEnd那个事件处理函数用
// 疑问:这里的progress和movableDis指的是什么?
this.data.progress=e.detail.x/(movableAreaWidth-movableViewWidth)*100
this.data.movableDis=e.detail.x
}
},
onTouchEnd(){
const currentTimeFmt=this._dateFormat(Math.floor(backgroundAudioManager.currentTime))
this.setData({
progress:this.data.progress,
movableDis:this.data.movableDis,
"showTime.currentTime":currentTimeFmt.min+':'+currentTimeFmt.sec
})
console.log(this.data.progress);
backgroundAudioManager.seek(duration*this.data.progress/100)
},
4.10自动播放下一首(有个组件通信)及一些优化
我们发现 ,播到最后会变成这样 我们应该处理一下,播放完是onEnded
知识点:我们现在应该在组件里调用player里的onNext() 该怎么调用呢
这里就用到了组件通信
triggerEvent(“XXX”) 然后在调用组件那里绑定事件(自定义事件) bindXXX="onNext "
我们还会发现
他有的时候会往回闪一下 为什么呢
产生了冲突
怎么解决呢 可以增加一个锁(知识点) 我们全局定义个变量ismoving (标志是否在拖拽) 如果在拖拽 就不执行onTimeUpdate
进入onChange里面设为true 进入onTouchEnd里面设为false 然后再onTimeUpdate里面判断如果是假 就执行我们以前写的代码
但是小程序还有个坑:按照我们的思路我们松开了手以后这个变量应该是false了 ,但是小程序有概率又触发一次onchange把它变成true
那又该怎么解决呢 每次播放都会触发onplay 我们在里面再设置一次false
我们要注意不能再小程序中频繁的setdata 应该多加一些判断避免
4.11-12歌词组件(正则)
注意,我们知道点击显示出歌词组件,但是原来的是用if else 还是用hidden好呢?–频繁切换用hidden好
一个疑问 为什么要把状态传入到歌词组件里面呢 而不是直接在player里面判断
我们这样可以监听歌词的信息
我们要对它处理一下
我们先根据换行进行分割,
methods: {
_parseLrc(sLrc){
let line=sLrc.split('\n')
let _lrcList=[]
// console.log(line);
line.forEach((elem)=>{
let time = elem.match(/\[(\d{2,}):(\d{2})(?:\.(\d{2,3}))?]/g)
if(time!=null){
// console.log(time); [00:00.39]
let lrc=elem.split(time)[1]
// console.log(lrc); //只留歌词
let timeReg=time[0].match(/(\d{2,}):(\d{2})(?:\.(\d{2,3}))?/)
// console.log(timeReg);
// 0: "02:03.26"
// 1: "02"
// 2: "03"
// 3: "26
//把时间转化为秒
let time2Seconds=parseInt(timeReg[1])*60+parseInt(timeReg[2])+parseInt(timeReg[3])/1000
//每一行歌词代表的时间和对应的文字
_lrcList.push({
lrc,
time:time2Seconds
})
}
})
this.setData({
lrcList:_lrcList
})
console.log();
}
},
4.13歌词联动(组件与组件通信 +scroll-top)
这个的思路是什么样呢?我们要在progressbar的onTimeUpdate里面拿到时间,与我们的歌词组件里面的lrcList的time做匹配,我们就知道对应的歌词了 然后把歌词变成高亮的,
所以我们就要知道如何把一个组件的值传到另外一个组件,又用到了上次的组件通信,但是跟上次的不一样,这次是传的值而且是在组件与组件中的通信 疑问:这个通信和发布订阅模式的区别?
第三张图是根据class名称选择的
可以看到lyric里面可以打印歌词了
我们接下来考虑如何匹配
update(currentTime){
console.log(currentTime);
let lrcList=this.data.lrcList
if(lrcList.length==0){
return
}
for(let i=0,len=lrcList.length;i<len;i++){
if(currentTime<=lrcList[i].time){
this.setData({
nowLyricIndex:i-1
})
break
}
}
},
我们可以看到当播放到一句歌词时,当前歌词会变红,但是我们平常听音乐正在播放的歌词是滚动到页面中间的
这时候用到了scrollview里的scroll-top 而且单位是px 我们的lyric是rpx单位,那么他们俩之间如何换算呢
获取手机信息:
wx.getSystemInfo
然后可以得到手机的px单位 然后除750 得到1rpx的大小 然后乘我们设置的歌词高度 (64)就获得了一句歌词对应的px大小
这里又有一个问题
拖到后面为什么不动了呢
如果拖到后面 他会一直大于歌词对应的时间 就不会执行下面的语句了
我们看怎么解决的
update(currentTime) {
// console.log(currentTime)
let lrcList = this.data.lrcList
if (lrcList.length == 0) {
return
}
//解决进度条拖到最后 歌词不滑到最下面问题
if (currentTime > lrcList[lrcList.length - 1].time) {
if (this.data.nowLyricIndex != -1) {
this.setData({
nowLyricIndex: -1, //没有歌词要高亮
scrollTop: lrcList.length * lyricHeight
})
}
}
for (let i = 0, len = lrcList.length; i < len; i++) {
if (currentTime <= lrcList[i].time) {
this.setData({
nowLyricIndex: i - 1,
scrollTop: (i - 1) * lyricHeight
})
break
}
}
},
然后解决没有歌词的歌曲显示歌词的问题
observers: {
lyric(lrc) {
if (lrc == '暂无歌词') {
this.setData({
lrcList: [{
lrc,
time: 0,
}],
nowLyricIndex: -1
})
} else {
this._parseLyric(lrc)
}
// console.log(lrc)
},
},
4.14修改亿点点bug
在播放音乐页面点击上一首或者下一首 返回到列表页还是以前点击的歌曲高亮
我们先想一下,高亮是在playlist里面判断index 设置的,上一首下一首是在player里面执行的onNext和onPrev实现的
这时候又要用到传值了 我们这次用到了全局的app.js
思路:在player里面把当前的musicId存下来 然后app.js里面把这个id存下来,然后musicList从app.js里取出来
app,js:
onLaunch: function () {
this.globalData={
playingMusicId:-1
}
},
setPlayingMusicId(musicId){
this.globalData.playingMusicId=musicId
},
getPlayingMUsicId(){
return this.globalData.playingMusicId
},
player.js:
app.setPlayingMusicId(musicId)
musicList:
pageLifetimes:{
show(){
let playingId=app.getPlayingMUsicId()
this.setData({
playingId
})
}
},
系统播放暂停与小程序不同步问题
怎么解决呢 我们发现在点击系统播放暂停时 会触发progress-bar里面的onplay与onpause事件
这时候我们可以去设置player.js里的isplaying 现在问题就变成了如何触发onpaly与onpause以后设置另一个组件isplaying的值 又用到了组件之间通信的知识点
progress-bar:
还可以再完善的
退出再播放同一首音乐会重新播放的bug
插播:听说:
4.15下一章预告
第五章 发现页面
5.1
5.2 搜索组件
阿里巴巴图标库也可用i标签
细节:图标点击区域小?那就给他个大盒子,点击盒子触发事件
然后
这里我们单独抽出来做组件
这个搜索组件中的placeholder我们可以直接在页面上写 但是呢我们如果引用到其他地方可能就不适用了 所以我们最好在逻辑层写placeholder
然后呢 我们组件里有一个搜索图标,可以在组件里建一个文件,导入阿里巴巴图标库图标
再介绍一种方法:组件外部样式类
但是有个坑要注意,我们发现样式是修改不了的 (样式隔离)我们就可以再添加一个class名称
5.3底部弹出层(有个知识点插槽)
我们再来了解一下第三种引用样式方法
链接:
https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html
组件里传入modalshow data里面是false 然后点击发布按钮以后变成true了 传入到组件里面,组件判断true的话不隐藏,false的话隐藏。 然后就是叉号,点击的话就不显示
然后这个功能是怎么实现的呢
里面的东西不是写死的–用到了插槽slot 相当于占位符
在子组件内占坑,在父组件里填坑。
用了插槽以后,组件的使用会更加的灵活
5.4授权组件
wx.getsetting:
https://developers.weixin.qq.com/miniprogram/dev/api/open-api/setting/wx.getSetting.html
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/authorize.html
通过上面的getsetting判断用户有没有授权,有授权的话就getUserInfo获取用户信息,没有的话就弹出我们的底部弹窗让用户登录。
这个底部弹窗我们专门做一个bottom-modal来放弹窗的壳子,因为不只是这里用到
我们先再创建一个login组件,传入modalShow 然后里面的父元素是一个组件,里面的子元素是要在底部弹窗组件里面的(填槽),先前底部弹窗组件已经有了一个名字为modal-content的插槽了,我们再login里面写的内容 都会显示在弹窗里面。
再login组件里写一个按钮,通过按钮获得用户信息,
5.5编辑页面
这里有个小功能,输入的时候会有输入字的个数 然后最多的字数为140字,
还有要注意的,我们模拟器是可以看到下面的footer的,但是到真机上,输入法会把footer盖住,我们要解决这个问题
基本布局:
注意原生组件 如textarea camera 等 原生组件是脱离webView的 他是层级最高的,非原生组件可以用z-index设置层级,调整层级关系,但是原生组件,不管把其他元素z-index设置多大,都覆盖不了原生组件,还有原生组件不能显示在容器里,比如不能放在scroll-view,swiper, movable-view,里面。还有,他是不支持css动画的。
我们这里的textarea是不能设置绝对定位的,还有一个坑 就是不能用bind:tap 可以用bindtap
onInput(event) {
// console.log(event.detail.value)
let wordsNum = event.detail.value.length
if (wordsNum >= MAX_WORDS_NUM) {
wordsNum = `最大字数为${MAX_WORDS_NUM}`
}
this.setData({
wordsNum
})
content = event.detail.value
},
然后就解决手机上输入法会遮挡footer的问题了 textarea 有一个属性auto-focus 我们再添加两个属性
更改footer的bottom 因为他是绝对定位。所以改变bottom的值就行了
5.6选择图片逻辑
我们最大图片个数设为9 选满9个加号消失
在选择图片图标上添加一个事件处理函数 里面用到了wx.chooseImage 里面有个count
我们可以定义一个数组,当选择完图片以后就存到数组里,然后根据数组的长度判断还能选择的个数
删除的话用到了splice 那么删除了以后加号应该又要出来了
插播上一节的内容
我们这里的footer没有加高度,直接元素撑起来的
.footer {
display: flex;
align-items: center;
position: fixed;
bottom: 0;
padding: 20rpx;
width: 100%;
box-sizing: border-box;
background: #f1f1f1;
}
5.7多文件上传云存储(知识点云存储,正则)
我们把数据放到云数据库里面,
然后我们的图片要放到云存储 云存储会有个fileID 他是我们的唯一标识 云文件的ID
所以我们的数据库存的是内容还有图片的fileId,另外还有用户的openid,昵称,头像,时间,
1.图片->云存储 fileID 云文件ID
2.数据->云数据库
图片上传:
wx.cloud.uploadFile
https://developers.weixin.qq.com/minigame/dev/api/network/upload/wx.uploadFile.html
我们再云存储里新建一个文件blog
然后
publish(){
// 因为云存储是单文件上传,我们要多文件上传,所以要加循环
for (let i = 0, len = this.data.imgArr.length; i < len; i++) {
let item = this.data.imgArr[i]
// 文件扩展名
let suffix = /\.\w+$/.exec(item)[0]
wx.cloud.uploadFile({
//这里是防止重名的情况 如果重名的话会覆盖掉以前的图片
cloudPath: 'blog/' + Date.now() + '-' + Math.random() * 1000000 + suffix,
filePath: item,
success: (res) => {
console.log(res)
resolve()
},
fail: (err) => {
console.error(err)
reject()
}
})
}
},
正则那里 \w:字母数字下划线 +:不止一个 $:以这个结尾
5.8存入到云数据库(知识点 三点运算符)
我们要知道,上传到云存储是个异步过程,存入云数据库又需要云存储返回的fileID
我们要多个图片上传任务都执行完以后,然后把fileId拼接成一个数组,然后把数组存入到数据库
这时候又用到了promise.all
思路:先定义一个promiseArr数组 用来存放每次循环得到的promise对象
循环完了以后,promise.all(promiseArr) 然后.then里面就是存放到数据库的一些操作
这时候我们到数据库新建一个集合blog 然后要到集合里面
- 对数据库进行初始化 const db=wx.cloud.database()
- 然后在promise.all里面 通过.add的方式往集合里面插入对应的数据(内容 图片 用户id 用户昵称和头像 时间 )
内容的话在handleInput里面可以拿到 图片 - 这里有个注意的地方就是如果输入是空的话就没必要返回了,直接return
- 然后我们的fileID有很多个,我们就把他存在数组里面 用到了concat
- openId呢是自带的
- 昵称 头像 可以再onload里面拿到 因为userinfo是个对象,想把每个属性都插入进来怎么办呢 用到了三点运算符
- 时间:两种方法,一种是获取客户端时间,第二种是获取服务端时间
客户端时间可能是与网络同步,也可能不同步
我们应该取服务端时间 db.serverDate() - 然后可以 一开始就loading 然后 .then 弹窗发布成功 并且hideloading 然后.catch 弹窗发布失败 hideloading
这里的id和openid都是自动生成的
打卡帖个代码
// miniprogram/pages/blog-edit/blog-edit.js
let TEXT_NUM_MAX = 100
let IMG_MAX = 9
const db=wx.cloud.database()
let content='' //输入文字内容
let userInfo={}
Page({
/**
* 页面的初始数据
*/
data: {
textCount: 0, //计数(输入框)
imgArr:[], //图片列表
showAddIcon: true, //是否显示加号
footerBottom: 0, //输入时 footer距底部高度
},
//输入字数显示
handleInput(e) {
let textCount = e.detail.value.length
this.setData({
textCount
})
if (textCount >= TEXT_NUM_MAX) {
this.setData({
textCount: `最多输入${TEXT_NUM_MAX}个字`
})
}
content=e.detail.value
},
//添加图片
addImg() {
//存放图片的数组
let count = 0
let imgArr = []
wx.chooseImage({
count: IMG_MAX - this.data.imgArr.length,
success: (e) => {
console.log(e);
imgArr = e.tempFilePaths
this.setData({
imgArr: this.data.imgArr.concat(imgArr)
})
let length = this.data.imgArr.length
if (length >= IMG_MAX) {
this.setData({
showAddIcon: false
})
}
},
})
},
//删除图片
deleteImg(e){
let index=e.currentTarget.dataset.index
let imgArr=this.data.imgArr
imgArr.splice(index,1)
this.setData({
imgArr
})
this.setData({
showAddIcon:true
})
},
// 更改footer距底部高度
onFocus(e) {
console.log(e.detail.height);
let height = e.detail.height
this.setData({
footerBottom: height
})
},
onBlur() {
this.setData({
footerBottom: 0
})
},
// 发布按钮
send(){
let promiseArr=[] //保存promise每个对象
let fileIds=[] //保存fileId
for(let i=0;i<this.data.imgArr.length;i++){
//每次循环都new一个新的promise对象
let p=new Promise((resolve,reject)=>{
let item=this.data.imgArr[i]
wx.cloud.uploadFile({
cloudPath:"blog/"+Date.now()+'.jpg',
filePath:item,
success:(res)=>{
console.log(res);
fileIds=fileIds.concat(res.fileID)
resolve()
},
fail:(err)=>{
console.log(err);
reject()
}
})
})
promiseArr.push(p)
}
Promise.all(promiseArr).then((res)=>{
// 存放到数据库
db.collection("blog").add({
data:{
...userInfo,
content,
img:fileIds,
creatTime:db.serverDate()
}
}).then((res)=>{
wx.showToast({
title: '发布成功',
})
})
})
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
console.log(options);
// 这里暂时不用 我们先写布局和样式
userInfo=options
},
})
5.9博客卡片组件
这里就要取出数据库内容了,
这个跟以前的歌单有点像,先来回顾一下歌单是怎么做的
我们在云函数接收 start和count start就是date里面存放playlist数组的长度,count就是每次加载多少条
然后onload里面触发一次 每次触底也触发
-
创建一个博客的云函数 然后安装一个tcb-router
-
准备工作
// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() const TcbRouter=require("tcb-router") const db=cloud.database() //引入数据库 // 云函数入口函数 exports.main = async (event, context) => { const app=new TcbRouter({ event }) return app.serve() }
-
云函数里面
// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() const TcbRouter=require("tcb-router") const db=cloud.database() //引入数据库 const blogCollection= db.collection("blog") // 云函数入口函数 exports.main = async (event, context) => { const app=new TcbRouter({ event }) app.router("list", async(ctx,next)=>{ let blogList=await blogCollection.skip(event.start) .limit(event.count) .orderBy("date",'desc') .get() .then((res)=>{ return res }) ctx.body=blogList }) return app.serve() }
-
回到pages里的blog.js
自己封装一个_loadBlogList方法_loadBlogList(){ console.log(111); wx.cloud.callFunction({ name:"blog", data:{ $url:'list', start:this.data.blogList.length, count:1 //因为数据少,所以每次下拉刷新就加载一个 } }).then((res)=>{ let data=res.result.data this.setData({ blogList:this.data.blogList.concat(data) }) }) },
-
卡片组件
5.10时间格式化处理
注意,因为我们的数据库的数据传入到组件里面 这时候时间是服务器端时间,长这样
Mon Feb 08 2021 00:11:07 GMT+0800 (中国标准时间)(云数据库类型)
我们new Date(Mon Feb 08 2021 00:11:07 GMT+0800)他就会变成js类型的时间
结果:2021-02-07T16:11:07.000Z
然后我们要在组件对他处理:又用到了observers监听 我们可以
但是呢,我们这个可以通用,所以可以放到utils里面
observers:{
["blog.date"](val){
let time=formatTime(val)
// console.log(time);
this.setData({
time
})
}
}
注意这里为什么新定义了一个time
因为是监听blog.date的变化 如果改变date会造成死循环
5.11细节处理(知识点 子页面如何调用父页面方法)
上次是根据数组长度赋给start的 现在第二种思路:
onLoad: function(options) {
console.log(options.scene)
this._loadBlogList() //不传值默认是0
}
onPullDownRefresh: function() {
this.setData({
blogList: []
})
this._loadBlogList(0)
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function() {
this._loadBlogList(this.data.blogList.length)
},
还有点击图片放大 用到了onPreviewImg
点击进入详情页 点击跳转 <s-blog-card blog="{{item}}" bindtap="goComment"></s-blog-card>
然后新建一个blog-comment
然后有个bug 我们点图片的时候 会发现他也跳转了 这样是不行的,点击图片应该是放大图片才对,为什么呢,我们的组件是用的bindtap 有个冒泡机制 ,我们要把他改成bindcatch
还有个要注意的,如果我们发布的时候想在下面加个蒙版,为了防止用户乱点,怎么办呢 可以在showloading里面加个mask
编辑完跳回blog页面并刷新 在navigateback下面写相应的代码 那么如何在blog-edit页面控制blog页面的onPullDownRefresh呢
这几涉及到知识点,如何在子界面调用父界面的功能
https://developers.weixin.qq.com/miniprogram/dev/reference/api/getCurrentPages.html getCurrentPage
插播:这里发现上古时期的bug
这个是给第一次登陆和以后登陆跳转到编辑页面用的
所以他们两个传入的值都应该相同 我当时组件里是这样写的
然后
这样的话他俩就能调用这个函数了
但是呢组件里的e打印出来是这样的,我还以为当时没看清 应该userInfo再.detail呢
然后我就组件里
但是其实并不是这样,这个detail是自动加上去的, 所以我们
应该先取到userinfo(里面有昵称等)(e.detail) 然后再调用loginSuccess
这是一种方法,
还有一种
5.12模糊查询
这里我们为了搜索组件有更高的复用性,就不在组件里操作了
把组件里输入的字符串传给blog 放在一个全局变量里
然后triggerevent 把keywords传出去 传到哪儿呢 传到调用组件的地方
在blog页面的search函数里把keywords存到一个全局变量
我们再search里面调用这个函数,但是呢 我们要改一下 应该再传入一个keywords
然后云函数里面就可以接收到keywords了
正则:
i 大小写不敏感
m 跨行匹配;让开始匹配符 ^ 或结束匹配符 $ 时除了匹配字符串的开头和结尾外,还匹配行的开头和结尾
s 让 . 可以匹配包括换行符在内的所有字符
云数据库查询知识点(正则):https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/Database.RegExp.html
app.router("list",async(ctx,next)=>{
const keywords = event.keywords
let w={}
if(keywords.trim()!=''){
w={
content:new db.RegExp({
regexp: keywords,
options: 'i',
})
}
}
let blog=await blogList.where(w).skip(event.start)
.limit(event.count)
.orderBy("date",'desc') //时间倒序
.get()
.then((res)=>{
return res
})
ctx.body=blog
})
为了让效率更高,我们可以建个索引,用空间换时间
5.13云数据库权限
我们发现 音乐,博客查询数据库操作都是在云函数进行的
为什么在云函数里而不是小程序呢
- 代码方便管理
关于音乐的内容都是在音乐云函数里面的,这样代码管理起来就比较容易
如果不这样的话 就要创建三个云函数 代码不好管理
-
小程序端对数据库请求有20条次数限制 而云函数端是1000条 突破:多次查询再拼接
-
在小程序端查询的话,我们得到的结果只有自己发布的 不过可以去数据库改权限
-
在小程序端读取数据库时间是个对象
解决办法:变成字符串
5.14小结
https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/Database.RegExp.html](https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/Database.RegExp.html)
app.router("list",async(ctx,next)=>{
const keywords = event.keywords
let w={}
if(keywords.trim()!=''){
w={
content:new db.RegExp({
regexp: keywords,
options: 'i',
})
}
}
let blog=await blogList.where(w).skip(event.start)
.limit(event.count)
.orderBy("date",'desc') //时间倒序
.get()
.then((res)=>{
return res
})
ctx.body=blog
})
为了让效率更高,我们可以建个索引,用空间换时间
5.13云数据库权限
我们发现 音乐,博客查询数据库操作都是在云函数进行的
为什么在云函数里而不是小程序呢
- 代码方便管理
关于音乐的内容都是在音乐云函数里面的,这样代码管理起来就比较容易
如果不这样的话 就要创建三个云函数 代码不好管理
-
小程序端对数据库请求有20条次数限制 而云函数端是1000条 突破:多次查询再拼接
-
在小程序端查询的话,我们得到的结果只有自己发布的 不过可以去数据库改权限
-
在小程序端读取数据库时间是个对象
解决办法:变成字符串
5.14小结
第六章 评论
6.1,6.2博客控制组件
知识点:组件引入iconfont三种方式https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html#%E5%A4%96%E9%83%A8%E6%A0%B7%E5%BC%8F%E7%B1%BB
- 直接放到组件内部
- 外部样式类
- 样式隔离
然后到组件里把这三个都放到externalClasses
点击评论的逻辑:
先在博客控制组件里面添加login和bottom-modal组件
点击判断没有登录的话就显示login组件
没有登录后来点了授权就要隐藏login组件,显示bottom-modal(以后要用插槽插入输入框)
没有登录后来拒绝授权 就提示要授权才能评论
已经登录的话就显示bottom-modal组件(以后要用插槽插入输入框)
这是两个控制login和bottom-modal是否显示的
// 登录组件是否显示
loginShow: false,
// 底部弹出层是否显示
modalShow: false,
点击的时候
getSetting判断是否已经登录过,能找到authSetting[‘scope.userInfo’] 能的话就用getUserInfo获取用户信息存到全局变量userInfo里,并且令modalShow为真(以后要用插槽插入输入框)
然后没有登录的话就显示login组件
还没有登录,弹出授权框,怎么判断有没有授权呢 用到了
如果成功的话就把userInfo传给全局变量 并且把loginShow关掉(这个在组件里我已经实现了),把modalShow打开,
如果失败了就提醒授权了才能评价
不完善
// components/blog-ctrl/blog-ctrl.js
Component({
/**
* 组件的属性列表
*/
properties: {
},
/**
* 组件的初始数据
*/
data: {
showLogin:false,
showModal:false
},
/**
* 组件的方法列表
*/
methods: {
onComment(){
//判断以前有没有授权登录
var that=this
wx.getSetting({
success(res){
console.log(res);
//授权过
if(res.authSetting["scope.userInfo"]){
that.setData({
showModal:true
})
console.log(res.authSetting["scope.userInfo"]);
}
else{
that.setData({
showLogin:true
})
}
}
})
}
}
})
可以看到上面贴的代码 点击授权以后,需要再次点击评论而不是直接跳出来
所以我们又用到了
刚点击授权的话我们的个人信息是login组件里传入进来的
然后<s-login modalShow="{{showLogin}}" bindloginSuccess="loginSuccess"></s-login>
还要把userinfo存起来
loginSuccess(e){
userInfo=e.detail
console.log(userInfo);
this.setData({
showModal:true
})
}
还要用slot把输入框还有发送按钮写出来 这里有个问题
6.3 数据库1-N关系设计方式
—取决于N有多大
-
几个,十几个,最多几十个: 存在数组里,比如说
比如说还有住址之类的 -
几十,几百个 比如说产品的零件之类的 缺点是需要两次查询
这种情况可以1和N存在两个不同的集合
然后把这些零件id存到product里面
-
像微博这样的评论条数有几千个上万个:在n的一方存储1的一方的id(因为如果像前两个那样的话 数组长度可能会有限制)
我们评论的功能适用于第三种
6.4 评论功能实现
点击发送以后 就可以取到输入框里的内容了
首先 先判断一下输入内容是否为空
if (content.trim() == '') {
wx.showModal({
title: '评论内容不能为空',
content: '',
})
return
}
然后呢 我们要还要把内容,时间,个人信息,博客id存到数据库
这里插入数据库有两种情况,可以在云函数端也可以在小程序端
云函数端就是类似长这样:
这里是在小程序端完成的
首先初始化const db = wx.cloud.database()
然后新建一个集合
博客id怎么获得呢
在博客页面存了blogList
然后就可以在组件里接收了 然后可以存到数据库了(这里的content是输入框里面的value值)
6.5云调用实现模板消息推送
什么是云调用呢 是通过云函数去调用服务端的开放接口 推送的话要使用form表单
所以我们的slot插槽里面应该是个form表单
然后要怎样取到content呢
这个formId测试的时候显示是这个,真机调试的话会显示真正的formID
formId是有有效期的 (7天) 每次提交一个表单就生成一个formId
然后可以选用模板了
然后可以拿到模板id
新建一个云函数 再在里面新建一个config.json文件
注意名字不能乱起
// 云函数入口文件
const cloud = require('wx-server-sdk')
cloud.init()
// 云函数入口函数
exports.main = async(event, context) => {
const {
OPENID
} = cloud.getWXContext()
const result = await cloud.openapi.templateMessage.send({
touser: OPENID,
page: `/pages/blog-comment/blog-comment?blogId=${event.blogId}`,
data: {
keyword1: {
value: '评价完成'
},
keyword2: {
value: event.content
}
},
templateId: 'PjUkFDsOsC3ktzUATsIVy0t1D4RlL-aKbuhGUb7TLS0',
formId: event.formId
})
return result
}
db.collection('blog-comment').add({
data: {
content,
createTime: db.serverDate(),
blogId: this.properties.blogId,
nickName: userInfo.nickName,
avatarUrl: userInfo.avatarUrl
}
}).then((res) => {
// 推送模板消息
wx.cloud.callFunction({
name: 'sendMessage',
data: {
content,
formId,
blogId: this.properties.blogId
}
}).then((res) => {
console.log(res)
})
6.6博客详情功能
https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/collection/Collection.get.html
app.router('detail',async(ctx,next)=>{
let blogId=event.blogId
let detail=await blogList.where({
_id:blogId
}).get().then((res)=>{
return res.data
})
let batchTimes=await blogComment.count()
const total=batchTimes.total
let task=[]
let commentList={
data:[]
}
batchTimes=Math.ceil(total/MAX_LIMIT)
for(let i=0;i<batchTimes;i++){
let promise=blogComment.skip(i*MAX_LIMIT).limit(MAX_LIMIT)
.where({
blogId
})
.get()
task.push(promise)
}
if(task.length>0){
commentList=(await Promise.all(task)).reduce((acc,cur)=>{
return {
data:acc.data.concat(cur.data)}
})
}
ctx.body={
detail,
commentList,
}
})
_getDetail(){
wx.cloud.callFunction({
name:"blog",
data:{
$url:"detail",
blogId:this.data.blogId,
}
}).then((res)=>{
console.log(res);
this.setData({
blogContent:res.result[0]
})
})
},
写好页面之后,评论了要自动刷新,要在blog-ctrl里面的send里面写一个triggerEvent 调用_getDetail()
后面都没记 不难的
第七章 我的功能实现
7.1获取用户信息的方式
不同方式获取用户信息–btn触发 云函数获取 api接口:wx.login/wx.getUserInfo
那么多方法,我们都是在什么情况下用呢
<!-- 这种方式获取用户信息很简单 但是当前这种方式获取用户信息只能在界面上面显示 因为在js中我们是没有办法获取这些信息的 所以也没有办法进行相对应的存储或者其他业务逻辑 只能用于展示
而且并没有弹出授权框 不需要授权的 这个只能获取到自己的数据 不能得到别人的头像昵称 -->
<open-data type="userAvatarUrl"></open-data>
<open-data type="userNickName"></open-data>
那这时候我想把信息写入数据库里面 或者说可能在后面的开发当中会用到头像或者昵称–wx.getUserInfo 他的作用就是获取用户信息 当获取成功以后就会进入success回调函数 这个是在已授权的情况下才能获取用户信息
来复习一下以前写的 使用这个方法的前提是用户已经授权过了
<!-- 我想弹出一个授权框 由用户来决定要不要授权 这种改怎么做呢 --按钮组件 -->
<button open-type="getUserInfo" bindgetuserinfo="onGetUserInfo"> 点击登录</button>
我们发现上面的方法都没有openId这个属性
传统的方式需要前后端配合
云开发的话直接在云函数端就可以获取了
<!-- wxml -->
<button bindtap="getOpenId">点击登录</button>
<!-- js -->
getOpenId(){
wx.cloud.callFunction({
name:'login'
}).then((res)=>{
console.log(res);
})
},
<!-- 云函数 (就是新建云函数初始的代码)-->
exports.main = async (event, context) => {
const wxContext = cloud.getWXContext()
return {
event,
openid: wxContext.OPENID,
appid: wxContext.APPID,
unionid: wxContext.UNIONID,
}
}
这样就能获取openid了
有个问题 我们能根据openid获取用户其他信息呢 比如说昵称啊 头像啊的 是不支持的 因为我们获取openid的时候不需要用户去授权,在用户不授权的情况下我得到了openid,又在用户不知情的情况下就得到了用户的信息,对于用户信息不安全
用户只有主动允许的情况下才能获取对应的这些信息
我们看他自带的代码还有个unionid 那这个unionid跟openid有什么区别吗
当前的小程序是微信开放平台的,微信开放平台的应用不止有小程序比如说还有微信公众号 openid的话对于每一个应用都是不一样的 unionid的话都是一样的 只有unionid有效的情况下才返回
7.2页面布局与base64
我们使用base64格式有什么好处呢
当我们请求网络路径的话 我们的小程序端要向服务端发送请求,需要请求图片才行 如果用base64的话就可以减少http请求,注意一下把图片转化成base64的时候图片不要太大(转化出来会很长)
7.3播放历史页面
我们可以把记录存在数据库里,这样在不同的手机上查看小程序的时候都能得到当前的播放历史,优点是在任何设备上看到的都是一样的数据 缺点是每一次请求都需要去读取数据库 这样相对来讲效率会低一些
第二种方案就是可以把每次播放的历史存在当前手机的本地 好处就是每次读取的速度会快,缺点就是如果换另外一台手机,另外一个手机上就没有播放历史
在这个项目里采取第二种方案
我们的key可以用openid 来标识唯一用户
可以在这时候获取
getOpenid(){
wx.cloud.callFunction({
name:"login"
}).then((res)=>{
this.globalData.openId=res.result.openid
console.log(this.globalData.openId);
})
},
getOpenid(){
wx.cloud.callFunction({
name:"login"
}).then((res)=>{
//注意这里要考虑到是否之前已经存在openId 如果已经存在了的话就不用执行(否则本来是有数据的后来让空数组给覆盖了),如果不存在(以前没有打开过)就要把openId放到缓存
const openId=res.result.openid
this.globalData.openId=openId
// console.log(this.globalData.openId);
if(wx.getStorageSync(openId)===''){
wx.setStorageSync(openId, [])
console.log('初始化了');
}
})
},
可以看到第一次被初始化了 第二次没有,因为判断缓存里openId已经存在了
player.js
//保存播放历史
savePlayHistory(){
let music=musicInfo[nowPlayingIndex] //回顾一下 这里的musicInfo保存的是对应歌单的所有音乐数据,nowPlayingIndex是保存的当前播放的下标,这样就可以取到当前播放音乐的数据了
let openid=app.globalData.openId
let history=wx.getStorageSync(openid)
let bhave=false //用来判断是不是已经存进缓存了
for(let i=0,len=history.length;i<len;i++){
if(history[i].id===music.id){
console.log("重复了");
bhave=true
break
}
}
console.log(bhave);
if(!bhave){
console.log("添加");
history.unshift(music)
wx.setStorageSync(openid, history)
}
},
然后呢 我们就可以在我们新建的文件夹取出数据 然后传入到组件里,对应的列表就出来了
现在觉得组件这个东西是真的妙啊 省去了好多代码
而且组件里面还能进行跳转
点一下直接就能进到播放页面了 真的太方便了
所以在写代码前就要规划好,应该做哪些组件,传给组件的值应该是什么,还有跳转的问题,要不要用组件进行跳转
还有,他自己播放完了会自动播放下一首,这时候播放的是歌单里面的下一首
所以我们要在我们新建的文件夹里面吧musiclist替换成播放历史的歌单
onLoad: function (options) {
let openid=app.globalData.openId
let musicHistory= wx.getStorageSync(openid)
wx.setStorageSync("musiclist", musicHistory)
console.log(musicHistory);
this.setData({
musicHistory
})
},
有bug:变红问题 这应该是上古时期的bug了就先不管了
还有比如
如果再回到歌单播放coldest winter 他应该跑到最上面,我们这个
7.4我的博客页面
有两种方案,一种是在云函数里面获取一种是在小程序中查询数据库
第一种:
现在博客云函数定义一个路由
大概就是长这样吧,这样就能取到评论的数据了
这时候可以引用blog-card和blog-ctrl组件了
然后下拉刷新 上拉加载啊都可以写上去了 还有点击跳转到blog-comment页面可以参考blog页面写
还有一种是在小程序端获取,两种方法各有利弊 云函数端的话能很容易的获取openid而且便于管理
小程序端的话可以通过设置权限 就可以不用openid查询了 这里时间的处理有个坑 需要注意一下
7.5获取小程序码
扫描小程序码可以直接进入小程序对应的页面
我们新建一个云函数:getQrCode
然后要建一个config.json来配置权限
{
"permission":{
...具体去官网看
}
}
我们按照官网给的例子来写代码
这个scene是怎么用的呢
比如说电商的小程序,有个分销,用户a给b分享,用户b扫描a的小程序码,产生了消费行为,会给a反佣,那他怎么知道用户b扫描的是a的码呢,说明a的码里面有对应的参数,参数里面会存储a的信息,扫描完以后就知道扫的是谁分享的码
那可不可以把openid放到参数里面呢?当用户扫描小程序码的时候,通过码里面的openid就能知道当前的码是由哪个用户分享出去的
那当前可以用openid来放到scene里 但是注意 openid的长度是28位,整个的参数只能传32位,如果传openid的话其他的参数就会传的很少了
注意page里面不能携带参数,要携带参数的话可以放到scene里面
因为小程序还没有发布,所以就先不写了
我们这里的代码是粘贴小程序官方的代码 就是scene传递的参数不一样了 我们用的是openid
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV,
})
exports.main = async (event, context) => {
const wxContext = cloud.getWXContext()
try {
const result = await cloud.openapi.wxacode.getUnlimited({
scene: wxContext.OPENID
})
console.log(result);
} catch (err) {
console.log(err);
}
}
返回的结果是buffer类型的 但是我们要的是一张图片该怎么做呢
我们可以把它上传到云存储
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV,
})
exports.main = async (event, context) => {
const wxContext = cloud.getWXContext()
try {
const result = await cloud.openapi.wxacode.getUnlimited({
scene: wxContext.OPENID
})
// console.log(result);
const upload=await cloud.uploadFile({
cloudPath:'qrcode/'+Date.now()+'-'+Math.random()+'.png',
fileContent:result.buffer
})
return upload.fileID
} catch (err) {
console.log(err);
}
}
这样我们在云存储就能看到二维码了
我们在小程序页面接收
然后怎么看图呢 可以用wx.previewImage
wx.previewImage({
urls: [res.result],
current:res.result
})
那如何获取到传递的openid值呢
可以直接onload里面获取 直接options.sence就ok了
第八章 小程序高级知识
8.1小程序渲染层与逻辑层交互原理
网页开发的时候渲染层和逻辑层是互斥的,比如说,渲染界面的时候,脚本不会执行,脚本执行的时候不会渲染界面
如果做很长的运算的时候,页面可能就会失去响应
我们不能频繁的setdata 还有数据不在界面上显示的话就不应该定义在data里面,因为如果这样的话他都会从逻辑层再到系统层再到渲染层 这样很消耗资源
8.2小程序的运行机制与更新机制
冷启动与热启动: 当第一次打开小程序叫做冷启动,还有一种虽然打开过,但是小程序被微信主动销毁过,这种情况如果打开小程序的话也属于冷启动的情况 热启动的话就是当前的用户已经打开过某个小程序,在一定时间内再次打开小程序(5分钟左右)这时候不需要重新启动,只需要将后台的小程序切换到前台,这个过程叫做热启动。
什么是前台和后台呢:当小程序在运行,我们叫做小程序当前在前台,
小程序销毁:当小程序进入后台以后小程序并不是马上销毁,而是会维持一段存活时间,这段时间一般是5分钟,5分钟以后就会被小程序主动销毁,如果小程序占用系统资源非常高,就会被微信客户端主动回收
小程序的更新机制:
检查版本 更新
8.3小程序性能与体验优化
-
合理设置可点击元素的响应区域大小
-
避免渲染页面耗时过长
-
避免执行脚本耗时过长
-
对网络请求做必要的缓存以避免多余的请求
-
不要引入未被使用的wxss样式
-
文字颜色和背景颜色要搭配好
-
所有资源请求都使用https (http有被篡改内容的风险)
-
不使用废用接口
-
避免过大的wxml节点数目
- 一个页面少于1000个wxml节点
- 节点数深度小于30层
- 子节点数不大于60个
-
避免将不可能访问到的页面打包到小程序包里面(比如说自己实验的demo页面)
-
及时回收定时器 小程序中定时器是全局的,当页面被销毁的时候,我们应该手动销毁定时器
-
避免使用:active伪类来实现点击态 他可以实现按钮的点击状态或者文本框的点击状态来实现效果,但是在小程序使用这个语法体验是比较差的,因为滑动或者滚动的时候点击态是不会消失的,那我们还想实现点击态的效果应该怎么做呢,可以用小程序里面的hover来实现这样的功能,比如说在我的界面中,使用了navigator组件,在默认情况下会有个点击态样式,我们给他设置了hover-class属性,给他赋值为null,来解决点击态的问题,
-
滚动区可以开启惯性滚动来增强用户体验
-
避免出现js异常 如果出现异常会导致交互无法进行下去
-
所有请求的相应都正常 某个请求失败的话也会导致交互无法进行下去
-
所有请求的耗时不应太久 而且每次请求都应该加一个showloading,让用户知道程序没有假死,如果真的请求耗时太长,我们应该适当优化服务器处理的逻辑
-
避免短时间内发起太多图片请求,我们应该适量控制请求图片的数量 字体图标
-
避免短时间内发起太多请求,会导致小程序加载的很慢
关于setData
- 避免setdata的数据过大(超过1M) 每次都会从逻辑层到系统层 再从系统层发送到视图层
- 避免setdata过于频繁
- 避免未绑定在wxml的变量传入setdata
可以用audits来体验评分
8.4详解setdata
先看看官方文档怎么说的
Page.prototype.setData(Object data, Function callback)
setData
函数用于将数据从逻辑层发送到视图层(异步),同时改变对应的 this.data
的值(同步)
8.5scene的作用与应用场景
场景值就是用来描述用户是通过什么方式进入到小程序的
比如说可以扫描小程序码,可以打开微信的搜索 ,下拉聊天菜单,公众号,
那场景值有什么用呢 --他可以为用户进行行为的埋点 可以记录用户的行为,可以为以后产品的更新提供行为的数据,可以根据场景值进行分流,比如说做的是一个外卖小程序,假设是通过二维码进入,很有可能用户是在店里面,可以进入点餐模式。如果是通过公众号进入,很有可能用户不在店里面,可能在家里面或者公司,通过公众号点击公众号的菜单打开对应小程序,这样可以进入一个预定的模式。这就是场景分流。
如何获取
点击切后台就能模拟当前用户通过哪个场景值进入到小程序
还有
当用到这上面的场景值
这里就会有数据
场景值在运营还有营销方面有很大的应用
8.6页面收录sitemap.json的作用和使用方法
在微信当中有搜索功能,可以查询到跟关键字关联的公众号啊聊天记录啊小程序信息啊这些东西
有了这个以后可以根据小程序的内容查询,很像网页的seo搜索引擎优化
后面还有很多,用到了再学吧
8.7上线
第九章 后台管理系统
我们可以改数据库 但是运营人员不了解,这样就需要给后台管理员提供一个具有界面的系统
9.1后台管理系统与前后端分离架构
轮播图换图片,更改数目
新建歌单,在歌单里关联歌曲,对歌单管理
发布博客若违规,就删除博客
我们用到了vue-admin-template
9.2vue-admin-template构建管理系统前端
git clone一下 https://github.com/PanJiaChen/vue-element-admin.git
我们这里只需要基础版本就行了 地址https://panjiachen.github.io/vue-admin-template/#/dashboard
npm install来安装这些依赖
建议在做项目的时候尽量不要使用cnpm
然后可以启动服务
这时候他会跳到这里
看地址发现是运行在我们电脑的本地的
但是左侧导航栏很多东西我们用不到所以先把代码删一下
老师说vue里面只留个login和404.vue
然后要创建一个文件夹 playlist 里面新建一个list.vue
还有blog和swiper
把这个填进去 这就是页面的基本结构
把剩下那俩也改成这样
他是如何管理的呢 打开router
发现是用vuerouter管理的 import Router from 'vue-router'
怎么把这些显示到页面呢 打开router下面有个
在vue里面每一个vue文件都应该看做是一个组件
这个@符号是指的src这个文件夹
9.3koa2构建后端
前端工程师不仅要完成前端界面的部分 同时nodejs也是我们要会的技能,所以我们选用使用nodejs的koa框架
新建文件夹 之后npm init
之后npm i koa
新建文件 app.js
const Koa=require('koa')
const app=new Koa()
app.use(async (ctx)=>{
ctx.body="你好,世界"
})
app.listen(3000,()=>{
console.log("已经开启在3000端口了");
})
9.4 access token
path :http://nodejs.cn/api/path.html#path_path_resolve_paths
writefile:http://nodejs.cn/api/fs.html#fs_fs_writefile_file_data_options_callback
getTime方法 https://www.runoob.com/jsref/jsref-gettime.html
access_token的缓存与更新
https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html
详细信息
https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
我们的代码再后端写 因为在后端发起请求,在后端进行缓存的刷新
在很多的方法都调用到了access_token 所以可以把它封装成一个公共方法
然后新建一个utils文件夹 新建一个方法 getAccessToken
npm i request
npm i request-promise
const rp=require('request-promise')
const APPID='' //填自己的
const APPSECRET='' //填自己的
const URL=`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}`
const updateAccessToken=async()=>{
const resStr=await rp(URL)
const res=JSON.parse(resStr)
console.log(res);
}
updateAccessToken()
返回内容:
{
access_token: '45_joRjJGQLnlq9QRopUYx2WNkurpeT3y7rKkEUoHSfuL4xTwn4tZ829uStaE-uHt4zp56dhGK0C_pGerhw0A2PDq7Pxk5-WB10phG97ttAdsnOdqVJc4PeFEEgp0Hnm2mNfG8lktTLnt0lFMy6ZMCbAAAKXN',
expires_in: 7200
}
这个需要妥善保存,那么保存到哪里呢
如果项目比较复杂,可以用redis储存
也可以存在数据库
我们保存在json文件当中
如何写文件呢 --fs模块
writefile是异步的 writefilesync是同步的
路径一种是相对路径,一种是绝对路径
在node里 如何获取一个文件的绝对路径?
我们还需要token的创建时间,因为后面要判断当前的token是否过期
const rp=require('request-promise')
const fs=require('fs')
const path=require('path')
const fileName=path.resolve(__dirname,'./access_token.json')
console.log(fileName);
const APPID='aaa'
const APPSECRET='b6c1bbb'
const URL=`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}`
const updateAccessToken=async()=>{
const resStr=await rp(URL)
const res=JSON.parse(resStr)
console.log(res);
// 写文件
if(res.access_token){
fs.writeFileSync(fileName,JSON.stringify({
access_token:res.access_token,
createTime:new Date()
}))
}
}
updateAccessToken()
const rp=require('request-promise')
const fs=require('fs')
const path=require('path')
const fileName=path.resolve(__dirname,'./access_token.json')
console.log(fileName);
const APPID='aaa'
const APPSECRET='b6c1bbb'
const URL=`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}`
const updateAccessToken=async()=>{
const resStr=await rp(URL)
const res=JSON.parse(resStr)
console.log(res);
// 写文件
if(res.access_token){
fs.writeFileSync(fileName,JSON.stringify({
access_token:res.access_token,
createTime:new Date()
}))
}else{
//并没有得到返回值 再次获取一次
await updateAccessToken()
}
}
const getAccessToken=async()=>{
// 读取文件
const readRes=fs.readFileSync(fileName,'utf8')
const readObj=JSON.parse(readRes)
console.log(readObj);
}
updateAccessToken()
getAccessToken() // 这里能取到 是因为以前存在 如果项目是第一次运行呢
不存在的话读取能成功吗 --不能 先运行的getAccessToken再运行的update…
那如何解决呢
改一下读取文件方法
const getAccessToken = async () => {
// 读取文件
try {
const readRes = fs.readFileSync(fileName, 'utf8')
const readObj = JSON.parse(readRes)
console.log(readObj);
} catch (error) {
await updateAccessToken()
await getAccessToken()
}
}
或者这样改成同步的也可以
const rp=require('request-promise')
const fs=require('fs')
const path=require('path')
const fileName=path.resolve(__dirname,'./access_token.json')
console.log(fileName);
const APPID='aaa'
const APPSECRET='b6c1bbb'
const URL=`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}`
const updateAccessToken=async()=>{
const resStr=await rp(URL)
const res=JSON.parse(resStr)
console.log(res);
// 写文件
if(res.access_token){
fs.writeFileSync(fileName,JSON.stringify({
access_token:res.access_token,
createTime:new Date()
}))
}else{
//并没有得到返回值 再次获取一次
await updateAccessToken()
}
}
const getAccessToken=async()=>{
// 读取文件
const readRes=fs.readFileSync(fileName,'utf8')
const readObj=JSON.parse(readRes)
console.log(readObj);
}
const launch=async ()=>{
await updateAccessToken()
await getAccessToken()
}
launch()
获取操作完成
const rp=require('request-promise')
const fs=require('fs')
const path=require('path')
const fileName=path.resolve(__dirname,'./access_token.json')
console.log(fileName);
const APPID='wxc0101e15f32abc6a'
const APPSECRET='b6c1798730d84949ba7bab467fbd03bc'
const URL=`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}`
const updateAccessToken=async()=>{
const resStr=await rp(URL)
const res=JSON.parse(resStr)
console.log(res);
// 写文件
if(res.access_token){
fs.writeFileSync(fileName,JSON.stringify({
access_token:res.access_token,
createTime:new Date()
}))
}else{
//并没有得到返回值 再次获取一次
await updateAccessToken()
}
}
const getAccessToken=async()=>{
// 读取文件
try {
const readRes=fs.readFileSync(fileName,'utf8')
const readObj=JSON.parse(readRes)
console.log(readObj);
return readObj.access_token
} catch (error) {
await updateAccessToken()
await getAccessToken()
}
}
// updateAccessToken() // 其实这里直接get就行了 get不到会执行catch里面的语句
console.log(getAccessToken());
定时刷新–两个小时再次获取一次 获取新的token
开启定时器–setinterval settimeout只能循环一次,setinterval可以循环获取多次
定时功能完成
setInterval(()=>{
await updateAccessToken()
},7200*1000)
这样还不够,需要提前五分钟刷新
access_token 的有效期通过返回的expires_in 来传达,目前是7200秒之内的值,中控服务器需要根据这个有效时间提前去刷新。在刷新过程中,中控服务器可对外继续输出的老 access_token,此时公众平台后台会保证在5分钟内,新老access_token都可用,这保证了第三方业务的平滑过渡﹔
setInterval(()=>{
await updateAccessToken()
},(7200-300)*1000)
为了测试方便 改成每五秒更新一次
再完善
正常情况 我们的定时任务可以执行,是没有问题的, 但是假设由于某些情况,服务器可能宕机了 那我们的任务可能不会执行也就不会定时更新token 如果宕机时间超过两个小时 我们文件里的token就是过期的,如果服务器又重新启动 token就是旧的 所以我们读取token的时候还应该有个判断 – 当前时间和创建时间的时间差是否超过2h 如果超过两小时了就要更新一下 那么问题来了 如何判断呢 可以用getTime方法来转化成毫秒数,然后来判断是否大于2h
完整版
const rp=require('request-promise')
const fs=require('fs')
const path=require('path')
const fileName=path.resolve(__dirname,'./access_token.json')
console.log(fileName);
const APPID='wxc0101e15f32abc6a'
const APPSECRET='b6c1798730d84949ba7bab467fbd03bc'
const URL=`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}`
const updateAccessToken=async()=>{
const resStr=await rp(URL)
const res=JSON.parse(resStr)
console.log(res);
// 写文件
if(res.access_token){
fs.writeFileSync(fileName,JSON.stringify({
access_token:res.access_token,
createTime:new Date()
}))
}else{
//并没有得到返回值 再次获取一次
await updateAccessToken()
}
}
const getAccessToken=async()=>{
// 读取文件
try {
const readRes=fs.readFileSync(fileName,'utf8')
const readObj=JSON.parse(readRes)
// console.log(readObj);
const createtime=new Date(readObj.createTime).getTime()
const nowTime=new Date().getTime()
if((nowTime-createtime)/1000/60/60>=2){
await updateAccessToken()
await getAccessToken()
}
return readObj.access_token
} catch (error) {
await updateAccessToken()
await getAccessToken()
}
}
setInterval(async()=>{
await updateAccessToken()
},(5)*1000)
// updateAccessToken() // 其实这里直接get就行了 get不到会执行catch里面的语句
console.log(getAccessToken());
module.exports=getAccessToken
9.5 触发云函数获取歌单列表
koa-router使用 https://github.com/koajs/router/blob/HEAD/API.md
MVC模式
model 模型
view 视图 前端的项目就相当于我们的view层
control 控制器
M层一般是和数据打交道的
V层 就相当于前端的项目
C层 是MV层之间的桥梁纽带 控制跳转 处理业务逻辑
我们新建一个controller文件夹
我们会把前端的请求都发到controller 然后通过controller去调用云开发里的内容 并且把结果返回到前端展示
controller再新建一个文件playlist.js
get是在请求头里 长度是有限制的
post是在请求体里的 能传递的参数会多一些,并且 get传递参数是明文的 并不安全 post相对安全
controller下新建一个playlist文件
const Router=require('koa-router')
const router =new Router()
//get post
router.get('/list',async(ctx,next)=>{
//查询歌单列表
})
module.exports=router //导出
app.js里面引入它
const playlist=require(‘./controller/playlist’)
导入完成以后还需要一个router.use 声明一个路由的名称
const playlist=require('./controller/playlist')
router.use('/playlist',playlist.routes())
定义好playlist这样一个路由以后,我们还需要一个方法,这个方法需要把当前的router下面的routes进行声明
还有,我们还要允许这些方法使用
现在app.js里的代码:
const Koa=require('koa')
const Router = require('koa-router')
const app=new Koa()
const router =new Router() // 接下来就可以在对象的下面定义相应的路由
const playlist=require('./controller/playlist')
router.use('/playlist',playlist.routes())
app.use(router.routes())
app.use(router.allowedMethods())
app.use(async (ctx)=>{
ctx.body="你好,世界"
})
app.listen(3000,()=>{
console.log("已经开启在3000端口了");
})
现在再playlist.js文件下写点东西看看能不能用
那么现在我们只要在这个里面写对应的获取歌单的相关代码就ok了
那现在的问题就是如何调用小程序的列表了–调用云函数
那么如何调用playlist云函数实现相应的功能
https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/functions/invokeCloudFunction.html
POST https://api.weixin.qq.com/tcb/invokecloudfunction?access_token=ACCESS_TOKEN&env=ENV&name=FUNCTION_NAME
playlist代码
const Router=require('koa-router')
const router =new Router()
const rp=require('request-promise')
const getAccessToken=require('../utils/getAccessToken')
//get post
router.get('/list',async(ctx,next)=>{
// const accessToken=await getAccessToken()
let accessToken=await getAccessToken()
accessToken=accessToken.access_token
// console.log(accessToken,typeof(accessToken));
//查询歌单列表
const env='test-9gp1l80316116bb7'
const name='music'
const url=`https://api.weixin.qq.com/tcb/invokecloudfunction?access_token=${accessToken}&env=${env}&name=${name}`
var options={
method:'POST',
uri:url,
body:{
$url:'playlist',
start:0,
count:50
},
json:true
}
// console.log(accessToken);
await rp(options).then((res)=>{
console.log(res);
}).catch((e)=>{
console.log(e);
})
ctx.body="aaa"
})
module.exports=router //导出~
const Router=require('koa-router')
const router =new Router()
const rp=require('request-promise')
const getAccessToken=require('../utils/getAccessToken')
//get post
router.get('/list',async(ctx,next)=>{
// const accessToken=await getAccessToken()
let accessToken=await getAccessToken()
accessToken=accessToken.access_token
// console.log(accessToken,typeof(accessToken));
//查询歌单列表
const env='test-9gp1l80316116bb7'
const name='music'
const url=`https://api.weixin.qq.com/tcb/invokecloudfunction?access_token=${accessToken}&env=${env}&name=${name}`
var options={
method:'POST',
uri:url,
body:{
$url:'playlist',
start:0,
count:50
},
json:true
}
// console.log(accessToken);
ctx.body= await rp(options).then((res)=>{
console.log(res);
return JSON.parse( res.resp_data).data
}).catch((e)=>{
console.log(e);
})
// ctx.body="aaa"
})
module.exports=router //导出~
后面还有一些内容 没时间写了
更多推荐
所有评论(0)