Huilder X開發-貓耳APP(H5+/MUI/VUE)

視覺派Pie發表於2019-09-10

image

前言

近年來國內出現了一些可以讓前端人員編寫移動端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...

image

新建專案

開啟HBuilder,在選單欄中選擇檔案——新建——專案,選擇5+App,建立一個mui專案,填寫檔名稱、儲存位置,點選建立,會給你生成一個包含mui的js、css、字型資源的專案模版。

檔案結構

新建完成後,會在左側的專案管理器中出現如下目錄結構,跟我們平時做前端開發的專案類似。mainifest.json檔案中儲存的是app相關的配置。

真機除錯

使用資料線連線手機和電腦,在Android裝置會自動安裝並啟動HBuilder除錯基座,IOS系統的同學請下載一個名字叫HBuilder的除錯外掛,點選視窗上方的播放鍵小圖示或者使用快捷鍵command+r在手機上執行。

真機執行有3個特點:

  1. 真實。雖然PC端HBuilder右側的內建瀏覽器也可以看大致的頁面,但真實的佈局效果以及手機上的特殊能力呼叫,還是必須在真機測試。
  2. 邊改邊看。在HBuilder更改頁面並儲存後,可立即同步在真機上看到儲存後的顯示效果。比開發原生應用還方便。
  3. 檢查錯誤和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"
                                    }
                                })
                            })

效果演示:

image

沉浸式導航欄

http://ask.dcloud.net.cn/arti...

此模式下應用佔用全屏區域,而系統狀態列會攔截使用者操作事件,此時需要預留出系統狀態列高度。
獲取系統狀態列高度及沉浸式狀態判斷參考:

如何動態判斷沉浸式狀態列模式:

http://ask.dcloud.net.cn/arti...

HBuilder建立的應用預設不使用沉浸式狀態列樣式,需要進行如下配置開啟:
開啟應用的manifest.json檔案,切換到程式碼檢視,在plus -> statusbar 下新增immersed節點並設定值為true。

"plus" : {
        "statusbar" : {
            "immersed" : true
        }
 }

注意:

  1. 真機執行不生效,需提交App雲端打包後才生效;
  2. 此功能僅在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("等待中...")
                            }
                        }

效果演示:

image

好玩頁面

建立頁面

在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值

  1. 跟diff演算法有關,為了提高效率

    如果在兩個元素之間插入新元素,如果沒有key的話需要把原位置的元素解除安裝了,把新元素插進來,然後依次解除安裝,會打亂後續元素的排列規則,如果有key值,只需要插入到對應位置即可,不會改變其他元素的走向。

  2. 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>

效果演示:

image

返回頂部

編寫返回頂部按鈕,新增點選事件。

<!-- 返回頂部 -->
            <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提供的返回頂部的方法

image

一開始不讓返回頂部按鈕顯示,在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;
                      }

效果演示:

image

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專案模板。

image

我們需要在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;
                                    }
                                }
                        );
                    }

效果演示:

image

解決登入後拿不到使用者資訊的問題

我們點選第三方登入,登入成功之後返回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可以觸發目標視窗的自定義事件。我們需要給它傳遞三個引數

image

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

image

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

效果演示:

image

音單分類

建立音單分類頁面

在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/...

檢視傳遞的引數

image

透過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);
                            }
                        });

重啟上拉載入

出現問題:從別的頁面跳轉到全部音單頁面上拉載入失效

原因:如果別的頁面只有一頁資料,切換到全部音單的時候,也認為資料請求完畢了,就把上拉載入功能禁用了

解決辦法:重置上拉載入

image

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從伺服器獲取新資料;
                        }
                      }
                    });
                }

效果演示:

image

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>

向下滾動獲取資料

image

在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)
                        }
                    }
                }

效果演示:

image

詳情頁

在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刪掉

image

我們在控制檯中輸入兩條語句,去掉當前視窗的頭部導航欄

image

image

我們宣告一個引數,返回一個窗體物件

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>

效果演示:

image

打包釋出

配置檔案

上線釋出之前我們需要在manifest.json中針對App進行設定,設定內容分別為:

  • 基礎配置:主要是基本的應用資訊,包含;應用名稱、版本號、入口檔案、是否需要根據重力感應橫豎屏。
  • 圖示配置:安裝應用後,顯示在主頁的入口圖示,可以根據你上傳的大圖示自動壓縮生成各種小圖示。
  • 啟動圖配置:應用啟動後展示給使用者的圖片。
  • SDK配置:這裡是你引用的第三方外掛的配置,例如:支付、推送、分享等。
  • 模組許可權配置:你需要使用的原生模組,去掉不需要的模組,可以減少原生安裝包的體積。
  • App常用其它設定
  • 原始碼檢視

雲端打包

HBuilder提供的打包有云打包和本地打包兩種。
HBuilder提供的雲打包對正常開發者是免費的。但過多浪費伺服器資源會額外收費。用本地打包無任何限制。
雲打包的特點是DCloud官方配置好了原生的打包環境,可以把HTML等檔案編譯為原生安裝包。

在manifest.json中配置完畢就可以進行雲端打包了,你只需要提交程式碼,不用部署xcode和Android sdk就可以打包應用。打包完畢下載安裝,打包速度取決於你的網速。

檔名右鍵選擇【發行】——原生APP雲打包

image

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

image

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

image

相關文章