前言
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"
- }
- }
- View Code
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 组件介绍: 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); } 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
App 调试, 打包
运行 -> 运行到手机或模拟器
需要安装个模拟器(我的是雷电), 或者直接用 USB 数据先连接进行调试(PS: 我的模拟器连接经常会断开, 不知道是什么回事, 有时候调试调试着就断开了, 检查了也没有其他应用占用 adb)
App 打包是在: 发行 -> 原生 App - 云打包
开发阶段, 使用 Dcloud 公司的公用证书云打包就可以了, 正式上线就需要自己的证书去打包
打包成功后控制台就会返回下载链接
后记
移动端开发暂时先记录到这, 后续再补充; 由于是公司的 App, 就不方便演示, 等有空了再做个 demo 把完整的一套东西再做完整演示;
来源: https://www.cnblogs.com/huanzi-qch/p/11972723.html