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

項目地址:github.com/wudiufo/WeC…

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


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

思路:css

like 默認爲 false,顯示空心小愛心html

觸摸執行tap:onLike 方法,由於 this.setData({count:count,like:!like})是異步的,先執行count = like ? count - 1 : count + 1,這時like仍是false,執行count+1。而後在執行this.setData()方法,將like變爲true,顯示實心小愛心。vue

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接口。android

剛開始實心就調用'like/cancel'接口,空心就調用'like'接口css3


底部左右切換組件 components/navi

思路:git

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}}"/>github

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

爲圖片綁定觸摸事件onLeft,在navi/index.js中:json

在 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中:

點擊的左右小三角要足夠大,用戶觸摸時才能點擊到。兩種方法,第一種是再切圖時,切得大一些;第二種是,本身編寫代碼控制操做區域



完成效果展現:

視頻地址

相關文章
相關標籤/搜索