CEF,全称Chromium Embedded Framework ,它是基于Google Chromium的开源项目,它的目标是能够向第三方程序添加WEB浏览器功能,以及可以使用HTML、CSS和JS渲染界面。
CEF框架是由Marshall Greenblatt 在 2008 年创立的,由C/C++编写而成
而JCEF则是CEF的java接口,可以使用java调用CEF的接口。
JCEF的官方网站(chromiumembedded / java-cef / wiki / Home — Bitbucket)
其他的帮助文档:JCEF帮助文档
https://github.com/fanfeilong/cefutil/blob/master/doc/CEF%20General%20Usage-zh-cn.md
POI文档:POI中文帮助文档
Cef3采用了多进程架构,Browser进程为主进程,负责窗口管理、界面绘制、网络交互等;Render进程负责页面渲染、V8引擎、Dom节点等。默认的进程模型中,会为每一个标签页创建一个新的Render进程。
Render进程
CEF 的渲染进程(Render Process)负责处理浏览器窗口内的内容渲染,包括 HTML、JavaScript、CSS 等。
例如,当用户在浏览器中访问一个网页时,CEF 会创建一个渲染进程来解析 HTML 代码,并根据 CSS 和 JavaScript 渲染出网页的内容。
frame render 进程
如果网页中包含了 frame 标签,CEF 会再创建一个 frame render 进程来渲染 frame 内的内容。
CEF 的 frame render 进程可以帮助开发者在应用内嵌入多个浏览器窗口,并且可以让每个浏览器窗口独立地加载和渲染网页内容,从而提升应用的性能和用户体验。
Browser被定义为主进程,负责窗口管理,界面绘制和网络交互。Blink的渲染和Js的执行被放在一个独立的Render 进程中;除此之外,Render进程还负责Js Binding和对Dom节点的访问。 默认的进程模型中,会为每个标签页创建一个新的Render进程。
Browser和Render进程可以通过发送异步消息进行双向通信。
一个从Browser进程发送到Render进程的消息将会在CefRenderProcessHandler::OnProcessMessageReceived()方法里被接收。一个从Render进程发送到Browser进程的消息将会在CefClient::OnProcessMessageReceived()方法里被接收。
JavaScript被集成在Render进程,但是需要频繁和Browser进程交互。 JavaScript API应该被设计成可使用闭包异步执行。
初始化JCEF:在应用程序启动时,必须先初始化JCEF
import org.cef.CefApp;
import org.cef.CefClient;
import org.cef.CefSettings;
import org.cef.OS;
import org.cef.browser.CefBrowser;
import org.cef.handler.CefAppHandlerAdapter;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class TestFrame extends JFrame {
private static final long serialVersionUID = -7410082787754606408L;
public static void main(String[] args) {
new TestFrame();
}
public TestFrame() {
//是否Linux系统
boolean useOSR = OS.isLinux();
//是否透明
boolean isTransparent = false;
//添加Handler,在CEFAPP状态为终止时退出程序
CefApp.addAppHandler(new CefAppHandlerAdapter(null) {
@Override
public void stateHasChanged(org.cef.CefApp.CefAppState state) {
//CefApp.CefAppState TERMINATED:CefApp已终止,无法再使用。您现在可以安全地关闭应用程序
if (state == CefApp.CefAppState.TERMINATED) System.exit(0);
}
});
//--------------------------初始化JCEF---------------------------
CefSettings settings = new CefSettings();
settings.windowless_rendering_enabled = useOSR;
//获取CefApp实例
CefApp cefApp = CefApp.getInstance(settings);
//创建客户端实例
CefClient cefClient = cefApp.createClient();
//-----------------------------创建浏览器实例-------------------------------
//创建一个新的浏览器实例,并打开指定的URL
CefBrowser cefBrowser = cefClient.createBrowser("http://hao.fly63.com/", useOSR, isTransparent);
//将浏览器UI添加到窗口中
getContentPane().add(cefBrowser.getUIComponent(), BorderLayout.CENTER);
pack();
setTitle("JCEF打开极简导航");
setSize(800, 600);
setVisible(true);
//添加一个窗口关闭监听事件
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
CefApp.getInstance().dispose();
dispose();
}
});
}
}
CefSettings
CefSettings结构体允许定义全局的CEF配置,经常用到的配置项如下
single_process 设置为true时,Browser和Renderer使用一个进程。
locale 此设置项将传递给Blink。如果此项为空,将使用默认值“en-US”。
log_file 此项设置的文件夹和文件名将用于输出debug日志。如果此项为空,默认的日志文件名为debug.log,位于应用程序所在的目录
log_severity 此项设置日志级别。只有此等级、或者比此等级高的日志的才会被记录。此项可以通过命令行参数“log-severity”配置,可以设置的值为“verbose”,“info”,“warning”,“error”,“error-report”,“disable”。
emote_debugging_port 此项可以设置1024-65535之间的值,用于在指定端口开启远程调试。例如,如果设置的值为8080,远程调试的URL为http://localhost:8080。CEF或者Chrome浏览器能够调试CEF
CefSettings定义了Cef的全局配置信息,比如指定单进程模式、指定渲染子进程路径、设置localstorage路径、设置日志等级、Cef资源文件路径。其中对于项目最重要的字段是single_process、multi_threaded_message_loop、windowless_rendering_enabled,分别用于指定单进程模式、多线程渲染模式、离屏渲染模式。
经常用到的配置项如下:
1
CefApp接口提供了不同进程的可定制回调函数,每一个进程对应一个CefApp接口
CefApp:公开用于管理全局 CEF 上下文的静态方法
1、addAppHandler(CefAppHandler appHandler)
//将一个AppHandler分配给CefApp,必须在初始化CefApp之前调用此方法 public static void addAppHandler(CefAppHandler appHandler) throws java.lang.IllegalStateException
2、createClient()
//创建一个新的客户端实例并将其返回给调用者 public CefClient createClient()
3、getInstance(
//CefApp已启动并正在运行。至少创建了一个CefClient,并且消息循环正在运行。您现在可以使用JCEF的所有类和方法。 public static final CefApp.CefAppState INITIALIZED //CefApp正在进行初始化过程。请等待初始化完成 public static final CefApp.CefAppState INITIALIZING //(CefApp是新创建的,但尚未初始化。到目前为止,还没有CefClient和CefBrowser被创建 public static final CefApp.CefAppState NEW
settings)
//获取CefApp类的实例 public static CefApp getInstance(CefSettings settings) throws java.lang.UnsatisfiedLinkError
4、dispose()
//调用此方法将关闭所有客户端实例以及每个客户端拥有的所有浏览器实例。此后,消息循环终止并且CEF关闭 public final void dispose()
CefApp.CefAppState 枚举常量
总共有6个
//CefApp已启动并正在运行。至少创建了一个CefClient,并且消息循环正在运行。您现在可以使用JCEF的所有类和方法 public static final CefApp.CefAppState INITIALIZED //CefApp已终止,无法再使用。您现在可以安全地关闭应用程序 public static final CefApp.CefAppState TERMINATED
Client that owns a browser and renderer.:拥有浏览器和渲染器的客户端
每一个CefBrowser对象会对应一个CefClient接口,用于处理浏览器页面的各种回调信息,包括了Browser的生命周期,右键菜单,对话框,状态通知显示,下载事件,拖曳事件,焦点事件,键盘事件,离屏渲染事件等。
public CefBrowser createBrowser(java.lang.String url,
boolean isOffscreenRendered,
boolean isTransparent)
//返回所有浏览器实例的列表
protected java.lang.Object[] getAllBrowser()
CefClient: 提供了一些获取Handler的方法。
Interface representing a browser. (代表浏览器的界面)
//获取与此浏览器关联的客户端
CefClient getClient()
//要求关闭浏览器。 close表示是否强制关闭
void close(boolean force)
//返回唯一的浏览器标识符
int getIdentifier()
//在此框架中执行一串JavaScript代码
void executeJavaScript(java.lang.String code,
java.lang.String url,
int line)
//返回浏览器窗口的框架
CefFrame getFocusedFrame()
//返回浏览器窗口的顶层框架
CefFrame getMainFrame()
//如果窗口是弹出窗口,则为true
boolean isPopup()
//重新加载当前页面
void reload()
//如果document已加载到浏览器中,则为true
boolean hasDocument()
关于Frame的
//返回当前存在的Frame个数
int getFrameCount()
//返回所有现有的框架的标识符
java.util.Vector getFrameIdentifiers()
//返回所有现有的框架的名称
java.util.Vector getFrameNames()
-----------------------------------------
//返回具有指定标识符的Frame;如果未找到,则返回NULL
CefFrame getFrame(long identifier)
//返回具有指定名称的框架,如果找不到,则返回NULL。
CefFrame getFrame(java.lang.String name)
//返回此框架的名称。如果框架具有分配的名称(例如,通过iframe的“名称”属性设置),则将返回该值。
//否则,将基于框架父级层次结构构造一个唯一的名称。
java.lang.String getName()
//返回此框架的父级;如果这是主要(顶级)框架,则返回NULL
CefFrame getParent()
//如果此框架是顶级,则为True,否则为false
boolean isMain()
//发出当前加载到该框架中的 URL
java.lang.String getURL()
//在此框架中执行一串JavaScript代码
void executeJavaScript(java.lang.String code,
java.lang.String url,
int line)
code -要执行的代码
url -可以找到相关脚本的网址
line -用于错误报告的基线号
CefBrowser对象的生命周期事件的回调接口。
//创建浏览器对象后会立马触发这个回调
void onAfterCreated(CefBrowser browser)
//当浏览器对象即将销毁时会触发这个回调
void onBeforeClose(CefBrowser browser)
//在浏览器收到关闭请求时调用
boolean doClose(CefBrowser browser)
//当单击了网页中会弹出新窗口的链接时,会触发这个回调,要取消创建弹出窗口,请返回true
boolean onBeforePopup(CefBrowser browser,
CefFrame frame,
java.lang.String target_url,
java.lang.String target_frame_name)
1
Interface representing a query callback. (表示查询回调的接口
//通知关联的 JavaScript onFailure 回调查询失败
void failure(int error_code,
java.lang.String error_message)
error_code -错误代码传递给JavaScript
error_message -错误消息传递给JavaScript
//通知关联的JavaScript onSuccess回调查询已成功完成
void success(java.lang.String response)
response - 传递给JavaScript的响应
//使用默认配置创建一个新路由器
public static final CefMessageRouter create()
//创建具有指定配置的新路由器
public static final CefMessageRouter create(CefMessageRouter.CefMessageRouterConfig config)
//添加一个新的查询处理程序
public abstract boolean addHandler(CefMessageRouterHandler handler,
boolean first)
handler-要添加的处理程序
first -如果为true,则将处理程序添加为第一个处理程序,否则将添加为最后一个处理程序
//删除现有的查询处理程序,成功删除则返回为True。
public abstract boolean removeHandler(CefMessageRouterHandler handler)
//如果不再使用CefMessageRouter实例,则必须调用
public abstract void dispose()
1
onQuery
如果没有处理程序从此方法返回true,则查询将被自动取消,并向JavaScript onFailure回调传递错误代码-1
//当浏览器收到JavaScript查询时调用
boolean onQuery(CefBrowser browser,
CefFrame frame,
long queryId,
java.lang.String request,
boolean persistent,
CefQueryCallback callback)
browser -相应的浏览器
frame -生成事件的框架。实例仅在此方法的范围内有效
queryId -查询的唯一ID
persistent -如果查询是持久的,则为true
callback -用于异步继续或取消查询的对象
1
//(取消待处理的JavaScript查询时调用
void onQueryCanceled(CefBrowser browser,
CefFrame frame,
long queryId)
browser -相应的浏览器
frame -生成事件的框架。实例仅在此方法的范围内有效
queryId -查询的唯一ID
CefMessageRouterHandlerAdapter用于接收消息路由器事件的抽象适配器类。此类中的方法为空。此类的存在是为了方便创建处理程序对象
CEF使用的V8 JavaScript 引擎用于内部JavaScript实现,每一个frame都有JS上下文(context),为JS代码执行提供范围和安全。CEF暴露了很多JS特性可以和客户端程序进行交互。
CefBrowser:一个普通的浏览器页面(HTML)
CefFrame:每一个页面都由至少一个frame组成,最顶层的为mainframe
context:JS执行环境,每个frame都有自己独立的context,CEF中使用V8 JavaScriptEngine解析和执行JS代码
执行JavaScript代码
cefBrowser.executeJavaScript("document.getElementById('myElement').innerHTML = 'Hello, JCEF!';", "",0);
CEF有专门的调用js方法的函数:executeJavaScript,它是一个属于CefFrame类的方法。所以我们想要调用js的方法,只需要获取到页面的frame,然后调用executeJavaScript就可以了。
//在此框架中执行一串JavaScript代码 void executeJavaScript(java.lang.String code, java.lang.String url, int line)
参数一是要执行的js语句;
参数二是可以找到问题脚本(如果有的话)的URL。渲染器可能会请求这个URL来向开发人员显示错误的来源;
第三个参数是用于错误报告的基线编号。
发送消息给网页:
CefProcessMessage message = CefProcessMessage.create("myMessage"); message.getArgumentList().setString(0, "Hello, JCEF!"); cefBrowser.sendProcessMessage(CefProcessId.BROWSER, message);
当JavaScript代码中调用cefQuery并传递"getJavaData"时,Java代码会返回一段数据。
// 在JavaScript中调用Java方法并获取返回值
cefQuery({
request: "getJavaData",
onSuccess: function (response) {
console.log("Data from Java: " + response);
},
onFailure: function (errorCode, errorMessage) {
console.error("Java query failed: " + errorCode + " - " + errorMessage);
},
});
render进程和browser进程之间是通两端的消息路由传递消息的,
JCEF使得在Java应用程序中嵌入Chromium引擎成为可能,并且通过使用CefMessageRouter,您可以在Java和JavaScript之间实现双向的交互。
将这个JCEFMessageRouterHandler注册到CefMessageRouter中。这样,您就可以在JavaScript中使用cefQuery来与Java进行交互了
//配置一个查询路由,html页面可使用 window.java({}) 和 window.javaCancel({}) 来调用此方法 CefMessageRouter.CefMessageRouterConfig cmrc = new CefMessageRouter.CefMessageRouterConfig("java", "javaCancel"); //创建查询路由 CefMessageRouter router = CefMessageRouter.create(cmrc); router.addHandler(new JCEFMessageRouterHandler(), true); cefClient.addMessageRouter(router);
创建了一个CefMessageRouterHandlerAdapter的子类,重写了onQuery方法来处理来自JavaScript的查询。
import org.cef.browser.CefBrowser; import org.cef.browser.CefFrame; import org.cef.callback.CefQueryCallback; import org.cef.handler.CefMessageRouterHandlerAdapter; public class JCEFMessageRouterHandler extends CefMessageRouterHandlerAdapter { //重写了onQuery方法来处理来自JavaScript的查询 @Override public boolean onQuery(CefBrowser browser, CefFrame frame, long query_id, String request, boolean persistent, CefQueryCallback callback) { if (request.equals("getJavaData")) { String javaData = "This data is from Java"; callback.success(javaData); return true; } return false; } }
举例: 用js去打开文件选择对话框
//打开文件选择器
public static void openFileChooser(CefBrowser browser, final CefQueryCallback callback) {
CefRunFileDialogCallback dialogCallBack = new CefRunFileDialogCallback() {
@Override
public void onFileDialogDismissed(int selectedAcceptFilter, Vector filePaths) {
if (filePaths.size() == 0) {
return;
}
File selectedFile = new File(filePaths.get(0));
if (selectedFile != null) {
String selectedPath = selectedFile.getAbsolutePath();
System.out.println(selectedPath);
callback.success(selectedPath);
}
}
};
browser.runFileDialog(CefDialogHandler.FileDialogMode.FILE_DIALOG_OPEN, "文件选择器", null, null, 0, dialogCallBack);
}
CefLifeSpanHandlerAdapter:弹出窗口创建一个处理的Handler
//onBeforePopup 在接口中 CefLifeSpanHandler
public boolean onBeforePopup(CefBrowser browser,
CefFrame frame,
java.lang.String target_url,
java.lang.String target_frame_name)
browser - 弹出请求的来源
frame - 弹出请求的来源。实例仅在此方法的范围内有效
target_url -如果请求中未指定任何内容,则可以为空
target_frame_name -如果请求中未指定任何内容,则可以为空
返回:如果为True,则取消创建弹出窗口;如果为false,则继续进行。)
当我们点击target值为_blank的链接时,JCEF默认以弹出窗口的形式打开新页面。创建一个实现CefLifeSpanHandlerAdapter的类,重写onBeforePopup方法:根据url创建一个CefBrowser对象。
public class DevToolsDialog extends JDialog {
/**
*
*/
private static final long serialVersionUID = 4115973232808017898L;
private final CefBrowser devTools_;
public DevToolsDialog(Frame owner, String title, CefBrowser browser) {
this(owner, title, browser, null);
}
public DevToolsDialog(Frame owner, String title, CefBrowser browser, Point inspectAt) {
super(owner, title, false);
setLayout(new BorderLayout());
setSize(1000, 800);
setLocation(owner.getLocation().x + 20, owner.getLocation().y + 20);
devTools_ = browser.getDevTools(inspectAt);
add(devTools_.getUIComponent());
addComponentListener(new ComponentAdapter() {
@Override
public void componentHidden(ComponentEvent e) {
dispose();
}
});
}
@Override
public void dispose() {
devTools_.close(true);
super.dispose();
}
}
怎么打开devtools呢
DevToolsDialog devToolsDlg = new DevToolsDialog(CefManager.getInstance().getFrame(), "开发者工具-"+browser.getMainFrame().getName(), browser);
devToolsDlg.setVisible(true);