1.服务端基于Flask-JSONRPC提供RPC接口css
4.移动端访问测试接口github
2.客户端展现界面ajax
3.在APP进行窗口和页面操做json
1.window 窗口flask
2.frame 帧页面windows
所谓的RPC,Remote Procedure Call
的简写,中文译做远程过程调用或者远程服务调用。
直观的理解就是,经过网络请求远程服务,获取指定接口的数据,而不用知晓底层网络协议的细节。
RPC
支持的格式不少,好比XML
格式,JSON
格式等等。最经常使用的确定是json-rpc。
-----------------------------------------------------
git地址:https://github.com/cenobites/flask-jsonrpc
文档:http://wiki.geekdream.com/Specification/json-rpc_2.0.html
客户端请求服务端完成某一个服务行为,因此RPC规范要求: 客户端发送的全部请求都是POST请求!!!
全部的传输数据都是单个对象,用JSON格式进行序列化。
请求要求包含三个特定属性:
jsonrpc: 用来声明JSON-RPC协议的版本,如今基本固定为“2.0” method,方法,是等待调用的远程方法名,字符串类型 params,参数,对象类型或者是数组,向远程方法传递的多个参数值 id,任意类型值,用于和最后的响应进行匹配,也就是这里设定多少,后面响应里这个值也设定为相同的 响应的接收者必须可以给出全部请求以正确的响应。这个值通常不能为Null,且为数字时不能有小数。
响应也有三个属性:
jsonrpc, 用来声明JSON-RPC协议的版本,如今基本固定为“2.0” result,结果,是方法的返回值,调用方法出现错误时,必须不包含该成员。 error,错误,当出现错误时,返回一个特定的错误编码,若是没有错误产生,必须不包含该成员。 id,就是请求带的那个id值,必须与请求对象中的id成员的值相同。请求对象中的id时发生错误(如:转换错误或无效的请求),它必须为Null
固然,有一些场景下,是不用返回值的,好比只对客户端进行通知,因为不用对请求的id进行匹配,因此这个id就是没必要要的,置空或者直接不要了。
pip install Flask-JSONRPC==0.3.1
import os,logging from flask_jsonrpc import JSONRPC # 初始化jsonrpc模块 jsonrpc = JSONRPC(service_url='/api') def init_app(config_path): """全局初始化""" # 初始化json-rpc jsonrpc.init_app(app)
# 实现rpc接口 from application import jsonrpc @jsonrpc.method(name="Home.index") def index(): return "hello world!"
3.固然,咱们能够经过postman发起post请求:
请求地址:http://127.0.0.1:5000/api 请求体: { "jsonrpc":"2.0", "method":"Home.index", "params":{}, "id":"1" }
1.postman向http://127.0.0.1:5000/api发送POST请求
请求体内容:
请求地址:http://127.0.0.1:5000/api 请求体: { "jsonrpc":"2.0", "method":"Home.index", "params":{"id":"abc"}, "id":"1" }
2.后端接口代码
from application import jsonrpc @jsonrpc.method(name="Home.index") def index(id): return "hello world!id=%s" % id
3.响应结果
响应内容: { "id":1, "jsonrpc":"2.0", "result":"hello world!id=abc" }
由于当前咱们的服务端项目安装在虚拟机里面,而且咱们设置了虚拟机的网络链接模式为NAT,因此通常状况下,咱们没法直接经过手机访问虚拟机。所以,咱们须要配置一下。
1.打开VM的“编辑“菜单,选中虚拟网络编辑器。
2.打开编辑器窗口,使用管理员权限,并点击“NAT设置”。
3.填写网关IP地址,必须和子网IP在同一网段。末尾通常为1。接着在端口转发下方点击“添加”。
4.在映射传入端口中,填写转发的端口和实际虚拟机的IP端口,填写完成之后,所有点击“肯定”,关闭全部窗口。未来,手机端访问PC主机的8083端口就自动访问到虚拟机。8083是自定义的,能够是其余端口。
5.此时在手机上访问你windows电脑本机IP+端口/api/browse便可成功访问到测试接口
<!DOCTYPE html> <html lang="en"> <head> <title>首页</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"> <meta name="format-detection" content="telephone=no,email=no,date=no,address=no"> <link rel="stylesheet" href="../static/css/main.css"> <script src="../static/js/vue.js"></script> <script src="../static/js/main.js"></script> </head> <body> <div class="app" id="app"> <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png"> <div class="bg"> <img src="../static/images/bg0.jpg"> </div> <ul> <li><img class="module1" src="../static/images/image1.png"></li> <li><img class="module2" src="../static/images/image2.png"></li> <li><img class="module3" src="../static/images/image3.png"></li> <li><img class="module4" src="../static/images/image4.png"></li> </ul> </div> <script> apiready = function(){ var game = new Game("../static/mp3/bg1.mp3"); // 容许ajax发送请求时附带cookie,设置为不容许 Vue.prototype.game = game; new Vue({ el:"#app", data(){ return { music_play:true, // 默认播放背景音乐 prev:{name:"",url:"",params:{}}, // 上一页状态 current:{name:"index",url:"index.html","params":{}}, // 下一页状态 } }, watch:{ music_play(){ if(this.music_play){ this.game.play_music("../static/mp3/bg1.mp3"); }else{ this.game.stop_music(); } } }, methods:{ } }) } </script> </body> </html>
<!DOCTYPE html> <html> <head> <title>登陆</title> <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"> <meta charset="utf-8"> <link rel="stylesheet" href="../static/css/main.css"> <script src="../static/js/vue.js"></script> <script src="../static/js/main.js"></script> </head> <body> <div class="app" id="app"> <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png"> <div class="bg"> <img src="../static/images/bg0.jpg"> </div> <div class="form"> <div class="form-title"> <img src="../static/images/login.png"> <img class="back" src="../static/images/back.png"> </div> <div class="form-data"> <div class="form-data-bg"> <img src="../static/images/bg1.png"> </div> <div class="form-item"> <label class="text">手机</label> <input type="text" name="mobile" placeholder="请输入手机号"> </div> <div class="form-item"> <label class="text">密码</label> <input type="password" name="password" placeholder="请输入密码"> </div> <div class="form-item"> <input type="checkbox" class="agree remember" name="agree" checked> <label><span class="agree_text ">记住密码,下次免登陆</span></label> </div> <div class="form-item"> <img class="commit" @click="game.play_music('../static/mp3/btn1.mp3')" src="../static/images/commit.png"> </div> <div class="form-item"> <p class="toreg">当即注册</p> <p class="tofind">忘记密码</p> </div> </div> </div> </div> <script> apiready = function(){ var game = new Game("../static/mp3/bg1.mp3"); // 在 #app 标签下渲染一个按钮组件 Vue.prototype.game = game; new Vue({ el:"#app", data(){ return { music_play:true, prev:{name:"",url:"",params:{}}, current:{name:"login",url:"login.html",params:{}}, } }, watch:{ music_play(){ if(this.music_play){ this.game.play_music("../static/mp3/bg1.mp3"); }else{ this.game.stop_music(); } } }, methods:{ } }) } </script> </body> </html>
<!DOCTYPE html> <html> <head> <title>注册</title> <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"> <meta charset="utf-8"> <link rel="stylesheet" href="../static/css/main.css"> <script src="../static/js/vue.js"></script> <script src="../static/js/main.js"></script> </head> <body> <div class="app" id="app"> <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png"> <div class="bg"> <img src="../static/images/bg0.jpg"> </div> <div class="form"> <div class="form-title"> <img src="../static/images/register.png"> <img class="back" @click="backpage" src="../static/images/back.png"> </div> <div class="form-data"> <div class="form-data-bg"> <img src="../static/images/bg1.png"> </div> <div class="form-item"> <label class="text">手机</label> <input type="text" name="mobile" placeholder="请输入手机号"> </div> <div class="form-item"> <label class="text">验证码</label> <input type="text" class="code" name="code" placeholder="请输入验证码"> <img class="refresh" src="../static/images/refresh.png"> </div> <div class="form-item"> <label class="text">密码</label> <input type="password" name="password" placeholder="请输入密码"> </div> <div class="form-item"> <label class="text">确认密码</label> <input type="password" name="password2" placeholder="请再次输入密码"> </div> <div class="form-item"> <input type="checkbox" class="agree" name="agree" checked> <label><span class="agree_text">赞成磨方《用户协议》和《隐私协议》</span></label> </div> <div class="form-item"> <img class="commit" @click="game.play_music('../static/mp3/btn1.mp3')" src="../static/images/commit.png"/> </div> </div> </div> </div> <script> apiready = function(){ var game = new Game("../static/mp3/bg1.mp3"); Vue.prototype.game = game; new Vue({ el:"#app", data(){ return { music_play:true, prev:{name:"",url:"",params:{}}, current:{name:"register",url:"register.html","params":{}}, } }, watch:{ music_play(){ if(this.music_play){ this.game.play_music("../static/mp3/bg1.mp3"); }else{ this.game.stop_music(); } } }, methods:{ backpage(){ this.prev.name = api.pageParam.name; this.prev.url = api.pageParam.url; this.prev.params = api.pageParam.params; this.game.back(this.prev); } } }) } </script> </body> </html>
window是APICloud提供的最顶级的页面单位.一个APP至少会存在一个以上的window窗口,在用户打开APP应用,应用在初始化的时候默认就会建立了一个name=root 的顶级window窗口显示当前APP配置的首页.
api.openWin({ name: 'page1', // 自定义窗口名称 bounces: false, // 窗口是否上下拉动 reload: true, // 若是页面已经在以前被打开了,是否要从新加载当前窗口中的页面 url: './page1.html', // 窗口建立时展现的html页面的本地路径[相对于当前代码所在文件的路径] animation:{ // 打开新建窗口时的过渡动画效果 type:"none", //动画类型(详见动画类型常量) subType:"from_right", //动画子类型(详见动画子类型常量) duration:300 //动画过渡时间,默认300毫秒 }, pageParam: { // 传递给下一个窗口使用的参数.未来能够在新窗口中经过 api.pageParam.name 获取 name: 'test' // name只是举例, 未来能够传递更多自定义数据的. } });
main.js
class Game{ ...... goWin(name,url,pageParam){ api.openWin({ name: name, // 自定义窗口名称 bounces: false, // 窗口是否上下拉动 reload: true, // 若是页面已经在以前被打开了,是否要从新加载当前窗口中的页面 url: url, // 窗口建立时展现的html页面的本地路径[相对于当前代码所在文件的路径] animation:{ // 打开新建窗口时的过渡动画效果 type: "push", //动画类型(详见动画类型常量) subType: "from_right", //动画子类型(详见动画子类型常量) duration:300 //动画过渡时间,默认300毫秒 }, pageParam: pageParam // 传递给下一个窗口使用的参数.未来能够在新窗口中经过 api.pageParam.name 获取 }); } ...... }
html/index.html
<div class="form-item"> <p class="toreg" @click="goto_register">当即注册</p> <p class="tofind">忘记密码</p> </div> <script> apiready = function(){ var game = new Game("../static/mp3/bg1.mp3"); Vue.prototype.game = game; new Vue({ el:"#app", data(){ return { prev:{name:"",url:"",params:{}}, current:{name:"login",url:"login.html",params:{}}, } }, methods:{ goto_register(){ this.game.goWin("register","./register.html", this.current); } } }) } </script>
-------------------------------------------------------
//关闭当前window,使用默认动画 api.closeWin(); //关闭指定window,若待关闭的window不在最上面,则无动画 api.closeWin({ name: 'page1' });
Tip:若是当前APP中只有剩下一个顶级窗口root,则没法经过当前方法关闭! 也有部分手机直接退出APP了
main.js
class Game{ ...... outWin(name){ // 关闭窗口 api.closeWin(name); } ...... }
html/register.html
<div class="form-title"> <img src="../static/images/register.png"> <img class="back" @click="back" src="../static/images/back.png"> </div> <script> apiready = function(){ var game = new Game("../static/mp3/bg1.mp3"); Vue.prototype.game = game; new Vue({ el:"#app", data(){ return { prev:{name:"",url:"",params:{}}, current:{name:"register",url:"register.html","params":{}}, } }, methods:{ back(){ this.game.outWin(); } } }) } </script>
帧相对于窗口的优势以及使用帧页面时须要注意的点:
若是APP中全部的页面所有窗口进行展开,则APP须要耗费大量的内存来维护这个窗口列表,从而致使, 用户操做APP时,APP响应缓慢甚至卡顿的现象.因此APP中除了经过新建窗口的方式展开页面之外, 还提供了帧的方式来展开页面.
帧,表明的就是一个窗口下开打的某个页面记录.所谓的帧就有点相似于浏览器中窗口经过地址栏新建的一个页面同样.
使用的时候注意:
1. APP每个window窗口均可以打开1到多个帧.新建窗口的时候,系统会默认顺便建立第一帧出来.
2. 每一帧表明的都是一个html页面,
3. 默认状况下, APP的window的窗口会自动默认满屏展现.而帧能够设置矩形的宽高.若是顶层的帧页面没有满屏显示,则用户能够看到当前这一帧下的其余帧的内容.
api.openFrame({ name: 'page2', // 帧页面的名称 url: './page2.html', // 帧页面打开的url地址 data: '', // 可选参数,若是填写了data,则不要使用url, data表示页面数据,能够是html代码 bounces:false, // 页面是否能够下拉拖动 reload: true, // 帧页面若是已经存在,是否从新刷新加载 useWKWebView:true, historyGestureEnabled:true, animation:{ type:"push", //动画类型(详见动画类型常量) subType:"from_right", //动画子类型(详见动画子类型常量) duration:300 //动画过渡时间,默认300毫秒 }, rect: { // 当前帧的宽高范围 // 方式1,设置矩形大小宽高 x: 0, // 左上角x轴坐标 y: 0, // 左上角y轴坐标 w: 'auto', // 当前帧页面的宽度, auto表示满屏 h: 'auto' // 当前帧页面的高度, auto表示满屏 // 方式2,设置矩形大小宽高 marginLeft:, //相对父页面左外边距的距离,数字类型 marginTop:, //相对父页面上外边距的距离,数字类型 marginBottom:, //相对父页面下外边距的距离,数字类型 marginRight: //相对父页面右外边距的距离,数字类型 }, pageParam: { // 要传递新建帧页面的参数,在新页面可经过 api.pageParam.name 获取 name: 'test' // name只是举例, 能够传递任意自定义参数 } });
// 关闭当前 frame页面 api.closeFrame(); // 关闭指定名称的frame页面 api.closeFrame({ name: 'page2' });
main.js
class Game{ goFrame(name,url,pageParam,rect=null){ // 建立帧页面 if(rect === null){ rect = { // 方式1,设置矩形大小宽高 x: 0, // 左上角x轴坐标 y: 0, // 左上角y轴坐标 w: 'auto', // 当前帧页面的宽度, auto表示满屏 h: 'auto' // 当前帧页面的高度, auto表示满屏 } } api.openFrame({ name: name, // 帧页面的名称 url: url, // 帧页面打开的url地址 bounces:false, // 页面是否能够下拉拖动 reload: true, // 帧页面若是已经存在,是否从新刷新加载 useWKWebView: true, historyGestureEnabled:true, animation:{ type:"push", //动画类型(详见动画类型常量) subType:"from_right", //动画子类型(详见动画子类型常量) duration:300 //动画过渡时间,默认300毫秒 }, rect: rect, // 当前帧的宽高范围 pageParam: pageParam, // 要传递新建帧页面的参数,在新页面可经过 api.pageParam.name 获取 }); } outFrame(name){ // 关闭帧页面 api.closeFrame({ name: name, }); } }
登陆页面,点击当即注册跳转到注册页面
<div class="form-item"> <p class="toreg" @click="goto_register">当即注册</p> <p class="tofind">忘记密码</p> </div> <script> apiready = function(){ var game = new Game("../static/mp3/bg1.mp3"); Vue.prototype.game = game; new Vue({ el:"#app", data(){ return { prev:{name:"",url:"",params:{}}, current:{name:"login",url:"login.html",params:{}}, } }, methods:{ goto_register(){ this.game.goFrame("register","./register.html", this.current); } } }) } </script>
注册页面,点击返回关闭页面
<div class="form-title"> <img src="../static/images/register.png"> <img class="back" @click="back" src="../static/images/back.png"> </div> <script> apiready = function(){ var game = new Game("../static/mp3/bg1.mp3"); Vue.prototype.game = game; new Vue({ el:"#app", data(){ return { prev:{name:"",url:"",params:{}}, current:{name:"register",url:"register.html","params":{}}, } }, methods:{ back(){ this.game.outFrame(); } } }) } </script>
api.openFrameGroup({ name: 'group1', // 组名 rect: { // 帧页面组的显示矩形范围 // 方式1: x:, //左上角x坐标,数字类型 y:, //左上角y坐标,数字类型 w:, //宽度,若传'auto',页面从x位置开始自动充满父页面宽度,数字或固定值'auto' h:, //高度,若传'auto',页面从y位置开始自动充满父页面高度,数字或固定值'auto' // 方式2: marginLeft:, //相对父页面左外边距的距离,数字类型 marginTop:, //相对父页面上外边距的距离,数字类型 marginBottom:, //相对父页面下外边距的距离,数字类型 marginRight: //相对父页面右外边距的距离,数字类型 }, frames: [{ name:'', //frame名字,字符串类型,不能为空字符串 url:'', // 页面地址 useWKWebView:true, historyGestureEnabled:false, //(可选项)是否能够经过手势来进行历史记录前进后退。 pageParam:{}, // 页面参数 bounces:true, // 是否能下拉拖动 }, { name:'', //frame名字,字符串类型,不能为空字符串 url:'', // 页面地址 useWKWebView:true, historyGestureEnabled:false, //(可选项)是否能够经过手势来进行历史记录前进后退。 pageParam:{}, // 页面参数 bounces:true, // 是否能下拉拖动 },{ ... },... ] }, function(ret, err) { // 当前帧页面发生页面显示变化时,当前帧的索引. var index = ret.index; });
api.closeFrameGroup({ name: 'group1' // 组名 });
api.setFrameGroupIndex({ name: 'group1', // 组名 index: 2 // 索引,从0开始 });
class Game{ ...... openGroup(name,frames,preload=1,rect=null){ // 建立frame组 if(rect === null){ rect = { // 帧页面组的显示矩形范围 x:0, //左上角x坐标,数字类型 y:0, //左上角y坐标,数字类型 w:'auto', //宽度,若传'auto',页面从x位置开始自动充满父页面宽度,数字或固定值'auto' h:'auto', //高度,若传'auto',页面从y位置开始自动充满父页面高度,数字或固定值'auto' }; } api.openFrameGroup({ name: name, // 组名 scrollEnabled: false, // 页面组是否能够左右滚动 index: 0, // 默认显示页面的索引 rect: rect, // 页面宽高范围 preload: preload, // 默认预加载的页面数量 frames: frames, // 帧页面组的帧页面成员 }, (ret, err)=>{ // 当前帧页面发生页面显示变化时,当前帧的索引. this.groupindex = ret.index; }); } outGroup(name){ // 关闭 frame组 api.closeFrameGroup({ name: name // 组名 }); } goGroup(name,index){ // 切换显示frame组下某一个帧页面 api.setFrameGroupIndex({ name: name, // 组名 index: index // 索引,从0开始 }); } }
<ul> <li><img class="module1" src="../static/images/image1.png"></li> <li><img class="module2" @click="gohome" src="../static/images/image2.png"></li> <li><img class="module3" src="../static/images/image3.png"></li> <li><img class="module4" src="../static/images/image4.png"></li> </ul> <script> apiready = function(){ var game = new Game("../static/mp3/bg1.mp3"); // 容许ajax发送请求时附带cookie,设置为不容许 Vue.prototype.game = game; new Vue({ el:"#app", data(){ return { prev:{name:"",url:"",params:{}}, // 上一页状态 current:{name:"index",url:"index.html","params":{}}, // 下一页状态 } }, methods:{ gohome(){ frames = [{ name: 'login', url: './login.html', },{ name: 'register', url: './register.html', }] this.game.openGroup("user",frames,frames.length); } } }) } </script> </body> </html>
<div class="form-item"> <p class="toreg" @click="goto_register">当即注册</p> <p class="tofind">忘记密码</p> </div> <script> apiready = function(){ var game = new Game("../static/mp3/bg1.mp3"); // 在 #app 标签下渲染一个按钮组件 Vue.prototype.game = game; new Vue({ el:"#app", data(){ return { prev:{name:"",url:"",params:{}}, current:{name:"login",url:"login.html",params:{}}, } }, created(){ }, methods:{ goto_register(){ // this.game.goWin("register","./register.html", this.current); // this.game.goFrame("register","./register.html", this.current); this.game.goGroup("user",1); }, } }) } </script>
<div class="form-title"> <img src="../static/images/register.png"> <img class="back" @click="back" src="../static/images/back.png"> </div> <script> apiready = function(){ var game = new Game("../static/mp3/bg1.mp3"); Vue.prototype.game = game; new Vue({ el:"#app", data(){ return { music_play:true, prev:{name:"",url:"",params:{}}, current:{name:"register",url:"register.html","params":{}}, } }, methods:{ back(){ // this.game.outWin(); // this.game.outFrame(); this.game.goGroup("user",0); } } }) } </script>