rpc接口
简介
服务端基于Flask-JSONRPC提供RPC接口.
JSON-RPC是一个无状态的、轻量级的远程过程调用(RPC)协议
什么是RPC?
所谓的RPC,Remote Procedure Call
的简写,中文译作远程过程调用或者远程服务调用
直观的理解就是,通过网络请求远程服务,获取指定接口的数据,而不用知晓底层网络协议的细节。
RPC
支持的格式很多,比如XML
格式,JSON
格式等等。最常用的肯定是json-rpc
RPC规范要求:
- 客户端发送的所有请求都是POST请求
- 所有的传输数据都是单个对象,用JSON格式进行序列化
请求要求包含4个特定属性:
jsonrpc: 用来声明JSON-RPC协议的版本,现在基本固定为“2.0”
method,方法,是等待调用的远程方法名,字符串类型
params,参数,对象类型或者是数组,向远程方法传递的多个参数值
id,任意类型值(一般是UUID格式),用于和最后的响应进行匹配。且为数字时不能有小数。
示例:
POST /api # 必须是post请求
{
"jsonrpc":"2.0",
"method": "login",
"params": {
"username": "xiaoming",
"password": "123456"
},
"id": "5b59969f-08e1-4309-a502-a7c078618f49"
}
响应也有3个属性:
jsonrpc, 用来声明JSON-RPC协议的版本,现在基本固定为“2.0”
result,返回正确响应的结果。
error,当出现错误时,返回一个特定的错误编码结果, result和error返回结果二选一
id, 就是请求带的那个id值,必须与请求对象中的id成员的值相同。
请求对象中的id时发生错误(如:转换错误或无效的请求),它必须为Null
有一些场景下,是不用返回值的,比如只对客户端进行通知,
由于不用对请求的id进行匹配,所以这个id就是不必要的,置空或者直接不要了
示例:
# 正确返回结果
{
"jsonrpc": "2.0",
"result": {
"errmsg":"ok",
"errnum":200,
},
"id": "5b59969f-08e1-4309-a502-a7c078618f49"
}
# 错误返回结果
{
"jsonrpc": "2.0",
"error": {
"errmsg":"num is default",
"traceback": "xxxx in xx.py ......终端的错误信息",
},
"id": "5b59969f-08e1-4309-a502-a7c078618f49"
}
安装Flask-JSONRPC模块
pip install flask-jsonrpc # 如果出现问题,则降低版本到0.3.1
pip install flask-jsonrpc==0.3.1 -i https://pypi.douban.com/simple
快速实现一个测试的RPC接口
- 项目入口文件中jsonrpc进行初始化,
application/__init__.py
import os
from flask import Flask
from flask_script import Manager
from flask_sqlalchemy import SQLAlchemy
from flask_redis import FlaskRedis
from flask_session import Session
from flask_jsonrpc import JSONRPC
from application.utils.config import init_config
from application.utils.logger import Log
from application.utils.commands import load_commands
from application.utils.bluerprint import register_blueprint, path, include, api_rpc
# 终端脚本工具初始化
manager = Manager()
# SQLAlchemy初始化
db = SQLAlchemy()
# redis数据库初始化
# - 1.默认缓存数据库对象,配置前缀为REDIS
redis_cache = FlaskRedis(config_prefix='REDIS')
# - 2.验证相关数据库对象,配置前缀为CHECK
redis_check = FlaskRedis(config_prefix='CHECK')
# - 3.验证相关数据库对象,配置前缀为SESSION
redis_session = FlaskRedis(config_prefix='SESSION')
# session储存配置初始化
session_store = Session()
# 自定义日志初始化
logger = Log()
# 初始化jsonrpc模块
jsonrpc = JSONRPC(service_url='/api')
# 全局初始化
def init_app(config_path):
"""全局初始化 - 需要传入加载开发或生产环境配置路径"""
# 创建app应用对象
app = Flask(__name__)
# 当前项目根目录
app.BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 开发或生产环境加载配置
init_config(app, config_path)
# SQLAlchemy加载配置
db.init_app(app)
# redis加载配置
redis_cache.init_app(app)
redis_check.init_app(app)
redis_session.init_app(app)
"""一定先加载默认配置,再传入APP加载session对象"""
# session保存数据到redis时启用的链接对象
app.config["SESSION_REDIS"] = redis_session
# session存储对象加载配置
session_store.init_app(app)
# 为日志对象加载配置
log = logger.init_app(app)
app.log = log
# json-rpc加载配置
jsonrpc.init_app(app)
# 自动注册蓝图
register_blueprint(app)
# 注册模型,创建表
with app.app_context():
db.create_all()
# 终端脚本工具加载配置
manager.app = app
# 自动注册自定义命令
load_commands(manager)
return manager
- 蓝图视图文件,编写接口代码
application/apps/home/views.py
from application import jsonrpc
@jsonrpc.method(name="Home.index")
def index(num): # 接收客户端发送的参数
data = {
"code":200,
"msg":"ok",
"data":"hello world!!",
"num":f"{num}",
"id":"1"
}
return data
客户端需要发起post请求,访问地址为:http://服务器地址:端口/api
默认情况下,/api
接口只能通过post请求访问。如果要使用jsonrpc提供的界面调试工具,则访问地址为:
http://服务器地址:端口/api/browse/
通过postman发送请求接口,访问数据格式应是:
POST http://127.0.0.1:5000/api
{
"jsonrpc": "2.0",
"method": "Home.User.login",
"params": { //客户端发送参数
"num":"123"
},
"id": "1"
}
自动注册rpc接口
- 我们蓝图注册路由关系那样, 定义api_rpc函数,把rpc方法名和视图的映射关系处理成字典
application/utils/blueprint.py
from flask import Blueprint
# 引入导包函数
from importlib import import_module
# 自动注册蓝图
def register_blueprint(app):
# 从配置文件中读取总路由路径信息
app_urls_path = app.config.get('URL_PATH')
# 导包,导入总路由模块
app_urls_module = import_module(app_urls_path)
# 获取总路由列表
app_urlpatterns = app_urls_module.urlpatterns
# 从配置文件中读取需要注册到项目中的蓝图路径信息
blueprint_path_list = app.config.get('INSTALL_BLUEPRINT')
# 遍历蓝图路径列表,对每一个蓝图进行初始化
for blueprint_path in blueprint_path_list:
# 获取蓝图路径中最后一段的包名作为蓝图的名称
blueprint_name = blueprint_path.split('.')[-1]
# 创建蓝图对象
blueprint = Blueprint(blueprint_name, blueprint_path)
# 蓝图路由的前缀
url_prefix = ""
# 蓝图下的子路由列表
urlpatterns = []
# 获取蓝图的父级目录, 即application/apps
blueprint_father_path = ".".join( blueprint_path.split(".")[:-1] )
# 循环总路由列表
for item in app_urlpatterns:
# 判断当前蓝图是否在总路由列表中
if blueprint_name in item["blueprint_url_subfix"]:
# 导入蓝图下的子路由模块
urls_module = import_module(blueprint_father_path + "." + item["blueprint_url_subfix"])
# 获取urls模块下蓝图路由列表urlpatterns
urlpatterns = urls_module.urlpatterns
# 提取蓝图路由的前缀
url_prefix = item["url_prefix"]
# 获取urls模块下rpc接口列表apipatterns
apipatterns = urls_module.apipatterns
# 从总路由中查到当前蓝图对象的前缀就不要继续往下遍历了
break
# 注册蓝图路由
for url in urlpatterns:
blueprint.add_url_rule(**url)
# 注册rpc接口
api_prefix_name = ''
# 判断是否开启补充api前缀
if app.config.get('JSON_PREFIX_NAME', False) == True:
api_prefix_name = blueprint_name.title() + '.'
# 循环rpc接口列表,进行注册
for api in apipatterns:
app.jsonrpc.site.register(api_prefix_name + api['name'], api['method'])
# 导入蓝图模型
import_module(blueprint_path + '.models')
# 把蓝图对象注册到app实例对象,并添加路由前缀
app.register_blueprint(blueprint, url_prefix=url_prefix)
# 把url地址和视图方法映射关系处理成字典
def path(rule, view_func, **kwargs):
"""绑定url地址和视图的映射关系"""
data = {
'rule':rule,
'view_func':view_func,
**kwargs
}
return data
# 把rpc方法名和视图的映射关系处理成字典
def api_rpc(name, method, **kwargs):
"""
:param name: rpc方法名
:param method: 视图名称
:param kwargs: 其他参数。。
:return:
"""
data = {
'name':name,
'method':method,
**kwargs
}
return data
# 路由前缀和蓝图进行绑定映射
def include(url_prefix, blueprint_url_subfix):
"""
:param url_prefix: 路由前缀
:param blueprint_url_subfix: 蓝图路由,
格式:蓝图包名.路由模块名
例如:蓝图目录是home, 路由模块名是urls,则参数:home.urls
:return:
"""
data = {
"url_prefix": url_prefix,
"blueprint_url_subfix": blueprint_url_subfix
}
return data
- 项目入口文件中, 为了方便在blueprint中调用jsonrpc对象,把jsonrpc作为子对象挂在app实例对象,绑定了视图函数和api访问名称的api_rpc函数,为了方便其他地方调用
application/__init__.py
import os
from flask import Flask
from flask_script import Manager
from flask_sqlalchemy import SQLAlchemy
from flask_redis import FlaskRedis
from flask_session import Session
from flask_jsonrpc import JSONRPC
from application.utils.config import init_config
from application.utils.logger import Log
from application.utils.commands import load_commands
from application.utils.bluerprint import register_blueprint, path, include, api_rpc
# 终端脚本工具初始化
manager = Manager()
# SQLAlchemy初始化
db = SQLAlchemy()
# redis数据库初始化
# - 1.默认缓存数据库对象,配置前缀为REDIS
redis_cache = FlaskRedis(config_prefix='REDIS')
# - 2.验证相关数据库对象,配置前缀为CHECK
redis_check = FlaskRedis(config_prefix='CHECK')
# - 3.验证相关数据库对象,配置前缀为SESSION
redis_session = FlaskRedis(config_prefix='SESSION')
# session储存配置初始化
session_store = Session()
# 自定义日志初始化
logger = Log()
# 初始化jsonrpc模块
jsonrpc = JSONRPC(service_url='/api')
# 全局初始化
def init_app(config_path):
"""全局初始化 - 需要传入加载开发或生产环境配置路径"""
# 创建app应用对象
app = Flask(__name__)
# 当前项目根目录
app.BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 开发或生产环境加载配置
init_config(app, config_path)
# SQLAlchemy加载配置
db.init_app(app)
# redis加载配置
redis_cache.init_app(app)
redis_check.init_app(app)
redis_session.init_app(app)
"""一定先加载默认配置,再传入APP加载session对象"""
# session保存数据到redis时启用的链接对象
app.config["SESSION_REDIS"] = redis_session
# session存储对象加载配置
session_store.init_app(app)
# 为日志对象加载配置
log = logger.init_app(app)
app.log = log
# json-rpc加载配置
jsonrpc.init_app(app)
# rpc访问路径入口(只有唯一一个访问路径入口),默认/api
jsonrpc.service_url = app.config.get('JSON_SERVER_URL', '/api')
jsonrpc.enable_web_browsable_api = app.config.get("ENABLE_WEB_BROWSABLE_API",False)
app.jsonrpc = jsonrpc
# 自动注册蓝图
register_blueprint(app)
# 注册模型,创建表
with app.app_context():
db.create_all()
# 终端脚本工具加载配置
manager.app = app
# 自动注册自定义命令
load_commands(manager)
return manager
开发环境配置文件配置rpc接口参数, application/settings/dev.py
"""开发环境配置"""
# 调试模式
DEBUG = True
"""SQLAlchemy数据库配置"""
# 数据库链接
SQLALCHEMY_DATABASE_URI = 'mysql://mofanguser:mofang@127.0.0.1:3306/mofang?charset=utf8mb4'
# 查询时会显示原始SQL语句
SQLALCHEMY_ECHO = False
"""redis数据库配置"""
# 默认缓存数据库 - 0号库
REDIS_URL = 'redis://:@127.0.0.1:6379/0'
# 验证相关数据 - 1号库
CHECK_URL = 'redis://:@127.0.0.1:6379/1'
# session储存数据库 - 2号库
SESSION_URL = 'redis://:@127.0.0.1:6379/2'
"""session数据储存到Redis的配置"""
# 配置session链接方式
SESSION_TYPE = 'redis'
# 如果设置session的生命周期是否是会话期, 为True,则关闭浏览器session就失效
SESSION_PERMANENT = False
# 设置session_id在浏览器中的cookie有效期
PERMANENT_SESSION_LIFETIME = 24 * 60 * 60 # session 的有效期,单位是秒
# 是否对发送到浏览器上session的cookie值进行加密
SESSION_USE_SIGNER = True
# 保存到redis的session数的名称前缀
SESSION_KEY_PREFIX = "session:"
"""日志基本信息配置"""
LOG_LEVEL = 'INFO' # 日志输出到文件中的等级
LOG_DIR = '/logs/mofang.log' # 日志存储路径
LOG_BACKPU_COUNT = 20 # 日志文件的最大备份数量
LOG_NAME = 'mofang' # 日志器的名字,flask的名为:flask.app
"""待注册的蓝图应用路径列表"""
INSTALL_BLUEPRINT = [
'application.apps.home',
]
"""JSONRPC配置"""
# api访问路径的入口
JSON_SERVER_URL = "/api"
# 是否允许通过浏览器访问api接口列表
ENABLE_WEB_BROWSABLE_API = True
# 是否自动补充当前蓝图名称作为api接口访问前缀,如访问名称 index,则变成 "Home.index",Home就是当前视图所在蓝图(首字母自动发泄)
JSON_PREFIX_NAME = True
- json-rpc视图与蓝图视图分开书写, 在
apps/home/api.py
写视图
# 函数视图
def index(id):
data = {
"code":200,
"msg":"ok",
"data":"hello world!!",
"id":f"{id}"
}
return data
# 类视图
class User():
def login(self,num):
data = {
"errno": 200,
"errmsg": "ok",
"data": "hello login!%s" % num
}
return data
def register(self):
data = {
"data":"hello register"
}
return data
- 在
apps/home/urls.py
写, rpc方法与视图映射关系
from application import path, api_rpc
# 引入当前蓝图应用视图
from . import views
# 引入rpc视图
from . import api
# 蓝图路径与函数映射列表
urlpatterns = [
path('/index', views.index, methods=['post']),
path('/demo', views.demo)
]
# rpcrpc方法与类映射 - 需要提前实例化类函数
user = api.User()
# rpc方法与函数映射列表[rpc接口列表]
apipatterns = [
api_rpc('index', api.index), # 函数映射
api_rpc('User.login', user.login), # 类映射
api_rpc('User.register', user.register),
]
- 使用postman进行测试
# 1.视图函数测试
POST /api 发送json数据
{
"jsonrpc": "2.0",
"method": "Home.index",
"params": {
"num":"123"
},
"id": "1"
}
# 2.视图类测试
POST /api 发送json数据
{
"jsonrpc": "2.0",
"method": "Home.User.login",
"params": {
"num":"123"
},
"id": "1"
}
移动端访问测试接口
因为当前我们的服务端项目安装在虚拟机里面,并且我们设置了虚拟机的网络连接模式为NAT,所以一般情况下,我们无法直接通过手机访问虚拟机。因此,我们需要配置虚拟机端口转发
1.打开VM的“编辑“菜单,选中虚拟网络编辑器
2.打开编辑器窗口, 选择NAT模式,使用管理员权限,并点击NAT“NAT设置”
3.端口转发下方点击“添加”。
4.在映射传入端口中,填写转发的端口(主机端口)和实际虚拟机的IP端口,填写完成以后,全部点击“确定”
真机测试:
config.xml
中指定显示页面
<content src="html/index.html" />
编辑客户端的显示页面html/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="format-detection" content="telephone=no, email=no, date=no, address=no">
<title>魔方APP</title>
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div id='app'>
<h1 @click='send_request'>{{message}}</h1>
</div>
</body>
<script>
apiready = function(){
axios.defaults.baseURL = 'http://192.168.19.58:5000/api'; // 服务端api接口网关地址
axios.defaults.timeout = 2500; // 请求超时时间
axios.defaults.withCredentials = false;// 跨域请求资源的情况下,忽略cookie的发送
Vue.prototype.axios = axios;
new Vue({
el:'#app',
data(){
return {
message : '魔方app',
}
},
methods:{
// 点击事件,发送post请求到服务端
send_request(){
//192.168.19.58 ip是APICloud中查看WIFI 真机同步 IP 和端口中ip地址
this.axios.post('http://192.168.19.58:5000/api',{
"jsonrpc":"2.0",
"id":1,
"method":"Home.index",
"params":{"num":"41"}
}).then((res)=>{//成功返回数据
api.alert({"msg":res.data});
}).catch((error)=>{
api.alert({"msg":error})
})
}
},
})
}
</script>
</html>
接口文档编写
这里我们使用showdoc/易文档 接口文档编写工具.
官网: https://www.showdoc.com.cn/
下载: https://www.showdoc.com.cn/clients
安装完成以后,第一次使用需要配置一个服务端地址, 这个服务端地址,可以是本地开发的真实服务端地址,也可以官方提供的测试地址.
那么接下来,我们只需要创建一个魔方项目的接口即可.后面有了接口以后就可以编写接口文档了.
客户端展示页面
音乐和图片在素材中,复制到客户端静态文件static
中
首页显示
html/index.html
,代码:
<!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(){
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>
static/css/main.css
,代码:
*{user-select: none;}
body,form,input,table,ul,li{margin: 0;padding: 0;}
body,th,td,.p1,.p2{font-family:arial}
p,form,ol,ul,li,dl,dt,dd,h3{margin:0;padding:0;list-style:none;}
table,img{border:0;}
img{max-height: 100%;max- 100%;100%}
td{font-size:9pt;line-height:18px;}
em{font-style:normal;}
em{font-style:normal;color:#c00;}
a em{text-decoration:underline;}
input,button{outline: 0;}
@keyframes scaleDraw { /*定义关键帧、scaleDrew是需要绑定到选择器的关键帧名称*/
0%{ transform: scale(1); }
25%{ transform: scale(1.5); }
50%{ transform: scale(1); }
75%{ transform: scale(1.5); }
}
@keyframes rotation{
from {-webkit-transform: rotate(0deg);}
to {-webkit-transform: rotate(360deg);}
}
html{
font-size: 12px;
}
.app{
margin: 0 auto;
max- 1125px;
100%;
overflow: hidden;
max-height: 100%;
height: 100rem;
position: relative;
}
.app .music{
position: absolute;
5rem;
height: 5rem;
top: 3rem;
right: 3rem;
z-index: 100;
border-radius: 50%;
}
.app .music2{
animation: rotation 6s ease-in infinite;
}
.app .bg{
margin: 0 auto;
100%;
max- 100rem;
position: relative;
z-index: -1;
}
.app .bg img{
100rem;
animation: scaleDraw 120s ease-in infinite;
}
.app .module1{
position: absolute;
top: 24.17rem;
left: 0;
16.39rem;
height: 29.72rem;
}
.app .module2{
position: absolute;
top: 13.5rem;
right: 0;
16rem;
height: 20.39rem;
}
.app .module3{
position: absolute;
11.94rem;
height: 18.06rem;
top: 40.56rem;
right: 2.67rem;
}
.app .module4{
position: absolute;
top: 6.94rem;
left: 5.17rem;
7.83rem;
height: 10.72rem;
}
.form{
margin: 0 auto;
position: absolute;
top: 16rem;
26.94rem;
left: 0;
right: 0;
}
.form-title{
margin: 0 auto;
16.86rem;
height: 5.33rem;
position: relative;
z-index: 0;
}
.form-title .back{
position: absolute;
right: -3rem;
top: 0.67rem;
3.83rem;
height: 3.89rem;
}
.form-data{
position: relative;
padding-top: 3.78rem;
z-index: 0;
}
.form-data-bg{
position: absolute;
top: -2.39rem;
26.94rem;
left: 0;
right: 0;
margin: auto;
z-index: -1;
}
.form-item{
23.1rem;
margin: 0 auto 1.5rem;
}
.form-item label.text{
6.67rem;
letter-spacing: 0.13rem;
text-align: right;
display: inline-block;
font-size: 1.33rem;
margin-bottom: 0.33rem;
font-weight: bold;
}
.form-item input[type=text],
.form-item input[type=password]
{
background-color: transparent;
border-radius: 3px;
14.44rem;
border: 0.08rem #666 solid;
height: 2.33rem;
font-size: 1rem;
text-indent: 1rem;
vertical-align: bottom;
}
.form-item input.code{
10.67rem;
}
.form-item .refresh{
2.56rem;
height: 2.44rem;
margin-left: 0.67rem;
vertical-align: middle;
}
.form-item .refresh:active{
margin-top: .5px;
margin-bottom: -.5px;
}
.form-item input.agree{
margin-left: 2.4rem;
margin-bottom: 0.33rem;
1rem;
height: 1rem;
vertical-align: sub;
}
.form-item .agree_text{
font-size: 1rem;
}
.form-item input.remember{
margin-left: 5.83rem;
vertical-align: sub;
}
.form-item .commit{
11.5rem;
height: 3.94rem;
margin: 0 auto;
display: block;
}
.form-item .commit:active{
margin-top: .5px;
margin-bottom: -.5px;
}
.form-item .toreg{
margin-left: 3.11rem;
background: url("../images/btn1.png") no-repeat 0 0;
background-size: 100% 100%;
}
.form-item .tofind{
background: url("../images/btn2.png") no-repeat 0 0;
background-size: 100% 100%;
}
.form-item .toreg,
.form-item .tofind
{
display: inline-block;
7.94rem;
height: 2.5rem;
margin-right: 1.67rem;
font-size: 1rem;
color: #fff;
line-height: 2.5rem;
text-align: center;
user-select: none;
}
static/js/main.js
,代码:
class Game{
constructor(bg_music){
// 构造函数,相当于python中类的__init__方法
this.init();
if(bg_music){
this.play_music(bg_music);
}
}
init(){
// 初始化
console.log("系统初始化");
this.rem(); // 自适配方案,根据当前受屏幕,自动调整rem单位
}
print(data){
// 打印数据
console.log(JSON.stringify(data));
}
rem(){
if(window.innerWidth<1200){
this.UIWidth = document.documentElement.clientWidth;
this.UIHeight = document.documentElement.clientHeight;
document.documentElement.style.fontSize = (0.01*this.UIWidth*3)+"px";
document.querySelector("#app").style.height=this.UIHeight+"px"
}
window.onresize = ()=>{
if(window.innerWidth<1200){
this.UIWidth = document.documentElement.clientWidth;
this.UIHeight = document.documentElement.clientHeight;
document.documentElement.style.fontSize = (0.01*this.UIWidth*3)+"px";
}
}
}
stop_music(){
this.print("停止背景音乐");
document.body.removeChild(this.audio);
}
play_music(src){
this.print("播放背景音乐");
this.audio = document.createElement("audio");
this.source = document.createElement("source");
this.source.type = "audio/mp3";
this.audio.autoplay = "autoplay";
this.source.src=src;
this.audio.appendChild(this.source);
document.body.appendChild(this.audio);
// 自动暂停关闭背景音乐
var t = setInterval(()=>{
if(this.audio.readyState > 0){
if(this.audio.ended){
clearInterval(t);
document.body.removeChild(this.audio);
}
}
},100);
}
}
登陆页显示
html/login.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/axios.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(){
new Vue({
el:"#app",
data(){
return {
music_play:true,
}
},
watch:{
music_play(){
if(this.music_play){
this.game.play_music("../static/mp3/bg1.mp3");
}else{
this.game.stop_music();
}
}
},
methods:{
}
})
}
</script>
</body>
</html>
注册页显示
html/register.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/axios.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(){
new Vue({
el:"#app",
data(){
return {
music_play:true,
}
},
watch:{
music_play(){
if(this.music_play){
this.game.play_music("../static/mp3/bg1.mp3");
}else{
this.game.stop_music();
}
}
},
methods:{
}
})
}
</script>
</body>
</html>
在APP客户端进行窗口和页面操作
在APICloud中提供了2种类型,三种方式给开发者打开或者新建页面.
1. window 窗口
window是APICloud提供的最顶级的页面单位.一个APP至少会存在一个以上的window窗口,在用户打开APP应用,应用在初始化的时候默认就会创建了一个name=root 的顶级window窗口显示当前APP配置的首页.
查看窗口列表
apiready = function(){
api.alert({"msg":api.windows()});
}
api.windows(); // [{"name":"root"}] // 表示root根窗口,系统自动创建的
新建window窗口
注意: 如果之前已经存在同名的窗口,则APP不会再次创建新窗口,而是把对应名称的窗口显示到最顶层给用户看到
apiready = function(){
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只是举例, 将来可以传递更多自定义数据的.
}
});
}
关闭窗口
如果当前APP中只有剩下一个顶级窗口root,则无法通过当前方法关闭! 也有部分手机直接退出APP了
//关闭当前文件的window窗口,使用默认动画
api.closeWin();
//关闭指定window窗口,若待关闭的window不在最上面,则无动画
api.closeWin({
name: 'page1'
});
接下来我们可以把创建/关闭窗口的代码封装到入口程序脚本static/js/main.js
中
class Game{
constructor(bg_music){
// 构造函数,相当于python中类的__init__方法
this.init();
if(bg_music){
this.play_music(bg_music);
}
}
init(){
// 初始化
console.log("系统初始化");
this.rem(); // 自适配方案,根据当前受屏幕,自动调整rem单位
}
print(data){
// 打印数据
console.log(JSON.stringify(data));
}
rem(){
if(window.innerWidth<1200){
this.UIWidth = document.documentElement.clientWidth;
this.UIHeight = document.documentElement.clientHeight;
document.documentElement.style.fontSize = (0.01*this.UIWidth*3)+"px";
document.querySelector("#app").style.height=this.UIHeight+"px"
}
window.onresize = ()=>{
if(window.innerWidth<1200){
this.UIWidth = document.documentElement.clientWidth;
this.UIHeight = document.documentElement.clientHeight;
document.documentElement.style.fontSize = (0.01*this.UIWidth*3)+"px";
}
}
}
stop_music(){
this.print("停止背景音乐");
document.body.removeChild(this.audio);
}
play_music(src){
this.print("播放背景音乐");
this.audio = document.createElement("audio");
this.source = document.createElement("source");
this.source.type = "audio/mp3";
this.audio.autoplay = "autoplay";
this.source.src=src;
this.audio.appendChild(this.source);
document.body.appendChild(this.audio);
// 自动暂停关闭背景音乐
var t = setInterval(()=>{
if(this.audio.readyState > 0){
if(this.audio.ended){
clearInterval(t);
document.body.removeChild(this.audio);
}
}
},100);
}
//创建窗口
openWin(name,url,pageParam){
if(!pageParam){
pageParam = {}
}
api.openWin({
name: name, // 自定义窗口名称,如果是新建窗口,则名字必须是第一次出现的名字。
bounces: false, // 窗口是否上下拉动
reload: true, // 如果窗口已经在之前被打开了,是否要重新加载当前窗口中的页面
url: url, // 窗口创建时展示的html页面的本地路径[相对于当前代码所在文件的路径]
animation:{ // 打开新建窗口时的过渡动画效果
type:"push", //动画类型(详见动画类型常量)
subType:"from_right", //动画子类型(详见动画子类型常量)
duration:300 //动画过渡时间,默认300毫秒
},
pageParam: pageParam,
});
}
// 关闭指定窗口
closeWin(name=''){
let params
if(name !== ''){
params = {
name:name,
}
}
api.closeWin(params);
}
}
2.frame 帧页面
如果APP中所有的页面全部窗口进行展开,则APP需要耗费大量的内存来维护这个窗口列表,从而导致, 用户操作APP时,APP响应缓慢甚至卡顿的现象.所以APP中除了通过新建窗口的方式展开页面以外, 还提供了帧的方式来展开页面.
帧 : 代表的就是一个窗口下开打的某个页面记录.所谓的帧就有点类似于浏览器中窗口通过地址栏新建的一个页面一样.
使用的时候注意:
- APP每一个window窗口都可以打开1到多个帧.新建窗口的时候,系统会默认顺便创建第一帧出来
- 每一帧代表的都是一个html页面
- 默认情况下, APP的window的窗口会自动默认满屏展示.而帧可以设置矩形的宽高.如果顶层的帧页面没有满屏显示,则用户可以看到当前这一帧下的其他帧的内容.
新建帧页面
apiready = function(){
// 延时两秒操作
setTimeout(()=>{
api.openFrame({
name: 'login', // 帧页面的名称
url: './login.html', // 帧页面打开的url地址
bounces:false, // 页面是否可以下拉拖动
reload: true, // 帧页面如果已经存在,是否重新刷新加载
useWKWebView:true, // 是否使用WKWebView来加载页面
historyGestureEnabled:true, // 是否可以通过手势来进行历史记录前进后退,只在useWKWebView参数为true时有效
vScrollBarEnabled: false, // 是否显示垂直滚动条
hScrollBarEnabled: false, // 是否显示水平滚动条
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只是举例, 可以传递任意自定义参数
}
});
},2000);
}
关闭帧页面
// 关闭当前文件所在frame页面
api.closeFrame();
// 关闭指定名称的frame页面
api.closeFrame({
name: 'login'
});
在主程序脚本static/js/main.js
中,添加操作帧页面的方法
class Game{
constructor(bg_music){
// 构造函数,相当于python中类的__init__方法
this.init();
if(bg_music){
this.play_music(bg_music);
}
}
init(){
// 初始化
console.log("系统初始化");
this.rem(); // 自适配方案,根据当前受屏幕,自动调整rem单位
}
print(data){
// 打印数据
console.log(JSON.stringify(data));
}
rem(){
if(window.innerWidth<1200){
this.UIWidth = document.documentElement.clientWidth;
this.UIHeight = document.documentElement.clientHeight;
document.documentElement.style.fontSize = (0.01*this.UIWidth*3)+"px";
document.querySelector("#app").style.height=this.UIHeight+"px"
}
window.onresize = ()=>{
if(window.innerWidth<1200){
this.UIWidth = document.documentElement.clientWidth;
this.UIHeight = document.documentElement.clientHeight;
document.documentElement.style.fontSize = (0.01*this.UIWidth*3)+"px";
}
}
}
stop_music(){
this.print("停止背景音乐");
document.body.removeChild(this.audio);
}
play_music(src){
this.print("播放背景音乐");
this.audio = document.createElement("audio");
this.source = document.createElement("source");
this.source.type = "audio/mp3";
this.audio.autoplay = "autoplay";
this.source.src=src;
this.audio.appendChild(this.source);
document.body.appendChild(this.audio);
// 自动暂停关闭背景音乐
var t = setInterval(()=>{
if(this.audio.readyState > 0){
if(this.audio.ended){
clearInterval(t);
document.body.removeChild(this.audio);
}
}
},100);
}
//创建窗口
openWin(name,url,pageParam){
if(!pageParam){
pageParam = {}
}
api.openWin({
name: name, // 自定义窗口名称,如果是新建窗口,则名字必须是第一次出现的名字。
bounces: false, // 窗口是否上下拉动
reload: true, // 如果窗口已经在之前被打开了,是否要重新加载当前窗口中的页面
url: url, // 窗口创建时展示的html页面的本地路径[相对于当前代码所在文件的路径]
animation:{ // 打开新建窗口时的过渡动画效果
type:"push", //动画类型(详见动画类型常量)
subType:"from_right", //动画子类型(详见动画子类型常量)
duration:300 //动画过渡时间,默认300毫秒
},
pageParam: pageParam,
});
}
// 关闭指定窗口
closeWin(name=''){
let params
if(name !== ''){
params = {
name:name,
}
}
api.closeWin(params);
}
// 创建帧页面
openFrame(name,url,pageParam){
if(!pageParam){
pageParam = {}
}
api.openFrame({
name: name, // 帧页面的名称
url: url, // 帧页面打开的url地址
bounces:false, // 页面是否可以下拉拖动
reload: true, // 帧页面如果已经存在,是否重新刷新加载
useWKWebView:true, // 是否使用WKWebView来加载页面
historyGestureEnabled:true, // 是否可以通过手势来进行历史记录前进后退,只在useWKWebView参数为true时有效
vScrollBarEnabled: false, // 是否显示垂直滚动条
hScrollBarEnabled: false, // 是否显示水平滚动条
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: pageParam // name只是举例, 可以传递任意自定义参数
}
});
}
// 关闭帧页面
closeFrame(name=''){
let params
if(name !== ''){
params = {
name: name
}
}
api.closeFrame(params);
}
}
不同页面之间的跳转
在config.xml
中, 设置默认打开页面
<content src="html/index.html" />
我们在页面的左上角签到图标中,绑定点击事件,让APP跳转到登录页面
html/index.html
<!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" @click='to_login'></li>
</ul>
</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:"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:{
// 点击签到跳转登陆页面
to_login(){
this.game.openWin('login','login.html')
}
}
})
}
</script>
</body>
</html>
在登陆页面,点击立即注册, 跳转到注册页面
html/login.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/axios.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" @click='to_register'>立即注册</p>
<p class="tofind">忘记密码</p>
</div>
</div>
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg2.mp3");
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
music_play:true,
}
},
watch:{
music_play(){
if(this.music_play){
this.game.play_music("../static/mp3/bg2.mp3");
}else{
this.game.stop_music();
}
}
},
methods:{
// 跳转注册页面
to_register(){
this.game.openFrame('register','register.html')
}
}
})
}
</script>
</body>
</html>
登陆页面返回上一页 : 本质就是关闭登陆页面 html/login.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/axios.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" @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="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" @click='to_register'>立即注册</p>
<p class="tofind">忘记密码</p>
</div>
</div>
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg2.mp3");
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
music_play:true,
}
},
watch:{
music_play(){
if(this.music_play){
this.game.play_music("../static/mp3/bg2.mp3");
}else{
this.game.stop_music();
}
}
},
methods:{
// 跳转注册页面
to_register(){
this.game.openFrame('register','register.html')
},
// 返回上一页,本质是关闭当前页面
backpage(){
this.game.closeWin()
}
}
})
}
</script>
</body>
</html>
注册页面返回上一页 ,本质就是关闭注册页面 html/register.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/axios.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/bg3.mp3");
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
music_play:true,
}
},
watch:{
music_play(){
if(this.music_play){
this.game.play_music("../static/mp3/bg3.mp3");
}else{
this.game.stop_music();
}
}
},
methods:{
// 返回上一页,本质是关闭当前页面
backpage(){
this.game.closeFrame()
}
}
})
}
</script>
</body>
</html>