The JavaScriptBridge singleton

在 Web 构建中,JavaScriptBridge 单例允许与 JavaScript 和 Web 浏览器交互,并且可用于实现一些 Web 平台独有的功能。

与 JavaScript 交互

有时,在将 Godot 导出到 Web 时,可能需要与第三方 SDK、库等外部 JavaScript 代码进行交互,或者只是访问 Godot 未直接公开的浏览器功能。

JavaScriptBridge 单例提供了将原生 JavaScript 对象包装到 Godot JavaScriptObject 内的方法,该对象能够在 Godot 编写脚本(例如 GDScript 和 C#)的上下文中自由使用。

JavaScriptBridge.get_interface() 方法在全局范围内检索对象。

extends Node

func _ready():
    # Retrieve the `window.console` object.
    var console = JavaScriptBridge.get_interface("console")
    # Call the `window.console.log()` method.
    console.log("test")

JavaScriptBridge.create_object() 通过 JavaScript new 构造函数创建一个新对象。

extends Node

func _ready():
    # Call the JavaScript `new` operator on the `window.Array` object.
    # Passing 10 as argument to the constructor:
    # JS: `new Array(10);`
    var arr = JavaScriptBridge.create_object("Array", 10)
    # Set the first element of the JavaScript array to the number 42.
    arr[0] = 42
    # Call the `pop` function on the JavaScript array.
    arr.pop()
    # Print the value of the `length` property of the array (9 after the pop).
    print(arr.length)

如你所见,通过将 JavaScript 对象包装到 JavaScriptObject 中,你可以像与原生 Godot 对象一样与它们进行交互,调用它们的方法,以及检索(甚至设置)它们的属性。

基本类型(整数、浮点数、字符串、布尔值)会自动转换(浮点数从 Godot 转换为 JavaScript 时可能会丢失精度)。其他任何类型(即对象、数组、函数)都被视为 JavaScriptObjects 本身。

回调

从 Godot 调用 JavaScript 代码很好,但有时你需要从 JavaScript 调用 Godot 函数。

这个例子稍微复杂一点。JavaScript 依赖于垃圾回收,而 Godot 使用引用计数进行内存管理。这意味着你必须显式创建回调(它们本身作为 JavaScriptObjects 返回)并且必须保留它们的引用。

JavaScript 传递给回调的参数将作为单个 Godot Array 传递。

extends Node

# Here we create a reference to the `_my_callback` function (below).
# This reference will be kept until the node is freed.
var _callback_ref = JavaScriptBridge.create_callback(_my_callback)

func _ready():
    # Get the JavaScript `window` object.
    var window = JavaScriptBridge.get_interface("window")
    # Set the `window.onbeforeunload` DOM event listener.
    window.onbeforeunload = _callback_ref

func _my_callback(args):
    # Get the first argument (the DOM event in our case).
    var js_event = args[0]
    # Call preventDefault and set the `returnValue` property of the DOM event.
    js_event.preventDefault()
    js_event.returnValue = ''

警告

Callback methods created via JavaScriptBridge.get_interface() (_my_callback in the above example) must take exactly one Array argument, which is going to be the JavaScript arguments object converted to an array. Otherwise, the callback method will not be called.

下面是另一个示例,它向用户请求通知权限并在授予权限后异步等待发送通知:

extends Node

# Here we create a reference to the `_on_permissions` function (below).
# This reference will be kept until the node is freed.
var _permission_callback = JavaScriptBridge.create_callback(_on_permissions)

func _ready():
    # NOTE: This is done in `_ready` for simplicity, but SHOULD BE done in response
    # to user input instead (e.g. during `_input`, or `button_pressed` event, etc.),
    # otherwise it might not work.

    # Get the `window.Notification` JavaScript object.
    var notification = JavaScriptBridge.get_interface("Notification")
    # Call the `window.Notification.requestPermission` method which returns a JavaScript
    # Promise, and bind our callback to it.
    notification.requestPermission().then(_permission_callback)

func _on_permissions(args):
    # The first argument of this callback is the string "granted" if the permission is granted.
    var permission = args[0]
    if permission == "granted":
        print("Permission granted, sending notification.")
        # Create the notification: `new Notification("Hi there!")`
        JavaScriptBridge.create_object("Notification", "Hi there!")
    else:
        print("No notification permission.")

可以用我喜欢的库吗?

You most likely can. First, you have to include your library in the page. You can customize the Head Include during export (see below), or even write your own template.

在下面的示例中,我们自定义 头部引用 以从内容分发网络添加外部库(axios),并添加第二个 <script> 标签来定义我们自己的自定义函数:

<!-- Axios -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<!-- Custom function -->
<script>
function myFunc() {
    alert("My func!");
}
</script>

然后我们可以从 Godot 访问库和函数,就像在前面的例子中所做的那样:

extends Node

# Here create a reference to the `_on_get` function (below).
# This reference will be kept until the node is freed.
var _callback = JavaScriptBridge.create_callback(_on_get)

func _ready():
    # Get the `window` object, where globally defined functions are.
    var window = JavaScriptBridge.get_interface("window")
    # Call the JavaScript `myFunc` function defined in the custom HTML head.
    window.myFunc()
    # Get the `axios` library (loaded from a CDN in the custom HTML head).
    var axios = JavaScriptBridge.get_interface("axios")
    # Make a GET request to the current location, and receive the callback when done.
    axios.get(window.location.toString()).then(_callback)

func _on_get(args):
    OS.alert("On Get")

eval 接口

eval 方法的工作原理与同名的 JavaScript 函数类似。它以字符串作为参数,并将其作为 JavaScript 代码执行。这允许以 Godot 中集成的脚本语言无法实现的方式与浏览器进行交互。

func my_func():
    JavaScriptBridge.eval("alert('Calling JavaScript per GDScript!');")

最后一个JavaScript语句的值转换为GDScript值,并在某些情况下由 eval() 返回:

  • JavaScript number 会以 float 返回

  • JavaScript boolean 会以 bool 返回

  • JavaScript string 会以 String 返回

  • JavaScript ArrayBufferTypedArrayDataView 会以 PackedByteArray 形式返回

func my_func2():
    var js_return = JavaScriptBridge.eval("var myNumber = 1; myNumber + 2;")
    print(js_return) # prints '3.0'

任何其他JavaScript值都返回为 null .

HTML5 导出模板可能在不支持单例的情况下构建,以提高安全性。使用这类模板,以及在 HTML5 以外的平台上,调用 JavaScriptBridge.eval 也将返回 null。可以使用 web 功能标签检查单例的可用性:

func my_func3():
    if OS.has_feature('web'):
        JavaScriptBridge.eval("""
            console.log('The JavaScriptBridge singleton is available')
        """)
    else:
        print("The JavaScriptBridge singleton is NOT available")

小技巧

GDScript中的多行字符串由3双引号 """ 包围, 如同上文中的 my_func3() 那样, 有助于保证JavaScript代码的可读性.

eval 方法还接受第二个可选的布尔参数,该参数指定是否在全局执行上下文中执行代码,默认为 false 以防止污染全局命名空间:

func my_func4():
    # execute in global execution context,
    # thus adding a new JavaScript global variable `SomeGlobal`
    JavaScriptBridge.eval("var SomeGlobal = {};", true)

下载文件

可以通过直接与 JavaScript 交互,将文件(例如,保存的游戏)从 Godot Web 导出下载到用户的计算机,但鉴于这是一个非常常见的用例,Godot 通过专用的 JavaScriptBridge.download_buffer() 函数将该功能公开给脚本,该函数可让你下载任何生成的缓冲区。

这是一个有关如何使用它的最小示例:

扩展节点

func _ready():
    # Asks the user download a file called "hello.txt" whose content will be the string "Hello".
    JavaScriptBridge.download_buffer("Hello".to_utf8_buffer(), "hello.txt")

这是关于如何下载以前保存的文件的更完整的示例:

extends Node

# Open a file for reading and download it via the JavaScript singleton.
func _download_file(path):
    var file = FileAccess.open(path, FileAccess.READ)
    if file == null:
        push_error("Failed to load file")
        return
    # Get the file name.
    var fname = path.get_file()
    # Read the whole file to memory.
    var buffer = file.get_buffer(file.get_len())
    # Prompt the user to download the file (will have the same name as the input file).
    JavaScriptBridge.download_buffer(buffer, fname)

func _ready():
    # Create a temporary file.
    var config = ConfigFile.new()
    config.set_value("option", "one", false)
    config.save("/tmp/test.cfg")

    # Download it
    _download_file("/tmp/test.cfg")