1. CEFPython是什么东西
CEFPython 是 CEF 的 Python 绑定实现。
简单来说, CEF 实现了浏览器外在的简单功能,可以直接渲染一个全功能的页面。它包含了页面布局渲染的引擎,也包含了执行 JS 的引擎(V8)。但是它不管一个完整浏览器还需要的其它功能,比如标签页,比如下载管理等等。
使用 CEFPython ,就可以很容易地把一个浏览器视图做到 GUI 的环境当中,比如 wxPython。这样最直接的作用,就是可以使用标准的 Web 技术,比如 HTML , JS 来完成桌面客户端的一些功能。
当然,仅仅这样是不够的,因为浏览器环境中,它自己本身是缺少一些系统级的功能的,比如文件系统的访问,比如数据的持久化存储。用 CEFPython ,你可以非常容易地添加这个浏览器视图的 API ,这样通过 JS 实现与 Python 代码的互相调用,你想干什么都可以了。
2. 一个简单的例子
# -*- coding: utf-8 -*-
from cefpython3 import cefpython
import wx
class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, parent=None, id=wx.ID_ANY, title='wx', size=(800,600))
mainPanel = wx.Panel(self)
windowInfo = cefpython.WindowInfo()
windowInfo.SetAsChild(mainPanel.GetGtkWidget())
br = cefpython.CreateBrowserSync(windowInfo,
browserSettings={},
navigateUrl='http://www.zouyesheng.com')
class App(wx.App):
timer = None
def OnInit(self):
self.timer = wx.Timer(self, 1)
self.timer.Start(10)
wx.EVT_TIMER(self, 1, lambda e: cefpython.MessageLoopWork())
frame = MainFrame()
frame.Show()
return True
if __name__ == '__main__':
settings = {
"locales_dir_path": cefpython.GetModuleDirectory() + "/locales",
"browser_subprocess_path": cefpython.GetModuleDirectory() + "/subprocess",
}
cefpython.Initialize(settings)
app = App(False)
app.MainLoop()
执行上面的代码,你会看到一个 wx 创建的窗口中,显示了一个 HTML 页面。
一个最简单的使用 CEFPython 的流程大概要做的事有:
获取组件, windowInfo = cefpython.WindowInfo() 。
嵌入对应的适配环境中, windowInfo.SetAsChild(mainPanel.GetGtkWidget()) 。
初始化浏览器环境, cefpython.CreateBrowserSync(windowInfo, browserSettings, navigateUrl) 。
处理事件刷新, wx.EVT_TIMER(self, 1, lambda e: cefpython.MessageLoopWork()) 。
3. 扩展浏览器视图的API
上面的代码,创建的浏览器环境算是一个标准的环境,对于:
br = cefpython.CreateBrowserSync(windowInfo,
browserSettings={},
navigateUrl='http://www.zouyesheng.com')
得到的这个 br ,我们可以添加额外的,可供 JS 使用的其它 API 。比如:
class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, parent=None, id=wx.ID_ANY, title='wx', size=(800,600))
mainPanel = wx.Panel(self)
windowInfo = cefpython.WindowInfo()
windowInfo.SetAsChild(mainPanel.GetGtkWidget())
br = cefpython.CreateBrowserSync(windowInfo,
browserSettings={},
navigateUrl='file:///home/zys/temp/cef/demo.html')
js = cefpython.JavascriptBindings()
js.SetFunction("py_func", self.py_func)
js.SetProperty("other", {"a": 1})
br.SetJavascriptBindings(js)
def py_func(self):
print 'I am in Python'
其中 demo.html 的内容为:
测试$(function(){
py_func();
alert(other.a);
});
上面 js 中的, py_func() 的实现是由 Python 完成的(你能在终端中看到输出的一段字符串),而 other.a 这个数据,也是在 CEFPython 中人为添加的。这一切都非常简单。
不过,这样直接添加全局的函数或者数据,会让前端变得混乱。 CEFPython 也提供了添加对象的方法, br.SetObject():
class Ext(object):
def get_info(self, callback):
callback.Call('123');
def action(self, msg):
print msg, type(msg)
class MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, parent=None, id=wx.ID_ANY, title='wx', size=(800,600))
mainPanel = wx.Panel(self)
windowInfo = cefpython.WindowInfo()
windowInfo.SetAsChild(mainPanel.GetGtkWidget())
br = cefpython.CreateBrowserSync(windowInfo,
browserSettings={},
navigateUrl='file:///home/zys/temp/cef/demo.html')
js = cefpython.JavascriptBindings()
js.SetObject('Ext', Ext())
br.SetJavascriptBindings(js)
SetObject 可以把 Python 的一个实例添加到 js 中作为一个对象(但是目前它有一个限制,就是只处理实例对象中的方法,不处理属性)。
对应的 demo.html 我们可以这样:
测试$(function(){
Ext.action('我在JS中');
Ext.get_info(function(data){
alert(data);
});
});
虽然 CEFPython 中还有其它的一些功能,文档在https://code.google.com/p/cefpython/wiki/API ,但我觉得,一个 SetObject 已经解决了 Python 和 JS 之间互相传递消息的所有问题:
JS -> Python , 调用, Ext.action('我在JS中') 。
Python -> JS , 回调, Ext.get_info(function(data){alert(data)}) 。
从这部分也可以看出,不同环境的相互传递消息,还是异步的方式。而且后面也可以看到,“主动控制”中执行的 JS ,也是异步执行的。
4. Python主动调用JS
前面讲的是扩展,从 Python 方面来看,这算是一个被动的形式。对于 CEFPython ,Python 这边是可以主动控制,调度浏览器视图的。下面以两个菜单按钮执行 JS 逻辑为例说明一下,先在Frame 上添加菜单栏:
clss MainFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, parent=None, id=wx.ID_ANY, title='wx', size=(800,600))
menu = wx.Menu()
act_a = menu.Append(1, '行为A')
act_b = menu.Append(2, '行为B')
menu_bar = wx.MenuBar()
menu_bar.Append(menu, '动作')
self.SetMenuBar(menu_bar)
self.Bind(wx.EVT_MENU, self.on_a, act_a)
self.Bind(wx.EVT_MENU, self.on_b, act_b)
... ...
这里把 act_a 和 act_b 两个菜单按钮的按键行为绑定到两个对应的实例方法上了,这两个方法:
def on_a(self, event):
self.br.GetMainFrame().ExecuteFunction('Ext.action', 'xx')
print 'async'
def on_b(self, event):
self.br.GetMainFrame().ExecuteJavascript('alert("Python")')
上面代码是两种执行 JS 逻辑的方法。第一种是直接以名字调用指定的 JS 函数,可以带参数。第二种是给定 JS 语句直接执行。两种对 JS 的执行方法,都是异步的。(所以实践中你会先看到async 的输出。)
这里的 ExecuteFunction 和 ExecuteJavascript 是在 Frame 对象上的(新版本的 CEFPython 好像在 Browser 上也添加了两个方法了)。
5. 事件模拟
浏览器中的各种事件,其实 JS 也可以处理,不过我在 CEFPython 的文档中看到了比较“暴力”的一个 API,SendMouseClickEvent ,它直接是指定坐标给一个 Click 。
先修改一个 HTML 文件:
测试$(function(){
$(document).on('click', function(eventObj){
Ext.log(eventObj.clientX, eventObj.clientY);
});
});
代码中的 Ext.log() 是我自己加的,因为这个环境中没有 console 用嘛,就把信息打到终端查看就好了,实现上是在 Ext 中加一个方法:
def log(self, *args):
print args
上面的 HTML 中,有一个链接,经过实际测试,它的位置在 39, 93 ,我们把之前的菜单按钮的处理改成:
def on_a(self, event):
self.br.SendMouseClickEvent(39, 93, cefpython.MOUSEBUTTON_LEFT, False, 1)
self.br.SendMouseClickEvent(39, 93, cefpython.MOUSEBUTTON_LEFT, True, 1)
print 'async'
之所以是两次调用,是因为我们需要的其实是, 鼠标按下,又松开 ,简单说就是我们要的是release ,不是 down 也不是 up 。
好了,现在去选中菜单项点击,就可以看到终端中的坐标信息,以及页面加载新的 demo2.html了。
6. 请求的交互流程控制
这部分是比较麻烦一点的地方了。
用一个嵌入的浏览器视图,前面说的扩展浏览器 API 部分,只是站在前端角度看到的“很有用”的一部分。而如果站在全局的系统角度,一个嵌入式的浏览器视图,它更多的威力,是来自对请求与响应的控制。
我之前在找, CEFPython 有没有办法,直接往视图中渲染一段指定的 HTML 内容,结果是,好像不能(也许是 CEFPython 没有封装相应的“原生”层面的 API)。就是说,在流程上,这个环境始终是 Web 式的,请求 + 响应。所以,实现“直接渲染指定的 HTML 内容”的方式,也就变成了如何自己创造一个“响应”的问题。考虑“请求 + 响应”的流程,这个问题就是,如何额外定义另一套,兼容 Web 方式的,“请求 + 响应”流程。
这样,这个问题就很明了,也很简单了。它的做法,我们都见过,比如 Chrome 中的“配置”,其实就是内置的 Web 页面。 Firefox 的“配置项”,也是内置的 Web 页面。这些 Web 页面的地址,都是使用的 自定义协议 的方式处理的。于是,我就想,我可以在 HTML 中加一个访问链接,它形如:
cef://some-data
自定义的 cef 协议。不过实际操作中发现这样不行,要做自定义协议,需要显式地自己去定义扩展(否则标准的处理流程会中断,你无法正常响应这个请求),而且不幸的是, CEFPython 中并没有封装相应的 API 。后来发现 CEF 自己默认的协议中: HTTP, HTTPS, FILE, FTP, ABOUT, DATA ,有一个 DATA ,暂且就把它用来自己扩展使用吧。
那么在 HTML 页面,我可以写一个地址:
data://some-data
这样,整个 CEF 的“请求 + 响应”流程完全不受影响。
6.1. 基本的流程与处理方式
这里的流程,指的就是“请求 + 响应”。在 CEFPython 中,你可以通过调用br.SetClientHandler() 方法,来自己完全指定一套流程处理的实现。或者,仅仅使用br.SetClientCallback() 来指定具体的事件回调函数。
在整个流程中, CEF 在扩展上的实现,是类似于“事件注册”的 Hook 方式。如果需要自定义,那么自己做一个对象,里面实现相应的方法即可。
注意一下,这里说的“事件点”,其实只有一套,但是在 CEF 的概念中,它又把这些事件点“分组”了,对应到文点中不同的 Handler ,比如 RequestHandler , LoadHandler ,KeyboarHandler 等,都是这些事件的分组。而总的你需要实现的 ClientHandler ,是可选地,需要去实现这些不同的“分组”中的方法的。
比如:
class MyClientHandler(object):
def OnBeforeResourceLoad(self, *args):
'来自RequestHandler的要求'
pass
def OnLoadingStateChange(self, *args):
'来自LoadHandler的要求'
pass
def OnKeyEvent(self, *args):
'来自KeyboarHandler的要求'
pass
这些内容虽然有点多( https://code.google.com/p/cefpython/wiki/API 中的 Client handlers 部分),但是我们单纯考虑“请求 + 响应”的话,只需要处理两个方法就可以了(其实是三个的)。因为我们面对的场景,只需要考虑三点:
如何修改请求。
如何修改响应。
如何创建响应。
6.2. 修改请求
修改请求指的是,当你在流程中“截取”到请求时,在这个请求正式“发出”之前,修改它的相关属性。
比如一个请求本来是到 file:///home/zys/temp/cef/demo.html ,你把它的 URL 改到http://www.zouyesheng.com 了(MB,这个时候我的 www.zouyesheng.com 好像被墙了)。这部分的实现很容易。
先修改一个 br ,注册一个 ClientHandler 给它:
br.SetClientHandler(ClientHandler())
然后 ClientHandler 这个类的实现,只需要做一个方法, RequestHandlerhttps://code.google.com/p/cefpython/wiki/RequestHandler 中的OnBeforeResourceLoad 方法:
class ClientHandler(object):
def OnBeforeResourceLoad(self, br, frame, request):
if request.GetUrl().endswith('www.zouyesheng.com'):
return
else:
request.SetUrl('http://www.zouyesheng.com')
return
注意一下, OnBeforeResourceLoad 对 request 的修改,是全局影响的,像上面代码中的这样一改,相应的 JS, CSS 请求就全失效了。
OnBeforeResourceLoad 如果 return True ,则请求中断。
6.3. 修改响应
修改响应是指,当请求完成,获取到响应内容之后,在这些内容被渲染前,修改内容。比如你想把页面上的广告内容全部删除。
像前面的“修改请求”一样,本来是实现一个方法就可以搞定的事,但不幸的是, CEF 中还没有实现这个唯一的 API OnResourceResponse ,文档中的说法是,这个 API 你可以自己搞定……
自己搞定的意思,就是后面讲的,自己创建需要的,任意的响应。
6.4. 创建响应
这部分,算是一个完整的流程控制了。“创建”,即指的是无中生有,不像前面的,只是“修改”层面的动作了(因为直接的“修改响应”的缺失,在这里只实现“修改响应”也是可以的)。
“请求 + 响应”中,最重要的一部分,对于 HTTP 协议来说,就是发出请求,然后按 HTTP 协议解析响应的报文。这部分, CEF 中原本是按标准的 HTTP 协议实现好了的,它提供了 API ,允许自定义这部分的实现(简单说,你甚至可以自己用 urllib 来搞,不考虑线程与阻塞什么的话)。
实现上,分两部分:
RequestHandler 中的 GetResourceHandler 方法,要求返回一个 ResourceHandler 的实例。
所以,我们要做的,就是做一个自己的 ResourceHandler 。 它和 ClientHandler 有些不同,虽然都不是继承已有的类,但 ClientHandler 只需要实现用得到的方法即可,而 ResourceHandler 中的所有被要求的方法,必须实现。
先处理 ClientHandler (前面的 br.SetClientHandler() 一样的):
class ClientHandler(object):
def GetResourceHandler(self, br, frame, request):
if not request.GetUrl().startswith('data://'):
return
self.res = ResourceHandler()
return self.res
GetResourceHandler 方法返回一个 ResourceHandler 实例。这里,我们只处理使用了 data 协议的请求。普通的 HTTP 协议的请求,按默认处理就好了,不动它。
注意: self.red = ResourceHandler() 这句的意义在于显式地保持一个 ResourceHandler 实例的引用(否则这个实例在真正想用它时已经被 Python 给 GC 了?),这块应该是从静态的 C++ 绑定到动态的 Python 时,对于引用与释放的一个无奈之举了。
ResourceHandler 的实现:
class ResourceHandler(object):
def ProcessRequest(self, request, callback):
callback.Continue()
return True
def GetResponseHeaders(self, response, length, redirect):
print response, length, redirect
def ReadResponse(self, data, bytes_to_read, bytes_readed, callback):
print data, bytes_to_read, bytes_readed, callback
def CanGetCookie(self, cookie):
return True
def CanSetCookie(self, cookie):
return True
def Cancel(self):
pass
虽然是自定义,但是形式上,却按 HTTP 报文解析的方式规定好了的,就是一般的先处理头,再处理 body 的形式。
CEFPython 因为是从 C++ 代码绑定而来,所以,这部分的实现,有一点很特别,就是使用 Python 中的 list 类型(因为 list 对象是“引用传递”),来处理 C++ 中的“地址”,比如上面的 length , bytes_readed 这些,虽然只是一个数字,但都是用 list 来保存,赋值时也是为这个list 的第一个成员赋值。(典型的“填坑”用法)
假设这里,我们要响应一段自己设置的 HTML 文本(比如就是 RES =
def GetResponseHeaders(self, response, length, redirect):
length[0] = len('
response.SetMimeType('text/html')
参照解析 HTTP 响应报文的一般方法, GetResponseHeaders 方法最重要的一点,就是获取接下来的body 部分的字节长度(如果是未知长度,按 -1 处理)。把这个长度值填到 length[0] 中即可。
另外,处理头的时候,可以处理 response 对象的相关属性https://code.google.com/p/cefpython/wiki/Response 。比如这里,我们设置了MimeType ,这样在渲染时就会按 HTML 页面处理了。
下面是处理 body 部分:
def ReadResponse(self, data, bytes_to_read, bytes_readed, callback):
data[0] = '
bytes_readed[0] = bytes_to_read
return True
同样考虑 HTTP 响应报文的一般解析方法,从原始的连接中读取内容的行为与结果,是不一定的。所以通常我们会在这块做一个 while ,然后从连接中不断地尝试读取,直接已读取的 chunk 长度等于我们期望的长度。这个过程的调度, CEF 已经在内部封装好了,我们只需要在这里给到的ReadResponse 方法实现上,控制它的几个参数即可:
data ,响应的 body 部分的内容。
bytes_to_read 还需要读取的字节数。
bytes_readed 已读取的字节数。
callback 控制方法。
因为“读取”行为是不断尝试的,所以这个 ReadResponse 方法会被不断调用,直到 bytes_to_read 减至 0 ,说明需要的内容已经读完。官网文档中的几句话,倒把这个“意会容易,说明难”的流程讲清楚了 https://code.google.com/p/cefpython/wiki/ResourceHandler :
Read response data. If data is available immediately copy up to
bytesToRead (bytes_to_read) bytes into dataOut[0] (data[0]),
set bytesReadOut[0] (bytes_readed[0]) to the number of bytes copied,
and return true. To read the data at a later time
set bytesReadOut[0] (bytes_readed[0]) to 0, return true and
call callback.Continue() when the data is available.
To indicate response completion return false.
我们这里自己需要响应的内容是固定的
def ReadResponse(self, data, bytes_to_read, bytes_readed, callback):
data[0] = '
bytes_readed[0] = bytes_to_read
return True
最终,当访问一个 data://xxxx 这样的地址时,你就可以在页面上看到两个大的 ok 字符,这个内容,就算是我们“直接创建的响应内容”了。
总结一下完整地控制请求与响应要做的事:
br.SetClientHandler() 设置一个 ClientHandler 实例。
ClientHandler 实例的 OnBeforeResourceLoad 方法可以修改请求。
ClientHandler 实例的 GetResourceHandler 方法可以根据 request 的属性判断,选择性地返回一个 ResourceHandler 实例。
如果 GetResourceHandler 返回了一个 ResourceHandler 实例,则这个实例在 ClientHandler 中需要被显示地保持引用。
ResourceHandler 实例通过 GetResponseHeaders 与 ReadResponse 方法完成内容获取。
GetResponseHeaders 中可以根据需要,设置 resposne 的各种属性。
几个概念梳理一下:
br 是一个 Browser 实例。
ClientHandler 实例是用于控制“请求 + 响应”的一套实现(它又是由各种 xxxHandler 组成的)。
ClientHandler 中处理响应的流程部分,由专门的 ResourceHandler 实例完成(它由ClientHandler 实例的一个方法返回得到)。
在过程当中,会用到 request 对象和 response 对象。
7. 请求流程的处理
就 HTTP 协议来说,从发送请求,到获取响应,中间还有一些细节的东西。一个例子就是,当你获取的响应头中的 Content-Type 不是一个适合在浏览器显示的类型,那么,应该弹出一个保存文件的提示框。所以,前面提到了 request 对象, response 对象,提到了就 CEF 的流程来说,是如何处理请求与响应的。但是,更深一层的 HTTP 协议的流程,如何去把握并没有介绍。
当然,这部分的实现,可以说是跟 CEF 这个东西没有直接关系的,前面也说过了,按 CEF 的基本流程,你完全可以通过 urllib 拿到响应之后再作后续处理。不过, CEF 其实有提供相应的工具,来对 HTTP 的请求与响应的解析过程作更细的控制的。
这里涉及到的对象有:
先讲一下 request 对象。之前提它时,是在 CEF 的自己的流程中,会遇到它。这里,我们要凭空创造一个 request ,也是可以的:
from cefpython3 import cefpython
req = cefpython.Request.CreateRequest()
req.SetUrl('http://www.zouyesheng.com')
req.SetFlags(cefpython.Request.Flags['ReportLoadTiming'] |
cefpython.Request.Flags['ReportUploadProgress'])
通过对静态方法 cefpython.Request.CreateRequest() 的调用,我们可以得到一个 request 对象,然后,通过 Set* 方法去设置它的各种属性。之后,可以把它扔给 WebRequest 进行实际的请求了。
wq = cefpython.WebRequest.Create(req, WebRequestClient())
注意,这个 wq 也像之前的 ResourceHandler 实例一样,需要显式地保持一个引用。
这里的 WebRequestClient() 就是对一组请求状态的方法实现,需要的方法有:
OnUploadProgress(wq, current, total)
OnDownloadProgress(wq, current, total)
OnDownloadData(wq, data)
OnRequestComplete(wq)
GetAuthCredentials(is_proxy, host, port, realm, schema, callback)
上面的几个方法中,除了它的一个主要作用是可以监控状态之外, OnDownloadData 方法,还是一个处理响应内容的手段。实际上,如果你使用 WebRequest 来自己发出请求的话,那么在OnDownloadData 中需要自己拼接多次响应的内容。
WebRequest 的使用看一个最简单的完整例子:
# -*- coding: utf-8 -*-
from cefpython3 import cefpython
import wx
class WebRequestClient(object):
count = 0
def OnUploadProgress(self, wq, current, total):
print wq, current, total
def OnDownloadProgress(self, wq, current, total):
print wq, current, total
def OnDownloadData(self, wq, data):
print wq, len(data)
def OnRequestComplete(self, wq):
print wq
class MainFrame(wx.Frame):
timer = None
def __init__(self):
wx.Frame.__init__(self, parent=None, id=wx.ID_ANY, title='wx', size=(800,600))
menu = wx.Menu()
mainPanel = wx.Panel(self)
windowInfo = cefpython.WindowInfo()
windowInfo.SetAsChild(mainPanel.GetGtkWidget())
self.br = cefpython.CreateBrowserSync(windowInfo,
browserSettings={},
navigateUrl='file:///home/zys/temp/cef/demo.html')
self.timer = wx.Timer(self, 1)
self.timer.Start(10)
wx.EVT_TIMER(self, 1, lambda e: cefpython.MessageLoopWork())
self.Bind(wx.EVT_CLOSE, self.OnClose, self)
def OnClose(self, event):
self.br.ParentWindowWillClose()
event.Skip()
class App(wx.App):
def OnInit(self):
frame = MainFrame()
frame.Show()
req = cefpython.Request.CreateRequest()
req.SetUrl('http://www.zouyesheng.com')
req.SetFlags(cefpython.Request.Flags['ReportLoadTiming'] |
cefpython.Request.Flags['ReportUploadProgress'])
self.wq = cefpython.WebRequest.Create(req, WebRequestClient())
return True
if __name__ == '__main__':
settings = {
"locales_dir_path": cefpython.GetModuleDirectory() + "/locales",
"browser_subprocess_path": cefpython.GetModuleDirectory() + "/subprocess",
}
cefpython.Initialize(settings)
app = App(False)
app.MainLoop()
del app
cefpython.Shutdown()
从上面的代码可以看到, WebRequest 是可以跟 Browser 完全没有关系的,处理好 WebReqest 的显式引用就好了。而 WebRequestClient 的实现则可以看到请求在每个阶段的状态(具体方法参数的含义见官方文档)。但话虽是这么说,如果仅仅是为了完成一个 HTTP 请求,那 urllib 也可以做到。 WebRequest 作为 CEF 提供的现成机制,目的应该还是跟 ClientHandler 配合使用,达到对一个“请求响应”流程的完全控制 + 状态监视。下一节专门考虑这个问题。
8. 自定义请求的处理
WebRequest 是 CEF 中提供的一套 HTTP 请求处理的实现。把自定义请求的处理整个流程拿出来看的话,大概是这样的:
GUI 的初始化阶段,创建 Browser 。
Browser 通过 SetClientHandler 方法设置一个 ClientHandler 的实例。
ClientHandler 中的方法,可以更改请求。
ClientHandler 的 GetResourceHandler 方法返回一个 ResourceHandler 实例。
ResourceHandler 负责处理请求,并得到响应。
ResourceHandler 中使用 WebRequest 处理请求。
通过指定 Request 和 WebRequestClient 得到了 WebRequest 。 WebRequestClient 中是完成请求交互的细节,比如要如何去读够整个 HTTP 响应的数据。
上面的概念可能有些多,一层套一层的。其实就是:
Browser -> ClientHandler -> ResourceHandler -> WebRequest(WebRequestClient)
这个地方,不用 WebRequest 直接用 httplib 拿数据是可行的,考虑效率,可能需要用多线程等并发方式处理请求。
但在我自己试的时候, CEF 总是会挂,不知道为什么。
不管是用 WebRequest ,还是像 httplib 等其它方法,请求的处理与 CEF 的对接点,都在ResourceHandler 上。准确地说,一般是:
__init__() 时创建相应的实例。
ProcessRequest() 发出请求。
GetResponseHeaders() 设置好响应头。
ReadResponse() 完成响应数据读取。
这里说一句话,使用 WebRequestClient 这东西,想要完善地处理通用的 HTTP 请求是不可能的,它最大的问题是没有作一个 OnHeaderGet() 的回调,这样,在流程上与 HTTP 的交互流程是不匹配的,会很麻烦。同时, WebRequestClient 没有提供关闭连接的方法,这意味着你无法中断一个请求的处理。
9. 正确处理引用与释放资源
CEFPython 是对 C++ 的 CEF 的绑定,所有在一些地方,资源的引用与释放,没有办法自动地做到那么彻底,这种情况下,就需要人为地处理掉。
9.1. ResourceHandler的显示引用保留
这个问题之前就提过了。在 ClientHandler 的 GetResourceHandler 方法中,返回的 ResourceHandler 需要显示地保留它的引用。比如之前的代码:
class ClientHandler(object):
def GetResourceHandler(self, br, frame, request):
if not request.GetUrl().startswith('data://'):
return
self.res = ResourceHandler()
return self.res
把 ResourceHandler 的实例放到 self.res 中。
9.2. WebRequest的显示引用保留
跟上面的 ResourceHandler 一样, WebRequest 的实例也需要显示保留引用。
class ResourceHandler(object):
def __init__(self, client_handler, id):
self.webrequest = None
self.webrequest_client = WebRequestClient(self)
self.header_callback = None
def ProcessRequest(self, request, callback):
request.SetFlags(cefpython.Request.Flags["AllowCachedCredentials"] |
cefpython.Request.Flags["AllowCookies"])
self.header_callback = callback
self.webrequest = cefpython.WebRequest.Create(request, self.webrequest_client)
return True
9.3. CEFPython的正确关闭
官方示例代码中的做法,在结束掉 wx 的 Application 实例之后,先清除 app 的引用,然后调用 CEFPython 的关闭方法。
if __name__ == '__main__':
settings = {
"locales_dir_path": cefpython.GetModuleDirectory() + "/locales",
"browser_subprocess_path": cefpython.GetModuleDirectory() + "/subprocess",
}
cefpython.Initialize(settings)
app = App(False)
app.MainLoop()
del app
cefpython.Shutdown()
9.4. Browser对象的正确结束
官方示例代码中的做法,因为涉及多个 Browser 实例的调度,在父窗口要关闭时,需要通知出去。 CEFPython 在 Browser 对象中提供了一个现成的方法, br.ParentWindowWillClose 。
class MainFrame(wx.Frame):
def __init__(self):
... ...
mainPanel = wx.Panel(self)
windowInfo = cefpython.WindowInfo()
windowInfo.SetAsChild(mainPanel.GetGtkWidget())
self.br = cefpython.CreateBrowserSync(windowInfo,
browserSettings={},
navigateUrl='')
... ...
self.Bind(wx.EVT_CLOSE, self.OnClose, self)
def OnClose(self, event):
self.br.ParentWindowWillClose()
event.Skip()
上面的代码,处理在 MainFrame 关闭时,总去处理 ParentWindowWillClose 。