导航
本指南介绍导航相关事件,并展示如何加载 URL 和文件、过滤导航请求、使用导航历史等。
加载 URL
要导航到由 URL 标识的资源,可以使用以下方法之一:
Navigation.loadUrl(String url)Navigation.loadUrl(LoadUrlParams params)
下面的示例展示了如何使用 Navigation.loadUrl(String) 方法导航到 https://html5test.jiku.co:
var navigation = browser.navigation();
navigation.loadUrl("https://html5test.jiku.co");
val navigation = browser.navigation
navigation.loadUrl("https://html5test.jiku.co")
上述代码会向给定资源发起导航请求后立即返回。它不会等待资源完全加载完毕。
如果你需要阻塞当前线程,直到资源完全加载完毕,可以使用 Navigation.loadUrlAndWait() 方法:
// 使用默认 45 秒超时时间等待。
navigation.loadUrlAndWait("https://html5test.jiku.co");
// 使用自定义超时时间。
navigation.loadUrlAndWait("https://html5test.jiku.co", Duration.ofSeconds(30));
// 使用默认 45 秒超时时间等待。
navigation.loadUrlAndWait("https://html5test.jiku.co")
// 使用自定义超时时间。
navigation.loadUrlAndWait("https://html5test.jiku.co", Duration.ofSeconds(30))
该方法会阻塞当前线程,直到资源的主 Frame 完全加载完成,或者达到指定的超时时间。如果在超时时间内资源仍未加载完成,将抛出 TimeoutException 异常。
你可以使用 Navigation.defaultTimeout() 方法获取默认超时时间的当前值:
Duration timeout = Navigation.defaultTimeout();
val timeout: Duration = Navigation.defaultTimeout()
如果导航失败,将抛出 NavigationException 异常。
使用 POST 加载 URL
要加载网页并发送 POST 数据,请使用 Navigation.loadUrl(LoadUrlParams) 方法。下面的代码演示了如何向 URL 发送纯文本 POST 数据:
var data = TextData.of("post data");
var params = LoadUrlParams.newBuilder(url)
.uploadData(data)
.addExtraHeader(HttpHeader.of("header name", "header value"))
.build();
navigation.loadUrl(params);
val params = LoadUrlParams(
url = url,
data = TextData("post data"),
extraHeaders = listOf(
HttpHeader("header name", "header value")
)
)
navigation.loadUrl(params)
除了纯文本数据外,你还可以发送 FormData、MultipartFormData 或任意的 ByteData:
import tech.fuzio.net.File;
...
var avatarFile = File.newBuilder()
.bytesValue(avatarBytes)
.contentType("image/png")
.build();
var data = MultipartFormData.newBuilder()
.addPair(Pair.of("user", "john"))
.addPair(Pair.of("avatar", avatarFile))
.build();
var data = FormData.newBuilder()
.addPair(Pair.of("street", "Forthlin Rd"))
.addPair(Pair.of("house", "20"))
.build();
// 从字符串创建。
var message = ByteData.of("from string");
// 从字节数组创建。
var avatar = ByteData.of(imageBytes);
// 指定内容类型。
var pdf = ByteData.of(
pdfBytes,
ContentType.newBuilder("application/pdf").build()
);
Fuzio 会自动注入 Content-Type 和 Content-Length 头。在 multipart 情况下,还会自动包含表单边界以及每个部分的 Content-Disposition 和 Content-Type。
默认的内容类型如下:
text/plain用于TextDataapplication/octet-stream用于ByteDataapplication/x-www-form-urlencoded用于FormDatamultipart/form-data用于MultipartFormData
如果你需要覆盖默认的 Content-Type 或 Content-Length 请求头,可以在 LoadUrlParams 中通过额外请求头添加您自己的值。
加载文件
你可以使用相同的方法从本地文件系统加载 HTML 文件,只需提供 HTML 文件的绝对路径,而不是 URL。
例如:
navigation.loadUrl(new File("index.html").getAbsolutePath());
val indexPage = File("index.html")
navigation.loadUrl(indexPage.absolutePath)
加载 HTML
本节介绍如何在 Frame 中加载 HTML。
有两种可行的方式:
这些方法有以下区别:
| 功能 | Data URL | 自定义方案 |
|---|---|---|
| 支持 JavaScript-Java 桥接 | 是 | 是 |
支持 InjectJsCallback | 是 | 是 |
支持 InjectCssCallback | 是 | 是 |
从 HTTP 加载 <iframe> | 是 | 是 |
从文件系统加载 <iframe> | 否 | 否 |
| 从 HTTP 加载图像 | 是 | 是 |
| 从文件系统加载图像 | 否 | 否 |
| 产生网络事件 | 否 | 是 |
| 产生导航事件 | 是 | 是 |
| 显示 PDF 和打印预览 | 是 | 是 |
在 <iframe> 中显示 PDF 和打印预览 | 是 | 是 |
Data URL
该方式的思路是:将所需 HTML 转换为 base64 字符串,使用转换后的字符串生成 Data URI,然后像下面这样加载该 URL:
var html = "<html><body>Hello</body></html>";
var base64Html = Base64.getEncoder().encodeToString(html.getBytes(UTF_8));
var dataUrl = "data:text/html;charset=utf-8;base64," + base64Html;
browser.navigation().loadUrl(dataUrl);
val html = "<html><body>Hello</body></html>"
val base64Html = Base64.getEncoder().encodeToString(html.toByteArray(UTF_8))
val dataUrl = "data:text/html;charset=utf-8;base64,$base64Html"
browser.navigation.loadUrl(dataUrl)
你也可以使用 Navigation.loadHtml(String html) 方法,它会自动根据给定的 HTML 生成 Data URI:
var html = "<html><body>Hello</body></html>";
browser.navigation().loadHtml(html);
val html = "<html><body>Hello</body></html>"
browser.navigation.loadHtml(html)
由于 Chromium 的限制,URL 字符串长度不得超过 2MB。尝试加载超过该限制的 URL 字符串将被 Chromium 忽略。
自定义方案
另一种从字符串加载 HTML 的方法基于 InterceptUrlRequestCallback。其思路是注册该回调,并拦截某个特定的 URL 请求,将所需 HTML 作为 HTTP 响应返回。例如:
InterceptUrlRequestCallback interceptCallback = params -> {
if (params.urlRequest().url().endsWith("?hello")) {
var bytes = "<html><body>Hello</body></html>".getBytes();
var job = params.newUrlRequestJob(
UrlRequestJob.Options
.newBuilder(HttpStatus.OK)
.addHttpHeader(HttpHeader.of("Content-Type", "text/html"))
.build()
);
job.write(bytes);
job.complete();
return InterceptUrlRequestCallback.Response.intercept(job);
}
return InterceptUrlRequestCallback.Response.proceed();
};
var options = EngineOptions.newBuilder(renderingMode)
.addScheme(HTTP, interceptCallback)
.build();
var engine = Engine.newInstance(options);
var browser = engine.newBrowser();
browser.navigation().loadUrl("http://load.html/?hello");
val interceptCallback = InterceptUrlRequestCallback { params ->
if (params.urlRequest().url().endsWith("?hello")) {
val bytes = "<html><body>Hello</body></html>".toByteArray()
val options = UrlRequestJobOptions(
status = HttpStatus.OK,
headers = listOf(HttpHeader("Content-Type", "text/html"))
)
val job = params.newUrlRequestJob(options).apply {
write(bytes)
complete()
}
InterceptUrlRequestCallback.Response.intercept(job)
} else {
InterceptUrlRequestCallback.Response.proceed()
}
}
val engine = Engine(renderingMode) {
schemes.add(HTTP, interceptCallback)
}
val browser = engine.newBrowser()
browser.navigation.loadUrl("http://load.html/?hello")
以 ?hello 结尾的 URL 请求将被拦截,并在浏览器中加载 <html><body>Hello</body></html> 这段HTML。
重新加载
重新加载当前已加载网页有多种方式:
使用 HTTP 缓存重新加载:
navigation.reload();
navigation.reload()
忽略 HTTP 缓存重新加载:
navigation.reloadIgnoringCache();
navigation.reloadIgnoringCache()
使用 HTTP 缓存重新加载并检查是否重新提交:
navigation.reloadAndCheckForRepost();
navigation.reloadAndCheckForRepost()
忽略 HTTP 缓存重新加载并检查是否重新提交:
navigation.reloadIgnoringCacheAndCheckForRepost();
navigation.reloadIgnoringCacheAndCheckForRepost()
停止
使用 Navigation.stop() 方法来取消任何待处理的导航或下载操作,并停止任何动态页面元素,如背景声音和动画。例如:
navigation.stop();
navigation.stop()
后退与前进
Fuzio 支持操作导航的后退-前进历史记录列表。
当你创建 Browser 实例时,默认情况下它会导航到 about:blank 网页,因此导航后退-前进列表中始终有一个条目。
要加载后退-前进列表中的上一个位置,请使用以下方式:
if (navigation.canGoBack()) {
navigation.goBack();
}
if (navigation.canGoBack) {
navigation.goBack()
}
要加载后退-前进列表中的下一个位置,请使用:
if (navigation.canGoForward()) {
navigation.goForward();
}
if (navigation.canGoForward) {
navigation.goForward()
}
要导航到后退-前进列表中特定索引处的条目,请使用:
if (index >= 0 && index < navigation.entryCount()) {
navigation.goToIndex(index);
}
if (index >= 0 && index < navigation.entryCount()) {
navigation.goToIndex(index)
}
你可以遍历后退-前进列表并获取每个导航条目的详细信息:
for (int index = 0; index < navigation.entryCount(); index++) {
var navigationEntry = navigation.entryAtIndex(index);
System.out.println("URL: " + navigationEntry.url());
System.out.println("Title: " + navigationEntry.title());
}
for (index in 0 until navigation.entryCount()) {
val navigationEntry = navigation.entryAtIndex(index)
println("URL: ${navigationEntry.url()}")
println("Title: ${navigationEntry.title()}")
}
你可以通过删除条目来修改后退-前进列表:
// 返回后退/前进列表中的条目数。
var entryCount = navigation.entryCount();
// 删除索引处的导航条目。
for (int i = entryCount - 2; i >= 0; i--) {
var success = navigation.removeEntryAtIndex(i);
System.out.println("Was the navigation entry at the index " + i +
" successfully removed? " + success);
}
// 返回后退/前进列表中的条目数。
val entryCount = navigation.entryCount()
// 删除索引处的导航条目。
for (i in entryCount - 2 downTo 0) {
val success = navigation.removeEntryAtIndex(i)
println("Was the navigation entry at the index $i successfully removed? $success")
}
过滤 URL
你可以决定是否应忽略对特定 URL 的导航请求。
以下代码演示如何忽略所有以 https://www.google 开头的 URL 的导航请求:
navigation.set(StartNavigationCallback.class, params -> {
// 忽略对以 "https://www.google" 开头的 URL 的导航请求。
if (params.url().startsWith("https://www.google")) {
return StartNavigationCallback.Response.ignore();
}
return StartNavigationCallback.Response.start();
});
navigation.register(StartNavigationCallback { params ->
// 忽略对以 "https://www.google" 开头的 URL 的导航请求。
if (params.url().startsWith("https://www.google")) {
StartNavigationCallback.Response.ignore()
} else {
StartNavigationCallback.Response.start()
}
})
自定义错误页面
当 Chromium 遇到 HTTP 或网络错误时,它会显示默认的错误页面。在 Fuzio 中,你可以用自己的页面替换该页面。

