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>