La programación en móviles ha conseguido poner de moda las WebViews. Esta técnica consiste en abrir un navegador y cargar la aplicación como si fuera una página web, embebida dentro del resto de la aplicación.
La primera vez que vi esta técnica, sin embargo, no fue en móvil, sino el cliente de Steam, que se ejecutaba bajo Windows (ahora ya también está disponible para GNU/Linux). Desde entonces me picó la curiosidad sobre cómo se haría… y aquí lo tenemos.
Para los ejemplos usaré Python y GTK, aprovechando el post sobre micro-framework web anterior.
La idea
El diseño es sencillo: Necesitamos al menos dos hilos, uno para gestionar el servidor web y otro para la interfaz. Dado que la interfaz debe estar en el hilo principal por necesidades de GTK, es lógico pensar que lo fácil es lanzar el servidor web en segundo plano. Por eso aquí está el código principal, que es lo más importante de todo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
deffinish(ignorable=None):gtk.main_quit()defmain():signal.signal(signal.SIGINT,finish)gtk.gdk.threads_init()web=WebServer()web.run()uri='https://localhost:{}'.format(web.port)print'serving at',web.portprint'connecting to ',urirun_gui(uri)gtk.mainloop()if__name__=='__main__':main()
Como vemos, lo primero es inicializar gtk y permitir que un CTRL+C finalice la aplicación. Inmediatamente después, se arranca el servidor web que, como dije, se ejecutará en segundo plano. Ahora iremos con él. A continuación lanzamos la GUI indicándole el puerto donde está escuchando el servidor web.
A partir de este punto tendremos dos aplicaciones: El servidor web y la GUI, comunicándose por HTTP.
Los que hayáis leído el artículo sobre micro-framework web veréis aquí algunas diferencias:
El método get recibe como parámetros los parámetros indicados en la url. Así, la url "/greet?name=MagMax" recibirá como argumentos “name = ‘MagMax’”.
El objeto WebServer tiene un método run que ejecuta el servidor en background. Eso es algo que necesito para que el GUI esté en primer plano.
No creo que resulte difícil entender el código.
El HTML que se muestra es muy cutre… Perdonad, pero no quería ensuciar el código con más HTML y he pretendido que se parezca lo máximo posible al del artículo micro-framework web.
La GUI
Ahora vamos con la interfaz gráfica. Uno de los problemas aquí es ver qué motor se va a usar. Podemos elegir Gecko o bien WebKit. La mejor opción es soportar ambos:
try:importwebkitexcept:webkit=Falsetry:importgtkmozembedasgeckoexcept:gecko=Falseifnotwebkitandnotgecko:raiseException('Failed to load both webkit and gecko modules')classWebKitDriver(object):defcreate_browser(self):returnwebkit.WebView()defopen_uri(self,browser,uri):browser.open(uri)classGeckoDriver(object):defcreate_browser(self):returngecko.MozEmbed()defopen_uri(self,browser,uri):browser.load_url(uri)defrun_gui(uri,echo=True):window=gtk.Window()driver=WebKitDriver()ifwebkitelseGeckoDriver()browser=driver.create_browser()box=gtk.VBox(homogeneous=False,spacing=0)window.add(box)box.pack_start(browser,expand=True,fill=True,padding=0)window.connect('destroy',finish)window.set_default_size(800,600)window.show_all()driver.open_uri(browser,uri)
Lo primero es tratar de cargar uno de los dos motores. Aquí se usa un patrón típico en python que consiste en crear una variable nula con el nombre del módulo cuando éste no existe. Si no se pudo cargar ninguno, se lanza un error.
Tenemos dos clases con la misma interfaz, aunque no he creado la interfaz explícitamente, sino que he hecho uso del “duck typing”. Éstas permiten crear el objeto GTK con el browser y abrir una URL. Así que lo primero que se hace es crear el “driver”, que oculta la implementación usada. Y así basta con crear un objeto básico y meterle el browser.
Para terminar preparamos la finalización y lo mostramos. Y, finalmente, mostramos la página con nuestra aplicación.
Todo el código
Ale, aquí tenéis el código todo junto. Yo lo dividiría en, al menos, 2 archivos, pero lo he puesto junto para que os sea más sencillo probarlo si queréis. De todas maneras he puesto comentarios donde yo dividiría, separando también los imports.
#!/usr/bin/env python# file webserver.pyimportsysimportreimporthttplibimportshutilfromStringIOimportStringIOfromurlparseimporturlparse,parse_qsimportthreadfromBaseHTTPServerimportHTTPServer,BaseHTTPRequestHandlerclassHandler(object):def__init__(self,request):self.request=requestself._output=StringIO()self.write=self._output.writedeffinish(self):length=self._output.tell()self._output.seek(0)self.request.send_response(httplib.OK)encoding=sys.getfilesystemencoding()self.request.send_header("Content-type","text/html; charset=%s"%encoding)self.request.send_header("Content-Length",str(length))self.request.end_headers()shutil.copyfileobj(self._output,self.request.wfile)self._output.close()defget(self,**kwargs):passclassMainHandler(Handler):defget(self):self.write('<html>'' <body>'' <form action="/greet" method="GET">'' <input name="name" placeholder="Your name"/>'' <input type="submit" value="Greet"/>'' </form>'' </body>''</html>')classGreetingsHandler(Handler):defget(self,**kwargs):self.write('<h1>Cheers, {name[0]}!</h1><br/><br/><a href="/">Go back</a>'.format(**kwargs))classHTTPRequestHandler(BaseHTTPRequestHandler):HANDLERS=[(re.compile('/greet'),GreetingsHandler),(re.compile('/'),MainHandler),]defdo_GET(self):url=urlparse(self.path)forregex,clazzinself.HANDLERS:m=regex.match(url.path)ifm:params=parse_qs(url.query,True)handler=clazz(self)handler.get(**params)returnhandler.finish()self.send_error(httplib.NOT_FOUND)classWebServer(object):def__init__(self):address=('',0)self._httpd=HTTPServer(address,HTTPRequestHandler)@propertydefport(self):returnself._httpd.server_address[1]defrun(self):thread.start_new_thread(self.__background_run,())def__background_run(self):self._httpd.serve_forever()## File gui.pyimportsignalimportgtkfromwebserverimportWebServertry:importwebkitexcept:webkit=Falsetry:importgtkmozembedasgeckoexcept:gecko=Falseifnotwebkitandnotgecko:raiseException('Failed to load both webkit and gecko modules')classWebKitDriver(object):defcreate_browser(self):returnwebkit.WebView()defopen_uri(self,browser,uri):browser.open(uri)classGeckoDriver(object):defcreate_browser(self):returngecko.MozEmbed()defopen_uri(self,browser,uri):browser.load_url(uri)defrun_gui(uri,echo=True):window=gtk.Window()driver=WebKitDriver()ifwebkitelseGeckoDriver()browser=driver.create_browser()box=gtk.VBox(homogeneous=False,spacing=0)window.add(box)box.pack_start(browser,expand=True,fill=True,padding=0)window.connect('destroy',finish)window.set_default_size(800,600)window.show_all()driver.open_uri(browser,uri)deffinish(ignorable=None):gtk.main_quit()defmain():signal.signal(signal.SIGINT,finish)gtk.gdk.threads_init()web=WebServer()web.run()uri='https://localhost:{}'.format(web.port)print'serving at',web.portprint'connecting to ',urirun_gui(uri)gtk.mainloop()if__name__=='__main__':main()
Depuración y testing
Resulta bastante complejo depurar el código JavaScript en la aplicación. Pero recordemos que tenemos una página web, así que podemos abrir un navegador en el puerto que se haya seleccionado y depurar ahí. Se depurará igual que cualquier aplicación web normal.
Es más: Se pueden ejecutar regresiones de tests sobre la página web o utilizar sistemas típicos como WebOb o cualquier otra librería de testing.
Para tests de aceptación, se podría proporcionar un argumento que permita seleccionar un puerto, guardarlo en un archivo o bien procesar la salida. Para los tests de JavaScript también pueden utilizarse los sistemas habituales.
Ventajas e inconvenientes
Manejar aplicaciones de esta manera tiene numerosas ventajas e inconvenientes. La mayor de las ventajas es que no hace falta aprender a usar una librería de GUI tan compleja como pueda ser GTK o QT. Estas librerías tienen sus propios componentes, sus sistemas de eventos, etc. Que hacen que no sea nada trivial cambiar de una a otra.
Además, a menudo resulta tedioso portar los programas creados con estas librerías a sistemas operativos como Windows. Es cierto que lo aquí expuesto se basa en usarlas como base, pero en Windows se podría hacer una aplicación mínima nativa que lance el servidor web y el browser, y a partir de ese momento todo sería compatible.
Evidentemente, resultará muy sencillo transformar la aplicación de escritorio en una página web, ya que es una página web. Como tal, además, podríamos utilizar recursos que se encuentren en la Web, aunque con esto perderíamos autonomía.
Hay quien pueda pensar que el mayor inconveniente es la seguridad, ya que se está compartiendo en un puerto de nuestra máquina. Este problema es fácil de solucionar, añadiendo un filtro en el método do_GET de manera que sólo se admita localhost.
Sin embargo, para mí el mayor problema es la comunicación Servidor-Cliente. ¿Qué ocurre cuando queremos mostrar algo que ha ocurrido en el servidor, en segundo plano? Existen dos soluciones, igual que en web:
Utilizar polling, de manera que el cliente pregunte periódicamente al servidor.
Utilizar websockets, lo que implica complicar un poquito el cliente y el servidor, además de tener otra conexión TCP abierta.
No hay que olvidar los recursos necesarios para ejecutar este tipo de aplicaciones, ya que estamos abriendo un navegador Web para algo que puede ser bastante “pequeño”.
Finalmente, las aplicaciones no son Python, sino que serán principalmente JavaScript. Esto puede verse como una ventaja, ya que se puede cambiar el servidor hasta de lenguaje sin tocar el cliente, consiguiendo así tener claramente diferenciadas las capas de visualización y negocio.
Algo que puede verse como una ventaja y una desventaja es que las aplicaciones no dependerán del estilo de ventanas propio del sistema operativo. El diseño de la aplicación será algo propio, de la misma manera que cada página web tiene su propio diseño y no se integra con el escritorio.
Para gustos los colores
Una vez más, en informática no exite la mejor solución. Este tipo de aplicaciones pueden ser geniales en algunos casos, pero en otros ser un auténtico tormento. Habrá que decidir en cada caso cuál es la mejor herramienta para obtener el resultado deseado.
Pero eso es justo lo que pretendo: Para proporcionar mayor número de herramientas para que nos sea más sencillo resolver problemas más adelante.