Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

JavaScriptBridge 单例

在 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 时可能会丢失精度)。其他任何类型(即对象、数组、函数)都被视为 JavaScriptObject

回调

从 Godot 调用 JavaScript 代码固然方便,但有时你需要反过来从 JavaScript 调用 Godot 函数。

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

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 = ''

警告

通过 JavaScriptBridge.get_interface() 创建的回调方法(如上例中的 _my_callback必须接受且仅接受一个 Array 参数,该参数将是 JavaScript 的 arguments 对象转换成的数组。否则,回调方法将不会被调用。

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

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.")

我能使用我喜欢的库吗?

大概率可以。首先,你必须在页面中包含你的库。你可以在导出时自定义头部引用(见下文),甚至可以编写你自己的模板

在下面的示例中,我们自定义头部包含以从内容分发网络添加外部库(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 的多行字符串使用三引号 """ 包围(如上文中的 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() 函数将该功能暴露给脚本,让你可以下载任何生成的缓冲区数据。

以下是使用它的最小示例:

extends Node

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")