前言
近年來國內出現了一些可以讓前端人員編寫移動端App的IDE,Hbuilder X是DCloud推出的一款免費開發工具,最大的亮點是可以開發App,利用html5+技術,結合mui+nativejs可以在雲端打包,主要用到的技術就是HTML5、JS、CSS,一套代碼,即可生成Android和IOS對應的兩種App。最早的App開發只有原生這個概念,Html頁面只是用來做一些簡單的靜態資源展示,但是隨着H5的興盛,大家發現很多功能、邏輯都可用web來實現,然後原生作為容器顯示,而且H5展示的頁面更炫酷、功能更豐富,在IOS、Andriod中都有很好的支持,這樣開發效率更高、成本更低,同時用户體驗也不錯。
項目已上傳github,歡迎大家下載交流。
前端項目地址:https://github.com/Hanxueqing...
在線項目手冊:https://hanxueqing.github.io/...
項目技術棧
UI框架:MUI(官方推薦的模擬原生App的UI框架)
JS框架:VUE
API:H5+、Native.js(原生40萬API隨意調用)
編輯器:HBuilder,在5+ App項目下編寫的HTML、js等文件,會被打包到原生的安裝包(Android是apk包、iOS是ipa包)。
項目運行
# 克隆到本地
git clone git@github.com:Hanxueqing/Maoer-App-HBuilder.git
# 放到HBuilder環境下運行
# 使用數據線連接手機
# IOS系統在AppStore下載HBuilder插件
# 在HBuilder中輸入ctrl+r開啓真機演示
項目開發
環境搭建
下載安裝HBuilder X
在官網地址選擇合適的版本下載安裝:
http://www.dcloud.io/hbuilder...
新建項目
打開HBuilder,在菜單欄中選擇文件——新建——項目,選擇5+App,創建一個mui項目,填寫文件名稱、保存位置,點擊創建,會給你生成一個包含mui的js、css、字體資源的項目模版。
文件結構
新建完成後,會在左側的項目管理器中出現如下目錄結構,跟我們平時做前端開發的項目類似。mainifest.json文件中存儲的是app相關的配置。
真機調試
使用數據線連接手機和電腦,在Android設備會自動安裝並啓動HBuilder調試基座,IOS系統的同學請下載一個名字叫HBuilder的調試插件,點擊窗口上方的播放鍵小圖標或者使用快捷鍵command+r在手機上運行。
真機運行有3個特點:
- 真實。雖然PC端HBuilder右側的內置瀏覽器也可以看大致的頁面,但真實的佈局效果以及手機上的特殊能力調用,還是必須在真機測試。
- 邊改邊看。在HBuilder更改頁面並保存後,可立即同步在真機上看到保存後的顯示效果。比開發原生應用還方便。
- 檢查錯誤和log。手機運行HTML等文件時如果發生錯誤以及打印的console.log,都可以在真機運行時從手機端反饋回到HBuilder的控制枱,在控制枱直接查看。
注意只有移動App項目才可以真機聯調。
如果你真機失敗,注意看控制枱的提示,或點HBuilder菜單-運行裏的故障排查指南。
注意:真機聯調App時,提供的是一個測試環境,並不真實發生打包,調試基座App的名字、圖標、啓動封面圖片、是否可旋轉這些只有打包才能更改的屬性不會因為開發者修改manifest文件而變化。只有修改manifest且點擊菜單發行-打包後,上述4個設置才能更改。
運行後,HBuilder中修改頁面代碼,保存後會自動同步到手機中,如果手機當前展示着被修改的頁面,則會刷新頁面。嘗試在js中在plus ready之後編寫console.log,或者改寫錯誤的js,可以直接在HBuilder的控制枱看到結果。如果真機運行遇到各種故障,請點擊運行菜單裏的真機運行常見故障指南。
底部Tab選項卡
頁面初始化
mui框架將很多功能配置都集中在mui.init方法中,要使用某項功能,只需要在mui.init方法中完成對應參數配置即可,目前支持在mui.init方法中配置的功能包括:創建子頁面、關閉頁面、手勢事件配置、預加載、下拉刷新、上拉加載、設置系統狀態欄背景顏色。
//mui初始化
mui.init();
編寫三個tab選項:首頁、好玩、設置,在href中填寫展示頁面的id。
<nav class="mui-bar mui-bar-tab">
<!-- href寫id -->
<a id="defaultTab" class="mui-tab-item mui-active" href="home.html">
<span class="mui-icon mui-icon-home"></span>
<span class="mui-tab-label">首頁</span>
</a>
</a>
<a class="mui-tab-item" href="play.html">
<span class="mui-icon mui-icon-paperplane"></span>
<span class="mui-tab-label">好玩</span>
</a>
<a class="mui-tab-item" href="mine.html">
<span class="mui-icon mui-icon-gear"></span>
<span class="mui-tab-label">設置</span>
</a>
</nav>
配置子頁面
先通過var self = plus.webview.currentWebview();創建一個主窗口self,然後內部通過循環拿到三個子窗口,通過H5+方法 Webview——create創建新的Webview窗口,判斷i是否大於0來判斷當前窗口是否是第2、3窗口,如果是則隱藏,如果不是則説明為第一個子窗口,就追加到self主窗口中,並且通過subpage_style樣式規定它在主窗口的展示位置。
H5 + create方法
WebviewObject plus.webview.create( url, id, styles, extras );
http://www.html5plus.org/doc/...
<script type="text/javascript" charset="utf-8">
//mui初始化
mui.init();
// var subpages = ['tab-webview-subpage-about.html', 'tab-webview-subpage-chat.html', 'tab-webview-subpage-contact.html', 'tab-webview-subpage-setting.html'];
//配置子頁面
var subpages = [
{url:"./pages/home/",id:"home.html"},
{url:"./pages/play/",id:"play.html"},
{url:"./pages/mine/",id:"mine.html"},
]
var subpage_style = {//規定子窗口在主窗口中的位置
top: '0px',
bottom: '51px'
};
var aniShow = {}; //創建一個空對象
//創建子頁面,首個選項卡頁面顯示,其它均隱藏;
mui.plusReady(function() {//放到plusReady中才可以調用h5+的plus方法
var self = plus.webview.currentWebview();//主窗口的對象
for (var i = 0; i < 3; i++) {//循環三次
var temp = {};
// WebviewObject plus.webview.create( url, id, styles, extras );
var sub = plus.webview.create(subpages[i].url+subpages[i].id,subpages[i].id,subpage_style);
if (i > 0) { //第二個與第三個窗口隱藏
sub.hide();//調用hide方法
}else{
temp[subpages[i].id] = "true"; //{home.html:"true"}
mui.extend(aniShow,temp); //對象擴展 aniShow = {home.html:"true"}
}
self.append(sub);//子窗口添加到主窗口
}
});
在app開發中,對於HTML5+應用的頁面有一個很重要的“plusready”事件,此事件會在頁面加載後自動觸發,表示所有HTML5+ API可以使用,在此事件觸發之前不能調用HTML5+ API,若要使用HTML5+擴展api,必須等plusready事件發生後才能正常使用,所以應該在此事件回調函數中調用頁面初始化需要調用的HTML5+ API,而不應該在onload或DOMContentLoaded事件中調用。mui將該事件封裝成了mui.plusReady()方法,涉及到HTML5+的api,建議都寫在mui.plusReady方法中。如下為打印當前頁面URL的示例:
mui.plusReady(function(){
console.log("當前頁面URL:"+plus.webview.currentWebview().getURL());
});
如果手機版本是ios10+系統,即使不寫plusready,內部也可以拿到plus對象,如果是安卓系統,系統報錯plus is not defined説明找不到plus對象,需要將方法寫在plusready中。
通過subpages[0].id獲取當前激活選項,通過事件委託,給所有的a標籤動態綁定事件,讓後續動態添加的元素也有之前的事件。
//當前激活選項
var activeTab = subpages[0].id; //"home.html"
//選項卡點擊事件
//事件委託 讓後續動態添加的元素也有之前的事件
mui('.mui-bar-tab').on('tap', 'a', function(e) { //給所有的a標籤動態綁定事件
var targetTab = this.getAttribute('href'); //獲得href屬性 "home.html"
if (targetTab == activeTab) { //如果href屬性和id相同
return;
}
通過Mui.os判斷平台
http://dev.dcloud.net.cn/mui/...
我們經常會有通過navigator.userAgent判斷當前運行環境的需求,mui對此進行了封裝,通過調用mui.os.XXX即可。
如果是ios操作系統直接打開對應頁面,如果是非ios系統並且第一次進入該頁面,則以fade-in動畫的形式打開。
//顯示目標選項卡
//若為iOS平台或非首次顯示,則直接顯示
//判斷平台
//如果是ios操作系統直接打開對應頁面 如果是非ios系統並且第一次進入該頁面 則以動畫的形式打開
if(mui.os.ios||aniShow[targetTab]){
plus.webview.show(targetTab);
}else{//如果是其他平台則以動畫的形式打開
//否則,使用fade-in動畫,且保存變量
var temp = {};
temp[targetTab] = "true"; //temp = [“play.html”:"true"]
mui.extend(aniShow,temp);//aniShow = ["home.html":"true","play.html":"true"]
plus.webview.show(targetTab,"fade-in",300);
}
請注意,mui只封裝了部分HTML5Plus Api,學會mui框架不代表可以不學習HTML5Plus規範。mui不會做的很重,只是很有限的通過封裝簡化了常見開發過程。
打開對應頁面之後需要將之前激活的頁面隱藏,然後將activeTab更改為當前的targetTab。
//隱藏當前;
plus.webview.hide(activeTab);
//更改當前活躍的選項卡
activeTab = targetTab;
最後通過自定義事件,模擬點擊"首頁選項卡",實現當前點擊的選項卡高亮顯示。
//自定義事件,模擬點擊“首頁選項卡”
document.addEventListener('gohome', function() {
var defaultTab = document.getElementById("defaultTab");
//模擬首頁點擊
mui.trigger(defaultTab, 'tap');
//切換選項卡高亮
var current = document.querySelector(".mui-bar-tab>.mui-tab-item.mui-active");
if (defaultTab !== current) {
current.classList.remove('mui-active');
defaultTab.classList.add('mui-active');
}
});
Banner輪播圖
引入swiper
上bootcdn將swiper的樣式和js文件複製到本地
https://www.bootcdn.cn/Swiper/
此項目我們要使用vue框架進行開發,所以將vue.js也複製到本地。
封裝rem.js文件,實現移動端響應式佈局。
document.documentElement.style.fontSize =
document.documentElement.clientWidth / 3.75 +"px"
window.onresize = function(){
document.documentElement.style.fontSize =
document.documentElement.clientWidth / 3.75 +"px"
}
在homt.html中將文件依次引入:
<link href="css/mui.css" rel="stylesheet" />
<link rel="stylesheet" href="../../css/swiper.css">
<script src = "../../js/rem.js"></script>
<script src="../../js/mui.js"></script>
<script src = "../../js/vue.js"></script>
<script src = "../../js/swiper.js"></script>
編寫banner結構
<body>
<div id = "app">
<home-banner></home-banner>
</div>
<template id = "home-banner">
<div class="home-banner swiper-container">
<div class = "swiper-wrapper">
<div class = "swiper-slide"></div>
</div>
<div class="swiper-pagination"></div>
</div>
</template>
//註冊home-banenr組件
Vue.component("home-banner",{
template:"#home-banner"
})
new Vue({
el:"#app"
})
</script>
</body>
利用ajax方法請求數據
mui框架基於htm5plus的XMLHttpRequest,封裝了常用的Ajax函數,支持GET、POST請求方式,支持返回json、xml、html、text、script數據類型;
本着極簡的設計原則,mui提供了mui.ajax方法,並在mui.ajax方法基礎上,進一步簡化出最常用的mui.get()、mui.getJSON()、mui.post()三個方法。
http://dev.dcloud.net.cn/mui/...
created(){
mui.ajax('https://www.missevan.com/mobileWeb/newHomepage3',{
dataType:'json',//服務器返回json格式數據
success:function(data){
console.log(JSON.stringify(data))
}
});
}
獲取數據
在data中聲明一個空數組
data(){ //組件裏面數據必須是函數的形式 為了讓每一個實例可以獲取一份被返回對象的獨立的拷貝
return{
banners:[]
}
}
將獲取到的數據賦值給banner,這裏注意下this指向問題,不能寫成普通函數,要寫成箭頭函數。
success:(data) => {
// console.log(JSON.stringify(data))
this.banners = data.info.banner
}
在頁面中利用v-for循環渲染數據
<template id = "home-banner">
<div class="home-banner swiper-container">
<div class = "swiper-wrapper">
<div class = "swiper-slide"
v-for= "(banner,index) in banners"
:key = "index"
>
<img width = "100%" :src = "banner.pic" />
</div>
</div>
<div class="swiper-pagination">
</div>
</div>
</template>
現在banner還無法滑動,需要實例化,但是會出現輪播圖劃不動的現象。這是因為我們需要等到因數據改變,生成虛擬dom,對比完成之後生成真實dom再去進行實例化,所以我們要將實例化操作寫在this.$nextTick中。
//數據改變,生成新的虛擬dom,與上一次虛擬dom結構做對比,對比完成之後,生成好了新的真實dom,然後在這個函數的回調函數內部就可以訪問到因數據變化而渲染出來的真實dom結構了,所以就可以進行實例化相關的操作.
this.$nextTick(()=>{
new Swiper (".home-banner",{
loop:true,
pagination:{
el:".swiper-pagination"
}
})
})
效果演示:
沉浸式導航欄
http://ask.dcloud.net.cn/arti...
此模式下應用佔用全屏區域,而系統狀態欄會攔截用户操作事件,此時需要預留出系統狀態欄高度。
獲取系統狀態欄高度及沉浸式狀態判斷參考:
如何動態判斷沉浸式狀態欄模式:
http://ask.dcloud.net.cn/arti...
HBuilder創建的應用默認不使用沉浸式狀態欄樣式,需要進行如下配置開啓:
打開應用的manifest.json文件,切換到代碼視圖,在plus -> statusbar 下添加immersed節點並設置值為true。
"plus" : {
"statusbar" : {
"immersed" : true
}
}
注意:
- 真機運行不生效,需提交App雲端打包後才生效;
- 此功能僅在Android4.4及以上系統有效。
navigator狀態欄樣式
設置系統狀態欄樣式
void plus.navigator.setStatusBarStyle(style);
http://www.html5plus.org/doc/...
説明:設置應用在前台運行時系統狀態欄的樣式,默認值可通過manifest.json文件的plus->statusbar->style配置。
注意:此操作是應用全局配置,調用的Webview窗口關閉後仍然生效。
參數:
- style: ( String) 必選* 系統狀態欄樣式
- 可取值:"dark":深色前景色樣式(即狀態欄前景文字為黑色),此時background建議設置為淺顏色; "light":淺色前景色樣式(即狀態欄前景文字為白色),此時background建設設置為深顏色;
在全局index.html中設置樣式
mui.plusReady(function() {
//設置導航條的顏色
plus.navigator.setStatusBarStyle("light")
})
my-sound內容區
組件嵌套
分別編寫my-sound-box、my-sound、my-sound-item組件,互相嵌套。
<template id = "my-sound-box">
<div class="my-sound-box">
<my-sound>
</my-sound>
</div>
</template>
<template id = "my-sound">
<div class="my-sound">
<div class="panel-head">
<p>日抓</p>
<p>更多</p>
</div>
<div class="panel-body">
<my-sound-item></my-sound-item>
</div>
</div>
</template>
<template id = "my-sound-item">
<div class="my-sound-item">
<div class="img-box">
<img src="" alt="">
</div>
<div class="title"></div>
<div class="detail">
<span class="play-count"></span>
<span class = "comments"></span>
</div>
</div>
</template>
當寬度小於330px時利用媒體查詢標籤添加橫向滾動條
@media only screen and (max-width: 330px) {
.my-sound .panel-body{
justify-content: flex-start;
overflow-x: auto;
}
}
數據請求
在最外層組件my-sound-box中請求數據,將獲取到的music賦值給sounds。
//註冊my-sound-box組件
Vue.component("my-sound-box",{
template:"#my-sound-box",
data(){
return {
sounds:[]
}
},
created(){
mui.ajax('https://www.missevan.com/sound/newhomepagedata',{
dataType:'json',
success:(data)=>{
this.sounds = data.music
}
})
}
})
在my-sound-box模版中循環遍歷sounds,並且將拿到的值傳遞給子組件my-sound:
<template id = "my-sound-box">
<div class="my-sound-box">
<my-sound
v-for = "sound in sounds"
:key = "sound.id"
:sound = "sound">
</my-sound>
</div>
</template>
my-sound通過props接收my-sound-box傳遞來的sound:
//註冊my-sound組件
Vue.component("my-sound",{
template:"#my-sound",
props:["sound"]
})
再在my-sound模板中循環遍歷sound.objects_point,並且將item傳遞給子組件my-sound-item:
<template id = "my-sound">
<div class="my-sound">
<div class="panel-head">
<p>{{sound.title}}</p>
<p>更多</p>
</div>
<div class="panel-body">
<my-sound-item
v-for = "item in sound.objects_point"
:key = "item.id"
:item = "item"
></my-sound-item>
</div>
</div>
</template>
my-sound-item通過props接收父組件my-sound傳遞過來的參數item,在自己的模板中打印對應的數據:
<template id = "my-sound-item">
<div class="my-sound-item">
<div class="img-box">
<img :src="item.cover_image" alt="">
</div>
<div class="title">
{{item.soundstr}}}
</div>
<div class="detail">
<span class="play-count">{{item.view_count}}}</span>
<span class = "comments">{{item.comment_count}}</span>
</div>
</div>
</template>
發現問題:圖片加載不出來
原因:圖片地址不完整,需要手動拼接字符串
我們獲取到的地址:201906/12/fdc535722aa97844750cbb3843c6ec22152202.jpg
實際圖片地址:http://static.missevan.com/co...
解決辦法:
為了方便維護,在my-sound-item中添加一個計算屬性computed,直接返回拼接好的字符串:
Vue.component("my-sound-item",{
template:"#my-sound-item",
props:["item"],
computed:{
getImgUrl(){
let baseDir = "http://static.missevan.com/coversmini/"
return baseDir + this.item.cover_image
}
}
})
然後前端頁面直接調用計算屬性即可,不需要打括號。
<img :src="getImgUrl" alt="">
filters過濾器
對請求到的數據進行優化,當數值大於10000時顯示保留一位小數後加"萬"的形式。
filters:{
filterVal(val){
if(val>10000){
val = val/10000;
val = val.toFixed(1);
val = val+"萬"
}
return val;
}
}
調用數據的時候在後面添加filters:
<div class="detail">
<span class="play-count">{{item.view_count | filterVal}}</span>
<span class = "comments">{{item.comment_count | filterVal}}</span>
</div>
顯示系統的等待對話框
showWaiting:顯示系統等待對話框
http://www.html5plus.org/doc/...
在請求數據之前添加showWaiting等待框:
created(){
plus.nativeUI.showWaiting("等待中...");
mui.ajax('https://www.missevan.com/mobileWeb/newHomepage3',{
dataType:'json',//服務器返回json格式數據
success:(data) => {
// console.log(JSON.stringify(data))
this.banners = data.info.banner
//數據改變,生成新的虛擬dom,與上一次虛擬dom結構做對比,對比完成之後,生成好了新的真實dom,然後在這個函數的回調函數內部就可以訪問到因數據變化而渲染出來的真實dom結構了,所以就可以進行實例化相關的操作.
this.$nextTick(()=>{
new Swiper (".home-banner",{
loop:true,
pagination:{
el:".swiper-pagination"
}
})
})
}
});
}
設置一個標誌isOk,默認是0:
let isOk = 0;
在數據請求到的時候,每請求一次執行執行isOk++,當isOk ===2時,執行關閉等待框的方法:
success:(data) => {
// console.log(JSON.stringify(data))
this.banners = data.info.banner
//數據改變,生成新的虛擬dom,與上一次虛擬dom結構做對比,對比完成之後,生成好了新的真實dom,然後在這個函數的回調函數內部就可以訪問到因數據變化而渲染出來的真實dom結構了,所以就可以進行實例化相關的操作.
this.$nextTick(()=>{
new Swiper (".home-banner",{
loop:true,
pagination:{
el:".swiper-pagination"
}
})
})
isOk++
if(isOk ===2 ){
plus.nativeUI.closeWaiting("等待中...")
}
}
效果演示:
好玩頁面
創建頁面
在play文件夾下,新建含mui的html頁面,新建Vue實例掛載到div上。
設置頭部
使用mui自帶header組件生成頭部,添加common-headerclass名
<div id="app">
<header class="mui-bar mui-bar-nav common-header">
<a class = "mui-action-back mui-icon mui-icon-left-nav mui-pull-left"></a>
<h1 class="mui-title">好玩</h1>
</header>
</div>
獲取系統狀態欄高度
此時頭部被系統狀態欄擋住,需要調整頭部距離頁面頂部的高度。
getStatusbarHeight:獲取系統狀態欄高度
http://www.html5plus.org/doc/...
在mui.min.js中編寫設置狀態欄的方法
function setStatusBar(){
let commonHeader = document.querySelector(".common-header");
let status_bar = plus.navigator.getStatusbarHeight();
commonHeader.style.paddingTop = statusbar + "px"
commonHeader.style.height = 44 + status_bar + "px"
}
在前端頁面created生命週期中調用此方法
new Vue({
el:"#app",
created(){
setStatusBar()
}
})
musicbox
編寫musicbox組件和music組件
<template id = "music-box">
<div class="music-box">
<music></music>
</div>
</template>
在music-box中請求數據,編寫getMusics請求數據的方法,並且將數據賦值給musics。在created中調用getMusics方法
Vue.component("music-box",{
template:"#music-box",
data(){
return{
musics:[]
}
},
methods:{
getMusics(){
mui.ajax('https://www.missevan.com/explore/tagalbum',{
data:{
order:0
},
dataType:'json',//服務器返回json格式數據
success:(data) => {
this.musics = data.albums
}
});
}
},
created(){
this.getMusics()
}
})
Vue.component("music",{
template:"#music",
props:["music"]
})
在music組件中接收父組件傳遞過來的music,渲染數據到頁面上。
<template id = "music-box">
<div class="music-box">
<music
v-for = "music in musics"
:key = "music.id"
:music = "music"
></music>
</div>
</template>
<template id="music">
<div class="music">
<div class="img-box">
<img :src="music.front_cover" alt="">
</div>
<p class = "title">{{music.title}}</p>
</div>
</template>
v-for指令循環渲染必須要設置key值
- 跟diff算法有關,為了提高效率
如果在兩個元素之間插入新元素,如果沒有key的話需要把原位置的元素卸載了,把新元素插進來,然後依次卸載,會打亂後續元素的排列規則,如果有key值,只需要插入到對應位置即可,不會改變其他元素的走向。
- key也為了減免一些出錯問題
例如在數組中,本來第一個是選中的,這時候我們再去添加新的元素,如果沒有key的話那麼新添加進來的元素就會被選中,加上key就是為了避免出現這樣的問題。
上拉加載功能
單webview模式
http://dev.dcloud.net.cn/mui/...
引入上拉刷新容器,放入我們的數據music-box。
<!--下拉刷新容器-->
<div id="refreshContainer" class="mui-content mui-scroll-wrapper">
<div class="mui-scroll">
<!--數據列表-->
<music-box></music-box>
</div>
</div>
初始化方法類似下拉刷新,通過mui.init方法中pullRefresh參數配置上拉加載各項參數
created(){
this.getMusics()
mui.init({
pullRefresh : {
container:"#refreshContainer",//待刷新區域標識,querySelector能定位的css選擇器均可,比如:id、.class等
up : {
height:50,//可選.默認50.觸發上拉加載拖動距離
//默認啓動的話就不需要執行this.getMusics()了
//auto:true,//可選,默認false.自動上拉加載一次
contentrefresh : "正在加載...",//可選,正在加載狀態時,上拉加載控件上顯示的標題內容
contentnomore:'沒有更多數據了',//可選,請求完畢若沒有更多數據時顯示的提醒內容;
//不需要打括號了,打括號就立馬執行了
callback :this.getMusics //必選,刷新函數,根據具體業務來編寫,比如通過ajax從服務器獲取新數據;
}
}
});
}
當數據請求成功的時候執行,結束上拉刷新操作,並且執行this.p++,請求第二頁數據,在參數中判斷當前頁碼是否大於總頁碼,將請求到的數據使用concat方法拼接到原數組中,否則新請求到的數據會將原數據覆蓋。將this.p 與data.pagination.maxpage(最大頁數)做對比,當前頁數大於最大頁數的時候停止請求。
success:(data) => {
this.musics = this.musics.concat(data.albums)
this.p++;
//若還有更多數據,則傳入False,否則傳入distribute
mui('#refreshContainer').pullRefresh().endPullupToRefresh(this.p > data.pagination.maxpage);
}
雙webview模式
http://dev.dcloud.net.cn/mui/...
通過兩個窗口來實現,在play.html中加載子頁面play-content.html
主頁面內容比較簡單,就只有一個頭部,在url中添加下拉刷新內容子頁面地址。
new Vue({
el:"#app",
created(){
// setStatusBar()
mui.init({
subpages:[{
url:"./play-content.html",//下拉刷新內容頁面地址
id:"play-content.html",//內容頁面標誌
styles:{
top:44 + plus.navigator.getStatusbarHeight(),//內容頁面頂部位置,需根據實際頁面佈局計算,若使用標準mui導航,頂部默認為48px;
bottom:0//其它參數定義
}
}]
});
}
})
創建子頁面,下拉刷新的操作寫在子頁面中:
<body>
<div id="app">
<!--下拉刷新容器-->
<div id="refreshContainer" class="mui-content mui-scroll-wrapper">
<div class="mui-scroll">
<!--數據列表-->
<music-box></music-box>
</div>
</div>
<!-- 置頂 -->
<div @tap="backtop" class = "back-top-box" v-if="isShow">
<div class = "back-top">
<i class = "mui-icon mui-icon-arrowup"></i>
</div>
</div>
</div>
<template id = "music-box">
<div class="music-box">
<music
v-for = "music in musics"
:key = "music.id"
:music = "music"
></music>
</div>
</template>
<template id="music">
<div class="music" @tap = "toAlbum(music.id)">
<div class="img-box">
<img :src="music.front_cover" alt="">
</div>
<p class = "title">{{music.title}}</p>
</div>
</template>
<script src="../../js/mui.min.js"></script>
<script src = "../../js/vue.js"></script>
<script type="text/javascript">
Vue.component("music-box",{
template:"#music-box",
data(){
return{
musics:[],
order:0,
p:1,
tid:0
}
},
mounted(){
window.addEventListener("getTid",e=>{
// console.log(e.detail.tid)
this.changeType(e.detail.tid)
})
},
methods:{
changeType(tid){
this.musics = [];
this.p = 1;
this.tid = tid
this.getMusics();
//重置上拉加載
mui('#refreshContainer').pullRefresh().refresh(true);
//需要實現滾動到頂部
mui("#refreshContainer").pullRefresh().scrollTo(0,0);
},
getMusics(){
// let {order,p} = this;
let data;
if(this.tid === 0){ //説明是全部分類
data = {order:this.order,p:this.p}
}else{
data = {order:this.order,p:this.p,tid:this.tid}
}
mui.ajax('https://www.missevan.com/explore/tagalbum',{
data,
dataType:'json',//服務器返回json格式數據
success:(data) => {
this.musics = this.musics.concat(data.albums)
this.p++;
//若還有更多數據,則傳入False,否則傳入distribute
mui('#refreshContainer').pullRefresh().endPullupToRefresh(this.p > data.pagination.maxpage);
}
});
}
},
created(){
//this.getMusics()
mui.init({
pullRefresh : {
container:"#refreshContainer",//待刷新區域標識,querySelector能定位的css選擇器均可,比如:id、.class等
up : {
height:50,//可選.默認50.觸發上拉加載拖動距離
//默認啓動的話就不需要執行this.getMusics()了
auto:true,//可選,默認false.自動上拉加載一次
contentrefresh : "正在加載...",//可選,正在加載狀態時,上拉加載控件上顯示的標題內容
contentnomore:'沒有更多數據了',//可選,請求完畢若沒有更多數據時顯示的提醒內容;
//不需要打括號了,打括號就立馬執行了
callback :this.getMusics //必選,刷新函數,根據具體業務來編寫,比如通過ajax從服務器獲取新數據;
}
}
});
}
})
Vue.component("music",{
template:"#music",
props:["music"],
methods:{
toAlbum(albumId){
//打開album.html這個窗口
mui.openWindow({
url:"../album/album.html",
id:"album.html",
extras:{
albumId
},
styles:{
//設置一個漸變式導航欄
"titleNView":{
backgroundColor: '#234245',//導航欄背景色
titleText: '貓耳FM',//導航欄標題
titleColor: '#fff',//文字顏色
type:'transparent',//透明漸變樣式
autoBackButton: true,//自動繪製返回箭頭
splitLine:{//底部分割線
color:'#cccccc'
}
}
}
})
}
}
})
new Vue({
el:"#app",
data:{
isShow:false
},
methods:{
backtop(){
//需實現滾動到頂部
mui('#refreshContainer').pullRefresh().scrollTo(0,0,100)
}
},
mounted(){ //可以拿到真實dom
//監聽滾動事件
document.querySelector('#refreshContainer' ).addEventListener('scroll', (e)=>{
var scroll = mui('#refreshContainer').pullRefresh(); //獲取具體容器的滾動條
// console.log(scroll.y);
if(scroll.y <= -300 && !this.isShow){
this.isShow = true;
}else if(scroll.y > -300 && this.isShow){
this.isShow = false;
}
})
}
})
</script>
</body>
效果演示:
返回頂部
編寫返回頂部按鈕,添加點擊事件。
<!-- 返回頂部 -->
<div @tap="backtop" class = "back-top-box">
<div class = "back-top">
<i class = "mui-icon mui-icon-arrowup"></i>
</div>
</div>
編寫返回頂部方法
methods:{
backtop(){
//需實現滾動到頂部
mui('#refreshContainer').pullRefresh().scrollTo(0,0,100)
}
}
mui提供的返回頂部的方法

