[TOC]


概述
------
QPython 支持 WEB APP,这能让所有熟悉 WEB 开发模式的工程师快速地进入移动开发。


目前已有不少成熟的 WEB APP 开发方案,比如 Cordova,Phonegap 等。 

相比于它们, QPython 的 WEB APP 方案有以下优势:

- 强大的本地逻辑处理解析器 Python 脚本解析器
QPython 有一个强大的本地逻辑处理解析器,不需要像它们一样需要将大量的工作放置在服务端才能完成,QPython 能有效地解决网络传输带来的延迟影响。

- 海量的库支持,更快实现
Python 世界中积累了多年的大部分的类库都能够被快速引入到 QPython 体系中,因此您可以不必重复造轮子。

- 掌上即可完成开发
进行 QPython 开发,您只需要有一台安卓手机即可。当然您也可以在 PC 端完成开发,再将程序上传到 QPython 上,您也可以直接在 QPython 中进行开发。


原理
------
通过 QPython,你可以在手机上启动自定义的的 Python 服务,包括 WEB 服务。在脚本中按照 [QPython 的 WEB APP 的规则](/zh/doc/program_guide/#_2)加入 WEB APP 所需要的定义信息,即可让 QPython 在运行时载入 WebView 控件,访问对应 Web 服务。 


接下来,我们开始以 Bottle 开发框架为例,带你进入 QPython 第一个 WEB APP 工程开发。 


项目:我在哪里
------
这是一个需要你一步步动手的教学项目,它主要是使用了 QPython 开发的 WEB APP。它能在地图上显示我的位置坐标,并分享出去。

在项目的过程中,你可以学习使用 WEB APP 建立起来的 WEB APP 的框架,以及如何使用 SL4A 接口获取地理位置信息的知识;最后能学习调用安卓体系内其他 APP 的公开接口的方法。

### 开始动手:1 新建项目 & WebApp 初探
你可以通过以下步骤,创建一个 WebApp 项目

- 1 打开编辑器,点击右上角的新建
- 2 多个项目类型的对话框就会弹出,选择 WebApp 
- 3 弹出对话框,你需要为项目取个名字
- 4 正确输入项目名称后, QPython 会自动按照项目名称在项目目录中创建目录,并且按照 WebApp 的模版创建一个 main.py 文件。

完成上述步骤后,你就可以开始编辑 main.py 了。下面介绍下默认创建好的 main.py。

    #-*-coding:utf8;-*-
    #qpy:2
    #qpy:webapp:Sample 
    #qpy://localhost:8080/
    """
    This is a sample for qpython webapp
    """
    from bottle import route, run
    
    @route('/')
    def hello():
        return "Hello World!"
    
    run(host='localhost', port=8080)


下面是一步步解释:

从 Bottle 中引入 route 和 run 方法:

    from bottle import route, run

引入了 route 方法之后,代码中通过 @route("/") 即可将 hello 方法注册为用户在访问 "http://(主机地址):(端口)/" 时默认响应的处理函数。而 hello 的动作:返回 "Hello World!"。


    @route('/')
    def hello():
        return "Hello World!"

完成上述的定义后,Bottle 框架就完成了对 WEB 服务器的行为定义,然后通过 run 指令就启动了 WEB 服务。

    run(host='localhost', port=8080)

WEB 服务启动的同时,QPython 会根据 main.py 文件头部的声明去确定 App 启动的模式,根据 [WebApp 头部定义](/zh/doc/program_guide/#_2)的协议,QPython 会启动 WebView 控件, 标题为: Sample,同时载入 //localhost:8080/。 因此,用户就看到了 "Hello World!" 。

    #qpy:webapp:Sample 
    #qpy://localhost:8080/

### 开始动手:2 深入 WebApp

通过新建项目而创建的 WebApp 仅给出了 QPython 中 WebApp 的最小框架体系,但是在实际中,需要考虑下列两个关键的功能点才算一个完整的 QPython WebApp。

#### 健康监测
QPython 在启动 WebApp 时,会访问 /__ping 路径,根据 HTTP 返回值来检测服务是否准备好,会在服务准备好时载入对应的 URL。因此,你可以在__ping 来中自定义健康检查。在这里,我们仅仅返回 "ok",因为当服务 OK 时,其 HTTP 返回值正是 200。



#### 包含自我结束功能的完整服务

在通过点 QPython 的退出键来关闭 QPython WebApp 时,最后都会通过调用 /__exit URL,如果你想要让 QPython WebApp 在退出时也关闭,则需要在该 URL 部分定义好退出函数。

由于默认的 bottle 在处理退出时比较难出来,所以我们引入了自定义的 MyWSGIRefServer,这能很好实现自我关闭。

看最后的启动 MyWSGIRefServer 的指令。

        app = Bottle()
        app.route('/', method='GET')(index)
        app.route('/hello', method='GET')(hello)
        app.route('/__exit', method=['GET','HEAD'])(__exit)
        app.route('/__ping', method=['GET','HEAD'])(__ping)

        try:
            server = MyWSGIRefServer(host="127.0.0.1", port="8080")
            app.run(server=server,reloader=False)
        except Exception,ex:
            print "Exception: %s" % repr(ex)

以下为 MyWSGIRefServer 定义以及 __exit 方法定义:

    class MyWSGIRefServer(ServerAdapter):
        server = None
        def run(self, handler):
            from wsgiref.simple_server import make_server, WSGIRequestHandler
            if self.quiet:
                class QuietHandler(WSGIRequestHandler):
                    def log_request(*args, **kw): pass
                self.options['handler_class'] = QuietHandler
            self.server = make_server(self.host, self.port, handler, **self.options)
            self.server.serve_forever()

        def stop(self):
            #sys.stderr.close()
            import threading
            threading.Thread(target=self.server.shutdown).start()
            #self.server.shutdown()
            self.server.server_close() #<--- alternative but causes bad fd exception
            print "# qpyhttpd stop"

    def __exit():
        Droid.stopLocating()
        global server
        server.stop()

### 开始动手:3 调用 SL4A 获取地理位置
完成上述步骤后,想必你也对 QPython 的 WEB APP 有了一个基本的了解,接下来我们将要实现如何使用 SL4A 接口来获得地理位置,并用图形展示出来。

根据 SL4A 的文档,开始查询地理位置的接口如下:

    import androidhelper
    Droid = androidhelper.Android()
    Droid.startLocating()

而想要获得地理位置的接口如下:

    location = Droid.getLastKnownLocation().result
    location = location.get('network', location.get('gps'))

将此代码片段在后面增加 print location 后运行,发现得到 None,因此首先检查地理位置开关是否打开?WIFI 是否打卡?确定打开之后再运行尝试。用 Python 的接口是不是很简单 ?

获取地理位置信息没问题后,我们再来看下百度的文档,[掌握在其 WEB 版本的地图上根据已知的坐标显示位置的接口](http://developer.baidu.com/map/index.php?title=uri/api/web):

因此确定其接口类似:
 
    http://api.map.baidu.com/marker?location=39.916979519873,116.41004950566&title=我的位置&content=我在这里&output=html


### 开始动手:4 完成运行

在了解了 QPython WebApp 的框架、SL4A 获得地理位置的方法、百度地图 API 的使用之后,结合你以掌握的 Web 开发知识,你就可以轻松地实现对应的目标了。最终代码如下:

    #-*-coding:utf8;-*-
    #qpy:2
    #qpy:webapp:Sample
    #qpy://localhost:8080/
    """
    This is a sample for qpython webapp
    """
    import os.path
    from bottle import Bottle, ServerAdapter
    from bottle import template,request,response,redirect,HTTPResponse
    root = os.path.dirname(os.path.abspath(__file__))

    import androidhelper
    Droid = androidhelper.Android()
    Droid.startLocating(5000,5)

    class MyWSGIRefServer(ServerAdapter):
        server = None
        def run(self, handler):
            from wsgiref.simple_server import make_server, WSGIRequestHandler
            if self.quiet:
                class QuietHandler(WSGIRequestHandler):
                    def log_request(*args, **kw): pass
                self.options['handler_class'] = QuietHandler
            self.server = make_server(self.host, self.port, handler, **self.options)
            self.server.serve_forever()

        def stop(self):
            #sys.stderr.close()
            import threading
            threading.Thread(target=self.server.shutdown).start()
            #self.server.shutdown()
            self.server.server_close() #<--- alternative but causes bad fd exception
            print "# qpyhttpd stop"

    def __exit():
        Droid.stopLocating()
        global server
        server.stop()

    def __ping():
        return "ok"


    def index():
        Droid.vibrate()
        return """<html><body><button onclick='location.href="/hello"'>显示我在哪里</button></body></html>"""

    def hello():
        location = Droid.getLastKnownLocation().result
        location = location.get('network', location.get('gps'))
        #location = {"latitude":"116.387884","longitude":"39.929986"}
        return template(root+'/baidu.tpl',lat=location['latitude'],lng=location['longitude'])

    if __name__ == '__main__':
        app = Bottle()
        app.route('/', method='GET')(index)
        app.route('/hello', method='GET')(hello)
        app.route('/__exit', method=['GET','HEAD'])(__exit)
        app.route('/__ping', method=['GET','HEAD'])(__ping)

        try:
            server = MyWSGIRefServer(host="127.0.0.1", port="8080")
            app.run(server=server,reloader=False)
        except Exception,ex:
            print "Exception: %s" % repr(ex)

其中 hello 页面所使用的 baidu.tpl 模板,由[百度地图]( http://api.map.baidu.com/lbsapi/creatmap/index.html)生成,注意需要替换 var markerArr = [{title:"我在这里",content:"我的位置",point:"xxx|yyy",isOpen:1,icon:{w:21,h:21,l:0,t:0,x:6,lb:5}} 中的 xxx 和 yyy 为 {{lng}} 及 {{lat}} 即可

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
    <meta name="keywords" content="百度地图,百度地图API,百度地图自定义工具,百度地图所见即所得工具" />
    <meta name="description" content="百度地图API自定义地图,帮助用户在可视化操作下生成百度地图" />
    <title>百度地图API自定义地图</title>
    <!--引用百度地图API-->
    <style type="text/css">
        html,body{margin:0;padding:0;}
        .iw_poi_title {color:#CC5522;font-size:14px;font-weight:bold;overflow:hidden;padding-right:13px;white-space:nowrap}
        .iw_poi_content {font:12px arial,sans-serif;overflow:visible;padding-top:4px;white-space:-moz-pre-wrap;word-wrap:break-word}
    </style>
    <script type="text/javascript" src="http://api.map.baidu.com/api?key=&v=1.1&services=true"></script>
    </head>

    <body>
      <!--百度地图容器-->
      <div style="width:480px;height:550px;border:#ccc solid 1px;" id="dituContent"></div>
    </body>
    <script type="text/javascript">
        //创建和初始化地图函数:
        function initMap(){
            createMap();//创建地图
            setMapEvent();//设置地图事件
            addMapControl();//向地图添加控件
            addMarker();//向地图中添加marker
        }

        //创建地图函数:
        function createMap(){
            var map = new BMap.Map("dituContent");//在百度地图容器中创建一个地图
            var point = new BMap.Point(116.387884,39.929986);//定义一个中心点坐标
            map.centerAndZoom(point,12);//设定地图的中心点和坐标并将地图显示在地图容器中
            window.map = map;//将map变量存储在全局
        }

        //地图事件设置函数:
        function setMapEvent(){
            map.enableDragging();//启用地图拖拽事件,默认启用(可不写)
            map.enableScrollWheelZoom();//启用地图滚轮放大缩小
            map.enableDoubleClickZoom();//启用鼠标双击放大,默认启用(可不写)
            map.enableKeyboard();//启用键盘上下左右键移动地图
        }

        //地图控件添加函数:
        function addMapControl(){
            //向地图中添加缩放控件
        var ctrl_nav = new BMap.NavigationControl({anchor:BMAP_ANCHOR_TOP_LEFT,type:BMAP_NAVIGATION_CONTROL_LARGE});
        map.addControl(ctrl_nav);
            //向地图中添加缩略图控件
        var ctrl_ove = new BMap.OverviewMapControl({anchor:BMAP_ANCHOR_TOP_LEFT,isOpen:1});
        map.addControl(ctrl_ove);

            //向地图中添加比例尺控件
        var ctrl_sca = new BMap.ScaleControl({anchor:BMAP_ANCHOR_BOTTOM_LEFT});
        map.addControl(ctrl_sca);
        }

        //标注点数组
        var markerArr = [{title:"我在这里",content:"我的位置",point:"{{lng}}|{{lat}}",isOpen:1,icon:{w:21,h:21,l:0,t:0,x:6,lb:5}}
             ];
        //创建marker
        function addMarker(){
            for(var i=0;i<markerArr.length;i++){
                var json = markerArr[i];
                var p0 = json.point.split("|")[0];
                var p1 = json.point.split("|")[1];
                var point = new BMap.Point(p0,p1);
                var iconImg = createIcon(json.icon);
                var marker = new BMap.Marker(point,{icon:iconImg});
                var iw = createInfoWindow(i);
                var label = new BMap.Label(json.title,{"offset":new BMap.Size(json.icon.lb-json.icon.x+10,-20)});
                marker.setLabel(label);
                map.addOverlay(marker);
                label.setStyle({
                            borderColor:"#808080",
                            color:"#333",
                            cursor:"pointer"
                });

                (function(){
                    var index = i;
                    var _iw = createInfoWindow(i);
                    var _marker = marker;

                    _marker.addEventListener("click",function(){
                        this.openInfoWindow(_iw);
                    });
                    _iw.addEventListener("open",function(){
                        _marker.getLabel().hide();
                    })
                    _iw.addEventListener("close",function(){
                        _marker.getLabel().show();
                    })
                    label.addEventListener("click",function(){
                        _marker.openInfoWindow(_iw);
                    })
                    if(!!json.isOpen){
                        label.hide();
                        _marker.openInfoWindow(_iw);
                    }
                })()
            }
        }
        //创建InfoWindow
        function createInfoWindow(i){
            var json = markerArr[i];
            var iw = new BMap.InfoWindow("<b class='iw_poi_title' title='" + json.title + "'>" + json.title + "</b><div class='iw_poi_content'>"+json.content+"</div>");
            return iw;
        }
        //创建一个Icon
        function createIcon(json){
            var icon = new BMap.Icon("http://app.baidu.com/map/images/us_mk_icon.png", new BMap.Size(json.w,json.h),{imageOffset: new BMap.Size(-json.l,-json.t),infoWindowOffset:new BMap.Size(json.lb+5,1),offset:new BMap.Size(json.x,json.h)})
            return icon;
        }

        initMap();//创建和初始化地图
    </script>
    </html>


有了 QPython 之后, 是不是开发 Mobile App 就和 Web 开发一样简单?