企業級微信小程式實戰詳解

無敵UFO發表於2019-04-10

專案地址:github.com/wudiufo/WeC…

完成效果展示:www.bilibili.com/video/av488…


小愛心是否點贊元件 components/like

企業級微信小程式實戰詳解

思路:

like 預設為 false,顯示空心小愛心

觸控執行tap:onLike 方法,因為 this.setData({count:count,like:!like})是非同步的,先執行count = like ? count - 1 : count + 1,這時like還是false,執行count+1。然後在執行this.setData()方法,將like變為true,顯示實心小愛心。

let behavior = like ? 'like' : 'cancel'
        //自定義事件
      this.triggerEvent('like', {
        behavior: behavior
      }, {})
複製程式碼

自定義事件like,當like為真時,behavior為like,在models/like.js中,let url = behavior === 'like' ? 'like/cancel' : 'like',因為behavior === 'like'為真,就呼叫伺服器介面'like/cancel',相反就呼叫like介面。

剛開始實心就呼叫'like/cancel'介面,空心就呼叫'like'介面


底部左右切換元件 components/navi

企業級微信小程式實戰詳解

思路:

navi/index.js中:
先定義哪些資料是外部傳來的資料,哪些資料是私有資料
properties: {//外部傳來的資料
    title: String,
    first: Boolean, //如果是第一期向右的箭頭就禁用,預設是false
    latest: Boolean //如果是最新的一期向左的箭頭就禁用,預設是false
  },
 data: {//私有資料
    disLeftSrc: './images/triangle.dis@left.png',
    leftSrc: './images/triangle@left.png',
    disRightSrc: './images/triangle.dis@right.png',
    rightSrc: './images/triangle@right.png'
  },

複製程式碼

左箭頭:

在navi/index.wxml中<image bind:tap="onLeft" class="icon" src="{{latest?disLeftSrc:leftSrc}}"/>

src顯示圖片規則:如果是最新的期刊,就顯示向左禁用狀態disLeftSrc箭頭;如果不是最新一期的期刊,就顯示向左可用狀態leftSrc箭頭

為圖片繫結觸控事件onLeft,在navi/index.js中:

在 methods 中:如果不是最新的期刊,就繼續繫結自定義事件left
onLeft: function(event) { //不是最新一期
     if (!this.properties.latest) {
       this.triggerEvent('left', {}, {})
     }

   },
複製程式碼

右箭頭:

在navi/index.wxml中<image bind:tap="onRight" class="icon" src="{{first?disRightSrc:rightSrc}}"/>

src顯示圖片規則:如果是第一期的期刊,就顯示向右禁用狀態disRightSrc箭頭;如果不是第一期的期刊,就顯示向右可用狀態rightSrc箭頭

為圖片繫結觸控事件onRight,在navi/index.js中:

在 methods 中:如果不是第一期的期刊,就繼續繫結自定義事件right
onRight: function(event) { //不是第一期
     if (!this.properties.first) {
       this.triggerEvent('right', {}, {})
     }

   }
複製程式碼
pages/classic中:
1:在 classic.json 中,註冊使用navi自定義元件
{
 "usingComponents": {
   "v-like": "/components/like/index",
   "v-movie": "/components/classic/movie/index",
   "v-episode": "/components/episode/index",
   "v-navi": "/components/navi/index"
 }
}
2:在 classic.wxml 中:繫結自定義事件left, 獲取當前一期的下一期;繫結自定義事件right,獲取當前一期的上一期
<v-navi bind:left="onNext" bind:right="onPrevious" class="nav" title="{{classic.title}}" first="{{first}}" latest="{{latest}}"/>
   
3:在 classic.js 中:
// 獲取當前一期的下一期,左箭頭
onNext: function(evevt) {this._updateClassic('next')
 },
// 獲取當前一期的上一期,右箭頭
onPrevious: function(evevt) { this._updateClassic('previous')
 },
// 重複程式碼過多,利用函式封裝的思想,新建一個函式抽取公共程式碼
// 傳送請求,獲取當前頁的索引,更新資料
 _updateClassic: function(nextOrPrevious) {
   let index = this.data.classic.index
   classicModel.getClassic(index, nextOrPrevious, (res) => {
     // console.log(res)
     this.setData({
       classic: res,
       latest: classicModel.isLatest(res.index),
       first: classicModel.isFirst(res.index)
     })
   })
 },
     
4:在 models/classic.js 中:
// 當前的期刊是否為第一期,first就變為true,右箭頭就顯示禁用
 isFirst(index) {
   return index === 1 ? true : false
 }
// 當前的期刊是否為最新的一期,latest就變為TRUE,左箭頭就顯示禁用
// 由於伺服器資料還會更新,確定不了最新期刊的索引,所以就要利用快取機制,將最新期刊的索引存入到快取中,如果外界傳進來的索引和快取的最新期刊的索一樣,latest就變為TRUE,左箭頭就顯示禁用
 isLatest(index) {
   let latestIndex = this._getLatestIndex()
   return latestIndex === index ? true : false
 }
// 將最新的期刊index存入快取
 _setLatestIndex(index) {
   wx.setStorageSync('latest', index)
 }

 // 在快取中獲取最新期刊的index
 _getLatestIndex() {
   let index = wx.getStorageSync('latest')
   return index
 }
複製程式碼

優化快取。解決每次觸控左右箭頭都會頻繁向伺服器傳送請求,這樣非常耗效能,使用者體驗極差。解決方法,就是把第一次傳送請求的資料都快取到本地,再次觸控箭頭時,會先查詢本地快取是否有資料,有就直接從快取中讀取資料,沒有就在向伺服器傳送請求,這樣利用快取機制大大的提高了使用者的體驗。(但也有一部分是需要實時更新的,比如是否點讚的小愛心元件,需要每次都向伺服器傳送請求獲取最新資料)

在 models/classic.js 中:
1:
// 設定快取中的key 的樣式,classic-1這種樣式
  _getKey(index) {
    let key = `classic-${index}`
    return key
  }
2:
  // 因為getPrevious,getNext實現程式碼相似,所以為了簡化程式碼可以合併為一個函式
  // 快取思路:在快取中尋找key,找不到就傳送請求 API,將key寫入到快取中。解決每次都呼叫Api向伺服器發請求,耗費效能
  // 在快取中,確定key
  getClassic(index, nextOrPrevious, sCallback) {
    //0: 是next,觸控向左箭頭獲取下一期,觸控向右箭頭否則獲取上一期
    let key = nextOrPrevious === 'next' ? this._getKey(index + 1) : this._getKey(index - 1)
    //1:在快取中尋找key
    let classic = wx.getStorageSync(key)
    //2:如果快取中找不到key,就呼叫伺服器API傳送請求獲取資料
    if (!classic) {
      this.request({
        url: `classic/${index}/${nextOrPrevious}`,
        success: (res) => {
            //將獲取到的資料設定到快取中
          wx.setStorageSync(this._getKey(res.index), res)
            //再把獲取到的資料返回,供使用者調取使用
          sCallback(res)
        }
      })
    } else { //3:如果在快取中有找到key,將快取中key對應的value值,返回給使用者,供使用者調取使用
      sCallback(classic)
    }

  }
--------------------------------------------------------------------------------

// 獲取最新的期刊利用快取機制進一步優化
 //獲取最新的期刊
  getLatest(cb) {
    this.request({
      url: 'classic/latest',
      success: (res) => {
//將最新的期刊index存入快取,防止觸控向左箭頭時,沒有設定latest的值,左箭頭會一直觸發傳送請求找不到最新的期刊報錯
        this._setLatestIndex(res.index)
          //再把獲取到的資料返回,供使用者調取使用
        cb(res)
        // 將最新的期刊設定到快取中,先調取 this._getKey() 方法,為最新獲取的期刊設定key值,呼叫微信設定快取方法將key,和對應的value值res存進去
        let key = this._getKey(res.index)
        wx.setStorageSync(key, res)
		
      }
    })
  }
複製程式碼

處理是否點贊小愛心元件的快取問題:他不需要快取,需要實時獲取最新資料

