需求背景
最近接到了一個需求,使用者在播放音訊的時候,有的音訊長度過長,使用者希望自主控制音訊的進度和時長。但是微信提供原生的<audio>
的元件只能進行播放和暫停的控制,沒有時長等元素,因此需要自己做一個播放器的元件。
技術背景
在需求調研之後,關於音訊方面的API大概分為兩種:
- audio相關的
createInnerAudioContext
:建立並返回一個innerAudioContext
物件。這個介面提供了一些音訊相關的屬性。 - 背景音樂相關的
getBackgroundAudioManager
:可以獲取全域性唯一的背景音訊管理器backgroundAudioManager
。和audio類似,提供一些音訊相關的方法和屬性。
同時,微信從v1.6.3
起開放了自定義元件部分,支援簡潔的元件化程式設計。想嘗試一下元件化的開發模式,因此準備開發一個player元件
來代替audio標籤
。同時低版本的基礎庫進行相容操作。
技術選型
因為需要實現一個有獨立作用域的單獨組建,並且組建之間儘量不要互相影響。如果用bgm相關API的話需要維護一個全域性的backgroundAudioManager
,因此選擇audio相關的API。
同時因為採取了自定義元件,因此只需要考慮1.6.4版本的基礎庫即可,只需要wx.createInnerAudioContext()
來實現即可。
開發
之前開發過不少其他框架的元件,這次需要設計一個播放器,首先要定位播放器的功能。一個最重要的功能點:拖動控制音訊播放進度。圍繞著這一點,開始設計輸入輸出。
建立自定義元件
如官方文件描述,一個元件由json
、wxml
、 wxss
、js
四個檔案組成。首先要在json中標註這是一個元件:
// player.json
{
"component": true
}
複製程式碼
在專案中進行應用時,同樣在json檔案中進行引用:
// page1.json
"usingComponents": {
"player": "component/player/player"
}
複製程式碼
即可在專案中類似常規元件的使用。
// page1.wxml
<player name="{{item.name}}" src="{{item.url}}"></player>
複製程式碼
在js部分,需要用Component
構造器構造一個例項來進行組建的管理,制定元件的屬性、資料、方法等一些元素。
引數接收
輸入最重要的就是音訊的url了,其次是音訊的其他資訊,例如名稱、作者、封面等等相關的部分。在元件中,定義對外屬性要用properties
進行定義。
properties
屬性是一個Object Map
,管理著所有的對外屬性,包含三個欄位:type
表示屬性型別、 value
表示屬性初始值、 observer
表示屬性值被更改時的響應函式。
如上面的兩個輸入,即可在構造器中定義:
properties: {
// 這裡定義了name和src屬性,屬性值可以在元件使用時指定
src: { // 屬性名
type: String, // 型別(必填),目前接受的型別包括:String, Number, Boolean, Object, Array, null(表示任意型別)
value: ``, // 屬性初始值(可選),如果未指定則會根據型別選擇一個
observer: function(newVal, oldVal){} // 屬性被改變時執行的函式(可選),也可以寫成在methods段中定義的方法名字串
},
name: String // 簡化的定義方式
}
複製程式碼
其中需要注意的一點是,在properties
中定義的對外屬性,可以在this.data
中直接呼叫,即為初始值。同時,可以用observer定義該值改動之後的回撥,很好用。
在properties
屬性中,屬性名採用駝峰寫法,呼叫時需要用連字元寫法,資料繫結時需要用駝峰寫法
作用域
在作用域部分,其實如果希望獨立作用域的話,可以把一些例項和屬性定義在Component
的data部分,這樣不與其他元件進行共享,為私有資料進行模板渲染。在探索中,發現如果希望多元件共享屬性的話,可以定義在元件外部,進行管理,這點仍需探索總結。
生命週期
自定義元件的生命週期有created
、attached
、ready
三種,家在順序分別是created
→attached
→ready
。ready
時,頁面基本佈局完成,相當於onLoad,在這裡可以進行一些元件的初始化操作。
另外,還有moved
和detached
兩個生命週期,可以在detached
中定義一些登出方面的操作。
audio例項的預載入
因為元件內部定義了一個audio物件,需要在元件載入完畢之後進行初始化的操作,因此在ready
中進行例項的定義。
ready: function() {
let audioItem = wx.createInnerAudioContext()
audioItem.autoplay = true;
audioItem.loop = false;
audioItem.src = this.data.playUrl;
}
複製程式碼
這時定義了audioItem例項,並儲存在data域中。
音訊的長度獲取
從需求出發,控制音訊播放進度的前提是獲取音訊的長度。wx. createInnerAudioContext()
提供了物件duration
來獲取音訊的長度,但是有個前提,**音訊必須載入之後才能獲取該屬性。**經過測試,所有事件回撥中,只有onTimeUpdate
和onStop
事件才能成功的獲取duration欄位。具體如下:
audioItem.onTimeUpdate(function(){
let totalIndex = audioItem.duration;
let duration = second2minute(totalIndex);
that.setData({
totalIndex,
duration,
})
})
複製程式碼
但是onTimeUpdate事件會隨著音訊的載入一直觸發,進而反覆執行回撥,因此採用定時器的方法,不斷的輪詢進行資料的載入。
let calcTimer = setInterval(function(){
calcIndex++;
// 反覆嘗試仍獲取失敗,則取消嘗試
if(calcIndex>50) {
clearInterval(that.data.calcTimer);
}
if(audioItem.duration>0) {
let totalIndex = audioItem.duration;
let duration = second2minute(totalIndex);
that.setData({
totalIndex,
duration,
})
clearInterval(that.data.calcTimer);
}
},100)
複製程式碼
這算是一個實現方式,一般在1000ms之內可以獲取總長度。
佈局樣式
首先看一下播放器的樣式:
和自帶的標籤相比,主要是增加了一個進度條的部分。關於進度條的實現,使用者需要來回拖動來進行音訊進度的控制。
具體的實現,是用一個<slider>
來進行拖動的操作,但是原生的<slider>
會比較大,因此可以做一個偽裝的進度播放來操作。
具體程式碼如下:
<view class="player">
<view class="player-poster">
<image mode="aspectFill" src="http://img.sharedaka.com/Fkg6nUUzd2bnigSz7vgViBFXrwLq" style="width: 100%;height: 100%;"/>
<view class="player-poster_button" bindtap="playMusic">
<image src="/component/player/image/play.png" style="width: 100%;height: 100%;" wx:if="{{!playState}}"/>
<image src="/component/player/image/stop.png" style="width: 100%;height: 100%;" wx:if="{{playState}}"/>
</view>
</view>
<view class="player-info">
<view class="player-info_title">{{playName}}</view>
<view class="player-info_time"></view>
<view class="player-info_control">
<text class="time">{{playtime}}</text>
<progress percent="{{downloadPercent}}" color="#E4E4E4" stroke-width="2" class="player-info_process">
<text class="playstate" style="left:{{percent}}%"></text>
<text class="playstate-outer" style="left:{{percent}}%"></text>
<text class="dpstate" style="width:{{percent}}%"></text>
<slider disabled="{{duration === `00:00`}}" class="slider" bindchanging="changeSeek" color="#d33a31" left-icon="cannel" value="{{percent}}"></slider>
</progress>
<text class="time">{{duration}}</text>
</view>
</view>
</view>
複製程式碼
具體操作
播放器中主要涉及兩個操作:
- 音訊的播放和暫停。
- 音訊進度條的拖動效果。
音訊播放及暫停
在音訊播放時,及觸發methods中的playMusic()
方法。
首先獲取目前播放器的狀態及例項:
let playState = this.data.playState;
let audioItem = this.data.audioItem;
複製程式碼
當播放器在未播放狀態下時,執行操作,並開始計時器的計算:
audioItem.play();
let timer = setInterval(function() {
let timeIndex = ++that.data.timeIndex;
if(timeIndex > that.data.totalIndex) {
clearInterval(that.data.timer);
that.setData({
timeIndex: 0,
percent: 0,
playtime: `00:00`,
playState: false
})
audioItem.stop();
return;
}
let playtime = second2minute(timeIndex);
that.setData({
timeIndex: timeIndex,
playtime,
percent: timeIndex/that.data.totalIndex*100
});
}, 1000)
複製程式碼
當播放器在播放狀態下,執行操作:
audioItem.pause();
clearInterval(this.data.timer);
複製程式碼
記得對計時器的儲存以及播放器狀態的儲存
播放器進度調整
當拖動播放器進度時,可以利用bindchanging
來進行控制。這時需要計算當前進度是音樂總時長的佔比,並跳轉到相應的位置播放。
// 調整位置
changeSeek: function(e) {
let value = e.detail.value;
let nowIndex = Math.floor(totalIndex * value/100);
if(playState) {
audioItem.seek(nowIndex);
}
that.setData({
percent: e.detail.value,
timeIndex: nowIndex,
})
}
複製程式碼
到此,即完成了player
播放器的整體開發工作。
相容性處理
前面說到,目前自定義元件只針對1.6.4以上的使用者可以使用。因此需要對以下的版本進行相容性的操作。
首先在js中判斷當前使用者基礎庫的版本:
wx.getSystemInfo({
success: function(res) {
let sdk = res.SDKVersion;
if(sdk > `1.6.3`) {
that.setData({
canUsePlayer: true
})
}
}
})
複製程式碼
用canUsePlayer
來進行版本的標記。在專案中,根據標記選用不同的元件。
<audio name="{{item.extra}}" src="{{qiNiuUrl + item.content}}" wx:if="{{!canUsePlayer}}"></audio>
<player name="{{item.extra}}" src="{{qiNiuUrl + item.content}}" wx:if="{{canUsePlayer}}"></player>
複製程式碼
尾巴
to be continue
近期還有一個新的需求,需要音樂在小程式收起的時候繼續播放。估計馬上就要踩坑getBackgroundAudioManager
了。頁面維護一個公用的例項。
小感悟
從最近的一些列新feature可以看出,微信小程式的生態要逐步完善了。分包、直播、元件的開放說明微信小程式的可玩性頁逐漸提高。
試水自定義元件的開發,感覺目前的開發模式還是比較簡單的,相較於之前用template
的模式更加方便,而且還有一些元件之間複用的新特性relations
等元件之間的關係,可玩性很高。
估計不久之後就有一套第三方元件支援小程式的各項功能。