小程序云已经开发和发布了一段时间。最近开始做一个基于云开发的小程序项目——模仿《微博鲜知》。虽然这个来自新浪的新风格小程序界面非常简洁清新,但是里面还是隐藏着很多谜团,我们在实现的路上也遇到了很多坎坷。在这里,我们与您分享。希望能给大家一些思路。
先展示一下最终结果:更多图片资源在这里。
在开发一个完整的小程序时,首先要分析它的内部结构。重复的结构被提取为一个组件,非常灵活,可以嵌入一页或多页。
在上面的gif中,我们可以看到首页的内容是一个个新闻块。虽然这个新闻块只在首页使用,但是我把它拉出来变成了一个组件。这样做的好处是页面结构会更清晰,耦合度会降低。例如,如果要更改主界面样式,可以直接添加另一个组件。
新闻内页有几个小标题,每个小标题都嵌入了不同数量的新闻。如果没有组件化,那么内页的wxml结构就会很乱。因此,这里的建议是尽量用组件来分隔。
我不熟悉组件,所以请阅读我之前的文章,基于组件的开发选项卡。
以下是项目的页面和组件目录:
,因为是“满栈”,后端必须被占用。后端的核心是数据。然后我们先分析一下数据库。这是我的分析,
从页面中获取字段,然后了解数据之间的关系,例如一对多和一对一。我在这里建立了五个收藏。
新鲜-主要新闻主页新闻集
子新闻字段是一个数字序列,它存储了新鲜子新闻文档的_id,从而绑定了这两个集合。稍后,我们将讨论在云函数中合并这两个集合,以返回具有完整数据的新集合。
有人可能会问,云数据库不是noSQL吗?为什么不把所有的数据集成到一个JSON中,这样JSON就只能被调用一次。
我的理解是:我们只需要查询我们想要的数据,不需要的数据可以在需要的时候根据关联进行请求。比如这个项目的首页新闻块,每个新闻块内部都关联着大量的子新闻。第一次加载这个小程序需要的所有数据有点疯狂。
Fresh-subNews内部页面新闻字幕收集fresh-评论评论收集fresh-detailNews详细新闻收集fresh-用户用户收集在这里查看更多数据库信息
至此,该谈谈页面建设了。页面可以被认为是一个架子和一个数据容器。当页面被数据访问时,它会变得活跃。MVVM,数据驱动的观点。交互数据、组件之间的通信、组件与页面之间的通信都是数据。{ { } }-就像流浪法师的魔法传送门。后面会给出一个组件通信的精彩例子(如何点击目录实现标题栏顶部)。
云开发的三个核心:
云:通俗的理解就是你写的功能在云中运行,可以把复杂的业务逻辑放到云功能中。
数据库:一个JSON数据库,可以在小程序前操作,在云函数中读写。
存储:直接在小程序前端上传/下载云文件,在云开发控制台可视化管理。你可以上传照片和下载照片,或者其他一些文件。
在这里详细介绍一下操作云函数提取数据库的流程, 这里我们以获取首页数据为例:
// 云函数入口文件const cloud = require('wx-server-sdk')// 云函数初始化cloud.init()//获取数据库句柄const db = cloud.database()// 云函数入口函数exports.main = async () => { const mainNewsList = []; //向fresh-mainNews集合中获得全部数据、因为数据库里面现在存的数据不多, //如果多的话可以设置一个limit以及skip来获取特定数量的数据 const mainNews = await db.collection("fresh-mainNews").get(); for(let i = 0; i < mainNews.data.length; i++) { const mainNew = mainNews.data[i]; let user_id = mainNew.setMan; //条件查询 获取特定id的docments const user = await db.collection('fresh-users').where({ _id: user_id }).get(); //限定条件如果有多条,只添加一条进去 if (user.data.length > 0) { mainNew.setMan = user.data[0] } //这个循环是集合的拼接 for (let i = 0; i < mainNew.subNews.length; i++) { const subNews = await db.collection("fresh-subNews").where({ _id: mainNew.subNews[i] }).get(); if (subNews.data.length > 0) { mainNew.subNews[i] = subNews.data[0] } } //把拼好的docments挨个放进mainNewsList里面也就是形成了一个全新的 //融合的数据更为完整的JSON数组 mainNewsList.push(mainNew); } return mainNewsList;}复制代码
var that = this; wx.cloud.callFunction({ // 声明调用的函数名 name: 'mainNewsGet', // data里面存放的数据可以传递给云函数的event 效果:event.a = 1 data: { a: 1 } }).then(res => { //res.result的值是云函数的return的值 //这里将查询的结果放入mainNewsList中,然后就可以在wxml中调用数据 that.setData({ mainNewsList: res.result }) //打印一下结果看看有没有成功获取数据 console.log(this.data.mainNewsList) }).catch(err => { console.log(err) })
获取的数据:
我们可以看到原本的subNews里面本来存放的是_id的数值,融合后变成_id对应的整个doc
变化: [_id1.value,_id2.value~~] ---> [{_id1:value,key1:value1,key2:value2},~~~]
云函数的调用,数据库的查询。就是这么简单的四步,云开发的门槛很低,功能也很强大,只要你去尝试,很轻松的就能够实现。
const formatTime = date => { var dateNow = new Date(); var date = new Date(date); const hour = date.getHours() const minute = date.getMinutes() var times = (dateNow - date) / 1000; let tip = ''; if (times <= 0) { tip = '刚刚' return tip; } else if (Math.floor(times / 60) <= 0) { tip = '刚刚' return tip; } else if (times < 3600) { tip = Math.floor(times / 60) + '分钟前' return tip; } else if (times >= 3600 && (times <= 86400)) { tip = Math.floor(times / 3600) + '小时前' return tip; } else if (times / 86400 <= 1) { tip = Math.ceil(times / 86400) + '昨天' } else if (times / 86400 <= 31 && times / 86400 > 1) { tip = Math.ceil(times / 86400) + '天前' } else if (times / 86400 >= 31) { tip = '好几光年前~~' } else tip = null; return tip + [hour, minute].map(formatNumber).join(':')}const formatNumber = n => { n = n.toString() return n[1] ? n : '0' + n}//将这个接口暴露module.exports = { formatTime: formatTime,}复制代码
在需要的页面的xx.js里面引入
import { formatTime } from '../../utils/api.js';
格式化获取的时间数据
let mainNewsList = that.data.mainNewsList for(let i =0; i < mainNewsList.length;i++) { let time = formatTime(mainNewsList[i].time) //这是setData()的数组用法,会经常用到 var str = 'mainNewsList['+i+'].time' that.setData({ [str]:time }) }复制代码
wx.previewImage({ current: imgUrl, // 当前显示图片的http链接 urls: imagePack // 需要预览的图片http链接列表 })复制代码
wx.pageScrollTo({ scrollTop: 一个数值(自带px单位), //滚动到数值所在的位置 duration: 50 //执行滚动所花的时间 })复制代码查询节点query.selectAll('类名')及query.select('#id')官方文档var that = thislet catalogIndex = that.data.catalogIndex;query.selectAll('类名').boundingClientRect(function (rects) { rects.forEach(function (rect) { rect.top // 节点的上边界坐标st,//还有一些别的属性,这个查询节点是后面讲到的目录跳转关键API }) }) }).exec() },复制代码
//给数组设置值 还可以有var xx = 'xx['+idx+'].key'的形式var doneList = 'doneList['+idx+']' that.setData({ [doneList]: true, })复制代码
有时候我们还可以先改变某个数的值再去setData()它,这是setData()的一个很好用的技巧,不过需要去运用一下才好理解 如:
dataPack.likeNum = (supLikeNum===-1 ? dataPack.likeNum: supLikeNum); this.setData({ comment: dataPack, })复制代码
这个效果在别的小程序里面都没有见过,应该是微博鲜知独创的,在这里先对原作者表达一下敬意。内部的构造也是非常巧妙,不同于我们常见的外卖的锚点定位。
我们先来分析一波:
mvvm,视图是由数据驱动的,我们要透过现象看本质,去思考底层的数据,这样我们很快就会有思路:
<block wx:for="{{subNews}}" wx:for-item="subNewsItem" wx:for-index="idx" wx:key="index"> <view class="subTitle-item" bind:tap="scrollFind" //关键1:绑定item索引 data-hi="{{idx}}"> <text>{{subNewsItem.title}}</text> </view> </block>复制代码
scrollFind: function(e) { //点击后 实现inner页面特定新闻小标题置顶 let curIndex = e.currentTarget.dataset.hi // 关键2: 与inner页面取得联系 var myEventDetail = {index: curIndex} // detail对象,提供给事件监听函数 var myEventOption = {} // 触发事件的选项 this.triggerEvent('catalog', myEventDetail) }复制代码
onCatalog: function(e) { e.detail // 自定义组件触发事件时提供的detail对象 console.log(e.detail.index) //关键:3 把索引存储到data this.setData({ catalogIndex : e.detail.index }) //关键4: 页面可以通过组件的id取得其页面引用组件的方法// this.subNews=this.selectComponent("#subNews") this.subNews.goTop(); },复制代码
<subNews ~省略~ catalogIndex="{{catalogIndex}}" id="subNews"></subNews>复制代码
//subNews/index.wxml//一个看不见的图片,来自瀑布流的灵感,能够产生主动触发的事件<view style="display:none"> <image src="{{mainImg}}" bindload="onImageLoad"></image></view>复制代码
//subNews/index.jsonImageLoad: function () { var that = this let offsetList = that.data.offsetList; const query = wx.createSelectorQuery().in(this)//之前讲到过的API获取节点信息,我们把它存储到offsetList偏移量数组,他存储着每一个节点在屏幕的位置,//配合wx.pageScrollTo可以达到新闻栏置顶的效果 query.selectAll('.subNews-wrapper').boundingClientRect(function (rects) { rects.forEach(function (rect) { rect.top // 节点的上边界坐标 offsetList.push(rect.top) that.setData({ offsetList, }) }) }).exec() },复制代码
goTop: function (e) { var that = this let catalogIndex = that.data.catalogIndex; //这里offsetList是一个data里面的数据,来保存所有的节点的上边距坐标 let offsetList = that.data.offsetList; wx.pageScrollTo({ scrollTop: offsetList[catalogIndex], //滚动到具体数值所在的位置 duration: 50 //执行滚动所花的时间 }) }复制代码
至此,你就实现了这个看似简单却非常巧妙的功能,组件->页面->组件,秀得眼花缭乱。如果还是有些不理解的话,等下可以下载我的代码去看。
至于为什么要弄一个图片的加载然后触发那个事件呢,这是因为如果你把获取offsetList偏移量数组的函数放在goTop里的话,进入页面第一次的点击会无效,这样产生的体验肯定是非常不舒服的。
先展示一下效果:
先说一下优化的是什么:点赞效果的延迟极大降低
因为点赞的变化是由用户产生的一个交互,传统的观点就是用户点赞->后端更新数据->前端拉取数据->数据驱动视图的变化。
真实的体验就是,非常的慢,慢到点击后2秒才能看到点赞的效果,这种差劲的交互简直就是一场灾难。
for(let i = 0; i< that.data.comments.length; i++) { //当点击该个评论时,只更新这一条数据 if (i == idx) { var str = 'comments['+idx+'].likeNum' that.setData({ [str]:res.result.data.likeNum, }) console.log(likeNumList[idx]) } }
data: { doneList: [], //是否按下 likeNumList: [], //模拟点赞数数组 likeAdd: 10, //点赞每次增加数,根据你的设置来,你后端每次加1这里就写1 }, var doneList = 'doneList['+idx+']'likeNumList[idx] = (that.data.comments[idx].likeNum + that.data.likeAdd); that.setData({ likeNumList, [doneList]: true, likeAdd: that.data.likeAdd+10 })复制
<text class="dianzanNum">{{likeNumList[idx]?likeNumList[idx]:item.likeNum}}</text>复制代码
优化思路是怎么样的呢?
用一个数组来存放/模拟更新的数据,如果数字的索引位置被赋值,则页面直接显示这个更新的数字,也是异曲同工之妙。因为用户关心的是数据的变化,我们可以先把数据的变化产生,至于数据后端的变化让他异步慢慢的去做。
从这里发散思想,是不是评论功能也能够用这样的思路同样去达到极致的速度与交互体验呢。
点赞的延迟几乎为无,体验到点赞的极致快感,让人几乎停不下来~~(暗示一波)篇幅所限,文章到这里就差不多了。
项目地址:github-HappyBirdwe-weiboFresh奉上
精心写的项目,细节很不错哟,欢迎大家☆☆☆☆star☆☆☆☆结语:
学习的道路上免不了坎坷,希望文章的分享能够为大家提供一些思路,学习的过程减少一点弯路,这就是这篇文章最大的价值,欢迎大家提问及指正。