一開始不讓返回頂部按鈕顯示,在data中定義一個數據isShow:false,通過v-if指令來控制按鈕的現實與隱藏,當滾動到一定高度的時候再顯示出來。
<div @tap="backtop" class = "back-top-box" v-if="isShow">
<div class = "back-top">
<i class = "mui-icon mui-icon-arrowup"></i>
</div>
</div>
在mounted鈎子函數中監聽滾動事件(注意不能寫在created生命週期中,因為created中獲取不到真實dom)
https://www.cnblogs.com/xzzzy...
mounted(){ //可以拿到真實dom
//監聽滾動事件
document.querySelector('#refreshContainer' ).addEventListener('scroll', function (e ) {
var scroll = mui('#refreshContainer').scroll();
console.log(scroll.y);
})
}
由於我們使用的是雙web view模式,所以會出現兩個滾動條,需要改成:
mounted(){ //可以拿到真實dom
//監聽滾動事件
document.querySelector('#refreshContainer' ).addEventListener('scroll', function (e ) {
var scroll = mui('#refreshContainer').pullRefresh(); //獲取具體容器的滾動條
console.log(scroll.y);
})
}
由於向下滾動是負值,所以需要判斷,數值小於等於-300的時候給isShow賦值為true讓返回頂部按鈕顯示,反之則為false,不顯示。同時需要將普通函數function改為箭頭函數,否則this指向有問題。
mounted(){ //可以拿到真實dom
//監聽滾動事件
document.querySelector('#refreshContainer' ).addEventListener('scroll', (e)=>{
var scroll = mui('#refreshContainer').pullRefresh(); //獲取具體容器的滾動條
// console.log(scroll.y);
if(scroll.y <= -300){
this.isShow = true;
}else{
this.isShow = false;
}
})
}
為了不讓isShow頻繁的賦值,給if添加判斷條件:
if(scroll.y <= -300 && !this.isShow){
this.isShow = true;
}else if(scroll.y > -300 && this.isShow){
this.isShow = false;
}
效果演示:

