JavaScript

本指南描述了如何在加载的网页上访问 JavaScript、执行 JavaScript 代码、注入 Java 对象以从 JavaScript 调用 Java 等。

执行 JavaScript

JxBrowser 允许在加载的网页上访问和执行 JavaScript 代码。

要访问 JavaScript,请确保网页已完全加载并启用 JavaScript。

要执行 JavaScript 代码,请使用 Frame.executeJavaScript(String)方法。 此方法阻止当前线程执行并等待直到给定代码被执行。 该方法返回一个表示执行结果的 java.lang.Object 。 如果执行结果为 nullundefined,则该方法返回 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("'Hello'");
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("['Apple', 'Banana']");
JsArrayBuffer arrayBuffer = frame.executeJavaScript("new ArrayBuffer(8)");
JsSet set = frame.executeJavaScript("new Set([1, 2, 3, 4])");
JsMap map = frame.executeJavaScript("new Map([['John', '32'], ['Mary', '26']])");
val number = frame.executeJavaScript<Double>("123")
val bool = frame.executeJavaScript<Boolean>("true")
val string = frame.executeJavaScript<String>("'Hello'")
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>("['Apple', 'Banana']")
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([['John', '32'], ['Mary', '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 节点对象被包装为 JsObjectEventTarget
  • JavaScript ArrayBuffer 被包装为 JsArrayBuffer
  • JavaScript Array 被包装为 JsArray
  • JavaScript Set 被包装为 JsSet
  • JavaScript Map 被包装为 JsMap

在上面的例子中我们知道 document.title 是一个字符串,所以我们将返回值设置为 java.lang.String

将 Java 转换为 JavaScript

以下规则用于将 Java 转换为 JavaScript 类型:

  • java.lang.Double 转换为 JavaScript Number
  • java.lang.String 转换为 JavaScript string
  • java.lang.Boolean 转换为 JavaScript boolean
  • Java null 转换为 JavaScript null
  • JsObject 转换为适当的 JavaScript object
  • JsPromise 转换为 JavaScript Promise
  • EventTarget 转换为适当的 JavaScript DOM 节点对象
  • java.lang.Object 被包装为一个 JavaScript 代理对象
  • java.util.List<?> 转换为 JavaScript Array 或代理对象
  • JsArray 转换为 JavaScript Array
  • java.util.Set<?> 转换为 JavaScript Set 或代理对象
  • JsSet 转换为 JavaScript Set
  • java.util.Map<?,?> 转换为 JavaScript Map 或代理对象
  • JsMap 转换为 JavaScript Map
  • byte[] 转换为 JavaScript ArrayBuffer
  • JsArrayBuffer 转换为 JavaScript ArrayBuffer

如果将非原始 Java 对象传递给 JavaScript,它将被转换为代理对象。 对此对象的方法和属性调用将委托给 Java 对象。 出于安全原因,JavaScript 只能访问那些使用 @JsAccessible 注释或通过 JsAccessibleTypes 类显式标记为可访问的注入 Java 对象的方法和字段。

使用 @JsAccessible注释或通过 JsAccessibleTypes 类无法访问 JavaScript 的 Java 集合将转换为 JavaScript 集合。 转换后的集合内容是 Java 集合的深拷贝。在 JavaScript 中修改转换后的集合不会影响 Java 中的集合。

使用 @JsAccessible 注释或通过 JsAccessibleTypes 类使 JavaScript 可以访问的 Java 集合被包装到 JavaScript 代理对象中。 此类代理对象可用于修改 Java 中的集合。

DOM 包装器

根据自动类型转换的规则,JavaScript DOM 对象被包装为JsObjectEventTarget。 它允许您通过 JxBrowser DOM API 使用 JavaScript DOM 对象。

在以下示例中,我们返回表示 JavaScript DOM 对象的 document 。 在这种情况下,返回值可以设置为 JsObjectDocument

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) 方法。 以下示例演示了如何调用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 ->
            "Hello, " + args[0]);
}
val window = frame.executeJavaScript<JsObject>("window")
window?.putProperty("sayHello", JsFunctionCallback { args -> "Hello, ${args[0]}" })

现在,在 JavaScript 中,您可以通过以下方式调用此函数:

window.sayHello('John');

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, "Hello world!");
    }
}
val window = frame.executeJavaScript<JsObject>("window")
if (window != null) {
    val alert = frame.executeJavaScript<JsFunction>("window.alert")
    alert?.invoke<Any>(window, "Hello world!")
}

JsPromise

7.17 开始,您可以直接在 Java 代码中使用 JavaScript Promises。 例如:

JsPromise promise = frame.executeJavaScript(
        "new Promise(function(resolve, reject) {\n"
                + "    setTimeout(function() {\n"
                + "        resolve('Hello 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('Hello 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 中访问。 带注释受保护的、私有的或包私有的方法和字段,或者在类中使用此类修饰符声明的方法和字段,在 JavaScript 中仍然不可访问。

要将 Java 对象注入 JavaScript,请定义 Java 对象类并使用 @JsAccessible 标记应可从 JavaScript 访问的公共方法:

public final class JavaObject {
    @JsAccessible
    public String sayHelloTo(String firstName) {
        return "Hello " + firstName + "!";
    }
}
class JavaObject {
    @JsAccessible
    fun sayHelloTo(firstName: String) = "Hello $firstName!"
}

在加载的网页上执行 JavaScript 之前,向 JavaScript 中注入一个 Java 对象的实例:

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("John");

注释规则

@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() {} // accessible
   }
}
class TopClass {
    open class BaseNestedClass {
        @JsAccessible
        open fun method() {
        }
    }
    class NestedClass : BaseNestedClass() {
        override fun method() {} // accessible
    }
}

已实现的接口方法是可访问的:

public static class TopClass {
   public interface NestedInterface {
       @JsAccessible
       void method();
   }
   public static class AccessibleImplementor implements NestedInterface {
       @Override
       public void method() { } // accessible
   }
}
class TopClass {
    interface NestedInterface {
        @JsAccessible
        fun method()
    }
    class AccessibleImplementor : NestedInterface {
        override fun method() {} // accessible
    }
}

如果可访问的 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 库中使用它们之前,我们建议将它们包装到代理中。 在这个例子中,我们创建了一个代理对象,它实现了对 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()
}
Go Top