要替换当 Chromium 收到来自服务器的空响应的 HTTP 错误时出现的错误页面,请使用 ShowHttpErrorPageCallback:
navigation.set(ShowHttpErrorPageCallback.class, params -> {
var url = params.url();
var httpStatus = params.httpStatus();
if (httpStatus == NOT_FOUND) {
// 显示自定义 HTML 页面。
return ShowHttpErrorPageCallback.Response.show(
"<p>Oops! Not found!</p>");
} else {
// 显示默认的 Chromium 错误页面。
return ShowHttpErrorPageCallback.Response.showDefault();
}
});
navigation.register(ShowHttpErrorPageCallback { params ->
val url = params.url()
val httpStatus = params.httpStatus()
if (httpStatus === NOT_FOUND) {
// 显示自定义 HTML 页面。
ShowHttpErrorPageCallback.Response.show(
"<p>Oops! Not found!</p>"
)
} else {
// 显示默认的 Chromium 错误页面。
ShowHttpErrorPageCallback.Response.showDefault()
}
})
要替换当 Chromium 遇到网络错误时出现的错误页面,请使用 ShowNetErrorPageCallback:
navigation.set(ShowNetErrorPageCallback.class, params -> {
var url = params.url();
var error = params.error();
if (error == CERT_DATE_INVALID) {
// 显示自定义 HTML 页面。
return ShowNetErrorPageCallback.Response.show(
"<p>Invalid certificate!</p>");
} else {
// 显示默认的 Chromium 错误页面。
return ShowNetErrorPageCallback.Response.showDefault();
}
});
navigation.register(ShowNetErrorPageCallback { params ->
val url = params.url()
val error = params.error()
if (error == CERT_DATE_INVALID) {
// 显示自定义 HTML 页面。
ShowNetErrorPageCallback.Response.show(
"<p>Invalid certificate!</p>"
)
} else {
// 显示默认的 Chromium 错误页面。
ShowNetErrorPageCallback.Response.showDefault()
}
})
过滤资源
使用 BeforeUrlRequestCallback 回调,你可以决定是否应加载资源,例如 HTML、图像、JavaScript 或 CSS 文件、favicon 等资源。默认情况下,所有资源都会被加载。若要修改默认行为,请注册你自己的回调实现,在其中决定哪些资源应被取消,哪些资源应被加载。
以下示例演示了如何阻止所有图像:
var network = engine.network();
network.set(BeforeUrlRequestCallback.class, params -> {
if (params.urlRequest().resourceType() == IMAGE) {
return BeforeUrlRequestCallback.Response.cancel();
}
return BeforeUrlRequestCallback.Response.proceed();
});
val network = engine.network
network.register(BeforeUrlRequestCallback { params ->
if (params.urlRequest().resourceType() === IMAGE) {
BeforeUrlRequestCallback.Response.cancel()
} else {
BeforeUrlRequestCallback.Response.proceed()
}
})
导航事件
加载网页是一个复杂的过程,在此期间会触发不同的导航事件。下图显示了加载网页时可能触发导航事件的顺序:
加载开始
要在内容开始加载时接收通知,请使用 LoadStarted 事件。例如:
navigation.on(LoadStarted.class, event -> {});
navigation.subscribe<LoadStarted> { event -> }
该事件对应于标签页加载指示器开始转动的时刻。
加载完成
要在内容加载完成时接收通知,请使用 LoadFinished 事件。例如:
navigation.on(LoadFinished.class, event -> {});
navigation.subscribe<LoadStarted> { event -> }
该事件对应于标签页加载指示器停止转动的时刻。
导航开始
要在导航开始时接收通知,请使用 NavigationStarted 事件。例如:
navigation.on(NavigationStarted.class, event -> {
var url = event.url();
// 指示导航是否将在同一文档范围内执行。
var isSameDocument = event.isSameDocument();
});
navigation.subscribe<NavigationStarted> { event ->
val url = event.url()
// 指示导航是否将在同一文档范围内执行。
val isSameDocument = event.isSameDocument
}
导航停止
要在导航停止时接收通知,请使用 NavigationStopped 事件。例如:
navigation.on(NavigationStopped.class, event -> {});
navigation.subscribe<NavigationStopped> { event -> }
当通过 Navigation.stop() 方法停止导航时,会触发该事件。
导航重定向
要在导航被重定向到新 URL 时接收通知,请使用 NavigationRedirected 事件。例如:
navigation.on(NavigationRedirected.class, event -> {
// 导航重定向的 URL。
var url = event.destinationUrl();
});
navigation.subscribe<NavigationRedirected> { event ->
// 导航重定向的 URL。
val url = event.destinationUrl()
}
导航完成
要在导航完成时接收通知,请使用 NavigationFinished 事件。例如:
navigation.on(NavigationFinished.class, event -> {
var url = event.url();
var frame = event.frame();
var hasCommitted = event.hasCommitted();
var isSameDocument = event.isSameDocument();
var isErrorPage = event.isErrorPage();
if (isErrorPage) {
var error = event.error();
}
});
navigation.subscribe<NavigationFinished> { event ->
val url = event.url()
val frame = event.frame()
val hasCommitted = event.hasCommitted()
val isSameDocument = event.isSameDocument
val isErrorPage = event.isErrorPage
if (isErrorPage) {
val error = event.error()
}
}
当导航被提交、中止或被新导航替换时,会触发此事件。若要了解导航是否已提交,请使用 NavigationFinished.hasCommitted();若要了解导航是否导致错误页面,请使用 NavigationFinished.isErrorPage()。
如果该事件是由于导航已提交而触发的,那么文档加载过程此时仍可能尚未完成。
该事件也会在 same-document(在同一文档范围内)导航时触发,例如片段导航或 window.history.pushState()/window.history.replaceState(),这类导航不会导致文档本身发生变化。请使用 NavigationFinished.isSameDocument() 检查它是否为同一文档内导航。
Frame 加载完成
要在 Frame 中内容加载完成时接收通知,请使用 FrameLoadFinished 事件。例如:
navigation.on(FrameLoadFinished.class, event -> {
var url = event.url();
var frame = event.frame();
});
navigation.subscribe<FrameLoadFinished> { event ->
val url = event.url()
val frame = event.frame()
}
该事件对应于 Frame 中的内容已完全加载的时刻。
Frame 加载失败
要在 Frame 中内容因某种原因加载失败时接收通知,请使用 FrameLoadFailed 事件。例如:
navigation.on(FrameLoadFailed.class, event -> {
var url = event.url();
var error = event.error();
});
navigation.subscribe<FrameLoadFailed> { event ->
val url = event.url()
val error = event.error()
}
Frame 文档加载完成
要在 Frame 中的文档加载完成时接收通知,请使用 FrameDocumentLoadFinished 事件。例如:
navigation.on(FrameDocumentLoadFinished.class, event -> {
var frame = event.frame();
});
navigation.subscribe<FrameDocumentLoadFinished> { event ->
val frame = event.frame()
}
此时,延迟脚本已执行完成,并且标记为 “document_end” 的内容脚本已注入到 Frame 中。
加载 PDF 文件
将 PDF 文件加载到浏览器中时,请使用 PdfDocumentLoaded 事件来检测文档何时加载完成:
browser.on(PdfDocumentLoaded.class, event -> {
var url = event.url();
var frame = event.frame();
// 此事件是开始 PDF 打印的好时机。
frame.print();
});
browser.subscribe<PdfDocumentLoaded> { event ->
val url = event.url()
val frame = event.frame()
// 此事件是开始 PDF 打印的好时机。
frame.print()
}
文档可能因意外错误而无法加载,网络故障、密码错误、PDF 文件损坏等。在这种情况下,Fuzio 将发出 PdfDocumentLoadFailed 事件:
browser.on(PdfDocumentLoadFailed.class, event -> {
var url = event.url();
var frame = event.frame();
});
browser.subscribe<PdfDocumentLoadFailed> { event ->
val url = event.url()
val frame = event.frame()
}
请注意,不应使用 Navigation.loadUrlAndWait() 方法和 FrameLoadFinished 事件来等待 PDF 文档加载。

