day99:MoFang:Flask-JSONRPC提供RPC接口&在APP进行窗口页面操作(窗口-帧-帧组)
1.Flask-JSONRPC简介
1.什么是Flask-JSONRPC??
所谓的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
2.Flask-JSONRPC的实现原理
客户端请求服务端完成某一个服务行为,所以RPC规范要求: 客户端发送的所有请求都是POST请求!!!
所有的传输数据都是单个对象,用JSON格式进行序列化。
3.Flask-JSONRPC的请求和响应
请求要求包含三个特定属性:
jsonrpc: 用来声明JSON-RPC协议的版本,现在基本固定为“2.0” method,方法,是等待调用的远程方法名,字符串类型 params,参数,对象类型或者是数组,向远程方法传递的多个参数值 id,任意类型值,用于和最后的响应进行匹配,也就是这里设定多少,后面响应里这个值也设定为相同的 响应的接收者必须能够给出所有请求以正确的响应。这个值一般不能为Null,且为数字时不能有小数。
响应也有三个属性:
jsonrpc, 用来声明JSON-RPC协议的版本,现在基本固定为“2.0” result,结果,是方法的返回值,调用方法出现错误时,必须不包含该成员。 error,错误,当出现错误时,返回一个特定的错误编码,如果没有错误产生,必须不包含该成员。 id,就是请求带的那个id值,必须与请求对象中的id成员的值相同。请求对象中的id时发生错误(如:转换错误或无效的请求),它必须为Null
当然,有一些场景下,是不用返回值的,比如只对客户端进行通知,由于不用对请求的id进行匹配,所以这个id就是不必要的,置空或者直接不要了。
2.
pip install Flask-JSONRPC==0.3.1
3.快速实现一个测试的RPC接口
1.初始化jsonRPC
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)
2.编写接口代码
# 实现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" }
3.基于api接口接受来自客户端的参数
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" }
4.移动端访问测试接口
因为当前我们的服务端项目安装在虚拟机里面,并且我们设置了虚拟机的网络连接模式为NAT,所以一般情况下,我们无法直接通过手机访问虚拟机。因此,我们需要配置一下。
1.打开VM的“编辑“菜单,选中虚拟网络编辑器。
2.打开编辑器窗口,使用管理员权限,并点击“NAT设置”。
3.填写网关IP地址,必须和子网IP在同一网段。末尾一般为1。接着在端口转发下方点击“添加”。
4.在映射传入端口中,填写转发的端口和实际虚拟机的IP端口,填写完成以后,全部点击“确定”,关闭所有窗口。将来,手机端访问PC主机的8083端口就自动访问到虚拟机。8083是自定义的,可以是其他端口。
5.此时在手机上访问你windows电脑本机IP+端口/api/browse即可成功访问到测试接口
2.客户端展示界面
1.首页/登录页面/注册页面初始化界面
<!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>
3.在APP进行窗口和页面操作
1.window 窗口
window是APICloud提供的最顶级的页面单位.一个APP至少会存在一个以上的window窗口,在用户打开APP应用,应用在初始化的时候默认就会创建了一个name=root 的顶级window窗口显示当前APP配置的首页.
1.新建窗口
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只是举例, 将来可以传递更多自定义数据的. } });
2.在客户端APP的main.js(主程序脚本)中,添加一个新建窗口的方法
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 获取 }); } ...... }
3.在登录页面中,用户点击立即注册,会去到一个新的窗口
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>
-------------------------------------------------------
4.关闭窗口
//关闭当前window,使用默认动画 api.closeWin(); //关闭指定window,若待关闭的window不在最上面,则无动画 api.closeWin({ name: 'page1' });
Tip:如果当前APP中只有剩下一个顶级窗口root,则无法通过当前方法关闭! 也有部分手机直接退出APP了
5.接下来我们可以把关闭窗口的代码封装到主程脚本main.js中
main.js
class Game{ ...... outWin(name){ // 关闭窗口 api.closeWin(name); } ...... }
6.在注册页面点击返回键调用关闭窗口的方法
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>
2.frame 帧页面
帧相对于窗口的优点以及使用帧页面时需要注意的点:
如果APP中所有的页面全部窗口进行展开,则APP需要耗费大量的内存来维护这个窗口列表,从而导致, 用户操作APP时,APP响应缓慢甚至卡顿的现象.所以APP中除了通过新建窗口的方式展开页面以外, 还提供了帧的方式来展开页面.
帧,代表的就是一个窗口下开打的某个页面记录.所谓的帧就有点类似于浏览器中窗口通过地址栏新建的一个页面一样.
使用的时候注意:
1. APP每一个window窗口都可以打开1到多个帧.新建窗口的时候,系统会默认顺便创建第一帧出来.
2. 每一帧代表的都是一个html页面,
3. 默认情况下, APP的window的窗口会自动默认满屏展示.而帧可以设置矩形的宽高.如果顶层的帧页面没有满屏显示,则用户可以看到当前这一帧下的其他帧的内容.
1.新建帧页面
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只是举例, 可以传递任意自定义参数 } });
2.关闭帧页面
// 关闭当前 frame页面 api.closeFrame(); // 关闭指定名称的frame页面 api.closeFrame({ name: 'page2' });
3.在主程脚本main.js中, 创建一个方法专门创建frame和删除frame页面
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, }); } }
4.在登录页面使用新建帧页面
登录页面,点击立即注册跳转到注册页面
<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>
5.在注册页面使用关闭帧页面
注册页面,点击返回关闭页面
<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>
3.framegroup 帧页面组
1.新建帧页面组
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; });
2.关闭帧页面组
api.closeFrameGroup({ name: 'group1' // 组名 });
api.setFrameGroupIndex({ name: 'group1', // 组名 index: 2 // 索引,从0开始 });
4.将开启帧页面组/关闭帧页面组/切换帧页面封装到main.js中
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开始 }); } }
5.在index.html/login.html/register.html页面使用帧页面组
<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>