小程序云开发发布有一段时间了,最近着手作了一个基于云开发的小程序项目--仿《微博鲜知》,来自新浪的这款全新风格的小程序虽然界面很是简约清新,可是内部仍是内藏了不少玄机,在实现的路上赶上了很多坎坷,在这里分享给你们。但愿给你们提供一些思路。html
先展现一下最终结果:更多图片资源在这里前端
开发一个完整的小程序时,咱们应该先分析其内部的结构。重复的结构抽离出来做为组件,组件很是的灵活,能够嵌入一个页面或多个页面。
node
在上面的gif图中咱们能够看到首页的内容是一个个的新闻块。
虽然这个新闻块只在首页中使用到,可是我仍是把它抽离成了一个组件。这样作的好处是页面结构将会更加的清晰,而且耦合度下降,好比想换个主界面风格时,你能够直接换另外一个组件添加进来。
git
还有新闻内部页面中,有多个小标题,每一个小标题里面嵌入了不等数量的新闻。若是不是采用组件化的话,到时候inner页面的wxml结构就会乱成一锅粥。因此这里的建议是尽可能组件化分离开来。
对于组件很陌生能够先看个人以前的这篇文章 组件化开发tabbar
下面是项目的页面与组件目录:github
既然是“全栈”,后端确定要搞搞。后端的核心就是数据。那么咱们就先把数据库分析一下。这里我是这样分析的,数据库
这里我构建了5个集合小程序
fresh-mainNews 主页新闻集合
subNews字段是一个数列,存储着fresh-subNews Doc的_id,这样就将这两个集合绑定了起来,在后面咱们会讲到在云函数中把这两个集合融合起来返回一个新的数据变得完整一些的集合。
有人可能会问,云数据库不是noSQL吗,为何不把全部数据所有整合到一个所有的JSON,那样就能够只调用一次JSON。
后端
个人理解是:
咱们查询只是须要查询咱们想要的数据,不须要的数据能够等须要的时候再根据关联去请求。
好比这个项目中的首页新闻块,每个新闻块内部都关联着大量的子新闻,第一次加载就所有把这个小程序须要的全部数据都加载出来就有点疯狂了。api
讲到这里就该说页面的构建了。页面能够想象成一个架子,一个承载数据的容器。页面通上数据,就变得活起来。MVVM,数据驱动视图。交互靠数据,组件间的通讯,组件与页面间的通讯都是数据。
{{}} -> 就像是流浪法师大招神奇的传送门。后面会将给出一个精彩的组件通讯例子(点击目录如何实现标题栏置顶)。
云开发三大核心:
云函数:通俗的理解就是你写的函数在云端运行,能够把复杂的业务逻辑放在云函数里
数据库:一个既可在小程序前端操做,也能在云函数中读写的 JSON 数据库
存储:在小程序前端直接上传/下载云端文件,在云开发控制台可视化管理,能够上传照片下载照片,或者一些其余文件。
在这里详细介绍一下操做云函数提取数据库的流程,
这里咱们以获取首页数据为例:
我这里用的是vsCode+node+yarn环境。
open in terminal(在终端中打开),yarn一下,添加依赖。
或者参考云函数官方文档
// 云函数入口文件 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, }
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 //执行滚动所花的时间 })
var that = this let 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,视图是由数据驱动的,咱们要透过现象看本质,去思考底层的数据,这样咱们很快就会有思路:
<font color=red size=4>show the code:</font>
1.catalog/index.wxml
<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>
catalog/index.js
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.js onImageLoad: 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>
优化思路是怎么样的呢?
用一个数组来存放/模拟更新的数据,若是数字的索引位置被赋值,则页面直接显示这个更新的数字,也是殊途同归之妙。由于用户关心的是数据的变化,咱们能够先把数据的变化产生,至于数据后端的变化让他异步慢慢的去作。
从这里发散思想,是否是评论功能也可以用这样的思路一样去达到极致的速度与交互体验呢。
点赞的延迟几乎为无,体验到<font color=red size=3>点赞</font>的极致快感,让人几乎停不下来~~(暗示一波)
篇幅所限,文章到这里就差很少了。
项目地址:https://github.com/HappyBirdwe/newsDance/tree/master/weiboFresh奉上
精心写的项目,细节很不错哟,欢迎你们☆☆☆ star ☆☆☆☆
结语:
学习的道路上免不了坎坷,但愿文章的分享可以为你们提供一些思路,学习的过程减小一点弯路,这就是这篇文章最大的价值,欢迎你们提问及指正。
最后在这里感谢一下:
腾讯云提供的技术支持
新浪团队的微博鲜知做者
掘金这个优秀的平台
点赞动做超帅的你
微博鲜知小程序官方传送门:
体验真的很不错哦,界面很是简约,你们能够体验一波