7 个版本
0.1.6 | 2022 年 12 月 9 日 |
---|---|
0.1.5 | 2022 年 12 月 1 日 |
0.1.4 | 2022 年 11 月 15 日 |
#10 在 #backends
每月 59 次下载
41KB
607 行
nx-request-handler
skyline-web 插件的消息处理器,作为 nx-request-api NPM 包的简化后端实现。
问题
此 crate 存在是为了解决特定问题。在 skyline-web 应用程序中,很难在 Rust 后端和 JavaScript 前端之间进行适当的职责分离。缺乏一种全面的方式来执行插件上的后端操作,这在前端使用标准异步模式和使用承诺进行工作的情况下变得更加严重。相反,只有一个全局事件监听器,我们可以为其添加监听器。此外,编写后端本身也是一个巨大的痛苦。大多数这些问题都归结为一个大的匹配语句,而且人们通常无法向前端返回复杂的数据类型。因此,skyline-web 应用程序在某种程度上往往难以维护。
解决方案
从根本上说,此 crate 试图确保您的后端实现 感觉像惯用的 Rust,而前端 感觉像惯用的 TypeScript/JavaScript。此 crate 和相关的 NPM 包提供了一种简化在 skyline-web 应用程序中管理前端-后端请求操作的方式。
在前端,它们将所有的 JavaScript/TypeScript 调用转换为 Promise
,提供了一种让后端操作报告进度的方法,并允许后端操作返回相对复杂的数据结构给前端(例如 list_dir_all
,它构建并返回给定目录的递归树结构)。
在 Rust 方面,它使用相对直接的构建器模式来注册命名后端操作的处理器并控制会话的生命周期。可以定义单个、简单和最小化的后端调用,这些调用合起来允许前端执行复杂的操作序列。此外,默认包含的处理器提供了插件可能从一开始就需要的大部分基本操作。
将整个系统结合起来,您可以更轻松地将业务逻辑集中在前端,并将后端(插件)视为服务器,为所有基本操作提供离散操作,其中许多操作默认内置。
示例用法
基本用法
首先,您必须打开一个 WebSession
。
然后,只需使用该会话创建一个 RequestEngine
即可。
let engine = RequestEngine::new(my_session);
然后,根据需要注册默认处理器(以下为可用默认调用)
engine.register_defaults();
这将注册各种常见需求的默认实现,例如 read_file
、write_file
、get_md5
、list_dir
、list_dir_all
、get_request
、delete_file
等。
注册您可能需要的任何自定义回调处理器
engine.register("my_call_name", Some(3), |context| {
let args = context.arguments.unwrap();
return Ok(format!("args: {}, {}, {}", args[0], args[1], args[2]));
})
my_call_name
:这是要注册的操作的字符串名称。Some(3)
:这是我们应期望的参数数量。如果从前端请求中提供的参数与该数量不匹配,则处理器甚至不会被调用,并将向前端返回错误(调用Promise
将被拒绝)。如果提供None
,则不会验证参数。|context| {...}
:这是一个闭包或函数,它接受一个MessageContext
并必须返回Result<String, String>
。然后,返回的值(Ok
或Err
)将被发送到前端,作为原始Promise
上的accept()
或reject()
。请注意,返回的字符串可以填充 JSON 数据。然后,可以通过JSON.parse()
在前端使用这些 JSON 数据来检索复杂结构。例如,默认处理器之一是list_dir_all
,它递归地返回从给定位置开始的整个目录结构,作为一个树对象。
最后,只需调用 engine.start();
。这将阻塞当前线程,监听请求并将调用委托给适当的已注册处理器,自动拒绝没有注册处理器或没有适当参数的调用。要关闭引擎,您可以在前端 API 中简单调用 exitSession()
。或者,您可以在任何已注册处理器中任意调用 context.shutdown()
。在调用 shutdown()
的处理器返回后,引擎将退出,start()
将返回。
汇总
插件端
更深入的示例用法可以在 HDR Launcher后端 中找到
// Create a WebSession instance, using skyline-web
let session = Webpage::new()
.htdocs_dir("hdr-launcher")
.file("index.html", &HTML_TEXT)
.file("index.js", &JS_TEXT)
.file("logo_full.png", &LOGO_PNG)
.background(skyline_web::Background::Default)
.boot_display(skyline_web::BootDisplay::Black)
.open_session(skyline_web::Visibility::InitiallyHidden).unwrap();
// show the session
session.show();
// create a RequestEngine, provided by nx-request-handler, to handle all requests
RequestEngine::new(session)
.register_defaults()
.register("get_sdcard_root", None, |context| {
Ok("sd:/".to_string())
})
.register("is_installed", None, |context| {
let exists = Path::new("sd:/ultimate/mods/hdr").exists();
Ok(exists.to_string())
})
.register("call_with_args", Some(2), |context| {
let args = context.arguments.unwrap();
// report progress on long operation
context.send_progress(Progress::new("Starting".to_string(), "getting started!".to_string(), 0));
do_something_long(args[0]);
// report more progress partway through
context.send_progress(Progress::new("Halfway there".to_string(), "we are halfway there!".to_string(), 50));
do_other_long_thing(args[1]);
Ok("finished!")
})
.register("get_version", None, |context| {
let path = "sd:/ultimate/mods/hdr/ui/hdr_version.txt";
let exists = Path::new(path).exists();
if !exists {
return Err("Version file does not exist!".to_string());
} else {
return match fs::read_to_string(path) {
Ok(version) => Ok(version.trim().to_string()),
Err(e) => Err(e.to_string())
}
}
})
.start();
此示例的前端
更深入的示例用法可以在 HDR Launcher前端 中找到
import { Progress, DefaultMessenger } from "nx-request-api"
let messenger = new DefaultMessenger();
try {
// examples using default messenger and register_defaults()
// download a file to a location on sd, while providing a progress callback for display
let download_result = await messenger.downloadFile(
"https://url.com/hugefile.json",
"sd:/hugefile.json",
(p: Progress) => console.info("Operation: " + p.title + ", Progress: " + p.progress)
);
// read the contents of the file (in this case a json file),
// and then parse the data into an object.
let contents = await messenger.readFile("sd:/hugefile.json");
let obj = JSON.parse(contents);
console.info(obj.some_field);
// generic invocation examples for custom handlers.
// These examples align with the custom handlers registered in the above Rust example.
// simple string-based request, no arguments
let version = await messenger.customRequest("get_sdcard_root", null);
// string-based request, with three arguments
let result = await messenger.customRequest("call_with_args", ["arg1", "arg2", "arg3"]);
// request which returns a bool instead of a string
let is_installed = await messenger.booleanRequest("is_installed", null);
// another example of a default message call
messenger.exitSession();
} catch (e) {
// This will be called if any of the requests are rejected.
// You can also use .then() and .catch() on the individual calls.
console.error(e);
}
注意:还可以通过扩展 DefaultMessenger
或 BasicMessenger
类来抽象化一些自定义调用的工作。
默认调用
在前端使用 DefaultMessenger
,并在后端 RequestEngine
上调用 register_defaults()
时,以下操作将默认支持
ping
- 如果后端响应了请求,则返回ok
read_file
- 返回文件的内容作为字符串
download_file
- 将指定的文件下载到指定的位置
delete_file
- 删除指定的文件
write_file
- 将指定的字符串写入指定的文件位置
get_md5
- 返回指定文件的md5校验和
unzip
- 将指定的文件解压到指定的位置
file_exists
- 返回给定的路径是否存在并且是一个文件
dir_exists
- 返回给定的路径是否存在并且是一个目录
list_all_files
- 递归地返回给定目录的树结构
list_dir
- 返回给定路径中的文件和目录列表(非递归)
get_request
- 执行GET请求(使用
smashnet
),并返回正文作为字符串
- 执行GET请求(使用
exit_session
- 向引擎发送关闭信号,关闭会话,取消阻塞
start()
- 向引擎发送关闭信号,关闭会话,取消阻塞
exit_application
- 完全关闭应用程序(您将返回到主菜单)
依赖项
~6-14MB
~148K SLoC