移動端App uni-app + mui 開發記錄


  前言

  1、uni-app

  uni-app是DCloud推出的終極跨平台解決方案,是一個使用Vue.js開發所有前端應用的框架,官網:https://uniapp.dcloud.io/

 

  2、mui

  號稱最接近原生APP體驗的高性能前端框架,官網:https://dev.dcloud.net.cn/mui/

  個人覺得,mui除了頁面設計很接近原生App之外,還有一個特點就是能方便的使用App擴展規范Html5 Plus(http://www.html5plus.org/doc/h5p.html),我們能在它的源碼中看到比較多的地方都有使用到

 

  3、開發工具

  使用HBuilderX開發工具寫uni-app的代碼,以及打包App等工作,主要的業務功能依舊是使用我們熟悉的idea開發,不過頁面從webPC端風格改成了移動端風格

 

   4、整體架構

  我們采用uni-app + mui的方式,使用的是官方推薦的 uni-app原生標題欄跟導航欄 + 嵌入webview遠程服務的頁面,也就是說除了頭部、尾部,中間的內容都是類似iframe嵌入進去

  簡單的說,uni-app,包括頭部標題欄、底部導航欄作為App的“殼”,java后端+mui前端頁面作為App的“內容”,這樣選型的目的是為了方便后期的運維、升級

  webview嵌入:直接升級后端服務並重新部署即可,無需重新打包、升級App

  頭尾使用原生組件:提升App流暢度

   

  為方便以后查閱,特此記錄

 

  uni-app部分

  我在App.vue中對uni對象進行全局賦值,這樣在每個頁面都調用到,這樣做的目的是為了方便全局修改,比如全局該監聽方法、后期需要換進度條樣式、更換后端服務地址等

   

  tabBar導航欄

  底部的導航欄比較簡單,在page.json進行配置就可以

 

  page.json

{
    "pages": [
        //pages數組中第一項表示應用啟動頁
        {
            "path": "pages/index/index",
            "style": {
                "navigationBarTitleText": "首頁",
                "titleNView": {
                    "buttons": [{
                        "type": "none",
                        "float": "left"
                    }, {
                        "type": "none",
                        "float": "right",
                        "fontSrc":"/static/fonts/mui.ttf"
                    }]
                }
            }
        }
    ],
    "globalStyle": {
        "navigationBarTextStyle": "black",
        "navigationBarTitleText": "",
        "navigationBarBackgroundColor": "#F8F8F8",
        "backgroundColor": "#F8F8F8",
        "backgroundColorTop": "#F4F5F6",
        "backgroundColorBottom": "#F4F5F6"
    },
    "tabBar": {
        "color": "#7A7E83",
        "selectedColor": "#007AFF", //#007AFF 藍色  #f07837 橙色
        "borderStyle": "black",
        "backgroundColor": "#F8F8F8",
        "list": [{
            "pagePath": "pages/index/index",
            "iconPath": "static/image/index/index_.png",
            "selectedIconPath": "static/image/index/index.png",
            "text": "首頁"
        }],
        "position": "bottom"
    }
}
View Code

   

  監聽標題欄按鈕

  設置進度條顏色

  設置進度條顏色、監聽webview的url變化判斷是否需要標題欄按鈕等操作全都在App.vue中進行,具體頁面可以直接調用樣式對象、監聽方法

  App.vue

<script>
    export default {
        onLaunch: function() {
            //應用加載后初始后端服務地址
            uni.phoneServiceAddress = "http://qch2.vipgz2.idcfengye.com"; //為了方便App演示,這里開了一個內網穿透
            
            //監聽軟鍵盤高度變化,隱藏或顯示tabbar
            uni.onKeyboardHeightChange(res => {
                if (res.height > 0) {
                    uni.hideTabBar();
                } else {
                    uni.showTabBar();
                }
            })

            //全局進度條樣式
            uni.webviewStyles = {
                progress: {
                    color: '#007AFF'
                }
            };

            //全局監聽標題欄按鈕
            uni.listenTitleButton = function(thid) {
                let webView = thid.$mp.page.$getAppWebview();

                //webView加載完成時觸發,開始監聽子對象的onloaded事件
                webView.onloaded = function() {
                    let wv = webView.children()[0];

                    //webView的子對象加載完成時觸發  
                    wv.onloaded = function() {
                        let url = wv.getURL();
                        
                          //判斷是否顯示返回按鈕
                        if (
                            url.indexOf("hybrid/html/error.html") >= 0 ||
                            url.indexOf("/index/index") >= 0 ||
                            url.indexOf("/login/index") >= 0
                        ) {
                            // console.log("標題欄隱藏返回按鈕");
                            webView.setTitleNViewButtonStyle(0, {
                                type: 'none'
                            });
                            thid.backFun = function(object){}
                        } else {
                            // console.log("標題欄顯示返回按鈕");
                            webView.setTitleNViewButtonStyle(0, {
                                type: 'back'
                            });
                            thid.backFun = function(object){
                                if(object.index == 0){
                                    //回退
                                    uni.navigateBack();
                                }
                            }
                        }
                        
                        //因為我們手動設置了一些屬性,導致標題欄的title不能自動獲取、設置,這里需要我們手動設置一下
                        uni.setNavigationBarTitle({
                            title: wv.getTitle()
                        });
                    }
                }

                //webView手動加載、便於觸發方法
                webView.loadURL(thid.url);
            }
        },
        onShow: function() {

        },
        onHide: function() {

        }
    }
</script>

<style>
    /*每個頁面公共css */
</style>
View Code

 

  index.vue

<!-- vue單文件組件 -->
<template>
    <!-- 注意必須有一個view,且只能有一個根view。所有內容寫在這個view下面 -->
    <view class="main">
        <!-- 直接嵌入頁面 -->
        <web-view id="webView" :src="url" :webview-styles="webviewStyles"></web-view>
    </view>
</template>

<!-- js代碼,es6語法 -->
<script>
    //外部文件導入
    import * as util from '../../common/js/util.js';

    export default {
        data() {
            return {
                //當前webview請求的url
                url: uni.phoneServiceAddress + "/index/index",
                //進度條顏色樣式
                webviewStyles: uni.webviewStyles,
                //回退按鈕事件,比如第一頁是不需要回退按鈕,點進去之后的頁面才需要
                backFun:function(object){}
            }
        },
        //點擊標題欄按鈕,這里主要是用於回退按鈕
        onNavigationBarButtonTap:function(object){
            this.backFun(object);
        },
        //頁面裝載完成,開始監聽webview路徑變化
        onReady: function(options) {
            console.log("onReady");
            // #ifdef APP-PLUS
            uni.listenTitleButton(this);
            // #endif
        },
        onLoad: function(options) {
            console.log("onLoad");
        },
        onShow: function(options) {
            console.log("onShow");
        },
        // 點擊導航欄,webview重新請求this.url
        onTabItemTap: function(object) {
            // #ifdef APP-PLUS
            let wv = this.$mp.page.$getAppWebview().children()[0]; 
            wv.loadURL(this.url);
            // #endif
        }
    }
</script>

<!-- css樣式代碼 -->
<style>
    /* css外部文件導入 */
    @import "../../common/css/uni.css";
</style>
View Code

 

  然后其他的頁面跟首頁差不多,只是this.url的路徑不同,同時,如果標題欄還需要其他按鈕(比如右邊再來個分享、或者添加按鈕),就再加一個按鈕,然后操作不同的下標

 

  配置錯誤頁面

 

 

   webview組件

  webview組件介紹:https://uniapp.dcloud.io/component/web-view

 

  webview網頁與App的交互

 

  1、webview調用uni-app的api,那幾個路徑的跳轉都沒有問題,postMessage說是在特定時機(后退、分享等)中才會觸發,但是我一次都沒有成功

  需要注意:在webview網頁中調uni-app的api或者是5+擴展規范,需要監聽原生擴展的事件,等待plus ready

document.addEventListener('UniAppJSBridgeReady', function() {
        uni.navigateTo({
            url: 'page/index/index'
        });
});    

  或者使用mui已經幫我們封裝好了方法,所有的5+規范的api都可以調

mui.plusReady(function() {
    plus.nativeUI.toast("xxxxxxx");
});

 

  2、uni-app調用webview網頁的方法,可以直接在uni-app的代碼里面使用5+規范中的webview對象的evaljs方法,將js代碼發生到webview頁面去執行,

  api地址:http://www.html5plus.org/doc/zh_cn/webview.html#plus.webview.WebviewObject.evalJS,例如

plus.webview.currentWebview()[0].evalJS("alert('哈哈哈')");

  webview頁面就會彈出"哈哈哈"彈窗

 

  但有一點要注意,比如在webview頁面使用5+規范去操作uni-app原生標題欄按鈕的回調事件中,我們發現,在回調方法的作用域可以訪問到外面的對象,也可以是獲取到dom文檔里的標簽、元素,但直接修改DOM文檔發現時不起作用的,看文檔才發現,原來webview的層級比里面的內容要高,這時候我們選擇下面這樣方案

mui.plusReady(function () {
    let webView = plus.webview.currentWebview();

    //webView加載完成時觸發,開始監聽子對象的onloaded事件
    webView.onloaded = function() {
        let wv = webView.children()[0];

        //webView的子對象加載完成時觸發
        wv.onloaded = function () {

            /* 標題欄按鈕 */
            webView.setTitleNViewButtonStyle(1, {
                onclick: function (event) {
                    // 將JS腳本發送到Webview窗口中運行,可用於實現Webview窗口間的數據通訊
                    wv.evalJS("show()");
                }
            });
        }
    }
});

function show() {
    
}

 

  檢查更新

  2020-06-09更新

  檢查更新是App必不可少的功能,我們在App.vue的onLaunch方法中調用服務接口,查詢是否有更新

  App打包時,設置版本名稱

 

   如需升級,部署后台時

 

  封裝ajax.js

/**
 * 封裝ajax請求
 */
function request(params, method) {
    return new Promise(function(resolve, reject) {
        uni.request({
            url: params.url,
            data: params.data,
            method: method,
            header: {
                "content-type": "application/x-www-form-urlencoded;charset=UTF-8",
                ...params.header
            },
            success(res) {
                // 成功回調
                if (res.statusCode == 200) {
                    resolve(res.data)
                } else {
                    reject(res)
                }
            },
            fail(err) {
                uni.hideLoading()
                uni.showToast({
                    title: '服務器錯誤',
                    icon: "none"
                })
                reject(err)
                // 失敗回調
            },
            complete() {
                uni.hideLoading()
                // 無論成功或失敗 只要請求完成的 回調
            }
        })
    })
};
export default {

    get(params) {
        return request(params, "GET");
    },
    post(params) {
        return request(params, "POST")
    },
    put(params) {
        return request(params, "PUT")
    },
 
}
View Code

  引入ajax

    import ajax from 'common/js/ajax.js';

  查詢接口,檢查更新

            //檢查更新
            ajax.get({
                url: uni.phoneServiceAddress + "/update",  //檢查更新地址
                data: { 
                    "ua": uni.getSystemInfoSync().platform,//獲取ua標識  蘋果 ios,安卓 android
                    "versionByApp": plus.runtime.version //獲取應用版本名稱
                }
            }).then(res => {
                if (res.status == "1") {
                    //提醒用戶更新
                    uni.showModal({ 
                        title: "更新提示",  
                        content: res.note,  
                        success: (res) => {  
                            if (res.confirm) {  
                                 plus.runtime.openURL(res.url);  
                            }  
                        }  
                    })  
                } 
            })

 

  java接口

    @Value("${version}")
    private String versionByServer;//從配置文件中讀取


    /**
     * 檢查更新
     */
    @GetMapping("update")
    public String update(String ua,String versionByApp) {
        //是否需要更新
        String status = "0";
        //新包地址
        String url = "http://xxx.xxx.com/android.apk";
        //更新內容
        String note = "" +
                "1、修復bug1;" +
                "2、修復bug2;" +
                "";

        if(!versionByServer.equals(versionByApp)){
            status = "1";
        }

        if("ios".equals(ua)){
            url ="itms-apps://itunes.apple.com/cn/app/hello-uni-app/idxxxxxxx";
        }
        return "{\"status\":\""+status+"\",\"note\":\""+note+"\",\"url\":\""+url+"\"}";
    }

  

  App防二次打包

  2020-06-09更新

  防二次打包只限於安卓App,主要通過校驗應用簽名,在App.vue的onLaunch方法中調用如下代碼進行校驗,簽名異常則彈窗提示並強制退出

//如果是安卓運行環境,校驗應用簽名是否正確  
if (plus.os.name == 'Android') {
    //校驗
    if (!checkApkSign("dd:xx:ff:dd:xx:dd:23:cc:di:x9")) {
        uni.showModal({
            title: '提示',
            content: '簽名異常,請下載安裝正版APK包',
            showCancel: false,
            success: function(res) {
                if (res.confirm) {
                    plus.runtime.quit();
                } else if (res.cancel) {
                    plus.runtime.quit();
                }
            }
        });
    }
}

/**  
 * 通過SHA1,檢查安卓APK簽名  
 */
function checkApkSign(value) {
    //獲取應用上下文  
    var context = plus.android.runtimeMainActivity();
    var PackageManager = plus.android.importClass("android.content.pm.PackageManager");
    var packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNING_CERTIFICATES ||
        PackageManager.GET_SIGNATURES)
    var Build = plus.android.importClass("android.os.Build");
    var signatures = null;
    //Android 28以后獲取包簽名信息方法改了  
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
        var signingInfo = packageInfo.plusGetAttribute('signingInfo');
        signatures = plus.android.invoke(signingInfo, "getApkContentsSigners")
    } else {
        signatures = packageInfo.plusGetAttribute('signatures');
    }
    if (signatures != null) {
        var signature;
        var byteArr;
        var currentSignature;//當前App的SHA1值
        var md = plus.android.invoke("java.security.MessageDigest", "getInstance", type);
        for (var i in signatures) {
            byteArr = plus.android.invoke(signatures[i], "toByteArray");
            plus.android.invoke(md, "update", byteArr);
            currentSignature = Bytes2HexString(plus.android.invoke(md, "digest")).toUpperCase();
            //調用方法轉成16進制時沒有拼接":",所以對比時value也要去掉  
            if (value.replace(/:/g,"").toUpperCase() == currentSignature) {
          return true;
            }
        }
    } else {
        console.info("應用未簽名");
    }
    return false;
}

