寫在前面:
小程式雲開發釋出有一段時間了,最近著手做了一個基於雲開發的小程式專案--仿《微博鮮知》,來自新浪的這款全新風格的小程式雖然介面非常簡約清新,但是內部還是內藏了很多玄機,在實現的路上遇上了不少坎坷,在這裡分享給大家。希望給大家提供一些思路。
先展示一下最終結果:更多圖片資源在這裡
一、 元件化思想
開發一個完整的小程式時,我們應該先分析其內部的結構。重複的結構抽離出來作為元件,元件非常的靈活,可以嵌入一個頁面或多個頁面。
在上面的gif圖中我們可以看到首頁的內容是一個個的新聞塊。
雖然這個新聞塊只在首頁中使用到,但是我還是把它抽離成了一個元件。這樣做的好處是頁面結構將會更加的清晰,並且耦合度降低,比如想換個主介面風格時,你可以直接換另一個元件新增進來。
還有新聞內部頁面中,有多個小標題,每個小標題裡面嵌入了不等數量的新聞。如果不是採用元件化的話,到時候inner頁面的wxml結構就會亂成一鍋粥。所以這裡的建議是儘量元件化分離開來。
對於元件很陌生可以先看我的之前的這篇文章 元件化開發tabbar
下面是專案的頁面與元件目錄:
二、資料庫設計
既然是“全棧”,後端肯定要搞搞。後端的核心就是資料。那麼我們就先把資料庫分析一下。這裡我是這樣分析的,
- 從頁面獲得欄位,
- 然後再理解資料間的關聯,如一對多,一對一。
這裡我構建了5個集合
fresh-mainNews 主頁新聞集合
subNews欄位是一個數列,儲存著fresh-subNews Doc的_id,這樣就將這兩個集合繫結了起來,在後面我們會講到在雲函式中把這兩個集合融合起來返回一個新的資料變得完整一些的集合。
有人可能會問,雲資料庫不是noSQL嗎,為什麼不把所有資料全部整合到一個全部的JSON,那樣就可以只呼叫一次JSON。
我的理解是:
我們查詢只是需要查詢我們想要的資料,不需要的資料可以等需要的時候再根據關聯去請求。
比如這個專案中的首頁新聞塊,每一個新聞塊內部都關聯著大量的子新聞,第一次載入就全部把這個小程式需要的所有資料都載入出來就有點瘋狂了。
- fresh-subNews 內部頁面新聞小標題集合
- fresh-comments 評論集合
- fresh-detailNews 詳細新聞集合
- fresh-users 使用者集合
這裡檢視更多的資料庫資訊
三、頁面構建
講到這裡就該說頁面的構建了。頁面可以想象成一個架子,一個承載資料的容器。頁面通上資料,就變得活起來。MVVM,資料驅動檢視。互動靠資料,元件間的通訊,元件與頁面間的通訊都是資料。
{{}} -> 就像是流浪法師大招神奇的傳送門。後面會將給出一個精彩的元件通訊例子(點選目錄如何實現標題欄置頂)。
四、關於雲開發。
雲開發三大核心:
雲函式:通俗的理解就是你寫的函式在雲端執行,可以把複雜的業務邏輯放在雲函式裡
資料庫:一個既可在小程式前端操作,也能在雲函式中讀寫的 JSON 資料庫
儲存:在小程式前端直接上傳/下載雲端檔案,在雲開發控制檯視覺化管理,可以上傳照片下載照片,或者一些其他檔案。
在這裡詳細介紹一下操作雲函式提取資料庫的流程,
這裡我們以獲取首頁資料為例:
- 先在雲函式目錄新建一個函式:mianNewsGet
- 編寫雲函式查詢資料
// 雲函式入口檔案
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;
}
複製程式碼
- 在首頁index.js裡面onLoad函式裡面呼叫雲函式
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},~~~]
雲函式的呼叫,資料庫的查詢。就是這麼簡單的四步,雲開發的門檻很低,功能也很強大,只要你去嘗試,很輕鬆的就能夠實現。
五、關於時間格式化。
- 寫在utils資料夾裡新增xx.js
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
})
}
複製程式碼
六、 關於一些很有用但是你可能不知道的小程式技巧
- 全屏顯示圖片,能夠實現多張圖片左右滑動並且還有數字索引現在在螢幕上,並且長按還能收藏以及下載(之前不知道這個API還特地做了一個元件來實現類似功能,簡直吐血)
wx.previewImage({
current: imgUrl, // 當前顯示圖片的http連結
urls: imagePack // 需要預覽的圖片http連結列表
})
複製程式碼
- 非常方便的一個API能夠滑動到某個位置
wx.pageScrollTo({
scrollTop: 一個數值(自帶px單位), //滾動到數值所在的位置
duration: 50 //執行滾動所花的時間
})
複製程式碼
- 查詢節點query.selectAll('類名')及query.select('#id') 官方文件
var that = this
let catalogIndex = that.data.catalogIndex;
query.selectAll('類名').boundingClientRect(function (rects) {
rects.forEach(function (rect) {
rect.top // 節點的上邊界座標st,
//還有一些別的屬性,這個查詢節點是後面講到的目錄跳轉關鍵API
})
})
}).exec()
},
複製程式碼
- setData()一些技巧。
//給陣列設定值 還可以有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,
})
複製程式碼
七、 專案最精彩的兩個部分
(文章程式碼排版閱讀可能會有不暢,喜歡可文末給出的專案地址去一探究竟)
1.點選目錄欄頁面將相應新聞欄置頂,先看下效果
這個效果在別的小程式裡面都沒有見過,應該是微博鮮知獨創的,在這裡先對原作者表達一下敬意。內部的構造也是非常巧妙,不同於我們常見的外賣的錨點定位。我們先來分析一波:
mvvm,檢視是由資料驅動的,我們要透過現象看本質,去思考底層的資料,這樣我們很快就會有思路:
- 點選目錄欄的item項如果繫結了一個data-idx等於迴圈的索引,可以在e.currentTarget.dataset.idx拿到這個item的索引。
- 我們把這個資料通過元件通訊傳遞到inner頁面,然後在由inner頁面把資料轉交給subNews
- 並且在inner頁面的js中繫結subNews的goTop事件,這樣產生了一個catalog元件->inner頁面->subNews的關聯,資料為item的索引。觸發catalog就能夠控制subNews元件的移動,是不是還有點繞, ok
show the code:
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>
複製程式碼
- 獲得索引,並繫結inner頁面 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)
}
複製程式碼
- inner/inner.js 取得與catalog的通訊
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,並且標上id
<subNews ~省略~ catalogIndex="{{catalogIndex}}" id="subNews"></subNews>
複製程式碼
- 在subNews中先定義一個圖片載入事件,這樣在頁面載入完成時會觸發其繫結的事件,這是來自瀑布流的靈感。可以在圖片載入出來的時候觸發onImageLoad函式,而在這個函式裡我們可以幹一些準備的事情。
//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事件
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. 點贊優化
先展示一下效果:
先說一下優化的是什麼:點贊效果的延遲極大降低,點贊也能如牛奶般絲滑因為點讚的變化是由使用者產生的一個互動,傳統的觀點就是使用者點贊->後端更新資料->前端拉取資料->資料驅動檢視的變化。
真實的體驗就是,非常的慢,慢到點選後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.com/HappyBirdwe…
精心寫的專案,細節很不錯喲,歡迎大家☆☆☆☆star☆☆☆☆
結語:
學習的道路上免不了坎坷,希望文章的分享能夠為大家提供一些思路,學習的過程減少一點彎路,這就是這篇文章最大的價值,歡迎大家提問及指正。
最後在這裡感謝一下:
騰訊雲提供的技術支援☆
新浪團隊的微博鮮知作者☆
掘金這個優秀的平臺☆
點贊動作超帥的你☆
微博鮮知小程式官方傳送門:
體驗真的很不錯哦,介面非常簡約,大家可以體驗一波