乐趣区

Huilder-X开发猫耳APPH5MUIVUE

前言

近年来国内出现了一些可以让前端人员编写移动端 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 个特点:

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

效果演示:

沉浸式导航栏

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("等待中...")
                            }
                        }

效果演示:

好玩页面

创建页面

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

效果演示:

返回顶部

编写返回顶部按钮,添加点击事件。

<!-- 返回顶部 -->
            <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 的公用证书,但是不能发布上线。已经打好的安装包,允许开发者在指定天内下载指定次数。超时或超次后服务器端会清除文件。

打包成功后会生成下载地址,点击下载后就可以进行安装使用了。

退出移动版