//字節數組轉十六進制字符串,對負值填坑  
function Bytes2HexString(arrBytes) {
    var str = "";
    for (var i = 0; i < arrBytes.length; i++) {
        var tmp;
        var num = arrBytes[i];
        if (num < 0) {
            //此處填坑,當byte因為符合位導致數值為負時候,需要對數據進行處理  
            tmp = (255 + num + 1).toString(16);
        } else {
            tmp = num.toString(16);
        }
        if (tmp.length == 1) {
            tmp = "0" + tmp;
        }
        str += tmp;
    }
    return str;
}

 

  mui部分

  項目工程結構就是我們之前熟悉的springboot + thymeleaf + springdata-jpa,開發起來除了頁面風格(移動端)不同,其他的都還好

 

  mui部分主要是業務頁面、功能的開發,有時候也需要調用5+規范的api,比如調用手機相機、文件管理、系統通知等,需要用到的時候就看api:http://www.html5plus.org/doc/h5p.html

  頁面開發主要就參考mui的新手文檔(https://dev.dcloud.net.cn/mui/getting-started/)、官網演示(https://www.dcloud.io/mui.html)、文檔(https://dev.dcloud.net.cn/mui/ui/)等,同時也參考別人的App頁面設計(QQ、微信、支付寶、京東等)

  

  封裝彈窗

  比如類似京東他們的這種彈窗,我認為比較好看,比較具有通用性

  

 

 

  所以也基於mui封裝了自己的一套彈窗效果

  先看下演示

  

  代碼

  css

  封裝在common.css中

/* 封裝自定義彈窗 上右下左,居中 */
.huanzi-dialog {
    position: fixed;
    background-color: white;
    z-index: -1;
    overflow: hidden;
}

.huanzi-dialog-top {
    width: 100%;
    top: -100%;
    border-radius: 0 0 13px 13px;
}

.huanzi-dialog-right {
    width: 85%;
    top: 0;
    right: -85%;
    bottom: 0;
    border-radius: 13px 0 0 13px;
}

.huanzi-dialog-bottom {
    width: 100%;
    bottom: -100%;
    border-radius: 13px 13px 0 0;
}

.huanzi-dialog-left {
    width: 85%;
    top: 0;
    left: -85%;
    bottom: 0;
    border-radius: 0 13px 13px 0;
}

.huanzi-dialog-center {
    border-radius: 13px;
    opacity: 0;
    /* 方案一 */
    /*margin: auto;
    left: 0;
    right: 0;
    bottom: 0;
    top: 0;*/

    /* 方案二 */
    top: 50%;
    left: 50%;
    transform: translate3d(-50%, -50%, 0) scale(1.185);
}
View Code

 

  js

  封裝在common.js中

/* 封裝自定義彈窗 */
var HuanziDialog = {
    mask: null,//mui遮陰層對象
    showSpeed: 300,//彈出速度
    hideSpeed: 100,//隱藏速度
    removeFlag: true,//close內部是否執行操作
    /**
     * 隱藏彈窗,內部方法
     * @param select jq元素選擇器,#xxx、.xxx等,如果為空,則隱藏所有
     * @param callback 回調方法
     * @param speed 速度
     */
    hideFun: function (select, callback, speed) {
        let $huanziDialog = select ? $(select) : $(".huanzi-dialog");
        speed = speed ? speed : HuanziDialog.hideSpeed;

        //上右下左,居中
        $huanziDialog.each(function () {
            let dialog = $(this);
            let clazz = dialog.attr("class");
            if (clazz.indexOf("huanzi-dialog-top") > -1) {
                dialog.animate({top: '-100%'}, speed);
            } else if (clazz.indexOf("huanzi-dialog-right") > -1) {
                dialog.animate({right: '-85%'}, speed);
            } else if (clazz.indexOf("huanzi-dialog-bottom") > -1) {
                dialog.animate({bottom: '-100%'}, speed);
            } else if (clazz.indexOf("huanzi-dialog-left") > -1) {
                dialog.animate({left: '-85%'}, speed);
            } else if (clazz.indexOf("huanzi-dialog-center") > -1) {
                dialog.animate({opacity: 0}, speed);
            }
            setTimeout(function () {
                dialog.css("z-index", "-1");
            }, speed)
        });

        callback && callback();
    },

    /**
     * 顯示彈窗,內部方法
     * @param select jq元素選擇器,#xxx、.xxx等,如果為空,則顯示所有
     * @param callback 回調方法
     * @param speed 速度
     */
    showFun: function (select, callback, speed) {
        let $huanziDialog = select ? $(select) : $(".huanzi-dialog");
        speed = speed ? speed : HuanziDialog.hideSpeed;

        //上右下左,居中
        $huanziDialog.each(function () {
            let dialog = $(this);
            dialog.css("z-index", "999");

            let clazz = dialog.attr("class");
            if (clazz.indexOf("huanzi-dialog-top") > -1) {
                dialog.animate({top: '0%'}, speed);
            } else if (clazz.indexOf("huanzi-dialog-right") > -1) {
                dialog.animate({right: '0%'}, speed);
            } else if (clazz.indexOf("huanzi-dialog-bottom") > -1) {
                dialog.animate({bottom: '0%'}, speed);
            } else if (clazz.indexOf("huanzi-dialog-left") > -1) {
                dialog.animate({left: '0%'}, speed);
            } else if (clazz.indexOf("huanzi-dialog-center") > -1) {
                dialog.animate({opacity: 1}, speed);
            }
        });
        HuanziDialog.removeFlag = true;
        callback && callback();
    },

    /**
     * 初始化mui遮陰層對象
     */
    init: function () {
        HuanziDialog.mask = mui.createMask();

        /**
         * 重寫close方法
         */
        HuanziDialog.mask.close = function () {
            if (!HuanziDialog.removeFlag) {
                return;
            }
            //方法直接在這里執行
            HuanziDialog.hideFun();
            //調用刪除
            HuanziDialog.mask._remove();
        };
    },

    /**
     * 顯示彈窗,供外部調用(參數同內部方法一致)
     */
    show: function (select, callback, speed) {
        HuanziDialog.showFun(select, callback, speed);
        HuanziDialog.mask.show();//顯示遮罩
    },

    /**
     * 隱藏彈窗,供外部調用(參數同內部方法一致)
     */
    hide: function (select, callback, speed) {
        HuanziDialog.hideFun(select, callback, speed);
        HuanziDialog.mask.close();//關閉遮罩
    },

    /**
     * 警告框
     * @param title 標題
     * @param message 內容
     * @param callback 點擊確認的回調
     */
    alert: function (title, message, callback) {
        let $html = $("<div class=\"mui-popup mui-popup-in\" style=\"display: block;\">" +
            "<div class=\"mui-popup-inner\">" +
            "   <div class=\"mui-popup-title\">" + title + "</div>" +
            "   <div class=\"mui-popup-text\">" + message + "</div>" +
            "</div>" +
            "<div class=\"mui-popup-buttons\">" +
            "<span class=\"mui-popup-button mui-popup-button-bold confirm-but\">確定</span>" +
            "</div>" +
            "</div>");
        $html.find(".confirm-but").click(function () {
            HuanziDialog.removeFlag = true;
            HuanziDialog.mask.close();
            $html.remove();
            callback && callback();
        });
        HuanziDialog.mask.show();//顯示遮罩
        HuanziDialog.removeFlag = false;
        $("body").append($html);
    },

    /**
     * 確認消息框
     * @param title 標題
     * @param message 內容
     * @param callback 點擊確認的回調
     */
    confirm: function (title, message, callback) {
        let $html = $("<div class=\"mui-popup mui-popup-in\" style=\"display: block;\">" +
            "<div class=\"mui-popup-inner\">" +
            "   <div class=\"mui-popup-title\">" + title + "</div>" +
            "   <div class=\"mui-popup-text\">" + message + "</div>" +
            "</div>" +
            "<div class=\"mui-popup-buttons\">" +
            "<span class=\"mui-popup-button mui-popup-button-bold cancel-but\" style='color: #585858;'>取消</span>" +
            "<span class=\"mui-popup-button mui-popup-button-bold confirm-but\">確定</span>" +
            "</div>" +
            "</div>");
        $html.find(".cancel-but").click(function () {
            HuanziDialog.removeFlag = true;
            HuanziDialog.mask.close();
            $html.remove();
        });
        $html.find(".confirm-but").click(function () {
            $html.find(".cancel-but").click();
            callback && callback();
        });

        HuanziDialog.mask.show();//顯示遮罩
        HuanziDialog.removeFlag = false;
        $("body").append($html);
    },

    /**
     * 自動消失提示彈窗
     * @param message 內容
     * @param speed 存在時間
     */
    toast: function (message, speed) {
        speed = speed ? speed : 2000;
        let $html = $("<div class=\"huanzi-dialog huanzi-dialog-center\" style=\"width: 45%;height: 20%;opacity: 1;z-index: 999;background-color: #5a5a5ad1;\">" +
            "    <p style=\" position: relative; top: 50%; left: 50%; transform: translate3d(-50%, -50%, 0) scale(1); color: #e0e0e0; font-size: 20px; \">" + message + "</p>" +
            "</div>");
        $("body").append($html);
        setTimeout(function () {
            $html.remove();
        }, speed);
    }
};

//先初始化自定義彈窗
HuanziDialog.init();
View Code

 

  html

  測試頁面

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>基於MUI封裝常用彈窗</title>
    <!-- jquery -->
     <script th:src="@{/webjars/jquery/3.1.1/jquery.min.js}"></script>

    <!-- 引入mui框架 -->
    <link rel='stylesheet' th:href="@{/common/mui/css/mui.css}"/>
    <script th:src="@{/common/mui/js/mui.js}"></script>

    <!-- 最后引入公用代碼 -->
    <link rel='stylesheet' th:href="@{/common/common.css}"/>
    <script th:src="@{/common/common.js}"></script>

    <style>
        body{
            text-align: center;
        }

        .mui-btn{
            width: 50%;
            margin: 10px auto;
        }
    </style>
</head>
<body>
    <h4>基於MUI封裝常用彈窗</h4>

    <button class="mui-btn" onclick="HuanziDialog.show('#top')"></button>
    <button class="mui-btn"  onclick="HuanziDialog.show('#bottom')"></button>
    <button class="mui-btn"  onclick="HuanziDialog.show('#left')"></button>
    <button class="mui-btn"  onclick="HuanziDialog.show('#right')"></button>
    <button class="mui-btn"  onclick="HuanziDialog.show('#center')">居中</button>
    <button class="mui-btn"  onclick="HuanziDialog.alert('系統提示','我是警告框!',function() {console.log('你已確認警告!')})">警告框</button>
    <button class="mui-btn"  onclick="HuanziDialog.confirm('系統提示','確認要XXX嗎?',function() {HuanziDialog.toast('很好,你點擊了確認!');console.log('很好,你點擊了確認!')})">確認框</button>
    <button class="mui-btn"  onclick="HuanziDialog.toast('提交成功')">自動消失提示框</button>

    <!---->
    <div id="top" class="huanzi-dialog huanzi-dialog-top" style="height: 500px">
        <h5>我從上邊彈出</h5>
    </div>

    <!---->
    <div id="bottom" class="huanzi-dialog huanzi-dialog-bottom" style="height: 500px">
        <h5>我從下邊彈出</h5>
    </div>

    <!---->
    <div id="left" class="huanzi-dialog huanzi-dialog-left">
        <h5>我從左邊彈出</h5>
    </div>

    <!---->
    <div id="right" class="huanzi-dialog huanzi-dialog-right">
        <h5>我從右邊彈出</h5>
    </div>

    <!-- 居中 -->
    <div id="center" class="huanzi-dialog huanzi-dialog-center" style="width: 65%;height: 30%">
        <h5>我從中間彈出</h5>
    </div>

</body>
</html>
View Code

 

  其實后面的警告框、確認框的樣式就是mui的5+端樣式,那我們為什么還要封裝呢?在開發中我們發現,在PS端瀏覽器將調試模式改成手機端,mui的封裝的彈窗是上面的效果,但到真機上運行它又變成原生的彈窗樣式,原來mui底層有進行了判斷,安卓、蘋果、5+等樣式都不一樣,這里我們為了彈窗風格的統一,同時也是為了方便后期的統一調整,因此再進行了一層封裝

 

  封裝頭部尾部

  這里的封裝其實就是文末補充的另一種方案,基於mui的標題欄、底部導航欄,進行簡單封裝

  common.css

/* 自定義頭部,系統狀態欄的高度暫時寫死30px */
.huanzi-header{
    position: fixed;
    top:0;
    right: 0;
    left: 0;
    background-image: linear-gradient(to bottom right, #0061ff, #6aa2ff);
    box-shadow: 0 1px 6px #ccc;
    height: 74px;
}

.huanzi-header  .statusbar {
    height: 30px;
    width: 100%;
}

.huanzi-header .titlebar{
    padding-right: 10px;
    padding-left: 10px;
    border-bottom: 0;
}

.huanzi-header .titlebar a {
    margin: 15px 5px;
}

.huanzi-header .titlebar * {
    color: white;
}

.huanzi-header .mui-title{
    line-height: 55px !important;
    right: 100px;
    left: 100px;
    display: inline-block;
    overflow: hidden;
    width: auto;
    margin: 0;
    text-overflow: ellipsis;
}

.huanzi-content {
    position: absolute;
    top: 74px;
    bottom: 50px;
}


/* 自定義頁腳(底部導航欄) */
.huanzi-footer{
    position: fixed;
    right: 0;
    left: 0;
    bottom: 0;
    background-color: white;
    box-shadow: 0 1px 6px #ccc;
    height: 50px;
    padding: 5px;
}

.huanzi-footer .huanzi-footer-buttom{
    height: 50px;
    float: left;
    color: black;
    /* 寬度為:100/按鈕個數 */
    width: 25%;
}

.huanzi-footer .huanzi-footer-buttom > p{
    color: black;
}

.huanzi-footer .select{
    color: #0091fb;
}

.huanzi-footer .select > p{
    color: #0091fb;
}
View Code

 

  common.js

//底部按鈕點擊切換顏色
$(document).on("click",".huanzi-footer-buttom", function (e) {
    $(".huanzi-footer-buttom").each(function () {
        $(this).removeClass("select");
    });
    $(this).addClass("select");
});
View Code

 

  自定義彈窗例子

  需要在head.html中引入jquery、mui、common的js、css

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>基於MUI封裝常用彈窗</title>
    <script th:replace="common/head::static"></script>
    <style>
        body{
            text-align: center;
        }

        .mui-btn{
            width: 50%;
            margin: 10px auto;
        }
    </style>
</head>
<body>
<!-- 頭部 -->
<header class="huanzi-header">
    <div class="statusbar"></div>
    <div class="titlebar">
        <a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left"></a>
        <h1 class="mui-title">基於MUI封裝常用彈窗</h1>
        <a class="mui-icon mui-icon-bars mui-pull-right"></a>
    </div>
</header>

<!-- 內容(可滑動區域) -->
<div class="huanzi-content mui-scroll-wrapper">
    <div class=" mui-scroll">
        <button class="mui-btn" onclick="HuanziDialog.show('#top')"></button>
        <button class="mui-btn"  onclick="HuanziDialog.show('#bottom')"></button>
        <button class="mui-btn"  onclick="HuanziDialog.show('#left')"></button>
        <button class="mui-btn"  onclick="HuanziDialog.show('#right')"></button>
        <button class="mui-btn"  onclick="HuanziDialog.show('#center')">居中</button>
        <button class="mui-btn"  onclick="HuanziDialog.alert('系統提示','我是警告框!',function() {console.log('你已確認警告!')})">警告框</button>
        <button class="mui-btn"  onclick="HuanziDialog.confirm('系統提示','確認要XXX嗎?',function() {HuanziDialog.toast('你點擊了確認!');console.log('很好,你點擊了確認!')})">確認框</button>
        <button class="mui-btn"  onclick="HuanziDialog.toast('提交成功')">自動消失提示框</button>

        <button class="mui-btn">無用按鈕</button>
        <button class="mui-btn">無用按鈕</button>
        <button class="mui-btn">無用按鈕</button>
        <button class="mui-btn">無用按鈕</button>
        <button class="mui-btn">無用按鈕</button>
        <button class="mui-btn">無用按鈕</button>
        <button class="mui-btn">無用按鈕</button>
    </div>
</div>
<!-- 例如彈窗等內容,需要放在外面 -->
<div>
    <!---->
    <div id="top" class="huanzi-dialog huanzi-dialog-top" style="height: 500px">
        <h5>我從上邊彈出</h5>
    </div>

    <!---->
    <div id="bottom" class="huanzi-dialog huanzi-dialog-bottom" style="height: 500px">
        <h5>我從下邊彈出</h5>
    </div>

    <!---->
    <div id="left" class="huanzi-dialog huanzi-dialog-left">
        <h5>我從左邊彈出</h5>
    </div>

    <!---->
    <div id="right" class="huanzi-dialog huanzi-dialog-right">
        <h5>我從右邊彈出</h5>
    </div>

    <!-- 居中 -->
    <div id="center" class="huanzi-dialog huanzi-dialog-center" style="width: 65%;height: 30%">
        <h5>我從中間彈出</h5>
    </div>

</div>

<!-- 底部導航欄 -->
<footer class="huanzi-footer">
    <div class="huanzi-footer-buttom select">
        <i class="mui-icon mui-icon-phone"></i>
        <p>電話</p>
    </div>
    <div class="huanzi-footer-buttom">
        <i class="mui-icon mui-icon-email"></i>
        <p>郵件</p>
    </div>
    <div class="huanzi-footer-buttom">
        <i class="mui-icon mui-icon-chatbubble"></i>
        <p>短信</p>
    </div>
    <div class="huanzi-footer-buttom">
        <i class="mui-icon mui-icon-weixin"></i>
        <p>微信</p>
    </div>
</footer>
</body>
</html>
View Code

 

  效果演示

 

  2020-03-04更新

  問題:按照前面的想法,我們每個頁面都要加入頭部、尾部,但這樣跳轉頁面時會造成“白屏”的情況,嚴重影響瀏覽效果

  解決辦法:我們創建一個main主頁面,只有主頁面有頭部、尾部,中間內容嵌入iframe內容子頁面(子頁面正常html頁面),如果在當前頁面進行跳轉操作,也是在iframe中進行跳轉,而如果點擊尾部按鈕切換模塊、頁面,那就切換iframe標簽的src進行更新url,這樣我們在跳轉頁面時,頭部、尾部都不會刷新,瀏覽效果更佳,而且還可以減少重復代碼

 

  common.js

  其他的都不變,尾部按鈕點擊事件需要修改一下,同時加入iframe標簽的load事件處理

//省略其他內容


//底部按鈕點擊事件
$(document).on("click", ".huanzi-footer-buttom", function (e) {
    //iframe跳轉新頁面
    $("#mainIframe")[0].src = ctx + $(this).data("url");

    //切換顏色
    $(".huanzi-footer-buttom").each(function () {
        $(this).removeClass("select");
    });
    $(this).addClass("select");
});

//mainIframe onload事件
function mainIframeLoadFun(mainIframe) {
    //自適應高度
    mainIframe.height = $('.huanzi-content')[0].scrollHeight;

    //修改標題

    //子頁面與父頁面同源獲取方法
    // let title = document.getElementById('mainIframe').contentWindow.document.title;//iframe中子頁面的title
    let $mainFrame=$('#mainIframe');
    let title = $mainFrame.contents().attr("title");

    $("title").text(title);
    $(".mui-title").text(title);
}

 

  main.html

  主頁面,主要分為頭部、中間內容、尾部,中間內容改成iframe標簽,在onload事件中進行高度自適應

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title></title>
    <script th:replace="common/head::static"></script>
    <style>
        body{
            text-align: center;
        }
    </style>
</head>
<body>
<!-- 頭部 -->
<header class="huanzi-header">
    <div class="statusbar"></div>
    <div class="titlebar">
        <a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left"></a>
        <h1 class="mui-title"></h1>
        <a class="mui-icon mui-icon-bars mui-pull-right"></a>
    </div>
</header>

<!-- 內容(可滑動區域) -->
<div class="huanzi-content mui-scroll-wrapper">
    <div class=" mui-scroll">
        <!-- 直接嵌入iframe,且自適應寬高 -->
        <iframe id="mainIframe" src="/test1" width="100%" onload="mainIframeLoadFun(this)"></iframe>
    </div>
</div>

<!-- 底部導航欄 -->
<footer class="huanzi-footer">
    <div class="huanzi-footer-buttom select" data-url="/test1">
        <i class="mui-icon mui-icon-phone"></i>
        <p>頁面1</p>
    </div>
    <div class="huanzi-footer-buttom" data-url="/test2">
        <i class="mui-icon mui-icon-email"></i>
        <p>頁面2</p>
    </div>
    <div class="huanzi-footer-buttom" data-url="/test3">
        <i class="mui-icon mui-icon-chatbubble"></i>
        <p>頁面3</p>
    </div>
    <div class="huanzi-footer-buttom" data-url="/test4">
        <i class="mui-icon mui-icon-weixin"></i>
        <p>頁面4</p>
    </div>
</footer>
</body>
</html>

 

  test1.html - test5.html(這幾個頁面內容都差不多,貼出一個就可以了,不同的是里面的值,還有就是test4.html頁面里面有個跳轉test5.html的按鈕)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>頁面4</title>
    <script th:replace="common/head::static"></script>
    <style>
        body{
            text-align: center;
        }
    </style>
</head>
<body>
    <button class="mui-btn" onclick="window.location.href = ctx + '/test5'">跳轉頁面5</button>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
    <h1>頁面4</h1>
</body>
</html>

 

  controller

  控制器控制頁面跳轉(代碼幾乎一模一樣,我就只貼一個就好了)

    //跳轉主頁面
    @GetMapping("main")
    public ModelAndView main() {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("main");
        return modelAndView;
    }

  

  效果演示

  

 

  App調試、打包

  運行 -> 運行到手機或模擬器

  需要安裝個模擬器(我的是雷電)、或者直接用USB數據先連接進行調試(PS:我的模擬器連接經常會斷開,不知道是什么回事,有時候調試調試着就斷開了,檢查了也沒有其他應用占用adb)

 

  App打包是在:發行 - > 原生App-雲打包

  開發階段,使用Dcloud公司的公用證書雲打包就可以了,正式上線就需要自己的證書去打包

   打包成功后控制台就會返回下載鏈接

 

  后記

  移動端App uni-app + mui 開發暫時先記錄到這,后續再補充;由於是公司的App,就不方便演示,等有空了再做個demo把完整的一套東西再做完整演示;

 

  另一種方案

  雖然官方推薦盡量使用原生導航。甚至有時需要犧牲一些不是很重要的需求。但有時候我們就是想自定義原生標題欄,特別是我們是webview嵌入的方式

    "globalStyle": {
        //隱藏原生標題欄,主意事項請查閱官網:https://uniapp.dcloud.io/collocation/pages?id=customnav
        "navigationStyle":"custom"
    },

  如果要自定義導航欄,有哪些主要的點,官方在這里已經說得很清楚了:https://uniapp.dcloud.io/collocation/pages?id=customnav,但如果我們采用的是webview嵌入的方式,就要注意了,<web-view> 組件默認鋪滿全屏並且層級高於前端組件,如果我們按照文檔中操作,發現還是會頂到系統狀態欄

   因此占高div我們最好也寫在webview里面,系統狀態欄的高度可以動態獲取:http://www.html5plus.org/doc/zh_cn/navigator.html#plus.navigator.getStatusbarHeight

mui.plusReady(function(){
    //獲取系統狀態欄的高度,單位為像素(px),值為Webview中的邏輯高度單位
    let statusbarHeight = plus.navigator.getStatusbarHeight();
    alert(statusbarHeight)
});

 

 

  自己寫標題欄的話可以直接用mui的這個,或者基於它,我們自己再封裝一個自己的標題欄

  

 

   但是這樣對代碼的書寫規范有一定的要求,頁面統一分為頭部、內容、尾部,中間的內容是可滑動區域,例如:

<body>
    <!-- 頭部 -->
    <header id="header" class="mui-bar mui-bar-nav">
        <!-- 系統狀態欄占高div -->
        <div></div>

        <a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left"></a>
        <h1 class="mui-title">頭部導航欄</h1>
        <a class="mui-icon mui-icon-bars mui-pull-right"></a>
    </header>
    
    <!-- 內容(可滑動區域) -->
    <div class="mui-scroll-wrapper">
        <div class=" mui-scroll">
            <p>這里是內容</p>
        </div>
    </div>
    
    <!-- 底部(如有需要,可擴展尾部導航欄) -->
</body>

  當然,我們可以進行統一封裝,使用thymeleaf的替換,或者使用js去追加,這樣可以減少每個頁面的代碼量,方便維護,但是視覺上就會有閃爍效果,因為每個頁面的頭部可能不一樣,需要用js去追加,這個就需要權衡利弊選擇合適的方式

  注:封裝代碼在前面mui封裝部分

 

 

  補充

  2020-02-25補充:自定義tabbar + webview解決方案

  uniapp原生頭尾+webview組合,底部的TabBar按鈕需要根據登錄角色的權限來動態控制數量,但目前官方並不支持動態修改TabBar隱藏或顯示某一項,因此我們選用uniapp自定義TabBar實現(用的是這個插件:自定義動態TabBar;圖片上傳七牛雲、阿里OSS;),同時配合Storage模塊(http://www.html5plus.org/doc/zh_cn/storage.html)在webview頁面進行存儲登錄角色權限,登錄成功后跳轉uniapp固定頁面,進行讀取判斷動態控制tabbar

  但webview組件默認全屏顯示,會覆蓋底部的tabbar按鈕,而且webview組件的webview-styles並不支持設置高度,需要使用APP擴展插件5+plus來控制(http://www.html5plus.org/doc/zh_cn/webview.html),但當我們調用setStyle設置百分比高度發現並沒有生效,原因不明,很奇怪

  無奈,只能用5+plus動態創建webview組件,創建時傳入style樣式控制高度,這樣就解決自定義tabbar按鈕被覆蓋的問題

            //動態創建,控制高度
            var w=plus.webview.create(this.url,'index',{height:'93%'}); 
            w.show();

 

  2020-06-09更新

  我們已經嘗試過了以下三種方案

  1、原生標題欄 + 原生tabbar

  2、自定義標題欄(webview) + 原生tabbar

  3、自定義標題欄(webview) + 自定義tabbar(uniapp)

  但我們有時就是想整個頁面都是從服務端返回,包括標題欄、tabbar,即自定義標題欄(webview)+ 自定義tabbar(webview),詳情請看另一篇博客:SpringBoot系列——基於mui的H5套殼APP開發web框架

 

  iOS上架App Store

   Android的打好apk包后我們可以隨便安裝,但iOS的ipa卻不行,除了測試證書打的測試包,並且是添加過UUID的手機才能安裝測試包,用發布證書打的ipa包是不能直接安裝的,只能通過App Store安裝

  下面簡單記錄一下iOS打包、發布流程,沒有蘋果電腦,可以用這個Appuploader工具來生成證書、以及上傳ipa:http://blog.applicationloader.net/blog/zh/72.html

  

  1、iOS證書(.p12)和描述文件(.mobileprovision)、以及發布推送證書申請(賬號權限要有證書相關權限,具體步驟查看官網文檔:https://ask.dcloud.net.cn/article/152

  2、使用發布證書雲打包ipa

  3、在iTunes Connect創建APP,上傳ipa(賬號要有管理App權限

  4、設置APP各項信息提交審核(上傳過程可以先設置App信息)

  (2019最詳細iOS APP上架App Store流程:https://www.jianshu.com/p/6f50130b6950

 

  注意點:

  1、App圖標有要求,提供的app store圖標需要是png圖片,且不透明即沒有alpha,(修改后按點擊“自動生成所有圖標並替換”重新生成應用圖標,並重新打包上傳,記得改版本號,不改上傳不了)

  2、要在manifest.json配置隱私權限

 

  3、如果App沒有注冊功能,一定要說明沒有注冊功能的原因,讓審核人員相信不是內部應用,並提供測試賬號

  4、App截圖也不能忽略,打測試包在不同機型的真機上安裝進行截圖,最后讓美工PS一下,類似這樣

 

 

  代碼開源

  代碼已經開源、托管到我的GitHub、碼雲:

  GitHub:https://github.com/huanzi-qch/springBoot

  碼雲https://gitee.com/huanzi-qch/springBoot

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM