在 HTML5 构建中使用 JavaScript

本指南介绍如何在运行 HTML5 构建时在 JavaScript 和 Lua 代码之间进行交互。

注意

HTML5 构建仍处于测试阶段。本指南中的内容可能会有所更改。

HTML5 提供了对各种 API(包括内置 API 和第三方集成)的极其丰富的访问权限。在 Lua 中为所有这些 API 创建绑定是不可能的。这就是 Solar2D 提供一种极其简单的方式在 Lua 和 JavaScript 代码之间桥接的原因。

JavaScript 模块加载器

当尝试从 Lua 中 require 一个模块时,可以利用 HTML5 构建中包含的 JavaScript 模块加载器。它会在项目根目录中查找扩展名为 .js 的文件。

例如

local demo_plugin = require "demo_plugin"

在 Web 浏览器中运行时,也会查找 demo_plugin.js 作为模块加载。请注意,模拟器无法执行 JavaScript 插件。

为了加载模块,该文件必须定义一个与模块名称相同的全局对象,在本例中为 demo_plugin。以下是 demo_plugin.js 的示例内容

demo_plugin = {
    init: function() { 
        console.log( "Init was called" );
    }
}

您可以在文件的正文中执行其他 JS 代码,但建议将所有初始化操作移到函数中,例如 init

请注意,如果文件包含语法错误,则在 HTML5 构建的运行时期间会产生运行时错误。如果您看到 ERROR: module "demo_plugin" not found 错误弹出窗口,请从浏览器的开发者工具中检查 JavaScript 控制台。如果它有类似 Module file is loaded, but object 'demo_plugin' is not found! 的警告,则表示模块包含语法错误,因此未能创建全局模块对象。

为模拟器创建虚拟包装器

开发总是出错的插件可能会很麻烦,因为 JS 代码在模拟器中不起作用。为此,我们建议创建一个 Lua 包装器。它应该检查 JavaScript 是否可以执行,如果可以则执行。

为此,您需要创建两个文件:demo_plugin.lua

if system.getInfo("platform") == 'html5' then
    return require "demo_plugin_js"
else
    local lib = {}
    setmetatable( lib, {__index = function( t, k )
        return function() 
            print( "WARNING: Placeholder is called for " .. k )
        end
    end} )
    return lib
end

这个简单的 Lua 包装器将为所有请求返回一个虚拟函数,除非它在 html5 平台上运行。在这种情况下,它将返回包含在 demo_plugin_js.js 文件中的真实插件

demo_plugin_js = {
    init: function() { 
        console.log( "Init was called" );
    }
}

请注意,模块的对象也必须重命名为 demo_plugin_js

Lua 和 JavaScript 之间的桥接

我们试图使 Lua 和 JavaScript 之间的桥接尽可能无缝。我们的桥接只在一个方向上工作:您可以从 Lua 调用 JS 模块对象的方法或访问其属性。

为了演示这一点,让我们向 demo_plugin_js.js 添加一些属性

demo_plugin_js = {
    init: function() { 
        console.log( "Init was called" );
    },
    someInt: 42,
    someString: "Hello World!",
    log: function( p ) {
        console.log( "Log called", p, this.someInt, this.someString );
    }
}

以下是我们如何从 main.lua 访问它们

local demo = require "demo_plugin"
demo.init()
print( "Values:", demo.someInt, demo.someString )
demo.someInt = -1
demo.someString = "Hi!"
demo.log( 42 )

现在将其构建为 HTML5。确保打开 /index-debug.html 以查看 Lua 的 print 指令的输出。它隐藏在默认索引文件中。您应该看到一切按预期工作。方法正在被调用,打印的值正在显示,并且分配的属性也能正常工作。作为参数传递、分配给属性或由方法返回的值将被复制并进行简单的类型转换,以便在 JS 和 Lua 环境之间传递。如果类型无法复制或透明地转换,则会被省略。

将函数传递给 JavaScript

我们已经看到从 Lua 调用 JavaScript 函数非常简单。但通常需要反过来 - 在某些 JavaScript 异步操作完成后调用 Lua 的回调函数。这很棘手,因为 JavaScript 没有提供任何与垃圾回收器交互的机制。更简单的值类型(如字符串、数字或事件表)可以在 Lua 和 JavaScript 之间复制,因此内存泄漏不是问题,因为副本在每一侧都单独处理。另一方面,函数必须与其环境保持连接才能执行预期任务。尽管如此,Solar2D 提供了两种方法,允许将 Lua 函数传递给 Javascript

1. 将 Lua 函数赋值给属性

main.lua

local function callbackFunction( message )
    print( message )
end
local demo = require "demo_plugin"
demo.callback = callbackFunction
demo.execute()

demo_plugin_js.js

demo_plugin_js = {
    execute: function() { 
        this.callback( "Hello World" );
    }
}

这将按预期工作,并将“Hello World”打印到 index-debug.html 控制台。如果 demo.callback 是从 Lua 分配的,则 JS 模块加载器将负责内存管理。如果您想从 JavaScript 向 this.callback 分配某些内容,请确保事先将其*释放*。下一节将详细介绍这一点。

2. 将 Lua 函数作为参数传递

前一种方法虽然简单,但从 Lua 的角度来看却不是很优雅或地道。在 Lua 中,我们通常将函数作为参数传递。这会导致与其他语言的接口桥接出现问题,因为我们必须“持有”传递的函数。当使用 Lua C API 时,我们必须手动告诉垃圾回收器正在使用特定函数,并且不应将其作为垃圾回收。

JS 模块加载器中实现了类似的方法。当函数从 Lua 传递到 JavaScript 时,它会被转换为*引用*,您可以选择“持有”它并创建一个可调用的 JavaScript 函数。当不再需要它时,您应该释放它以防止内存泄漏。

以下是处理传递给 JavaScript 的函数的所有 API

  • LuaIsFunction 返回 boolean 值。如果传递的值表示有效的 Lua 函数*引用*,则为 true
  • LuaCreateFunction 将 Lua 函数*引用*转换为可调用的 JavaScript 函数并返回它。
  • LuaReleaseFunction 释放对底层 Lua 函数的*引用*。调用已释放的函数将导致无操作(不执行任何操作)。

操作方式应如下:接收 Lua 函数*引用*作为参数。从此*引用*创建一个 JavaScript 函数包装器。调用包装器函数,并在不再需要时释放它。

让我们用一个例子来演示它是如何工作的:main.lua

local function callbackFunction( message )
    print( message )
end
local demo = require "demo_plugin"
demo.execute( callbackFunction )

demo_plugin_js.js:

demo_plugin_js = {
    execute: function( callbackReference ) {
        if( LuaIsFunction( callbackReference ) ) {
            var f = LuaCreateFunction( callbackReference );
            f( "Hello World" );
            LuaReleaseFunction( f );
        }
    }
}

这种方法也可以用于异步调用。在这种情况下,请确保在接收参数后立即使用 LuaCreateFunction。所有函数引用在退出传递给它们的方法后立即释放

demo_plugin_js.js:

demo_plugin_js = {
    execute: function( callbackReference ) {
        if( LuaIsFunction( callbackReference ) ) {
            var f = LuaCreateFunction( callbackReference );
            setTimeout( function() {
                f( "Hello World" );
                LuaReleaseFunction( f );
            }, 1000 )
        }
    }
}

插件示例

要查看真实插件的示例,请查看我们在 GitHub 上提供的 VK Direct Games 插件。让我们回顾一些值得注意的部分。

init()

函数 init() 最有趣。它分为 2 个部分。首先 - 它记住回调函数以将事件分派给

        LuaReleaseFunction( this.callback );
        if ( LuaIsFunction( callback ) ) {
            this.callback = LuaCreateFunction( callback );
        } else {
            this.callback = function(){};
        }

第一行释放现有的回调函数(如果有)。然后,它从引用创建新的回调函数,并将其分配给 callback 属性。如果没有设置新的回调函数,我们只使用空函数作为回调函数,以防止运行时错误和过度检查。

第二部分

        if ( this.init_internal ) {
            this.init_internal();
            this.init_internal = null;
        }

它调用 init_internal 方法,然后将其设置为 null,这样它就不会再次被调用。这是确保 init_internal 只被调用一次的简单方法。

init_internal()

VK Direct Games 是一个第三方集成。我们必须使用外部 JavaScript 库来使用它。该库在 init_internal() 方法中加载和初始化。让我们来看看

    init_internal: function()
    {
        var script = document.createElement('script');
        script.setAttribute('src', 'https://vk.com/js/api/mobile_sdk.js');
        script.setAttribute('type', 'text/javascript');
        script.setAttribute('charset', 'utf-8');

        script.onerror = function()
        {
            // ...
        };

        script.onload = function()
        {
            // ... initOK = ... initFail = ...
            VK.init(initOK, initFail, '5.60');
        };
        document.head.appendChild(script);
    }

在这段代码中,我们以编程方式创建一个 <script/> 元素并将其附加到我们的 HTML 页面。我们还设置了 onerroronload 处理程序,我们在其中分派消息。此外,我们在脚本加载时调用 VK.init 以初始化 VK API。查看原始代码以获取更多详细信息。

打包 HTML5 插件

遵循现有的 插件提交指南。要添加 HTML5 JS 插件,请将您的 JS 和 Lua 文件与 metadata.lua 一起放在与其他平台插件相邻的 web 文件夹中。

示例 metadata.lua

local metadata = {
    plugin =
    {
        format = 'js',
    },
}
return metadata

使用 VK Direct Games 插件 作为示例。

支持

如果您有任何问题或想法,请随时加入论坛Discord上的社区讨论。