springboot+layui实现PC端用户的增删改查 & 整合mui实现app端的自动登录和用户的上拉加载 & HBuilder打包app并在手机端下载安装
springboot整合web开发的各个组件在前面已经有详细的介绍,下面是用springboot整合layui实现了基本的增删改查。
同时在学习mui开发app,也就用mui实现了一个简单的自动登录和用户列表上拉刷新的app。
下面是自己实现前的思路:
1. web端实现用户的增删改查,SSM实现。 Spring + SpringMVC +Mybatis + PageHelper
表主要有两个user表和token表。
user表就是基本的信息(ID、username、password、userfullname、createtime等字段)
token表id、token创建时间、token串、失效时间、username、userIdcode等信息
2. 安卓端实现token自动登录和下拉刷新查询列表。
1. 自动登录的思路:
(1) 首次登录: 带着username、password去后台请求登录信息,
后台如果登录成功返回一个{ token,username,userfullname} , token表包括:id、token创建时间、token串、失效时间、username、userIdcode等信息。token的生成方式可以用md5算法加密username生成
(2) 主页面访问的时候每次先带着本地的token去后台验证一下token,如果token失效的话就返回到登录界面,如果token有效期在5天以内就在后台生成一个token并且重新刷新本地token
后台返回json数据,msg包括:invalidToken--表示无效token,需要退出到登陆界面;
ok:表示有效,然后根据返回的token串与本地串是否一致,如果不一致更新本地token即可。
(3)安卓端退出的时候清除本地的token
2.安卓端分页下拉刷新展示所有的user信息
===========Web端实现用户增删改查========
主要是springboot+layui+thymeleaf。
主要是layui整合thymeleaf的时候,html的所有的自闭标签都必须带结束标记,比如<input > 必须写为<input />;而且标签的属性必须有值,例如<input checked /> 必须是<input checked="checked"/>,否则会报解析错误。而且页面的JS有时在解析时也报语法错误,所以JS尽量写在单独的JS文件中。
另外需要注意的是使用springboot+thymeleaf的时候,如果需要访问html页面,所有的请求都必须经过后台进行转发。即使是在layui的框架中使用iframe等进行引进html也需要到后台进行转发一下。
目录结构如下:
下面贴出pom.xml和application.properties
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cn.qlq</groupId> <artifactId>springboot-ssm</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.2.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 设置Tomcat打包的时候不打包下面配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency> <!--springboot单元测试 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--热加载 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> <!-- spring-boot整合mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.1.1</version> </dependency> <!-- mysql驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> </dependency> <!-- 使用事务需要引入这个包 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- 引入 spring aop 依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!--pagehelper插件 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.1.2</version> </dependency> <!-- 引入 freemarker 模板依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <!-- 引入 thymeleaf 模板依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- commons工具包 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.1</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.4</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2</version> </dependency> <!-- 阿里的fastjson用于手动转JSON --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.56</version> </dependency> </dependencies> <distributionManagement> <repository> <id>releases</id> <url>http://192.168.0.133:8081/repository/maven-releases/</url> </repository> <snapshotRepository> <id>snapshots</id> <url>http://192.168.0.133:8081/repository/maven-snapshots/</url> </snapshotRepository> </distributionManagement> <build> <!-- 配置了很多插件 --> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <configuration> <source>1.7</source> <target>1.7</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
############################################################ # # 日志相关配置(默认集成的有slf4j,Logback等) # ############################################################ #指定配置文件的位置,只能是xml或者groovy结尾 #logging.config=classpath:logback.xml #默认的日志级别 logging.level.root=INFO # mapper 接口所在的包设置为 debug logging.level.cn.qlq.mapper=DEBUG #生成日志文件的位置 logging.file=G:/springboot.log #生成日志文件的目录,名称默认为spring.log #logging.path=e:/ #指定日志的格式 #logging.pattern.console=%d{yyyy/MM/dd-HH:mm:ss} [%thread] %-5level %clr(%logger){cyan} %clr(%msg%n){green} #logging.pattern.file=%d{yyyy/MM/dd-HH:mm} [%thread] %-5level %logger- %msg%n ############################################################ # # Mybatis settings # ############################################################ #jiazai mybatis peizhiwenjian(**代表任意目录,*代表任意多个字符) mybatis.mapper-locations = classpath:mapper/**/*Mapper.xml mybatis.config-location = classpath:mybatis/SqlMapConfig.xml mybatis.type-aliases-package = cn.qlq.bean ############################################################ # # datasource settings # ############################################################ spring.datasource.driver-class-name= com.mysql.jdbc.Driver spring.datasource.url = jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=utf-8 spring.datasource.username = root spring.datasource.password = 123456 ############################################################ # # Server 服务端相关配置 # ############################################################ # 配置api端口号 server.port=8088 # 配置context-path, 一般来说这个配置在正式发布的时候不配置 #server.context-path=/MySpringboot server.serverlet-path=*.do,*.action # 错误页,指定发生错误时,跳转的URL --> BasicErrorController #server.error.path=/error # session最大超时时间(分钟),默认为30分钟 server.session-timeout=60 # 该服务绑定IP地址,启动服务器时如本机不是该IP地址则抛出异常启动失败, # 只有特殊需求的情况下才配置, 具体根据各自的业务来设置 #server.address=192.168.1.2 ############################################################ # Server - tomcat 相关常用配置 ############################################################ # tomcat最大线程数, 默认为200 #server.tomcat.max-threads=250 # tomcat的URI编码 server.tomcat.uri-encoding=UTF-8 # 存放Tomcat的日志、Dump等文件的临时文件夹,默认为系统的tmp文件夹 #(如:C:%users\Shanhy\AppData\Local\Temp) #server.tomcat.basedir=H:/springboot-tomcat-tmp # 打开Tomcat的Access日志,并可以设置日志格式的方法: #server.tomcat.access-log-enabled=true #server.tomcat.access-log-pattern= # accesslog目录,默认在basedir/logs #server.tomcat.accesslog.directory= # 日志文件目录 #logging.path=H:/springboot-tomcat-tmp # 日志文件名称,默认为spring.log #logging.file=myapp.log ############################################################ # # freemarker 静态资源配置 # ############################################################ #设定ftl文件路径 spring.freemarker.template-loader-path=classpath:/templates # 关闭缓存, 即时刷新, 上线生产环境需要改为true spring.freemarker.cache=false #spring.freemarker.enabled=false spring.freemarker.charset=UTF-8 spring.freemarker.check-template-location=false spring.freemarker.content-type=text/html spring.freemarker.expose-request-attributes=true spring.freemarker.expose-session-attributes=true spring.freemarker.request-context-attribute=request spring.freemarker.suffix=.ftl ############################################################ # # thymeleaf 静态资源配置 # ############################################################ spring.thymeleaf.prefix=classpath:/templates/thymeleaf/x-admin/ spring.thymeleaf.suffix=.html spring.thymeleaf.mode=HTML5 spring.thymeleaf.encoding=UTF-8 spring.thymeleaf.content-type=text/html # 关闭缓存, 即时刷新, 上线生产环境需要改为true spring.thymeleaf.cache=false #关闭thymeleaf引擎 #spring.thymeleaf.enabled=false ############################################################ # # JSP 配置 # ############################################################ #spring.mvc.view.suffix=.jsp #spring.mvc.view.prefix=/WEB-INF/jsp/ #设定静态文件路径,js,css等 spring.mvc.static-path-pattern=/static/** #静态资源的存储位置 #spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/ ############################################################ # # 配置i18n 资源文件,供thymeleaf 读取 # ############################################################ spring.messages.basename=i18n/messages spring.messages.cache-seconds=3600 spring.messages.encoding=UTF-8 ############################################################ # # 格式化日期类型为JSON的格式 # ############################################################ spring.jackson.date-format=yyyy-MM-dd spring.jackson.time-zone=GMT+8 spring.jackson.serialization.write-dates-as-timestamps=false
git地址:https://github.com/qiao-zhi/springboot-ssm.git
===========app端实现自动登录和用户列表查询========
app的开发主要是采用HBuilder建的移动app工程,以及mui框架。mui可以调用手机自带的API,比如手机二维码扫描、微信登录等。而且HBuilder已经集成了mui,所以新建工程的时候选择的是带登录模板的工程。而且只有apk打包的程序才可以访问h5+。直接在浏览器访问是不可行的。
下面是自己开发过程中对mui注意事项的总结:
0.每一个页面对应一个JS。而且在JS第一行首先调用mui.init();进行初始化。 1.mui的localStorage 可用于本地存储。除非用JS删除 localStorage.setItem('$settings', JSON.stringify(settings)); var stateText = localStorage.getItem('$state') || "{}"; 2.不要在没有plus和mui的环境下调用相关API 普通浏览器里没有plus环境,只有HBuilder真机运行和打包后才能运行plus api。 在普通浏览器里运行时plus api时控制台必然会输出plus is not defined错误提示。 mui作为一个前端框架,你必须保证当前页面引入了mui.js。否则也会出现mui is not defined。 3.不要在plus和mui未完成初始化时调用相关API 就像在dom初始化完成前(DOMContentLoaded)去操作dom,就会报错是一样的道理。 plus和mui都需要初始化,在初始化完成后调用再调用。 一般我们在plusready的回调事件里调用plus api。=======也就是说一般我们都要先mui.init({...}),然后mui相关所有的JS函数都写在mui.plusReady里面,遵循大部分前端框架的规则 mui框架对此进行了封装,写法更简单: mui.init({ pullRefresh: { container: '#pullrefresh', down: { style:'circle', callback: pulldownRefresh }, up: { auto:true, contentrefresh: '正在加载...', callback: pullupRefresh } } }); mui.plusReady(function() { mui.toast("提示"); }); 4.mui页面跳转的方式:===预加载的方式 (1)在$.plusReady中提前加载页面 var mainPage = $.preload({ "id": 'main', "url": 'main.html' }); (2)通过函数跳转页面的方法: var toMain = function() { $.fire(mainPage, 'show', null); setTimeout(function() { $.openWindow({ id: 'main', show: { aniShow: 'pop-in' }, waiting: { autoShow: false } }); }, 0); }; 通过mui.fire()方法可触发目标窗口的自定义事件。 5.mui页面跳转的方式:===预加载的方式,也可以通过a标签的方式进行加载 一般在新的页面用下面格式: <body> <!--上面的导航窗格,mui-pull-left表示居左还是居右--> <header class="mui-bar mui-bar-nav"> <!--返回按钮--> <a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left"></a> <h1 class="mui-title">三行列表</h1> </header> <!--下面存放所有的内容--> <div class="mui-content"> </div> <body> 6.mui重写手机返回按钮的事件: <script> mui.init({ swipeBack:true //启用右滑关闭功能 }); mui.plusReady(function() { mui.back = function(event) { alert("xxx"); }; }); </script> 一般在主页面重写返回按钮的事件:--双击退出应用 //-- $.oldBack = mui.back; var backButtonPress = 0; mui.back = function(event) { backButtonPress++; if (backButtonPress > 1) { plus.runtime.quit(); } else { plus.nativeUI.toast('再按一次退出应用'); } setTimeout(function() { backButtonPress = 0; }, 1000); return false; }; 7. mui带箭头的列表导航的格式 <ul id="list" class="mui-table-view mui-table-view-chevron"> <li class="mui-table-view-cell"> <a class="mui-navigate-right" href="view/user.html"> 进入用户列表 </a> </li> <li class="mui-table-view-cell mui-collapse"> <a class="mui-navigate-right" href="#"> 测试连接 </a> <ul class="mui-table-view mui-table-view-chevron"> <li class="mui-table-view-cell"> <a class="mui-navigate-right" href="examples/actionsheet.html"> 页面1 </a> </li> <li class="mui-table-view-cell mui-plus-visible"> <a class="mui-navigate-right" href="examples/actionsheet-plus.html"> 页面2 </a> </li> </ul> </li> </ul>
1.自动登录的主要思想:
如果对模板的项目稍有研究可以发现,app默认的页面是login.html,如果登录成功或者是自动登录而且自动登录的信息在本地存储的是正确的就调用toMain()方法跳转到main.html。
自动登录的思想就是:登录成功之后将从后台生成一个token串保存到本地,每次打开app的时候如果是自动登录就带着本地的token去后台验证,当然后台的代码在上面的springboot项目地址都实现了。这里主要介绍前端的交互。
1.重写app.js的登录方法:(我们知道mui的ajax请求本身是跨域的)
/** * 用户登录 **/ owner.login = function(loginInfo, callback) { callback = callback || $.noop; loginInfo = loginInfo || {}; loginInfo.account = loginInfo.account || ''; loginInfo.password = loginInfo.password || ''; if (loginInfo.account.length < 5) { return callback('账号最短为 5 个字符'); } if (loginInfo.password.length < 5) { return callback('密码最短为 5 个字符'); } var users = JSON.parse(localStorage.getItem('$users') || '[]'); //模拟登陆 var loginAddress = appServerAddressPrefix + "/mobile/doLogin.html"; $.post(loginAddress, { "username":loginInfo.account, "password":loginInfo.password }, function(res){ if(res && res.success){ return owner.createState(res.data, callback); }else{ return callback(res.msg); } }, 'json'); };
appServerAddressPrefix 是自己的后台服务器的地址,如下:
var appServerAddressPrefix = "http://192.168.1.6:8088";
createState 以及setState是保存登录的token等信息到本地:
owner.createState = function(token, callback) { var state = owner.getState(); state.account = token.username; state.token = token.tokenstr; owner.setState(state); return callback(); };
/** * 设置当前状态 **/ owner.setState = function(state) { state = state || {}; localStorage.setItem('$state', JSON.stringify(state)); //var settings = owner.getSettings(); //settings.gestures = ''; //owner.setSettings(settings); };
2.重写login.html的toMain()方法:也就是在跳转之前先对本地的token进行验证:
var toMain = function() { //跳转之前先验证token var stateText = localStorage.getItem('$state') || "{}"; var state = JSON.parse(stateText); var isValidToken = true; if(state != null && state.token!=null){ //如果token生效就加载页面,如果不生效就返回到登录页面 $.ajax({ url:appServerAddressPrefix+"/mobile/token/validateToken.html", async:false, data:{"tokenStr":state.token}, success:function(res){ if(res && res.success){ //根据tokenStr判断是否需要更新本地token if(res.data.tokenstr != state.token){ $.alert("需要更新Token"); state.token = res.data.tokenstr; localStorage.setItem('$state', JSON.stringify(state)); } }else{ //提示token无效且将标记设为false不进行跳转 $.toast(res.msg); isValidToken = false; } }, dataType:'json' }); } if(!isValidToken){//如果Token无效就返回不跳转主页面 return; } //进行页面跳转 $.fire(mainPage, 'show', null); setTimeout(function() { $.openWindow({ id: 'main', show: { aniShow: 'pop-in' }, waiting: { autoShow: false } }); }, 0); };
2.下拉刷新的思想:
在mui.init事件中声明下拉刷新,如果是最后一页就结束下拉刷新事件。 mui.init...的auto:true,是页面初始化自动调用一次对应的事件。
页面:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello MUI</title> <meta name="viewport" content="width=device-width, initial-scale=1,maximum-scale=1,user-scalable=no"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <!--标准mui.css--> <link rel="stylesheet" href="../css/mui.min.css"> <!--App自定义的css--> <link rel="stylesheet" type="text/css" href="../css/app.css"/> <style> .mui-content>.mui-table-view:first-child { margin-top: -1px; } #pullrefresh{ margin-top: 80px; } .mui-table-view div{ float: left; } </style> </head> <body> <header class="mui-bar mui-bar-nav"> <a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left"></a> <h1 class="mui-title">用户列表</h1> </header> <div class="mui-content"> <div style="margin:5px 0px ;"> <table width="100%" class="table" id="tablevalue"> <tr> <th width=25%>序号</th> <th width=25%>姓名</th> <th width=25%>性别</th> <th width=25%>地址</th> </tr> </table> </div> <!--下拉刷新容器--> <div id="pullrefresh" class="mui-scroll-wrapper"> <div class="mui-scroll"> <!--数据列表--> <ul class="mui-table-view mui-table-view-chevron"> <!--<li class="mui-table-view-cell"> <a class="mui-navigate-right">init</a> </li>--> </ul> </div> </div> </div> </body> <script src="../js/mui.min.js" type="text/javascript" charset="utf-8"></script> <script src="../js/constants.js" type="text/javascript" charset="utf-8"></script> <script src="../js/user.js" type="text/javascript" charset="utf-8"></script> </html>
页面对应的JS:
mui.init({ pullRefresh: { container: '#pullrefresh', down: {//下拉刷新未实现 style:'circle', callback: pulldownRefresh }, up: {//上拉刷新实现 auto:true, contentrefresh: '正在加载...', callback: pullupRefresh } } }); var downPage = 0,upPage=0;//标记下拉、上滑动的页号 var count = 0; function pullupRefresh() { setTimeout(function() {//将主要代码有个延迟是为了有个缓冲的过程,当然可以不延迟,显示效果不太好 upPage++; mui.post(appServerAddressPrefix+"/mobile/user/getUsers.html",{"pageNum":upPage},function(res){ //1.添加数据 addData(res.list,res.isFirstPage); //2.刷新数据,如果是最后一页停止加载加载事件 /*if(res.isLastPage){//刷新数据,如果是最后一页就停止上滑刷新事件 mui('#pullrefresh').pullRefresh().endPullupToRefresh(true); }else{ mui('#pullrefresh').pullRefresh().endPullupToRefresh(); }*/ mui('#pullrefresh').pullRefresh().endPullupToRefresh(res.isLastPage); },'json'); }, 1500); } function addData(datas,isfirstpage) { var table = document.body.querySelector('.mui-table-view'); var cells = document.body.querySelectorAll('.mui-table-view-cell'); for(var i = cells.length, len = i + datas.length; i < len; i++) { var li = document.createElement('li'); var user = datas[i-cells.length]; li.className = 'mui-table-view-cell'; li.innerHTML = '<a class="mui-navigate-right">'+user.username +' '+user.sex+ '</a>'; //下拉刷新,新纪录插到最前面; table.insertBefore(li, table.firstChild); } if(!isfirstpage){//如果不是第一页就提示加载 了数据 mui.toast("为你加载了"+datas.length+"条数据!"); } } /** * 下拉刷新具体业务实现 */ function pulldownRefresh() { setTimeout(function() { addData(); mui('#pullrefresh').pullRefresh().endPulldownToRefresh(); mui.toast("为你推荐了5篇文章"); }, 1500); }
效果如下:
app代码git地址:https://github.com/qiao-zhi/MyApp.git
后端git与上面的web项目是同一个地址:https://github.com/qiao-zhi/springboot-ssm.git
至此实现了一个简单的app,在使用的时候我们可以通过HBuilder打一个apk安装包出来,然后通过连接进行下载或者通过二维码扫描进行下载。安卓手机访问http连接后缀是apk的时候会自动安装软件。
========HBuilder中app的打包以及手机端的安装========
1.右键项目:发行->发行为原生安装包
2.然后选择参数配置去补全参数之后进行配置
3.填写应用的appid、选择图标等信息。====一般需要修改默认的log、启动的flash等替代默认图标,当然需要配置一些权限。如果第三方登录还需要在SDK设置选项卡设置appid等信息。当然需要在对应的应用程序后台建立对应的app实现对接。
4.然后保存之后再次发行为原生安装包(这里可能需要改一下Android的包名)
5.然后等待打包即可
6.打包成功之后下载安装包
7.下载完成之后文件如下:
8.接下来创建一个http链接进行访问apk即可 (或者通过QQ、微信等将apk传到手机都可以,只是http访问后缀为apk的时候会自动作为app安装软件)
例如我将它放在自己的服务器上,然后手机端的网页通过http访问链接的时候效果如下:
http://qiaoliqiang.cn/fileDown/qlq.mytestapp2_0226180452.apk
9.手机端通过网页访问下载app即可,如下:
当然可以做一个二维码来实现扫码下载,二维码中的内容存放上面的地址链接就可以了。如下面:(只能通过浏览器扫码下载,不能通过微信和QQ下载,腾讯只允许扫码安装自己域名下的app、)
补充:关于 localStorage
localStorage 会可以将第一次请求的数据直接存储到本地,这个相当于一个 5M 大小的针对于前端页面的数据库,相比于 cookie 可以节约带宽,但是这个却是只有在高版本的浏览器中才支持的。
使用 localStorage 创建一个本地存储的 name/value 对。localStorage 用于长久保存整个网站的数据,保存的数据没有过期时间,直到手动去删除。localStorage 属性是只读的。
#保存 localStorage.setItem("key", "value"); #读取 var lastname = localStorage.getItem("key"); #删除 localStorage.removeItem("key");
(1) localStorage的key不允许重复,会产生覆盖
localStorage.setItem("name", "zhangsan"); localStorage.setItem("name", "lisi"); localStorage.setItem("age", "25"); console.log(localStorage);
结果:
(2)localStorage的数据会永久生效。同一浏览器,标签页全部共享,它是直接存到电脑硬盘上的。
(3)sessionStorage是当前会话有效,用法同localStorage。