List icon 目录

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("'你好'");
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 节点对象被包装为 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 对象的方法和字段。

如果 Java 集合没有使用 @JsAccessible 或通过 JsAccessibleTypes 类来使 JavaScript 可以访问,它们将被转换为 JavaScript 集合。转换后的集合内容是 Java 集合的深拷贝。JavaScript 中对转换后集合的修改不会影响 Java 中的集合。

如果 Java 集合通过 @JsAccessible 注解或通过 JsAccessibleTypes 类被设置为 JavaScript 可访问,它们将被包装成 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) 方法。以下示例演示了如何调用 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()
}
Go Top