mine頁面
搭建頁面
搭建mine我的頁面,添加登錄按鈕,給登錄按鈕添加點擊事件,點擊跳轉到login登錄頁面。
<div id="app">
<div class="user-info">
<div class="login-info">
<div class="img-box">
<img :src="getUserimg" alt="">
</div>
<p v-if = "!userInfo" class = "login"><button @tap = "login ">登錄</button></p>
<p v-else class = "username">{{userInfo.nickname}}</p>
</div>
<div class = "exit" @tap= "exit"><i class = "mui-icon mui-icon-more"></i></div>
</div>
</div>
登錄功能
mui提供登錄模版,右鍵——新建項目選擇帶登錄和設置的MUI項目模板。
我們需要在mine頁面打開login新頁面,將login.html頁面添加到我們pages中login目錄下,修改文件引入路徑。
打開新頁面方法:
http://dev.dcloud.net.cn/mui/...
在methods中編寫打開login頁面的方法:
new Vue({
el:"#app",
methods:{
login(){
mui.openWindow({
url:"../login/login.html",
id:"login.html",
styles:{
top:0,//新頁面頂部位置
bottom:0,//新頁面底部位置
},
show:{
autoShow:true,//頁面loaded事件發生後自動顯示,默認為true
aniShow:"slide-in-top",//頁面顯示動畫,默認為”slide-in-right“;
duration:2000//頁面動畫持續時間,Android平台默認100毫秒,iOS平台默認200毫秒;
}
})
}
}
})
在h5+中查看AnimationTypeShow的方法:一組用於定義頁面或控件顯示動畫效果
http://www.dcloud.io/docs/api...
第三方登錄
OAuth模塊管理客户端的用户登錄授權驗證功能,允許應用訪問第三方平台的資源。
getServices:獲取登錄授權認證服務列表
http://www.html5plus.org/doc/...
Hbuilder目前支持的第三方登錄列表有QQ、微信、新浪微博,所以for循環之後就會把Hbuilder內部支持的第三方登錄列表跟我們所期待的(var authBtns = ['qihoo', 'weixin', 'sinaweibo', 'qq'];)進行一個匹配,如果匹配上之後,它就會在頁面上渲染圖標,並且給圖標自動綁定一個authId,這個authId和你提供的service.id匹配上了才追加上去,另外它還對weixin的id進行了強制性判斷,如果service.id名稱叫weixin並且未安裝,就添加disabled禁用。
$.plusReady(function() {
plus.screen.lockOrientation("portrait-primary");
//第三方登錄 定義了需要支持的第三方登錄名稱
var authBtns = ['qihoo', 'weixin', 'sinaweibo', 'qq']; //配置業務支持的第三方登錄
var auths = {};
var oauthArea = doc.querySelector('.oauth-area');
plus.oauth.getServices(function(services) {
//終端支持的登錄授權認證服務列表
//所以hbuilder第三方服務認證列表目前支持的Services:weixin sinaweibo qq
for (var i in services) {
var service = services[i];
auths[service.id] = service;//{weixin:weixinService,qq:qqService}
if (~authBtns.indexOf(service.id)) { //==if (authBtns.indexOf(service.id) > -1)
var isInstalled = app.isInstalled(service.id);
var btn = document.createElement('div');
//如果微信未安裝,則為不啓用狀態
btn.setAttribute('class', 'oauth-btn' + (!isInstalled && service.id === 'weixin' ? (' disabled') : ''));
btn.authId = service.id;
btn.style.backgroundImage = 'url("../../images/' + service.id + '.png")'
oauthArea.appendChild(btn);
}
}
通過事件委託給按鈕添加點擊事件,通過getUserInfo方法獲取用户信息,並存儲在localStorage中。
//事件委託
$(oauthArea).on('tap', '.oauth-btn', function() {
if (this.classList.contains('disabled')) {
plus.nativeUI.toast('您尚未安裝微信客户端');
return;
}
var auth = auths[this.authId];
var waiting = plus.nativeUI.showWaiting();
auth.login(function() {
waiting.close();
plus.nativeUI.toast("登錄認證成功");
auth.getUserInfo(function() {
plus.nativeUI.toast("獲取用户信息成功");
var name = auth.userInfo.nickname || auth.userInfo.name;
//nickname headimgurl
localStorage.userInfo = JSON.stringify(auth.userInfo)
}, function(e) {
plus.nativeUI.toast("獲取用户信息失敗:" + e.message);
});
}, function(e) {
waiting.close();
plus.nativeUI.toast("登錄認證失敗:" + e.message);
});
});
在mine頁面中聲明一個data,userInfo用來存放用户數據,在methods中添加從localStorage中獲取用户信息的方法。
getUserInfo(){
this.userInfo = JSON.parse(localStorage.userInfo ? localStorage.userInfo : "null")
}
給p標籤添加v-if/v-else指令,通過userInfo來控制登錄按鈕的顯示與隱藏。
<p v-if = "!userInfo" class = "login"><button @tap = "login ">登錄</button></p>
<p v-else class = "username">{{userInfo.nickname}}</p>
這時候系統會報錯,説是沒有辦法給圖片賦值為空,所以在頁面中獲取圖片屬性的時候,需要等到數據請求到時再獲取,這時候添加一個計算屬性getUserimg,判斷一下是否有圖片信息,如果沒有圖片信息則使用未登錄默認圖片。
computed:{
getUserimg(){
return this.userInfo?this.userInfo.headimgurl:"../../images/"
}
}
在頁面中調用計算屬性getUserimg,獲取圖片數據。
<img :src="getUserimg" alt="">
* 注意:需要重新啓動程序才可以看到頭像和用户名,因為從mine頁面跳入login窗口的時候,mine窗口沒有被銷燬,所以沒有走created生命週期函數。
AuthService:登錄授權認證服務對象
http://www.html5plus.org/doc/...
退出功能
編寫exit方法,使用h5+的actionSheet方法
actionSheet:彈出系統選擇按鈕框
http://www.html5plus.org/doc/...
ActionSheetCallback:系統選擇按鈕框的回調函數
http://www.html5plus.org/doc/...
在回調函數中我們可以拿到用户點擊的項目下標,數據類型為number,根據返回的數值進行switch判斷,當點擊註銷登錄的時候,清除localStorage中的用户信息,並且重新執行getUserInfo,當點擊切換賬號時直接跳轉到登錄頁面(注意箭頭函數的this指向問題)。
exit(){
plus.nativeUI.actionSheet(
{
title:"Plus is ready!",
cancel:"取消",
buttons:[
{
style:"destructive",
title:"註銷登錄"
},
{
title:"切換登錄"
}
]},
(e)=>{
console.log("User pressed: "+e.index);
switch(e.index){
case 1:
localStorage.removeItem("userInfo",null)//清除用户信息
this.getUserInfo()//重新執行,頁面重新渲染
break;
case 2:
this.login() //點擊切換賬號直接跳到登錄頁面
break;
}
}
);
}
效果演示:

解決登錄後拿不到用户信息的問題
我們點擊第三方登錄,登錄成功之後返回mine頁面應該顯示用户信息,但是沒有顯示,原因是這個組件沒有進行銷燬,沒有銷燬我們看不到結果,因為created只會執行一次,現在我們就要使用mui中提供的窗口之間的通信。
添加自定義事件
http://dev.dcloud.net.cn/mui/...
我們在mine頁面中的mounted鈎子函數中添加一個監聽自定義事件,等待這個事件被觸發,初始化的時候會執行這個函數,定義一個方法:"login:end",回調函數中獲取用户信息。
mounted(){
//定義自定義事件
window.addEventListener("login:end",e=>{
this.getUserInfo()
console.log(e.detail.a)
})
}
在login頁面,登錄成功之後需要調用mine頁面中的login:end方法,通過mui.fire可以觸發目標窗口的自定義事件。我們需要給它傳遞三個參數

由於我們之前在總的index.html中定義了id,所以這裏可以通過getWebviewById的方法獲得相應的webView

//觸發mine.html裏面login:end方法
let mine = plus.webview.getWebviewById("mine.html")
mui.fire(mine,"login:end",{a:100})
效果演示:

音單分類
創建音單分類頁面
在play文件夾中新建一個play-type頁面,我們希望在play頁面點擊右上角三個點打開play-type頁面,給a標籤添加點擊事件。
<a @tap = "changeType" class = "mui-icon-more mui-icon mui-icon-left-nav mui-pull-right"></a>
在methods中編寫changeType方法,調用mui中的打開新頁面方法
http://dev.dcloud.net.cn/mui/...
methods:{
changeType(){
//打開新窗口
mui.openWindow({
url:"./play-type.html",
id:"play-type.html",
styles:{
bottom:0,//新頁面底部位置
height:260
},
show:{
autoShow:true,//頁面loaded事件發生後自動顯示,默認為true
aniShow:"slide-in-bottom",//頁面顯示動畫,默認為”slide-in-right“;
duration:200//頁面動畫持續時間,Android平台默認100毫秒,iOS平台默認200毫秒;
}
})
添加遮罩層
找到H5+中的Webview方法中的WebviewObject:Webview窗口對象,用於操作加載HTML頁面的窗口
http://www.html5plus.org/doc/...
setStyle方法:
http://www.html5plus.org/doc/...
查看傳遞的參數

通過currentWebview獲得當前窗體,給當前窗體通過setStyle設置遮罩層。
//設置遮罩層
let self = plus.webview.currentWebview()
self.setStyle({mask:'rgba(0,0,0,0.5)'})
添加關閉遮罩層事件,當點擊遮罩層的時候讓遮罩層消失,並且讓play-type頁面關閉
mounted(){
//綁定自定義事件
let self = plus.webview.currentWebview()
self.addEventListener('maskClick', function(){ //點擊遮罩層
self.setStyle({mask:'none'}); //讓遮罩層消失
plus.webview.getWebviewById("play-type.html").close();//讓play-type窗口關閉
},false);
}
請求數據
在data中聲明musicType
data:{
musicType:null
}
在created鈎子函數中使用ajax請求數據
created(){
mui.ajax({
url:"https://www.missevan.com/malbum/recommand",
dataType:"json",
success:(data)=>{
console.log(JSON.stringify(data))
//{"success":true,"info":{"情感":[[170,"熱血"],[28,"治癒"],[4421,"抖腿"]],"場景":[[26310,"玩遊戲"],[26311,"運動聽"],[25,"作業向"]],"主題":[[370,"OP"],[376,"ED"],[273,"翻唱"],[5,"古風"],[850,"同人音樂"],[13349,"遊戲原聲"],[4,"廣播劇"]]}}
this.musicType = data.info;
}
})
}
在頁面中通過v-for循環渲染數據
<div class="play-type-box">
<div
class="play-type"
v-for = "(value,key,index) in musicType"
:key = "index"
>
<span>{{key}}</span>
<button
v-for = "(item,i) in value"
:key = "i"
>{{item[1]}}</button>
</div>
</div>
在頭部插入一個數據
在musicType中添加一個"全部音單"數據
this.musicType["全部"] = [[0,"全部音單"]]
如果我們想讓全部音單在前面顯示就需要將這條語句寫在前面,但是之後賦值會將它覆蓋,所以我們需要將musicType賦值為空數組,然後用ES6中的展開符將它展開,然後再展開data.info。
this.musicType["全部"] = [[0,"全部音單"]]
this.musicType = {...this.musicType,...data.info}
或者通過ES5中的Object.assign方法將數據合併
//或者通過ES5中的assign方法
this.musicType = Object.assign({},this.musicType,data.info)
close關閉功能
添加一個點擊事件,執行關閉功能
<div class="close" @tap="close">
<i class = "mui-icon mui-icon-closeempty"></i>
</div>
我們需要調用play.html中的方法來關閉play-type頁面
在play中將關閉窗體的方法,單獨封裝為
closeType(self){
self.setStyle({mask:'none'});//讓遮罩層消失
plus.webview.getWebviewById("play-type.html").hide();//讓play-type窗口關閉
}
在mounted鈎子函數中調用,並且將"close:type"方法傳遞給play-type.html
mounted(){
//綁定自定義事件
let self = plus.webview.currentWebview()
self.addEventListener('maskClick', (e)=>{//點擊遮罩層
this.closeType(self)
},false);
//綁定自定義事件
window.addEventListener("close:type",e=>{
this.closeType(self)
})
}
在play-type頁面中的close方法中通過mui.fire來調用該方法
methods:{
close(){
//需要關閉遮罩層與play-type.html
let play = plus.webview.getWebviewById("play.html")
mui.fire(play,"close:type")
}
}
默認選中全部音單
默認讓它選中全部音單,在data中聲明一條數據activeId,默認是0。
data:{
musicType:{},
activeId:0
}
在button按鈕上動態添加class,判斷當前Id是否等於activeId,如果相等,則添加class。
<button
v-for = "(item,i) in value"
:key = "i"
:class = "{'mui-btn-danger' : item[0] === activeId}"
>{{item[1]}}</button>
給按鈕添加點擊事件,將當前id變成activeId,實現點擊相應按鈕出現選中狀態。
<button
v-for = "(item,i) in value"
:key = "i"
:class = "{'mui-btn-danger' : item[0] === activeId}"
@click = "activeId = item[0]"
>{{item[1]}}</button>
但是當我們關閉列表頁面再打開的時候,選中狀態又變回了全部音單,這是因為我們關閉列表頁的時候這個組件被銷燬了,activeId又變回了0,所以我們在closeType方法中不能使用close方法,需要使用hide隱藏方法。
hide:隱藏Webview窗口
http://www.html5plus.org/doc/...
closeType(self){
self.setStyle({mask:'none'}); //讓遮罩層消失
// plus.webview.getWebviewById("play-type.html").close();//讓play-type窗口關閉
plus.webview.getWebviewById("play-type.html").hide();//讓play-type窗口隱藏
}
點擊切換相應音單
當我們點擊按鈕的時候activeId發生變化,這時候我們需要後面的play-content請求相應數據,所以ajax中的data需要發生變化,要獲取url相應的id,在play-content的mounted中添加一個事件監聽。
mounted(){
window.addEventListener("getTid",e=>{
console.log(e.detail.tid)
})
}
我們需要編寫一個changeType方法,在mounted鈎子函數中調用,並且將play-type傳遞過來的tid作為參數傳遞過去。
mounted(){
window.addEventListener("getTid",e=>{
// console.log(e.detail.tid)
this.changeType(e.detail.tid)
})
}
在play-type添加一個watch監聽,將activeId最新的值val傳遞給play-content,當我們的數據一旦變化它就可以獲取到對應的tid。
watch:{
activeId(val){
let playContent = plus.webview.getWebviewById("play-content.html")
mui.fire(playContent,"getTid",{tid:val})
}
}
編寫changeType方法,在音單類型改變的時候需要將musics清空,p頁碼變為1,tid變為傳遞過來的tid,並且重新執行請求數據操作,調用getMusics方法。
changeType(tid){
this.musics = [];
this.p = 1;
this.tid = tid
this.getMusics();
}
在getMusics中對data數據進行判斷,當tid為0的時候顯示全部音單,傳遞order和p字段過去,當tid不為0的時候顯示相對應的數據,並且將tid傳遞過去。
let data;
if(this.tid === 0){ //説明是全部分類
data = {order:this.order,p:this.p}
}else{
data = {order:this.order,p:this.p,tid:this.tid}
}
將data傳遞給mui.ajax:
mui.ajax('https://www.missevan.com/explore/tagalbum',{
data,
dataType:'json',//服務器返回json格式數據
success:(data) => {
this.musics = this.musics.concat(data.albums)
this.p++;
//若還有更多數據,則傳入False,否則傳入distribute
mui('#refreshContainer').pullRefresh().endPullupToRefresh(this.p > data.pagination.maxpage);
}
});
重啓上拉加載
出現問題:從別的頁面跳轉到全部音單頁面上拉加載失效
原因:如果別的頁面只有一頁數據,切換到全部音單的時候,也認為數據請求完畢了,就把上拉加載功能禁用了
解決辦法:重置上拉加載

changeType(tid){
this.musics = [];
this.p = 1;
this.tid = tid
this.getMusics();
//重置上拉加載
mui('#refreshContainer').pullRefresh().refresh(true);
}
出現問題:從全部音單跳轉到別的頁面時數據從上方加載進來
原因:全部音單的頁碼為第三頁,跳轉到別的頁面的第一頁
解決辦法:每次切換的時候讓它滾動到最上面,從頭部開始加載數據
changeType(tid){
this.musics = [];
this.p = 1;
this.tid = tid
this.getMusics();
//重置上拉加載
mui('#refreshContainer').pullRefresh().refresh(true);
//需要實現滾動到頂部
mui("#refreshContainer").pullRefresh().scrollTo(0,0);
}
體驗優化
每次切換完畢應該讓列表頁關閉
在play-type中的watch監聽裏調用下封裝好的close方法,關閉遮罩與play-type窗口。
watch:{
activeId(val){
let playContent = plus.webview.getWebviewById("play-content.html")
mui.fire(playContent,"getTid",{tid:val})
//關閉遮罩與play-type窗口
this.close()
}
}
在play-content中的上拉加載操作中有一個auto屬性,如果註釋掉請求數據的操作this.getMusics,將auto屬性設置為true,會默認執行一次上拉加載,效果與請求數據操作相同。
created(){
//this.getMusics()
mui.init({
pullRefresh : {
container:"#refreshContainer",//待刷新區域標識,querySelector能定位的css選擇器均可,比如:id、.class等
up : {
height:50,//可選.默認50.觸發上拉加載拖動距離
//默認啓動的話就不需要執行this.getMusics()了
auto:true,//可選,默認false.自動上拉加載一次
contentrefresh : "正在加載...",//可選,正在加載狀態時,上拉加載控件上顯示的標題內容
contentnomore:'沒有更多數據了',//可選,請求完畢若沒有更多數據時顯示的提醒內容;
//不需要打括號了,打括號就立馬執行了
callback :this.getMusics //必選,刷新函數,根據具體業務來編寫,比如通過ajax從服務器獲取新數據;
}
}
});
}
效果演示:

album詳情頁
點擊跳轉
在play-content頁面中給每一個music專輯綁定一個點擊事件toAlbum,在調用它的時候傳遞music.id。
<div class="music" @tap = "toAlbum(music.id)">
編寫toAlbum方法,通過mui.openWindow添加打開新頁面方法,將albumId傳遞給album,通過styles設置一個漸變式導航。
Vue.component("music",{
template:"#music",
props:["music"],
methods:{
toAlbum(albumId){
//打開album.html這個窗口
mui.openWindow({
url:"../album/album.html",
id:"album.html",
extras:{
albumId
},
styles:{
//設置一個漸變式導航欄
"titleNView":{
backgroundColor: '#234245',//導航欄背景色
titleText: '貓耳FM',//導航欄標題
titleColor: '#fff',//文字顏色
type:'transparent',//透明漸變樣式
autoBackButton: true,//自動繪製返回箭頭
splitLine:{//底部分割線
color:'#cccccc'
}
}
}
})
}
}
})
在album頁面中let self = plus.webview.currentWebview(),通過self.albumId就可以拿到傳遞過來的參數,參數命名的時候不要直接命名id,不然他會優先打印當前窗口文件的id(album.html),而不是你傳遞過來的參數。在mui.ajax中獲取數據。
created(){
let self = plus.webview.currentWebview()
// console.log(self.albumId)
mui.ajax({
url:"https://www.missevan.com/sound/soundalllist",
data:{
albumid:self.albumId
},
dataType:"json",
success:data => {
this.album = data.info.album;
this.owner = data.info.owner;
}
})
}
在前端頁面將數據渲染輸出:
<div class="album-box">
<div class="album-bg">
<img :src="album.front_cover" alt="">
</div>
<div class="img-box">
<img :src="album.front_cover" alt="">
</div>
<div class="album-info">
<p class = "title">{{album.title}}</p>
<p class="auther">
<img class = "headimg" :src="owner.boardiconurl2" alt="">
<span class = "nickname">{{album.username}}</span>
</p>
</div>
</div>
請求list數據
在data中聲明sounds和sound數據:
data:{
album:{},
owner:{},
sounds:[],
sound:[]
}
請求數據的時候給sounds賦值,並且通過.splice方法切分出十條數據複製給sound。
success:data => {
this.album = data.info.album;
this.owner = data.info.owner;
this.sounds = data.info.sounds;//[0,115] ==> [10,115]
this.sound = this.sounds.splice(0,10) //[0,9]
}
在頁面上渲染數據
<div class="album-list">
<div class="album-list-item"
v-for = "item in sound"
:key = "item.id"
@tap = "toDetail(item.id)"
>
<div class="img-box">
<img :src="item.front_cover" alt="">
</div>
<div class="album-detail">
<p class = "title">{{item.soundstr}}</p>
<p class="album-desc">
<span class="play">{{item.view_count_formatted}}</span>
<span class="time">{{item.duration}}</span>
</p>
</div>
</div>
</div>
向下滾動獲取數據

在data中聲明cHeight和pHeight
data:{
album:{},
owner:{},
sounds:[],
sound:[],
cHeight:"",
pHeight:""
}
在mounted鈎子函數中綁定滾動事件,在當前窗口高度document.documentElement.clientHeight+滾動高度document.body.scrollTop || document.documentElement.scrollTop()+距離底部高度>整個文檔的高度document.documentElement.offsetHeight的時候在之前的基礎上追加十條數據。
mounted(){
window.addEventListener("scroll",e=>{
let sTop = document.body.scrollTop || document.documentElement.scrollTop()//獲取滾動高度
this.cHeight = document.documentElement.clientHeight;//獲取當前可視區域的高度
this.pHeight = document.documentElement.offsetHeight;//獲取整個文檔的高度
if(this.cHeight+sTop+50 >= this.pHeight){//50:距離底部的高度
// console.log("滾動到底部了")
//在之前的基礎上追加十條數據
this.sound = this.sound.concat(this.sounds.splice(0,10))
}
})
}
發現問題:當數據加載完畢的時候向下滾動會繼續加載
解決辦法:將滾動監聽單獨封裝一個方法listenScroll
methods:{
listenScroll(e){
let sTop = document.body.scrollTop || document.documentElement.scrollTop;
this.cHeight = document.documentElement.clientHeight;
this.pHeight = document.documentElement.offsetHeight;
if(this.cHeight+sTop+50 >= this.pHeight){
console.log("滾動到底部了!")
this.sound = this.sound.concat(this.sounds.splice(0,10))
}
}
}
在頁面初始化的時候添加事件監聽。
mounted(){
window.addEventListener("scroll",this.listenScroll)
}
添加一個watch監聽,監聽sounds的變化,當sounds數組長度為0的時候,移除事件。
watch:{
sounds(val){
if(val.length === 0){
window.removeEventListener("scroll",this.listenScroll)
}
}
}
效果演示:
詳情頁
在album中添加點擊事件toDetail,將detailId傳遞給detail.html。
<div class="album-list-item"
v-for = "item in sound"
:key = "item.id"
@tap = "toDetail(item.id)"
>
編寫toDetail方法,打開對應的頁面。
toDetail(detailId){
mui.openWindow({
url:"../detail/detail.html",
extras:{
detailId
}
})
}
在詳情頁detail.html中通過self.detailId打開相應的目標窗口。
new Vue({
el:"#app",
created(){
let self = plus.webview.currentWebview();
mui.openWindow({
url:"https://m.missevan.com/sound/"+self.detailId,
id:"detail.html"
})
}
})
現在我們想將頭部的貓耳FM刪掉
我們在控制枱中輸入兩條語句,去掉當前窗口的頭部導航欄

我們聲明一個參數,返回一個窗體對象
let albumDetail = mui.openWindow({
url:"https://m.missevan.com/sound/"+self.detailId,
id:"detail.html"
})
調用對象的onloaded方法,當窗體載入的時候執行這兩條js語句
evalJS
調用evalJS在Webview窗口中執行JS腳本
http://www.html5plus.org/doc/...
albumDetail.onloaded = function(){
albumDetail.evalJS('document.getElementsByTagName("header")[0].innerHTML = "";document.getElementsByTagName("container")[0].style.padding = 0')
}
在style中設置一個漸變式導航欄
styles:{
//設置一個漸變式導航欄
"titleNView":{
backgroundColor: '#234245',//導航欄背景色
titleColor: '#fff',//文字顏色
type:'transparent',//透明漸變樣式
autoBackButton: true,//自動繪製返回箭頭
splitLine:{//底部分割線
color:'#cccccc'
}
}
}
讓頭部顯示對應標題
在success中通過self.setStyle方法設置成titleNView的樣式,讓頭部顯示對應的標題。
self.setStyle({
"titleNView":{
titleText:this.album.title
}
})
優化時間顯示方式
編寫filter過濾器,將毫秒數轉成"分鐘:秒數"的形式。
filters:{
timer(val){
val = val/1000;
let second = Math.ceil(val%60)
let min = parseInt(val/60)
second = second < 10 ? "0" + second:second
return min + ":" + second
}
}
在時間展示的span標籤中應用
<span class="time">{{item.duration | timer}}</span>
效果演示:

打包發佈
配置文件
上線發佈之前我們需要在manifest.json中針對App進行設置,設置內容分別為:
- 基礎配置:主要是基本的應用信息,包含;應用名稱、版本號、入口文件、是否需要根據重力感應橫豎屏。
- 圖標配置:安裝應用後,顯示在主頁的入口圖標,可以根據你上傳的大圖標自動壓縮生成各種小圖標。
- 啓動圖配置:應用啓動後展示給用户的圖片。
- SDK配置:這裏是你引用的第三方插件的配置,例如:支付、推送、分享等。
- 模塊權限配置:你需要使用的原生模塊,去掉不需要的模塊,可以減少原生安裝包的體積。
- App常用其它設置
- 源碼視圖

雲端打包
HBuilder提供的打包有云打包和本地打包兩種。
HBuilder提供的雲打包對正常開發者是免費的。但過多浪費服務器資源會額外收費。用本地打包無任何限制。
雲打包的特點是DCloud官方配置好了原生的打包環境,可以把HTML等文件編譯為原生安裝包。
在manifest.json中配置完畢就可以進行雲端打包了,你只需要提交代碼,不用部署xcode和Android sdk就可以打包應用。打包完畢下載安裝,打包速度取決於你的網速。
文件名右鍵選擇【發行】——原生APP雲打包

選擇IOS平台或者Andriod平台,如果只是自己測試可以使用DCloud的公用證書,但是不能發佈上線。已經打好的安裝包,允許開發者在指定天內下載指定次數。超時或超次後服務器端會清除文件。

打包成功後會生成下載地址,點擊下載後就可以進行安裝使用了。
