前言
uni-app
uni-app是DCloud推出的终极跨平台解决方案,是一个使用Vue.js开发所有前端应用的框架,官网:https://uniapp.dcloud.io/
mui
号称最接近原生APP体验的高性能前端框架,官网:https://dev.dcloud.net.cn/mui/
个人觉得,mui除了页面设计很接近原生App之外,还有一个特点就是能方便的使用App扩展规范Html5 Plus(http://www.html5plus.org/doc/h5p.html),我们能在它的源码中看到比较多的地方都有使用到
开发工具
使用HBuilderX开发工具写uni-app的代码,以及打包App等工作,主要的业务功能依旧是使用我们熟悉的idea开发,不过页面从webPC端风格改成了移动端风格
整体我们采用uni-app + mui的方式,使用的是官方推荐的uni-app原生标题栏跟导航栏+嵌入webview远程服务的页面,也就是说除了头部、尾部,中间的内容都是类似iframe嵌入进去
为方便以后查阅,特此记录
uni-app部分
我在App.vue中对uni对象进行全局赋值,这样在每个页面都调用到,这样做的目的是为了方便全局修改
设置进度条颜色、监听webview的url变化判断是否需要导航栏按钮等操作
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" } }
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组件介绍:https://uniapp.dcloud.io/component/web-view
webview网页与App的交互
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"); });
但有一点要注意,比如在操作标题栏按钮的回调事件中,我们直接去修改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() { }
mui部分
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页面设计
项目工程结构就是我们之前熟悉的springboot + thymeleaf + springdata-jpa,开发起来除了页面风格(移动端)不同,其他的都还好
mui封装弹窗
比如类似京东他们的这种弹窗,我认为比较好看,比较具有通用性
所以也基于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>
App调试、打包
运行 -> 运行到手机或模拟器
需要安装个模拟器(我的是雷电)、或者直接用USB数据先连接进行调试(PS:我的模拟器连接经常会断开,不知道是什么回事,有时候调试调试着就断开了,检查了也没有其他应用占用adb)
App打包是在:发行 - > 原生App-云打包
开发阶段,使用Dcloud公司的公用证书云打包就可以了,正式上线就需要自己的证书去打包
打包成功后控制台就会返回下载链接
后记
移动端开发暂时先记录到这,后续再补充;由于是公司的App,就不方便演示,等有空了再做个demo把完整的一套东西再做完整演示;