本指南介绍如何在运行 HTML5 构建时在 JavaScript 和 Lua 代码之间进行交互。
HTML5 构建仍处于测试阶段。本指南中的内容可能会有所更改。
HTML5 提供了对各种 API(包括内置 API 和第三方集成)的极其丰富的访问权限。在 Lua 中为所有这些 API 创建绑定是不可能的。这就是 Solar2D 提供一种极其简单的方式在 Lua 和 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 调用 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 环境之间传递。如果类型无法复制或透明地转换,则会被省略。
我们已经看到从 Lua 调用 JavaScript 函数非常简单。但通常需要反过来 - 在某些 JavaScript 异步操作完成后调用 Lua 的回调函数。这很棘手,因为 JavaScript 没有提供任何与垃圾回收器交互的机制。更简单的值类型(如字符串、数字或事件表)可以在 Lua 和 JavaScript 之间复制,因此内存泄漏不是问题,因为副本在每一侧都单独处理。另一方面,函数必须与其环境保持连接才能执行预期任务。尽管如此,Solar2D 提供了两种方法,允许将 Lua 函数传递给 Javascript
在 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
分配某些内容,请确保事先将其*释放*。下一节将详细介绍这一点。
前一种方法虽然简单,但从 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 页面。我们还设置了 onerror
和 onload
处理程序,我们在其中分派消息。此外,我们在脚本加载时调用 VK.init
以初始化 VK API。查看原始代码以获取更多详细信息。
遵循现有的 插件提交指南。要添加 HTML5 JS 插件,请将您的 JS 和 Lua 文件与 metadata.lua
一起放在与其他平台插件相邻的 web
文件夹中。
示例 metadata.lua
local metadata = { plugin = { format = 'js', }, } return metadata
使用 VK Direct Games 插件 作为示例。
如果您有任何问题或想法,请随时加入论坛或 Discord上的社区讨论。