前言
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" } }
監聽標題欄按鈕
設置進度條顏色
設置進度條顏色、監聽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>
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>
然后其他的頁面跟首頁差不多,只是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") }, }
引入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); }
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();
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>
其實后面的警告框、確認框的樣式就是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; }
common.js

//底部按鈕點擊切換顏色 $(document).on("click",".huanzi-footer-buttom", function (e) { $(".huanzi-footer-buttom").each(function () { $(this).removeClass("select"); }); $(this).addClass("select"); });
自定義彈窗例子
需要在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>
效果演示
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