在 models/like.js 中:
//編寫一個獲取點贊資訊的方法,從伺服器獲取最新點贊資訊的資料
  // 獲取點贊資訊
  getClassicLikeStatus(artID, category, cb) {
    this.request({
      url: `classic/${category}/${artID}/favor`,
      success: cb
    })
  }

複製程式碼
在 pages/classic/classic.js 中:
//設定私有資料初始值
data: {
    classic: null,
    latest: true,
    first: false,
    likeCount: 0,//點讚的數量
    likeStatus: false //點讚的狀態

  },
      
 // 在classic.wxml中: <v-like class="like" bind:like="onLike" like="{{likeStatus}}" count="{{likeCount}}"/>
      
 // 編寫一個私有方法獲取點贊資訊
  // 獲取點贊資訊
  _getLikeStatus: function(artID, category) {
    likeModel.getClassicLikeStatus(artID, category, (res) => {
      this.setData({
        likeCount: res.fav_nums,
        likeStatus: res.like_status
      })
    })
  },
      
  //生命週期函式--監聽頁面載入
  onLoad: function(options) {
    classicModel.getLatest((res) => {
      console.log(res)
        // this._getLikeStatus(res.id, res.type) //不能這樣寫,會多發一次favor請求,消耗效能
      this.setData({
        classic: res,
        likeCount: res.fav_nums,
        likeStatus: res.like_status
      })
    })
複製程式碼

在 classic/music/index.js 中:

解決切換期刊時,其他期刊也都是播放狀態的問題。應該是,切換期刊時音樂就停止播放,回到預設不播放狀態

利用元件事件的通訊機制,小程式中只有父子元件

在 components/classic/music/inddex.js 中:

方案一:

//利用元件生命週期,只有 wx:if 才可以從頭掉起元件生命週期
// 元件解除安裝的生命週期函式
  // 元件解除安裝音樂停止播放,但這時不生效是因為,在classic.wxml中用的是hidden,應改為if
  detached: function(event) {
    mMgr.stop()
  },
 // 在 pages/classic/classic.wxml 中
 //     <v-music wx:if="{{classic.type===200}}" img="{{classic.image}}" content="{{classic.content}}" src="{{classic.url}}" title="{{classic.title}}"/>
      
複製程式碼

知識點補充:

wx:if vs hidden,和Vue框架的v-if和v-show 指令一樣: wx:if 》他是惰性的,如果初始值為false框架什麼也不做,如果初始值為true框架才會區域性渲染。true或false的切換就是從頁面中區域性加入或移除的過程。wx:if 有更高的切換消耗,如果在執行時條件不大可能改變則 wx:if 較好。生命週期會重新執行。 hidden 》元件始終會被渲染,只是簡單的控制顯示與隱藏。hidden 有更高的初始渲染消耗。如果需要頻繁切換的情景下,用 hidden 更好。生命週期不會重新執行。

方案二:(推薦使用)

解決切換期刊時音樂可以當做背景音樂一直播放,而其他的期刊是預設是不播放狀態

在 components/classic/music/inddex.js 中:
//為了保證期刊在切換時,背景音樂可以一直播放,就要去除掉 mMgr.stop() 事件方法
detached: function(event) {
    // mMgr.stop() //為了保證背景音樂的持續播放就不能加stop
  },
      
// 監聽音樂的播放狀態,如果當前頁面沒有播放的音樂,就設定playing為false。如果當前頁面的音樂地址classic.url和當前正在播放的音樂的地址一樣,就讓播放狀態為true
_recoverStatus: function() {
      if (mMgr.paused) {
        this.setData({
          playing: false
        })
        return
      }
      if (mMgr.src === this.properties.src) {
        
          this.setData({
            playing: true
          })
        
        
      }
    },
        
        // 監聽播放狀態,總控開關就可以控制播放狀態,結局總控開關和頁面不同步問題
    _monitorSwitch: function() {
      console.log('monitorSwitch背景音訊', '觸發3')
        // 監聽背景音訊播放事件
      mMgr.onPlay(() => {
          this._recoverStatus()
          console.log('onPlay ' + this.data.playing)
        })
        // 監聽背景音訊暫停事件
      mMgr.onPause(() => {
          this._recoverStatus()
          console.log('onPause ' + this.data.playing)
        })
        // 關閉音樂控制檯,監聽背景音訊停止事件
      mMgr.onStop(() => {
          this._recoverStatus()
          console.log('onStop ' + this.data.playing)
        })
        // 監聽背景音訊自然播放結束事件
      mMgr.onEnded(() => {
        this._recoverStatus()
        console.log('onEnded ' + this.data.playing)
      })
    },
        
  //呼叫生命週期函式,每次切換都會觸發attached生命週期
        // 在元件例項進入頁面節點樹時執行
  // hidden,ready,created都觸發不了生命週期函式
  attached: function(event) {
    console.log('attach例項進入頁面', '觸發1')
    this._monitorSwitch()
    this._recoverStatus()


  },
複製程式碼

播放動畫旋轉效果製作:

在 components/classic/music/index.wxss 中:
//定義幀動畫用CSS3
.rotation {
  -webkit-transform: rotate(360deg);
  animation: rotation 12s linear infinite;
  -moz-animation: rotation 12s linear infinite;
  -webkit-animation: rotation 12s linear infinite;
  -o-animation: rotation 12s linear infinite;
}

@-webkit-keyframes rotation {
  from {
    -webkit-transform: rotate(0deg);
  }
  to {
    -webkit-transform: rotate(360deg);
  }
}

複製程式碼

補充css3知識點:

》使用CSS3開啟GPU硬體加速提升網站動畫渲染效能: 為動畫DOM元素新增CSS3樣式-webkit-transform:transition3d(0,0,0)或-webkit-transform:translateZ(0);,這兩個屬性都會開啟GPU硬體加速模式,從而讓瀏覽器在渲染動畫時從CPU轉向GPU,其實說白了這是一個小伎倆,也可以算是一個Hack,-webkit-transform:transition3d和-webkit-transform:translateZ其實是為了渲染3D樣式,但我們設定值為0後,並沒有真正使用3D效果,但瀏覽器卻因此開啟了GPU硬體加速模式。 》這種GPU硬體加速在當今PC機及移動裝置上都已普及,在移動端的效能提升是相當顯著地,所以建議大家在做動畫時可以嘗試一下開啟GPU硬體加速。

》適用情況 通過-webkit-transform:transition3d/translateZ開啟GPU硬體加速的適用範圍:

使用很多大尺寸圖片(尤其是PNG24圖)進行動畫的頁面。 頁面有很多大尺寸圖片並且進行了css縮放處理,頁面可以滾動時。 使用background-size:cover設定大尺寸背景圖,並且頁面可以滾動時。(詳見:coderwall.com/p/j5udlw) 編寫大量DOM元素進行CSS3動畫時(transition/transform/keyframes/absTop&Left) 使用很多PNG圖片拼接成CSS Sprite時 》總結   通過開啟GPU硬體加速雖然可以提升動畫渲染效能或解決一些棘手問題,但使用仍需謹慎,使用前一定要進行嚴謹的測試,否則它反而會大量佔用瀏覽網頁使用者的系統資源,尤其是在移動端,肆無忌憚的開啟GPU硬體加速會導致大量消耗裝置電量,降低電池壽命等問題。

在 components/classic/music/index.wxml 中:
//為圖片加上播放就旋轉的類,不播放 就就為空字串
<image class="classic-img {{playing?'rotation':''}}"  src="{{img}}"></image>

複製程式碼

用 slot 插槽,解決在公用元件中可以加入其他修飾內容問題。其實就是,在定義公用元件時,用 slot 命名插槽佔位,在父元件呼叫時可以傳遞需要的內容補位。和Vue的指令 v-slot 相似。

在 components/tag/index.js 中:
//在 Component 中加入
// 啟用slot
  options: {
    multipleSlots: true
  },
複製程式碼
在定義的公共元件 components/tag/index.wxml 中:
//定義幾個命名插槽,供父元素佔位使用
<view class="container tag-class">
  <slot name="before"></slot>
  <text>{{text}}</text>
  <slot name="after"></slot>
</view>
複製程式碼
在 pages/detail/detail.json 中:
//註冊並使用元件
{
  "usingComponents": {
    "v-tag": "/components/tag/index"
  }
}
複製程式碼
在 pages/detail/detail.wxml 中:
//使用元件v-tag,補位命名插槽
<v-tag tag-class="{{index===0?'ex-tag1':''||index===1?'ex-tag2':''}}" text="{{item.content}}">
     <text class="num" slot="after">{{'+'+item.nums}}</text>
</v-tag>
複製程式碼

pages/detail/detail 中,解決評論內容自定義元件 v-tag 評論前兩條顯示兩種顏色的做法:
第一種方法:(推薦使用)
在 pages/detail/detail.wxss 中:
/* v-tag是自定義元件,不能使用css3,在微信小程式中,只有內建元件才可以用css3 */
/*用CSS hack方式給自定義元件加樣式*/
.comment-container>v-tag:nth-child(1)>view {
  background-color: #fffbdd;
}

.comment-container>v-tag:nth-child(2)>view {
  background-color: #eefbff;
}
複製程式碼
第二種方法:

定義外部樣式方法,像父子元件傳遞屬性一樣,傳遞樣式類

在 detail.wxss 中:
/* 定義外部樣式 */

.ex-tag1 {
  background-color: #fffbdd !important;
}

.ex-tag2 {
  background-color: #eefbff !important;
}
複製程式碼
在 detail.wxml 中:
/*將自定義的樣式類通過屬性傳值的方式傳遞給自定義子元件v-tag */
<v-tag tag-class="{{index===0?'ex-tag1':''||index===1?'ex-tag2':''}}" text="{{item.content}}">
        <text class="num" slot="after">{{'+'+item.nums}}</text>
</v-tag>
複製程式碼
在 components/tag/index.js 中:
//將外部傳進來的樣式寫在Component中,宣告一下
// 外部傳進來的css,樣式
  externalClasses: ['tag-class'],

複製程式碼
在 components/tag/index.wxml 中:
// 把父元件傳遞過來的類 tag-calss 寫在 class 類上
<view class="container tag-class">
  <slot name="before"></slot>
  <text>{{text}}</text>
  <slot name="after"></slot>
</view>
複製程式碼

解決伺服器返回的內容簡介有 \n 換行符的問題:

原因:

是因為伺服器返回的原始資料 是 \\n ,經過轉義就變成 \n

\n 在text文字標籤中預設轉義為換行

解決方法:

WXS:WXS(WeiXin Script)是小程式的一套指令碼語言,結合 WXML,可以構建出頁面的結構。和Vue 中的 Vue.filter(過濾器名,過濾器方法) 很相似。WXS 與 JavaScript 是不同的語言,有自己的語法,並不和 JavaScript 一致。由於執行環境的差異,在 iOS 裝置上小程式內的 WXS 會比 JavaScript 程式碼快 2 ~ 20 倍。在 android 裝置上二者執行效率無差異。

在 utils/filter.wxs 中:
// 定義過濾器函式,處理伺服器返回的資料,將 \\n 變成 \n
// 會列印兩次,undefined和請求得到的資料,因為第一次初始時text為null,傳送請求得到資料後呼叫setData更新資料一次
var format = function(text) {
  console.log(text)
    
  if (!text) {
    return
  }
  var reg = getRegExp('\\\\n', 'g')
  return text.replace(reg, '\n')
}

module.exports.format = format
複製程式碼
在 pages/detail/detail.wxml 中:
//引入
<wxs src="../../utils/filter.wxs" module="util"/>
//在需要過濾的資料中使用
<text class="content">{{util.format(book.summary)}}</text>
複製程式碼

解決解決伺服器返回的內容簡介首行縮排的問題:

在 pages/detail/detail.wxss 中:
//對需要縮排的段落前加以下的類,但這時只有第一段縮排
.content {
  text-indent: 58rpx;
  font-weight: 500;
}
複製程式碼
在 utils/filter.wxs 中:
//用轉義字元 &nbsp; 作為空格,但這時小程式會以&nbsp;樣式輸出,不是我們想要的效果
var format = function(text) { 
  if (!text) {
    return
  }
  var reg = getRegExp('\\\\n', 'g')
  return text.replace(reg, '\n&nbsp;&nbsp;&nbsp;&nbsp;')
}

module.exports.format = format
複製程式碼
在 pages/detail/detail.wxml 中:
//加入屬性  decode="{{true}}",首行縮排問題解決
<text class="content" decode="{{true}}">{{util.format(book.summary)}}</text>
複製程式碼

解決短評過多讓其只顯示一部分的問題:

在 utils/filter.wxs 中:
//新增一個限制短評長度的過濾器,並匯出
// 限制短評的長度的過濾器
var limit = function(array, length) {
  return array.slice(0, length)
}

module.exports = {
  format: format,
  limit: limit
};
複製程式碼
在 pages/detail/detail.wxml 中:
<wxs src="../../utils/filter.wxs" module="util"/>

<view class="sub-container">
    <text class="headline">短評</text>
    <view class="comment-container">
      <block wx:for="{{util.limit(comments,10)}}" wx:key="content">
        <v-tag tag-class="{{index===0?'ex-tag1':''||index===1?'ex-tag2':''}}" text="{{item.content}}">
        <text class="num" slot="after">{{'+'+item.nums}}</text>
        </v-tag>
      </block>
    </view>
  </view>


複製程式碼
在 pages/detail/detail.wxml 中:進一步優化
// 由於 <v-tag tag-class="{{index===0?'ex-tag1':''||index===1?'ex-tag2':''}}" text="{{item.content}}"> 過於亂,改寫成wxs形式:
   
//先定義wxs過濾器
    <wxs module="tool">
  var highlight = function(index){
    if(index===0){
      return 'ex-tag1'
    }
    else if(index===1){
      return 'ex-tag2'
    }
    return ''
  }
  module.exports={
    highlight:highlight
  }
</wxs>
    
    //改寫為:
    <v-tag tag-class="{{tool.highlight(index)}}" text="{{item.content}}">
    
複製程式碼

詳情最底部短評的實現:

使用者提交評論內容:

點選標籤向伺服器提交評論內容:

在 components\tag\index.wxml 中:
//為短評元件繫結出沒事件 onTap
<view bind:tap="onTap" class="container tag-class">
  <slot name="before"></slot>
  <text>{{text}}</text>
  <slot name="after"></slot>
</view>
複製程式碼
在 components\tag\index.js 中:
// 當觸控短評小標籤時,觸發一個自定義事件,將短評內容傳進去,公父元件呼叫自定義事件tapping
 methods: {
    // 觸控短評小標籤時,觸發的事件,觸發一個自定義事件
    onTap(event) {
      this.triggerEvent('tapping', {
        text: this.properties.text
      })
    }
  }
複製程式碼
在 pages\detail\detail.wxml 中:
//在父元件中呼叫子元件的自定義tapping事件,並且觸發事件onPost
<v-tag bind:tapping="onPost" tag-class="{{tool.highlight(index)}}" text="{{item.content}}">
    <text class="num" slot="after">{{'+'+item.nums}}</text>
</v-tag>
複製程式碼
在 models\book.js 中:
//調取新增短評的介面
// 新增短評
  postComment(bid, comment) {
    return this.request({
      url: '/book/add/short_comment',
      method: 'POST',
      data: {
        book_id: bid,
        content: comment
      }
    })
  }
複製程式碼
在 pages\detail\detail.js 中:
// 觸控tag元件會觸發,input輸入框也會觸發事件onPost
// 獲取使用者的輸入內容或觸發tag裡的內容,並且對使用者輸入的評論做校驗,如果評論的內容長度大於12就彈出警告不向伺服器傳送請求
//如果評論內容符合規範,就呼叫新增短評介面並將最新的評論插到comments陣列的第一位,更新資料,並且把蒙層mask關閉
onPost(event) {
    // 獲取觸發tag裡的內容
    const comment = event.detail.text
      // 對使用者輸入的評論做校驗
    if (comment.length > 12) {
      wx.showToast({
        title: '短評最多12個字',
        icon: 'none'
      })
      return
    }

    // 呼叫新增短評介面並將最新的評論插到comments陣列的第一位
    bookModel.postComment(this.data.book.id, comment).then(res => {
      wx.showToast({
        title: '+1',
        icon: 'none'
      })
      this.data.comments.unshift({
        content: comment,
        nums: 1 //這是後面的數量顯示
      })
      this.setData({
        comments: this.data.comments,
        posting: false
      })
    })
  },

複製程式碼
在 pages\detail\detail.wxml 中:
// input有自己的繫結事件bindconfirm,會呼叫手機鍵盤完成按鍵
<input bindconfirm="onPost"  type="text" class="post" placeholder="短評最多12個字"/>
複製程式碼
在 pages\detail\detail.js 中:

點選標籤向伺服器提交評論內容完成:

// 觸控tag元件會觸發,input輸入框也會觸發事件onPost事件;然後獲取觸發tag裡的內容或獲取使用者input輸入的內容;對tag裡的內容或對使用者輸入的評論做校驗並且輸入的內容不能為空;最後呼叫新增短評介面並將最新的評論插到comments陣列的第一位,並且把蒙層mask關閉

 onPost(event) {
    const comment = event.detail.text||event.detail.value
    console.log('comment'+comment)
    console.log('commentInput'+comment)
    if (comment.length > 12|| !comment) {
      wx.showToast({
        title: '短評最多12個字',
        icon: 'none'
      })
      return
    }
    bookModel.postComment(this.data.book.id, comment).then(res => {
      wx.showToast({
        title: '+1',
        icon: 'none'
      })
      this.data.comments.unshift({
        content: comment,
        nums: 1 //這是後面的數量顯示
      })
      this.setData({
        comments: this.data.comments,
        posting: false
      })
    })
  },
複製程式碼

細節處理:

如果沒有短評顯示問題:

在 pages\detail\detail.wxml 中:
//在短評後加上還沒有短評標籤,如果沒有comments短評就不顯示還沒有短評標籤
<text class="headline">短評</text>
<text class="shadow" wx:if="{{!comments.length}}">還沒有短評</text>

//在儘可點選標籤+1後加上暫無評論標籤,如果沒有comments短評就不顯示暫無評論標籤
<text wx:if="{{!comments.length}}">儘可點選標籤+1</text>
<text wx:else>暫無短評</text>
複製程式碼

由於需要載入的資料較多,為了提高使用者體驗,需要加一個loading提示資料正在載入中,資料載入完成後就消失;

由於都是利用promise非同步載入資料,這時取消loading顯示應該加到每個promise後,顯然不符合需求。如果利用回撥函式機制,先載入1在一的回撥函式裡在載入2依次順序載入,在最後一個回撥函式中寫取消loading操作,這樣的方式雖然可以實現,但非常耗時間,請求是序列的,假如一個請求需要花費2s中,發三個請求就要花費6秒,非常耗時,而且還會出現回撥地獄的現象,不推薦使用。

解決方法:在Promise中,有一個Promise.all()方法就可以解決。

補充知識點:

Promise.all(iterable) 方法返回一個 Promise 例項,此例項在 iterable 引數內所有的 promise 都“完成(resolved)”或引數中不包含 promise 時回撥完成(resolve);如果引數中 promise 有一個失敗(rejected),此例項回撥失敗(reject),失敗原因的是第一個失敗 promise 的結果。簡單來說就是:只要有一個陣列裡的promise獲取失敗就呼叫reject回撥,只有全部陣列裡的promise都成功才呼叫resolve回撥。

Promise.race(iterable) 方法返回一個 promise,一旦迭代器中的某個promise解決或拒絕,返回的 promise就會解決或拒絕。race 函式返回一個 Promise,它將與第一個傳遞的 promise 相同的完成方式被完成。它可以是完成( resolves),也可以是失敗(rejects),這要取決於第一個完成的方式是兩個中的哪個。如果傳的迭代是空的,則返回的 promise 將永遠等待。如果迭代包含一個或多個非承諾值和/或已解決/拒絕的承諾,則Promise.race 將解析為迭代中找到的第一個值。簡單來說就是:不論成功還是失敗的回撥,哪一個快就執行哪個。

在 pages\detail\detail.js 中:
//用了 Promise.all(iterable) 方法就不用寫三個Promise方法分別來更新資料了,可以簡寫成一個all方法再返回的成功的promise中呼叫setData(),更新請求回的資料
onLoad: function(options) {
    // 資料載入時顯示loading效果
    wx.showLoading()
    const bid = options.bid
    console.log(bid)
      // 獲取書籍詳細資訊
    const detail = bookModel.getDetail(bid)
      // 獲取書籍點贊情況
    const likeStatus = bookModel.getLikeStatus(bid)
      // 獲取書籍短評
    const comments = bookModel.getComments(bid)

    // 資料載入完成時取消顯示loading效果
      Promise.all([detail, comments, likeStatus]).then(res => {
      console.log(res)
      this.setData({
        book: res[0],
        comments: res[1].comments,
        likeStatus: res[2].like_status,
        likeCount: res[2].fav_nums
      })
      wx.hideLoading()
    })

    
  },
複製程式碼

圖書的搜尋:

高階元件:如果一個元件裡面的內容比較複雜,包含大量的業務

知識點補充:

工作中我們通常把業務處理邏輯寫在models中:

可以寫在單個公用元件裡,只供自己寫業務邏輯調取使用;

可以寫在components中,只供components內的元件調取使用,如果想把components釋出出去給其他專案用或者提供給其他開發者使用;

可以寫在專案根目錄models下,供整個專案調取使用寫業務邏輯,如果只是做個專案建議寫在這裡不會亂。

在 components\search\index 中:

處理歷史搜尋和熱門搜尋:

歷史搜尋:將歷史搜尋關鍵字寫入快取中,在從快取中獲取歷史搜尋關鍵字。

熱門搜尋:調取伺服器API

GET /book/hot_list

將業務邏輯寫在 models\keyword.js 中:
//首先從快取中獲取歷史搜尋關鍵字陣列,判斷獲取的陣列是否為空,如果為空,為了防止報錯就返回空陣列;如果不為空就直接返回獲取的陣列。
//將搜尋關鍵字寫入快取中,先從快取中獲取歷史關鍵字的陣列,判斷是否包含此次輸入的關鍵字,如果沒有此關鍵字,如果獲取的長度大於最大長度,就將陣列的最後一項刪除;如果獲取陣列的長度小於最大長度,就將此次輸入的關鍵字加到陣列的第一位,並且設定到快取中。
class KeywordModel {
  constructor() {
    // 把key屬性掛載到當前例項上,供例項調取使用
    this.key = 'q',
    this.maxLength = 10 //搜尋關鍵字的陣列最大長度為10
  }

  //從快取中,獲取歷史搜尋關鍵字陣列,如果快取中沒有直接返回空陣列
  getHistory() {
    const words = wx.getStorageSync(this.key)
    if (!words) {
      return []
    }
    return words
  }

  // 將歷史搜尋關鍵字寫入快取中。先從快取中獲取歷史關鍵字的陣列,判斷陣列中是否已經有此關鍵字。如果沒有,並且獲取陣列的長度大於最大長度,就將陣列最後一項刪除。獲取陣列的長度小於最大長度就將此次輸入的關鍵字加到陣列第一位,並且設定到快取中;
  addToHistory(keyword) {
    let words = this.getHistory()
    const has = words.includes(keyword)
    if (!has) {
      const length = words.length
      if (length >= this.maxLength) {
        words.pop()
      }
      words.unshift(keyword)
      wx.setStorageSync(this.key, words)
    }

  }

  // 獲取熱門搜素搜關鍵字
  getHot() {

  }
}

export {KeywordModel}
複製程式碼
在 components\search\index.wxml 中:
//為input輸入框繫結onConfirm事件
<input bind:confirm="onConfirm" type="text" class="bar" placeholder-class="in-bar" placeholder="書籍名" auto-focus="true"/>
複製程式碼
在 components\search\index.js 中:
// onConfirm事件執行,呼叫將輸入的內容新增到快取中的方法Keywordmodel.addToHistory(word),就可以將歷史關鍵字新增到快取中
methods: {
    // 點選取消將搜尋元件關掉,有兩種方法:一是,在自己的內部建立一個變數控制顯隱,不推薦,因為萬一還有其他操作擴充套件性不好。二是,建立一個自定義事件,將自定義事件傳給父級,讓父級觸發
    onCancel(event) {
      this.triggerEvent('cancel', {}, {})
    },

    // 在input輸入框輸入完成將輸入的內容加到快取中
    onConfirm(event) {
      const word = event.detail.value
      Keywordmodel.addToHistory(word)
    }
  }
複製程式碼
在 components\search\index.js 中:
//將歷史搜尋的內容從快取中取出來
data: {
    historyWords: [] //歷史搜尋關鍵字
  },

  // 元件初始化時,小程式預設呼叫的生命週期函式
  attached() {
    const historywords = Keywordmodel.getHistory()
    this.setData({
      historyWords: historywords
    })
  },
複製程式碼
在 components\search\index.json 中:
//註冊引用小標籤 tag 元件,元件中也可以引入其他元件
"usingComponents": {
    "v-tag": "/components/tag/index"
  }
複製程式碼
在 components\search\index.wxml 中:
// 遍歷historyWords陣列中的每一項,呈現在頁面中
     <view class="history">
      <view class="title">
        <view class="chunk"></view>
        <text>歷史搜尋</text>
      </view>
      <view class="tags">
        <block wx:for="{{historyWords}}" wx:key="item">
          <v-tag text='{{item}}'/>
        </block>
      </view>
    </view>
複製程式碼

熱門搜尋:

在 models\keyword.js 中:
// 引入自己封裝的API請求方法
import {
  HTTP
} from '../utils/http-promise'

// 獲取熱門搜素搜關鍵字
  getHot() {
    return this.request({
      url: '/book/hot_keyword'
    })
  }
複製程式碼
在 components\search\index.js 中:
//定義元件初始值,通過呼叫傳進來的getHot方法獲取熱門搜尋關鍵字,並更新到初始值hotWords中
data: {
    historyWords: [], //歷史搜尋關鍵字
    hotWords: [] //熱門搜尋關鍵字
  },
// 元件初始化時,小程式預設呼叫的生命週期函式
  attached() {
    const historywords = Keywordmodel.getHistory()
    const hotword = Keywordmodel.getHot()
    this.setData({
      historyWords: historywords
    })

    hotword.then(res => {
      this.setData({
        hotWords: res.hot
      })
    })
  },
複製程式碼
在 components\search\index.wxml 中:
//將從伺服器獲取到的hotWords陣列遍歷,呈現到頁面中
<view class="history hot-search">
      <view class="title">
        <view class="chunk"></view>
        <text>熱門搜尋</text>
      </view>
      <view class="tags">
        <block wx:for="{{hotWords}}" wx:key="item">
          <v-tag text='{{item}}'/>
        </block>
      </view>
    </view>
複製程式碼

注意點:

由於在 components\search\index.js 呼叫了 Keywordmodel.getHot()方法,這個方法是和伺服器相關聯的,這樣做,會使元件複用性降低。

如果要想讓search元件複用性變高,應該在 components\search\index.js 的 properties 中開放一個屬性,然後再引用search元件的pages頁面裡呼叫models裡的方法,再把資料通過屬性傳遞給search元件,然後再做資料繫結,這樣就讓search元件具備了複用性

在 models\book.js 中:
//定義search函式,封裝向伺服器傳送請求功能
// 書籍搜尋
  search(start, q) {
    return this.request({
      url: '/book/search?summary=1',
      data: {
        q: q,
        start: start
      }
    })
  }
複製程式碼
在 components\search\index.js 中:
// 匯入並例項化BookModel類,負責向伺服器傳送搜尋圖書的請求;在data中宣告私有變數 dataArray 陣列,為搜尋圖書當summary=1,返回概要資料。在使用者輸入完成點選完成時,呼叫bookmodel.search方法,並更新資料到dataArray中。
//注意點:不能使用者輸入什麼都儲存在快取中,只有使用者搜尋到有效的關鍵字時才儲存到快取中
import {
  BookModel
} from '../../models/book'
const bookmodel = new BookModel()

data: {
    historyWords: [], //歷史搜尋關鍵字
    hotWords: [], //熱門搜尋關鍵字
    dataArray: [] //搜尋圖書當summary=1,返回概要資料
  },
      
       // 在input輸入框輸入完成將輸入的內容加到快取中
    onConfirm(event) {
      const word = event.detail.value

      // 獲取搜尋的關鍵詞q,調取search方法返回當summary=1,返回概要資料:並更新資料到dataArray中
      const q = event.detail.value
      bookmodel.search(0, q).then(res => {
        this.setData({
          dataArray: res.books
        })

        // 不能使用者輸入什麼都儲存在快取中,只有使用者搜尋到有效的關鍵字時才儲存到快取中
        Keywordmodel.addToHistory(word)

      })

    }
複製程式碼
在 components\search\index.wxml 中:

解析得到的搜尋資料,並遍歷呈現到頁面中:

//搜尋得到的資料和熱門搜尋,歷史搜尋是不能一起顯示的,一個顯示,另一個就得隱藏,搜尋得到的結果頁面是預設不顯示的,需要定義searching一個變數來控制顯隱
<view wx:if="{{searching}}" class="books-container">
    <block wx:for="{{dataArray}}" wx:key="{{item.id}}">
      <v-book book="{{item}}" class="book"></v-book>
    </block>
  </view>

複製程式碼
在 components\search\index.js 中:
//在data私有屬性中定義searching變數來控制顯隱,預設為false;在觸發onConfirm事件中, 為了使用者體驗好,應該點選完立即顯示搜尋頁面,並將searching改為true,讓其搜尋的內容顯示到頁面上
data: {
    historyWords: [], //歷史搜尋關鍵字
    hotWords: [], //熱門搜尋關鍵字
    dataArray: [], //搜尋圖書當summary=1,返回概要資料
    searching: false //控制搜尋到的圖書資料的顯隱,預設不顯示
  },

    // 在input輸入框輸入完成將輸入的內容加到快取中
    onConfirm(event) {
      // 為了使用者體驗好,應該點選完立即顯示搜尋頁面
      this.setData({
        searching: true
      })

      // 獲取搜尋的關鍵詞q,調取search方法返回當summary=1,返回概要資料:並更新資料到dataArray中
      const q = event.detail.value
      bookmodel.search(0, q).then(res => {
        this.setData({
          dataArray: res.books
        })

        // 不能使用者輸入什麼都儲存在快取中,只有使用者搜尋到有效的關鍵字時才儲存到快取中
        Keywordmodel.addToHistory(q)

      })

    }
複製程式碼

實現 搜尋框裡的 x 按鈕功能:

在 components\search\index.js 中:
// 在 components\search\index.wxml 中,為 x 圖片繫結觸控時觸發的 onDelete 事件<image bind:tap="onDelete" class="cancel-img" src="images/cancel.png"/>

// 觸控搜尋圖片裡的x回到原來輸入搜尋的頁面
onDelete(event){
      this.setData({
        searching: false
      })
    },


複製程式碼

實現 使用者點選歷史搜尋和熱門搜尋裡的標籤也能跳轉到相應的搜尋到的結果顯示頁面:只要監聽到使用者點選標籤的事件就可以實現

在 components\search\index.js 中:
// 在 components\search\index.wxml 中:繫結v-tag元件自定事件tapping觸發onConfirm事件:`<v-tag bind:tapping="onConfirm" text='{{item}}'/>`


  // 在input輸入框輸入完成將輸入的內容加到快取中
    onConfirm(event) {
      // 為了使用者體驗好,應該點選完立即顯示搜尋頁面
      this.setData({
        searching: true
      })

      // 獲取搜尋的關鍵詞q:一種是使用者輸入的內容或是通過呼叫tag元件的自定義事件tapping,裡面有攜帶的text文字;調取search方法返回當summary=1,返回概要資料:並更新資料到dataArray中
      const q = event.detail.value || event.detail.text
      bookmodel.search(0, q).then(res => {
        this.setData({
          dataArray: res.books
        })

        // 不能使用者輸入什麼都儲存在快取中,只有使用者搜尋到有效的關鍵字時才儲存到快取中
        Keywordmodel.addToHistory(q)

      })

    }
複製程式碼

解決再點選tag標籤搜尋時應該在input輸入框中顯示書名的問題:

在 components\search\index.js 中:
//通過資料繫結給input輸入框繫結value="{{q}}"
//`<input value="{{q}}" bind:confirm="onConfirm" type="text" class="bar" placeholder-class="in-bar" placeholder="書籍名" auto-focus="true"/>`

//先在data中定義私有資料 q: ''  代表輸入框中要顯示的內容,當資料請求完成後把點選標籤的內容q賦值給私有資料q並更新
data: {
    historyWords: [], //歷史搜尋關鍵字
    hotWords: [], //熱門搜尋關鍵字
    dataArray: [], //搜尋圖書當summary=1,返回概要資料
    searching: false, //控制搜尋到的圖書資料的顯隱,預設不顯示
    q: '' //輸入框中要顯示的內容
  },
   // 在input輸入框輸入完成將輸入的內容加到快取中
    onConfirm(event) {
      // 為了使用者體驗好,應該點選完立即顯示搜尋頁面
      this.setData({
        searching: true
      })

      // 獲取搜尋的關鍵詞q,調取search方法返回當summary=1,返回概要資料:並更新資料到dataArray中
      const q = event.detail.value || event.detail.text
      bookmodel.search(0, q).then(res => {
        this.setData({
          dataArray: res.books,
          q: q
        })

        // 不能使用者輸入什麼都儲存在快取中,只有使用者搜尋到有效的關鍵字時才儲存到快取中
        Keywordmodel.addToHistory(q)

      })

    }
複製程式碼

實現資料分頁載入功能:

第一種方法:用微信小程式提供的內建元件 scroll-view

第二種方法:用 pages 裡的 頁面上拉觸底事件的處理函式 onReachBottom。:

在 pages\book\book.js 中:
//在 data裡設定私有變數more為false,代表的是是否需要載入更多資料,預設是不載入
data: {
    books: [],
    searching: false, //控制搜尋框元件search顯隱,預設不顯示
    more: false //是否需要載入更多資料,預設是不載入
  },
      
 //用pages裡自帶的 頁面上拉觸底事件的處理函式 onReachBottom 監聽頁面是否到底了,如果到底了就會就會將more改變為true,就可以實現載入更多資料方法    
  onReachBottom: function() {
    console.log('到底了')
    this.setData({
      more: true
    })
  },
      
 //由於 search 元件不是頁面級元件,沒有 onReachBottom 函式,就需要通過屬性傳值的方式將more私有變數控制是否載入更多資料傳給子元件search
 // 在pages\book\book.wxml中: `<v-search more="{{more}}" bind:cancel="onCancel" wx:if="{{searching}}"/>`
      
 //然後在search元件裡接收父級傳遞過來的屬性more,並利用監聽函式observer,只要外界傳來的資料改變就會觸發此函式執行
  properties: {
    more: {
      type: String,
      observer: '_load_more'
    } //從pages/book/book.js 傳來的屬性,監聽滑到底步操作.只要外界傳來的屬性改變就會觸發observer函式執行
  },
      
 methods: {
    // 只要外界傳來的屬性改變就會觸發observer函式執行
    _load_more() {
      console.log('監聽函式觸發到底了')
    },
 }

複製程式碼

但現在存在一個問題就是:

observer只會觸發一次,因為下拉到底會把more變為true,之後就都是true不會再發生變化了,就不會再觸發監聽函式observer執行。

解決方法:用隨機字串觸發observer函式,因為observer函式的執行必須是監聽的資料發生改變才會執行此函式。和Vue中的watch很相似。

在 pages\book\book.js 中:
//將私有資料data中的more改為空字串
data: {
    books: [],
    searching: false, //控制搜尋框元件search顯隱,預設不顯示
    more: '' //是否需要載入更多資料,預設是不載入
  },
      
//觸發 頁面上拉觸底事件的處理函式,將more變為隨機數,匯入random自定義隨機處理函式,問題解決
 onReachBottom: function() {
    console.log('到底了')
    this.setData({
      more: random(16)
    })
  },
      
// 在 utils\common.js 中:
// 定義隨機生成字串處理函式,n是生成隨機字串的位數
const charts = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']

const random = function generateMixed(n) {
  var res = ''
  for (var i = 0; i < n; i++) {
    var id = Math.ceil(Math.random() * 35)
    res += charts[id]
  }
  return res
}

export {
  random
}

複製程式碼
在 components\search\index.js 中:

實現載入更多資料:

// 和onConfirm一樣都需要調取search方法返回當summary=1,返回概要資料:並更新資料到dataArray中
// 先判斷已得到的搜尋資料的長度,在呼叫search方法將最新獲取的資料和原來的資料拼接到一起更新資料然後呈現到頁面中
 _load_more() {
      console.log('監聽函式觸發到底了')
      const length = this.data.dataArray.length
      bookmodel.search(length, this.data.q).then(res => {
        const tempArray = this.data.dataArray.concat(res.books)
        this.setData({
          dataArray: tempArray
        })
      })

    },


複製程式碼

細節完善:

//如果關鍵字q初始沒有值就直接返回什麼也不做
_load_more() {
      console.log('監聽函式觸發到底了')
    if (!this.data.q) {
        return
      }
      const length = this.data.dataArray.length
      bookmodel.search(length, this.data.q).then(res => {
        const tempArray = this.data.dataArray.concat(res.books)
        this.setData({
          dataArray: tempArray
        })
      })

    },

//問題:當下拉重新整理沒有更多資料時,還會繼續向伺服器傳送請求非常耗效能;還有就是使用者操作過快沒等第一次請求的結果回來,就又傳送一次相同的請求,會載入重複的資料,非常耗效能
        
//解決:使用鎖的概念解決重複載入資料的問題
//其實就是事件節流操作,在data中定義一個loading:false,表示是否正在傳送請求,預設是沒有傳送請求,在_load_more中,判斷如果正在傳送請求就什麼也不做,如果沒有正在傳送請求就將loading變為true,呼叫search方法向伺服器傳送請求,待請求完畢並返回結果時將loading變為false。
        data:{
           loading: false //表示是否正在傳送請求,預設是沒有傳送請求 
        },
        _load_more() {
      console.log('監聽函式觸發到底了')
    if (!this.data.q) {
        return
      }
    // 如果是正在傳送請求就什麼也不做
      if (this.data.loading) {
        return
      }
      const length = this.data.dataArray.length
      bookmodel.search(length, this.data.q).then(res => {
        const tempArray = this.data.dataArray.concat(res.books)
        this.setData({
          dataArray: tempArray,
            loading: false
        })
      })

    },
       
複製程式碼

進一步封裝優化,元件行為邏輯抽象分頁行為,順便解決 是否還有更多資料的問題:

在 components中,建立並封裝一個公用行為和方法的元件pagination:

#####在 components\behaviors\pagination.js 中:

//封裝一個公用行為和方法的類paginationBev
const paginationBev = Behavior({
  data: {
    dataArray: [], //分頁不斷載入的資料
    total: 0 //資料的總數
  },

  methods: {
    // 載入更多拼接更多資料到陣列中;新載入的資料合併到dataArray中
    setMoreData(dataArray) {
      const tempArray = this.data.dataArray.concat(dataArray)
      this.setData({
        dataArray: tempArray
      })
    },

    // 呼叫search方法時返回起始的記錄數 
    getCurrentStart() {
      return this.data.dataArray.length
    },

    // 獲取設定從伺服器得到資料的 總長度
    setTotal(total) {
      this.data.total = total
    },

    // 是否還有更多的資料需要載入。如果得到資料的長度大於伺服器返回的總長度,代表沒有更多資料了,就停止發請求
    hasMore() {
      if (this.data.dataArray.length >= this.data.total) {
        return false
      } else {
        return true
      }
    }
  }

})

export {
  paginationBev
}

複製程式碼
在 components\search\index.js 中:
// 先匯入封裝的公用行為方法,再進一步改寫_load_more和onConfirm方法,將寫好的公用方法用上
import {
  paginationBev
} from '../behaviors/pagination'

 _load_more() {
      console.log('監聽函式觸發到底了')
       
      if (!this.data.q) {
        return
      }
      // 如果是正在傳送請求就什麼也不做
      if (this.data.loading) {
        return
      }
     
     
      if (this.hasMore()) {
        this.data.loading = true//必須放在hasMore()裡
        bookmodel.search(this.getCurrentStart(), this.data.q).then(res => {
          this.setMoreData(res.books)
          this.setData({
			loading: false
          })
        })
      }


    },
          onConfirm(event) {
      // 為了使用者體驗好,應該點選完立即顯示搜尋頁面
      this.setData({
        searching: true
      })

      // 獲取搜尋的關鍵詞q,調取search方法返回當summary=1,返回概要資料:並更新資料到dataArray中
      const q = event.detail.value || event.detail.text

      bookmodel.search(0, q).then(res => {
        this.setMoreData(res.books)
        this.setTotal(res.total)
        this.setData({

          q: q
        })

        // 不能使用者輸入什麼都儲存在快取中,只有使用者搜尋到有效的關鍵字時才儲存到快取中
        Keywordmodel.addToHistory(q)

      })

    }
複製程式碼

但這時又會出現一個小問題:就是每次點x回退到搜尋頁面時,再次搜尋同樣的書籍時,會存在以前請求的資料沒有清空又會重新向伺服器傳送請求,就會出現更多的重複資料

解決方法:就是在每次點x時,清空本次搜尋的資料也就是Behavior裡面的資料狀態 ,上一次搜尋的資料才不會影響本次搜尋

在 components\behaviors\pagination.js 中:
//加入清空資料,設定初始值的方法
initialize() {
      this.data.dataArray = []
      this.data.total = null
    }
複製程式碼
在 components\search\index.js 中:
//在觸發onConfirm函式時呼叫this.initialize()方法先清空上一次搜尋的資料在載入
onConfirm(event) {
      // 為了使用者體驗好,應該點選完立即顯示搜尋頁面
      this.setData({
          searching: true
        })
        // 先清空上一次搜尋的資料在載入
      this.initialize()
        // 獲取搜尋的關鍵詞q,調取search方法返回當summary=1,返回概要資料:並更新資料到dataArray中
      const q = event.detail.value || event.detail.text

      bookmodel.search(0, q).then(res => {
        this.setMoreData(res.books)
        this.setTotal(res.total)
        this.setData({

          q: q
        })

        // 不能使用者輸入什麼都儲存在快取中,只有使用者搜尋到有效的關鍵字時才儲存到快取中
        Keywordmodel.addToHistory(q)

      })

    }
複製程式碼
搜尋程式碼重構:增強程式碼可閱讀性:
在 components\search\index.js 中:
//多封裝一些小的函式
import {
  KeywordModel
} from '../../models/keyword'
import {
  BookModel
} from '../../models/book'
import {
  paginationBev
} from '../behaviors/pagination'

const Keywordmodel = new KeywordModel()
const bookmodel = new BookModel()

// components/search/index.js
Component({
  // 元件使用行為需加
  behaviors: [paginationBev],


  /**
   * 元件的屬性列表
   */
  properties: {
    more: {
      type: String,
      observer: 'loadMore'
    } //從pages/book/book.js 傳來的屬性,監聽滑到底步操作.只要外界傳來的屬性改變就會觸發observer函式執行
  },

  /**
   * 元件的初始資料
   */
  data: {
    historyWords: [], //歷史搜尋關鍵字
    hotWords: [], //熱門搜尋關鍵字
    // dataArray: [], //搜尋圖書當summary=1,返回概要資料
    searching: false, //控制搜尋到的圖書資料的顯隱,預設不顯示
    q: '', //輸入框中要顯示的內容
    loading: false //表示是否正在傳送請求,預設是沒有傳送請求
  },

  // 元件初始化時,小程式預設呼叫的生命週期函式
  attached() {
    // const historywords = Keywordmodel.getHistory()
    // const hotword = Keywordmodel.getHot()
    this.setData({
      historyWords: Keywordmodel.getHistory()
    })

    Keywordmodel.getHot().then(res => {
      this.setData({
        hotWords: res.hot
      })
    })
  },

  /**
   * 元件的方法列表
   */
  methods: {
    // 只要外界傳來的屬性改變就會觸發observer函式執行
    loadMore() {
      console.log('監聽函式觸發到底了')
        // 和onConfirm一樣都需要調取search方法返回當summary=1,返回概要資料:並更新資料到dataArray中
        // 先判斷已得到的搜尋資料的長度,在呼叫search方法將最新獲取的資料和原來的資料拼接到一起更新資料然後呈現到頁面中
      if (!this.data.q) {
        return
      }
      // 如果是正在傳送請求就什麼也不做
      if (this._isLocked()) {
        return
      }
      // const length = this.data.dataArray.length

      if (this.hasMore()) {
        this._addLocked()
        bookmodel.search(this.getCurrentStart(), this.data.q).then(res => {
          this.setMoreData(res.books)
          this._unLocked()
        })
      }


    },



    // 點選取消將搜尋元件關掉,有兩種方法:一是,在自己的內部建立一個變數控制顯隱,不推薦,因為萬一還有其他操作擴充套件性不好。二是,建立一個自定義事件,將自定義事件傳給父級,讓父級觸發
    onCancel(event) {
      this.triggerEvent('cancel', {}, {})
    },

    // 觸控搜尋圖片裡的x回到原來輸入搜尋的頁面
    onDelete(event) {
      this._hideResult()
    },

    // 在input輸入框輸入完成將輸入的內容加到快取中
    onConfirm(event) {
      // 為了使用者體驗好,應該點選完立即顯示搜尋頁面
      this._showResult()
        // 先清空上一次搜尋的資料在載入
      this.initialize()
        // 獲取搜尋的關鍵詞q,調取search方法返回當summary=1,返回概要資料:並更新資料到dataArray中
      const q = event.detail.value || event.detail.text

      bookmodel.search(0, q).then(res => {
        this.setMoreData(res.books)
        this.setTotal(res.total)
        this.setData({

          q: q
        })

        // 不能使用者輸入什麼都儲存在快取中,只有使用者搜尋到有效的關鍵字時才儲存到快取中
        Keywordmodel.addToHistory(q)

      })

    },

    // 更新變數的狀態,顯示搜尋框
    _showResult() {
      this.setData({
        searching: true
      })
    },

    // 更新變數的狀態,隱藏搜尋框
    _hideResult() {
      this.setData({
        searching: false
      })
    },

    // 事件節流機制,判斷是否加鎖
    _isLocked() {
      return this.data.loading ? true : false
    },

    // 加鎖
    _addLocked() {
      this.data.loading = true
    },

    // 解鎖
    _unLocked() {
      this.data.loading = false
    },


  }
})

複製程式碼

小問題:當載入的時候突然斷網,資料還沒載入完,等在恢復網路的時候,就不能繼續向伺服器傳送請求了。問題存在的原因在於出現死鎖,只有請求成功才會解鎖繼續傳送請求,如果請求失敗,就不會解鎖什麼也做不了。

解決方法:

在 components\search\index.js 中:
//只要在請求失敗的回撥函式里加上解鎖就可以了

      if (this.hasMore()) {
        this._addLocked()
        bookmodel.search(this.getCurrentStart(), this.data.q).then(res => {
          this.setMoreData(res.books)
          this._unLocked()
        }, () => {
          this._unLocked()
        })
      }
複製程式碼

加入loading效果,提升使用者體驗:

先建立一個loading公共元件,只需寫簡單的樣式效果就行,在search元件中註冊並使用。

在 components\search\index.js 中:
// 在 components\search\index.wxml 中:加入兩個loading元件。 第一個在中間顯示,獲取搜獲資料中;第二個在底部顯示,資料載入更多時顯示
//<v-loading class="loading-center" wx:if="{{loadingCenter}}"/>
//  <v-loading class="loading" wx:if="{{loading}}"/>

//在data中新增一個loadingCenter變數控制loading效果是否在中間顯示,並且加兩個私有函式控制loading的顯隱。在onConfirm函式中呼叫this._showLoadingCenter()函式,顯示loading效果,在 資料載入完成,調取this._hideLoadingCenter(),取消顯示loading效果,
data: {loadingCenter: false},
 // 改變loadingCenter的值
    _showLoadingCenter() {
      this.setData({
        loadingCenter: true
      })
    },

    // 改變loadingCenter的值
    _hideLoadingCenter() {
      this.setData({
        loadingCenter: false
      })
    }

onConfirm(event) {
      // 為了使用者體驗好,應該點選完立即顯示搜尋頁面
      this._showResult()
        // 顯示loading效果
      this._showLoadingCenter()
      this.initialize()   
      const q = event.detail.value || event.detail.text
      bookmodel.search(0, q).then(res => {
        this.setMoreData(res.books)
        this.setTotal(res.total)
        this.setData({
          q: q
        })
        Keywordmodel.addToHistory(q)
        // 資料載入完成,取消顯示loading效果
        this._hideLoadingCenter()
      })

    },
複製程式碼

知識點補充:

特別注意setData與直接賦值的區別:

setData:呼叫setData函式更新的資料會觸發頁面重新渲染,和REACT裡的setState相似。

而直接賦值,只是在記憶體中改變的狀態,並沒有更新到頁面中

空搜尋結果的處理:

在 components\behaviors\pagination.js 中:
//在公共行為中加入noneResult:false,控制是否顯示沒有得到想要的搜尋結果,在setTotal方法中,如果返回的結果為0,就是沒有得到想要的搜尋結果。將noneResult:true顯示出來。在initialize設定初始值並清空資料函式,再將noneResult:false,取消顯示。

 data: {
    dataArray: [], //請求返回的陣列
    total: null, //資料的總數
    noneResult: false //沒有得到想要的搜尋結果
  },
  
  // 獲取設定資料的 總長度
    // 如果返回的結果為0,就說明沒有得到搜尋結果,將提示內容顯示出來
    setTotal(total) {
      this.data.total = total
      if (total === 0) {
        this.setData({
          noneResult: true
        })
      }
    },
        
  // 清空資料,設定初始值,將提示隱藏
    initialize() {
      this.setData({
        dataArray: [],
        noneResult: false
      })
      this.data.total = null
    }
  
複製程式碼
在 components\search\index.wxml 中:
//加入空搜尋顯示的結果結構
<text wx:if="{{ noneResult}}" class="empty-tip">沒有搜尋到書籍</text>
複製程式碼
在 components\search\index.js 中:
// 觸控搜尋圖片裡的x回到原來輸入搜尋的頁面,先回到初始值,再將搜尋元件隱藏。在onConfirm中,不用等資料載入完,輸入完成後就把輸入的內容顯示在輸入框中。
onDelete(event) {
      this.initialize()
      this._hideResult()
    },
        
    onConfirm(event) {
      this._showResult()
      this._showLoadingCenter()
      const q = event.detail.value || event.detail.text
        // 不用等資料載入完,輸入完成後就把輸入的內容顯示在輸入框中。
      this.setData({
        q: q
      })

      bookmodel.search(0, q).then(res => {
        this.setMoreData(res.books)
        this.setTotal(res.total)
        Keywordmodel.addToHistory(q)
        this._hideLoadingCenter()

      })

    },        
  
複製程式碼

處理一個小問題:就是在熱門搜尋裡搜尋王小波,返回的搜尋結果頁面每本書裡會顯示有喜歡字樣,去掉喜歡字樣。

在 components\book\index.js 中:
//在 components\search\index.wxml 中:
//通過搜尋元件搜尋顯示的書籍都不顯示喜歡字樣,通過屬性傳值的方式將喜歡字樣去掉,把false傳遞給子元件,子元件通過showLike變數接收,通過資料控制顯隱將喜歡字樣去掉。
<v-book book="{{item}}" class="book" show-like="{{false}}"></v-book>

//新增一個showLike屬性,代表每本書裡面的喜歡字樣是否顯示
properties: {
    book: Object,
    showLike: { //控制每本書下面有個喜歡字樣的顯示與隱藏
      type: Boolean,
      value: true
    }
  },

//在 components\book\index.wxml 中:
//showLike屬性的顯示和隱藏控制喜歡字樣的顯示和隱藏
  <view class="foot" wx:if="{{showLike}}">
      <text class="footer">{{book.fav_nums}}  喜歡</text>
  </view>
複製程式碼

對 search 元件進一步優化,將鎖提取到分頁行為中:

在 components\behaviors\pagination.js 中:
//把在components\search\index.js中的三個鎖方法提取到公用行為方法中,在公用行為方法中,在data裡新增loading:false屬性。在initialize函式中,把loading:false也加進去即可

// 事件節流機制,判斷是否加鎖
    isLocked() {
      return this.data.loading ? true : false
    },

    // 加鎖
    addLocked() {
      this.setData({
        loading: true
      })

    },

    // 解鎖
    unLocked() {
      this.setData({
        loading: false
      })
    },
複製程式碼

兩種方法監聽移動端觸底的操作:

scroll-view 或 Pages 裡的 onReachBottom。如果要想用scroll-view把view元件換成scroll-view就可以。


微信open-data顯示使用者資訊:https://developers.weixin.qq.com/miniprogram/dev/api/wx.getUserInfo.html

使用者授權,需要使用 button 來授權登入。

很多時候我們需要把得到資訊儲存到我們自己的伺服器上去,需要通過js程式碼中操作使用者頭像等資訊。封裝一個image-button通用元件就可以,改變他的圖片,並且可以在不同的方式中呼叫,只需要改變open-type屬性就可以。


分享的實現:自定義分享button:


小程式之間的跳轉:這兩個小程式都必須關聯同一個公眾號


==============================================================


bug解決

components/episode/index.js

報錯RangeError: Maximum call stack size exceeded:

原因:

//錯誤寫法
properties: {
    index: { //預設顯示為0
      type: String,
      observer: function(newVal, oldVal, changedPath) {
        let val = newVal < 10 ? '0' + newVal : newVal
        this.setData({
          index: val
        })
      }
    }
  },
//小程式的observer,相當於vue中的watch監聽函式,只要監聽的index資料改變,就會呼叫observer方法,會形成死迴圈,就會報錯RangeError: Maximum call stack size exceeded
複製程式碼

解決:

  //第一種解決方法   
this.setData({
          _index: val
        })

  data: {
    year: 0,
    month: '',
    _index: ''
  },
複製程式碼
//第二種解決方法  (推薦)

複製程式碼

components/music/index.js中:

報錯:setBackgroundAudioState:fail title is nil!;at pages/classic/classic onReady function;at api setBackgroundAudioState fail callback function

Error: setBackgroundAudioState:fail title is nil!

原因:

少 title 外傳屬性

解決:

//在`components/music/index.js`中:
properties: {
    src: String,
    title: String
  },
methods: {
    // 為圖片繫結觸控播放事件
    onPlay: function() {
      //圖片切換
      this.setData({
        playing: true
      })
      mMgr.src = this.properties.src
      mMgr.title = this.properties.title
    }
  }
-----------------------------------------------------------------------------
//在 app.json 中加上:
"requiredBackgroundModes": [
    "audio"
  ],
複製程式碼


============================================================================


移動端增加使用者體驗優化

components/navi中:

點選的左右小三角要足夠大,使用者觸控時才能點選到。兩種方法,第一種是再切圖時,切得大一些;第二種是,自己編寫程式碼控制操作區域



完成效果展示:

視訊地址

相關文章