JavaScript
本指南描述了如何在加载的网页上访问 JavaScript、执行 JavaScript 代码、注入 Java 对象以从 JavaScript 调用 Java 等操作。
执行 JavaScript
JxBrowser 允许在加载的网页上访问和执行 JavaScript 代码。
要访问 JavaScript,请确保网页已完全加载,并启用 JavaScript。
要执行 JavaScript 代码,请使用 Frame.executeJavaScript(String)
方法。此方法会阻塞当前线程的执行,并等待给定的代码执行完毕。该方法返回一个表示执行结果的 java.lang.Object
。如果执行结果为 null
或 undefined
,则该方法返回 null
。
以下示例执行返回 document
标题的 JavaScript 代码:
String title = frame.executeJavaScript("document.title");
val title = frame.executeJavaScript<String>("document.title")
您可以执行任何 JavaScript 代码:
double number = frame.executeJavaScript("123");
boolean bool = frame.executeJavaScript("true");
String string = frame.executeJavaScript("'你好'");
JsFunction alert = frame.executeJavaScript("window.alert");
JsObject window = frame.executeJavaScript("window");
Element body = frame.executeJavaScript("document.body");
JsPromise promise = frame.executeJavaScript("Promise.resolve('Success')");
JsArray array = frame.executeJavaScript("['苹果', '香蕉']");
JsArrayBuffer arrayBuffer = frame.executeJavaScript("new ArrayBuffer(8)");
JsSet set = frame.executeJavaScript("new Set([1, 2, 3, 4])");
JsMap map = frame.executeJavaScript("new Map([['李白', '32'], ['李清照', '26']])");
val number = frame.executeJavaScript<Double>("123")
val bool = frame.executeJavaScript<Boolean>("true")
val string = frame.executeJavaScript<String>("'你好'")
val alert = frame.executeJavaScript<JsFunction>("window.alert")
val window = frame.executeJavaScript<JsObject>("window")
val body = frame.executeJavaScript<Element>("document.body")
val promise = frame.executeJavaScript<JsPromise>("Promise.resolve('Success')")
val array = frame.executeJavaScript<JsArray>("['苹果', '香蕉']")
val arrayBuffer = frame.executeJavaScript<JsArrayBuffer>("new ArrayBuffer(8)")
val set = frame.executeJavaScript<JsSet>("new Set([1, 2, 3, 4])")
val map = frame.executeJavaScript<JsMap>("new Map([['李白', '32'], ['李清照', '26']])")
如果您不想阻塞当前线程的执行,您可以使用 Frame.executeJavaScript(String javaScript, Consumer<?> callback)
方法。此方法异步执行给定的 JavaScript 代码,并通过提供的 callback
返回执行结果:
frame.executeJavaScript("document.body", (Consumer<Element>) body -> {
String html = body.innerHtml();
});
frame.executeJavaScript("document.body", Consumer<Element> { body ->
val html = body.innerHtml()
})
类型转换
JavaScript 和 Java 使用不同的原始类型。JxBrowser 实现了从 JavaScript 到 Java 类型的自动转换,反之亦然。
将 JavaScript 转换为 Java
以下规则用于将 JavaScript 转换为 Java 类型:
- JavaScript numbers 转换为
java.lang.Double
- JavaScript
string
转换为java.lang.String
- JavaScript
boolean
转换为java.lang.Boolean
- JavaScript
null
或者undefined
转换为null
- JavaScript
Promise
转换为JsPromise
- JavaScript 对象被包装为
JsObject
- JavaScript 函数被包装为
JsFunction
- JavaScript DOM 节点对象被包装为
JsObject
和EventTarget
- JavaScript
ArrayBuffer
被包装为JsArrayBuffer
- JavaScript
Array
被包装为JsArray
- JavaScript
Set
被包装为JsSet
- JavaScript
Map
被包装为JsMap
在上面的例子中我们知道 document.title
是一个字符串,所以我们将返回值设置为 java.lang.String
。
将 Java 转换为 JavaScript
以下规则用于将 Java 转换为 JavaScript 类型:
java.lang.Double
转换为 JavaScriptNumber
java.lang.String
转换为 JavaScriptstring
java.lang.Boolean
转换为 JavaScriptboolean
- Java
null
转换为 JavaScriptnull
JsObject
转换为适当的 JavaScript objectJsPromise
转换为 JavaScriptPromise
EventTarget
转换为适当的 JavaScript DOM 节点对象java.lang.Object
被包装为 JavaScript 代理对象java.util.List<?>
转换为 JavaScriptArray
或代理对象JsArray
转换为 JavaScriptArray
java.util.Set<?>
转换为 JavaScriptSet
或代理对象JsSet
转换为 JavaScriptSet
java.util.Map<?,?>
转换为 JavaScriptMap
或代理对象JsMap
转换为 JavaScriptMap
byte[]
转换为 JavaScriptArrayBuffer
JsArrayBuffer
转换为 JavaScriptArrayBuffer
如果将非原始 Java 对象传递给 JavaScript,它将被转换为代理对象。对此对象的方法和属性调用将委托给 Java 对象。出于安全原因,JavaScript 只能访问那些使用 @JsAccessible
注释或 JsAccessibleTypes
类显式标记为可访问的注入 Java 对象的方法和字段。
如果 Java 集合没有使用 @JsAccessible
或通过 JsAccessibleTypes
类来使 JavaScript 可以访问,它们将被转换为 JavaScript 集合。转换后的集合内容是 Java 集合的深拷贝。JavaScript 中对转换后集合的修改不会影响 Java 中的集合。
如果 Java 集合通过 @JsAccessible
注解或通过 JsAccessibleTypes
类被设置为 JavaScript 可访问,它们将被包装成 JavaScript 代理对象。此类代理对象可用于修改 Java 中的集合。
DOM 包装器
根据自动类型转换的规则,JavaScript DOM 对象会被同时包装为 JsObject
和 EventTarget
。这使您能够通过 JxBrowser DOM API 操作 JavaScript DOM 对象。
在以下示例中,我们返回表示 JavaScript DOM 对象的 document
。在这种情况下,返回值可以设置为 JsObject
或 Document
:
Document document = frame.executeJavaScript("document");
val document = frame.executeJavaScript<Document>("document")
JsObject document = frame.executeJavaScript("document");
val document = frame.executeJavaScript<JsObject>("document")
使用 JsObject
要从 Java 代码中操作 JavaScript 对象,请使用 JsObject
类。该类允许操作对象的属性并调用其函数。
属性
要获取 JavaScript 对象的属性名称,包括原型对象的属性,请使用 propertyNames()
方法:
List<String> propertyNames = jsObject.propertyNames();
val propertyNames = jsObject.propertyNames()
要检查 JavaScript 对象是否具有指定属性,请使用 hasProperty(String)
方法:
boolean has = jsObject.hasProperty("<property-name>");
val has = jsObject.hasProperty("<property-name>")
要通过名称获取 JavaScript 对象属性的值,请使用 property(String)
。例如:
JsObject document = frame.executeJavaScript("document");
document.property("title").ifPresent(title -> {});
val document = frame.executeJavaScript<JsObject>("document")!!
document.property<String>("title").ifPresent { title -> }
返回值表示 java.lang.Object
它可以被设置为所需的类型。请参阅类型转换。
您可以使用以下方法移除属性:
boolean success = jsObject.removeProperty("<property-name>");
val success = jsObject.removeProperty("<property-name>")
函数
要调用具有所需名称和参数的函数,请使用 call(String methodName, Object... args)
方法。以下示例演示了如何调用 JavaScript 中的 document.getElementById()
函数:
JsObject element = document.call("getElementById", "elementId");
val element: JsObject = document.call("getElementById", "elementId")
这相当于 JavaScript 中的以下代码:
var element = document.getElementById("demo");
如果在函数执行期间发生错误,该方法将抛出 JsException
。
关闭
具有 JsObject
对应项的 V8 对象不会受到 V8 垃圾回收机制的管理。默认情况下,我们将这些对象保留在内存中,直到页面被卸载。
为了优化内存使用,您可以基于每个对象启用垃圾回收:
jsObject.close();
jsObject.close()
关闭 JsObject
会将相应的 Blink 对象标记为可回收对象,但它不会立即释放该对象。在调用 close()
方法后,尝试使用 JsObject
将导致 ObjectClosedException
。
JsFunctionCallback
另一种从 JavaScript 调用 Java 的方法是使用 JsFunctionCallback
。
JavaScript-Java 桥接允许您将 JsFunctionCallback
与 JavaScript 属性相关联,该属性将被视为可以在 JavaScript 代码中调用的函数。
例如,您可以使用以下代码注册一个与 JsFunctionCallback
实例相关联的 JavaScript 函数:
JsObject window = frame.executeJavaScript("window");
if (window != null) {
window.putProperty("sayHello", (JsFunctionCallback) args ->
"你好," + args[0]);
}
val window = frame.executeJavaScript<JsObject>("window")
window?.putProperty("sayHello", JsFunctionCallback { args -> "你好,${args[0]}" })
现在,在 JavaScript 中,您可以通过以下方式调用此函数:
window.sayHello('李白');
JsFunction
从 7.7 版本开始,您可以直接从 Java 代码中使用 JavaScript 函数,并将对函数的引用从 JavaScript 传递给 Java。例如:
JsObject window = frame.executeJavaScript("window");
if (window != null) {
JsFunction alert = frame.executeJavaScript("window.alert");
if (alert != null) {
alert.invoke(window, "你好,世界!");
}
}
val window = frame.executeJavaScript<JsObject>("window")
if (window != null) {
val alert = frame.executeJavaScript<JsFunction>("window.alert")
alert?.invoke<Any>(window, "你好,世界!")
}
JsPromise
从 7.17 版本开始,您可以直接在 Java 代码中使用 JavaScript Promises。例如:
JsPromise promise = frame.executeJavaScript(
"new Promise(function(resolve, reject) {\n"
+ " setTimeout(function() {\n"
+ " resolve('你好 Java!');\n"
+ " }, 2000);"
+ "})");
promise.then(results -> {
System.out.println(results[0]);
return promise;
}).then(results -> {
System.out.println(results[0]);
return promise;
}).catchError(errors -> {
System.out.println(errors[0]);
return promise;
});
val promise = frame.executeJavaScript<JsPromise>(
"""new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('你好 Java!');
}, 2000);
})
"""
)!!
promise.then { results ->
println(results[0])
promise
}.then { results ->
println(results[0])
promise
}.catchError { errors ->
println(errors[0])
promise
}
从 JavaScript 调用 Java
当您将 java.lang.Object
作为属性值传递,或者在调用 JavaScript 函数时作为参数传递时,Java 对象将自动包装成 JavaScript 对象。
它允许将 Java 对象注入 JavaScript,并从 JavaScript 调用其公共方法和字段。
出于安全原因,只有使用 @JsAccessible
注解的公共非静态方法和字段,或者在标记了 @JsAccessible
注解的类中声明的方法和字段,才能从 JavaScript 中访问。带注释的 protected、private 或 package-private 方法和字段,或者在类中使用此类修饰符声明的方法和字段,在 JavaScript 中是不可访问的。
要将 Java 对象注入 JavaScript,请定义 Java 对象类,并使用 @JsAccessible
标记应可从 JavaScript 中访问的公共方法:
public final class JavaObject {
@JsAccessible
public String sayHelloTo(String firstName) {
return "你好 " + firstName + "!";
}
}
class JavaObject {
@JsAccessible
fun sayHelloTo(firstName: String) = "你好 $firstName!"
}
在加载的网页上执行 JavaScript 之前,将一个 Java 对象的实例注入到 JavaScript 中:
browser.set(InjectJsCallback.class, params -> {
JsObject window = params.frame().executeJavaScript("window");
window.putProperty("java", new JavaObject());
return InjectJsCallback.Response.proceed();
});
browser.set(InjectJsCallback::class.java, InjectJsCallback { params ->
val window = params.frame().executeJavaScript<JsObject>("window")
window?.putProperty("java", JavaObject())
InjectJsCallback.Response.proceed()
})
现在您可以从 JavaScript 引用该对象并调用其方法:
window.java.sayHelloTo("李白");
注释规则
@JsAccessible
注解允许将注入的 Java 对象的方法和字段暴露给 JavaScript。
您只能使公共类型、方法和字段可访问。支持的完整情况列表如下:
- 顶级类或接口
- 嵌套的静态类或接口
- 类或接口的非静态方法
- 类的非静态字段
该注释不能应用于非公共类型、方法和字段。非公共类型的公共方法和字段被认为是非公共的。当您注释一个类型时,其所有公共方法和字段都会变得对 JavaScript 可访问。当您注释一个未注释类型的方法或字段时,只有注释的成员会对 JavaScript 可访问。
当可访问的方法在子类中被重写时,它仍然保持可访问。这意味着您可以使一个接口可访问,并将其任何实现传递给 JavaScript:接口中声明的所有方法都将从 JavaScript 可访问。在实现类中声明的其他方法和字段将保持不可访问,除非您显式地用此注释标记它们或整个类型。
另一种使类型可从 JavaScript 访问的方法是使用 JsAccessibleTypes
。当您想要一个核心 Java 类型之一(例如 java.util.List
)可访问,或第三方库中的类型可访问而无法使用此注释访问时,这特别有用。
例子:
公共顶级类的注释方法和字段是可访问的:
public final class TopClass {
@JsAccessible
public Object accessibleField;
@JsAccessible
public void accessibleMethod() {}
}
class TopClass {
@JsAccessible
var accessibleField: Any? = null
@JsAccessible
fun accessibleMethod() {}
}
公共静态嵌套类的注释方法和字段是可访问的:
public final class TopClass {
public static class NestedClass {
@JsAccessible
public Object accessibleField;
@JsAccessible
public void accessibleMethod() {}
}
}
class TopClass {
class NestedClass {
@JsAccessible
var accessibleField: Any? = null
@JsAccessible
fun accessibleMethod() {}
}
}
带注释类的未注释方法和字段是可访问的:
@JsAccessible
public final class TopClass {
public Object accessibleField;
public void accessibleMethod() {}
}
@JsAccessible
class TopClass {
var accessibleField: Any? = null
fun accessibleMethod() {}
}
带注释的基类的方法和字段可以从继承者访问:
public final class TopClass {
@JsAccessible
public static class BaseNestedClass {
public Object accessibleFieldFromInheritor;
public void accessibleMethodFromInheritor() {}
}
public static class NestedClass extends BaseNestedClass {
public Object inaccessibleField;
public void inaccessibleMethod() {}
}
}
class TopClass {
@JsAccessible
open class BaseNestedClass {
var accessibleFieldFromInheritor: Any? = null
fun accessibleMethodFromInheritor() {}
}
class NestedClass : BaseNestedClass() {
var inaccessibleField: Any? = null
fun inaccessibleMethod() {}
}
}
如果继承的方法和字段或它们所声明的类没有被注释,则不可访问:
public final class TopClass {
public static class BaseNestedClass {
public Object inaccessibleField;
public void inaccessibleMethod() {}
}
@JsAccessible
public static class NestedClass extends BaseNestedClass {
public Object accessibleField;
public void accessibleMethod() {}
}
}
class TopClass {
open class BaseNestedClass {
var inaccessibleField: Any? = null
fun inaccessibleMethod() {}
}
@JsAccessible
class NestedClass : BaseNestedClass() {
var accessibleField: Any? = null
fun accessibleMethod() {}
}
}
覆盖类的方法是可访问的:
public final class TopClass {
public static class BaseNestedClass {
@JsAccessible
public void method() {}
}
public static class NestedClass extends BaseNestedClass {
@Override
public void method() {} // 可访问
}
}
class TopClass {
open class BaseNestedClass {
@JsAccessible
open fun method() {
}
}
class NestedClass : BaseNestedClass() {
override fun method() {} // 可访问
}
}
已实现接口的方法是可访问的:
public static class TopClass {
public interface NestedInterface {
@JsAccessible
void method();
}
public static class AccessibleImplementor implements NestedInterface {
@Override
public void method() { } // 可访问
}
}
class TopClass {
interface NestedInterface {
@JsAccessible
fun method()
}
class AccessibleImplementor : NestedInterface {
override fun method() {} // 可访问
}
}
如果可访问的 Java 方法的签名包含原始数字参数,则将检查从 JavaScript 传递的数字是否有可能转换为 Java 参数类型。如果可以在无损情况下执行转换,并且没有找到其他合适的重载方法,则调用该方法。
如果存在多个可以接受传递参数的方法,JavaScript 将会抛出异常,指示请求的方法调用存在歧义,无法执行。
如果找不到与请求的名称对应的方法或字段,JavaScript 将会抛出异常,指示请求的成员不存在。
如果 JavaScript 请求的方法和字段具有相同名称,则 JavaScript 将会抛出异常,指示请求的成员不明确,无法访问。
自动类型转换
当从 JavaScript 调用注入的 Java 对象的公共方法时,JavaScript-Java 桥接提供自动类型转换功能。
该库会在可能的情况下,自动将给定的 JavaScript Number
转换为所需的 Java 类型。如果我们检测到给定的数字不能无损地转换为 Java 的某个类型,例如 byte
,那么该库会抛出一个异常并通知 JavaScript 没有合适的 Java 方法。如果给定的值可以在无损情况下进行转换,那么库会进行转换并调用相应的 Java 方法。
例如,如果您将以下 Java 对象注入到 JavaScript 中:
public final class JavaObject {
@JsAccessible
public int method(int intValue) {
return intValue;
}
}
class JavaObject {
@JsAccessible
fun method(intValue: Int) = intValue
}
然后您就可以从 JavaScript 调用它,并传递一个可以无损转换为 Integer
的 JavaScript Number
值:
window.javaObject.method(123);
但是,如果您传递的 Double
值无法在无损情况下转换为 Integer
,则会出现错误:
window.javaObject.method(3.14); // <- error
从库中调用
注入的 Java 对象是一种特殊的对象。它们的行为与常规 JavaScript 对象不同,并且不打算直接传递给 JavaScript 库。
在 JavaScript 库中使用它们之前,我们建议使用 Proxy 进行封装。在这个例子中,我们创建了一个代理对象,它实现了对 JS 可访问成员的读取访问:
const proxy = new Proxy({__java: myJavaObject}, {
get(target, prop, receiver) {
for (let javaMemberName in target.__java) {
if (prop === javaMemberName) {
return target.__java[prop]
}
}
return Reflect.get(...arguments);
},
...
});
控制台消息
JxBrowser 允许接收通过 console.log()
JavaScript 函数发送到控制台的所有输出消息。您可以监听以下级别的消息:
DEBUG
LOG
WARNING
ERROR
要在控制台收到消息时获得通知,请使用 ConsoleMessageReceived
事件。例如:
browser.on(ConsoleMessageReceived.class, event -> {
ConsoleMessage consoleMessage = event.consoleMessage();
ConsoleMessageLevel level = consoleMessage.level();
String message = consoleMessage.message();
});
browser.on(ConsoleMessageReceived::class.java) { event ->
val consoleMessage = event.consoleMessage()
val level = consoleMessage.level()
val message = consoleMessage.message()
}