5个版本 (3个重大更改)
新 0.7.0 | 2024年8月9日 |
---|---|
0.3.0 | 2024年7月2日 |
0.2.1 | 2024年2月29日 |
0.2.0 | 2024年2月27日 |
0.1.0 | 2024年2月24日 |
42 在 模板引擎 中
97 每月下载量
33KB
335 行
Rhai引擎对Loco的集成
为什么包含脚本引擎
尽管基于Loco的系统通常为了最大性能而编译,但有时用户需求是动态的,需要适应,最好是不需要重新编译。
脚本在以下情况下非常有用
-
在不同地点安装的复杂自定义配置或自定义业务逻辑,无需重新编译。在另一种编程语言中,可以使用DLL或动态链接库。
-
快速适应不断变化的环境(例如,处理新的数据格式、输入更改或新的用户错误等),而不需要硬编码规则(这些规则可能很快又需要更改)。
-
通过快速迭代测试新功能或业务逻辑(无需重新编译)。一旦稳定,最终版本可以转换为本地Rust代码以提高性能。
-
在脚本中开发Tera过滤器,以便快速迭代。有用的过滤器可以转换为Rust本地过滤器。这通常可以通过Tera宏来实现,但Rhai脚本语言比Tera表达式更强大和更具表现力,允许实现更复杂的逻辑。
用法
在Cargo.toml
中导入rhai-loco
[dependencies]
rhai-loco = "0.3.0"
配置
可以使用initializers
中的Loco config
部分来设置Rhai引擎的选项。
# Initializers configuration
initializers:
# Scripting engine configuration
scripting:
# Directory holding scripts
scripts_path: assets/scripts
# Directory holding Tera filter scripts
filters_path: assets/scripts/tera/filters
启用脚本化的Tera过滤器
修改ViewEngineInitializer
下的src/initializers/view_engine.rs
┌─────────────────────────────────┐
│ src/initializers/view_engine.rs │
└─────────────────────────────────┘
// Within this method...
async fn after_routes(&self, router: AxumRouter, ctx: &AppContext) -> Result<AxumRouter> {
let mut tera_engine = engines::TeraView::build()?;
:
:
///////////////////////////////////////////////////////////////////////////////////
// Add the following to enable scripted Tera filters
// Get scripting engine configuration object
let config = ctx.config.initializers.as_ref()
.and_then(|m| m.get(rhai_loco::ScriptingEngineInitializer::NAME))
.cloned().unwrap_or_default();
let config: rhai_loco::ScriptingEngineInitializerConfig = serde_json::from_value(config)?;
if config.filters_path.is_dir() {
// This code is duplicated from the original code
// to expose the i18n `t` function to Rhai scripts
let i18n = if Path::new(I18N_DIR).is_dir() {
let arc = ArcLoader::builder(I18N_DIR, unic_langid::langid!("de-DE"))
.shared_resources(Some(&[I18N_SHARED.into()]))
.customize(|bundle| bundle.set_use_isolating(false))
.build()
.map_err(|e| Error::string(&e.to_string()))?;
Some(FluentLoader::new(arc))
} else {
None
};
rhai_loco::RhaiScript::register_tera_filters(
&mut tera_engine,
config.filters_path,
|_engine| {}, // custom configuration of the Rhai Engine, if any
i18n,
)?;
info!("Filter scripts loaded");
}
// End addition
///////////////////////////////////////////////////////////////////////////////////
Ok(router.layer(Extension(ViewEngine::from(tera_engine))))
}
每个Rhai脚本文件(扩展名.rhai
)可以包含多个过滤器。子目录将被忽略。
除非被标记为private
,Rhai脚本文件中的每个函数都构成一个过滤器。函数的名称就是过滤器的名称。
函数签名
每个过滤器函数必须恰好接受一个参数,该参数是一个包含过滤器调用中所有变量的对象映射。
此外,过滤器调用中的变量也可以作为独立的变量访问。
原始数据值映射到this
。
示例
对于一个过滤器调用
┌───────────────┐
│ Tera template │
└───────────────┘
{{ "hello" | super_duper(a = "world", b = 42, c = true) }}
可以在Rhai脚本文件中如下定义过滤器函数super_duper
┌─────────────┐
│ Rhai script │
└─────────────┘
// This private function is ignored
private fn do_something(x) {
...
}
// This function has the wrong number of parameters and is ignored
fn do_other_things(x, y, z) {
...
}
// Filter 'super_duper'
fn super_duper(vars) {
// 'this' maps to "hello"
// 'vars' contains 'a', 'b' and 'c'
// The stand-alone variables 'a', 'b' and 'c' can also be accessed
let name = if vars.b > 0 { // access 'b' under 'vars'
...
} else if c { // access 'c'
...
} else !a.is_empty() { // access 'a'
...
} else {
...
}
// 'this' can be modified
this[0].to_upper();
// Return new value
`${this}, ${name}!`
}
脚本过滤器作为转换/格式化工具
脚本过滤器在即兴转换/格式化方面非常灵活,因为它们允许快速迭代和更改,而无需重新编译。
┌────────────────────┐
│ Rhai filter script │
└────────────────────┘
/// Say we have in-house status codes that we need to convert into text
/// for display with i18n support...
fn status(vars) {
switch this {
case "P" => t("Pending", lang),
case "A" => t("Active", lang),
case "C" => t("Cancelled", lang),
case "X" => t("Deleted", lang),
}
}
/// Use script to inject HTML also!
/// The input value is used to select from the list of options
fn all_status(vars) {`
<option value="P" ${if this == "P" { "selected" }}>t("Pending", lang)</option>
<option value="A" ${if this == "A" { "selected" }}>t("Active", lang)</option>
<option value="C" ${if this == "C" { "selected" }}>t("Cancelled", lang)</option>
<option value="X" ${if this == "X" { "selected" }}>t("Deleted", lang)</option>
`}
/// Say we have CSS classes that we need to add based on certain data values
fn count_css(vars) {
if this.count > 1 {
"error more-than-one"
} else if this.count == 0 {
"error missing-value"
} else {
"success"
}
}
┌───────────────┐
│ Tera template │
└───────────────┘
<!-- use script to determine the CSS class -->
<div id="record" class="{{ value | count_css }}">
<!-- use script to map the status display -->
<span>{{ value.status | status(lang="de-DE") }} : {{ value.count }}</span>
</div>
<!-- use script to inject HTML directly -->
<select>
<option value="">t("All", "de-DE")</option>
<!-- avoid escaping as text via the `safe` filter -->
{{ "A" | all_status(lang="de-DE") | safe }}
</select>
上述内容等同于以下Tera模板。
从技术上讲,您可以在脚本中或Tera模板本身中维护此类即兴行为,但在脚本中这样做可以提供重用和更干净的模板。
┌───────────────┐
│ Tera template │
└───────────────┘
<div id="record" class="{% if value.count > 1 %}
error more-than-one
{% elif value.count == 0 %}
error missing-value
{% else %}
success
{% endif %}">
<span>
{% if value.status == "P" %}
t(key = "Pending", lang = "de-DE")
{% elif value.status == "A" %}
t(key = "Active", lang = "de-DE")
{% elif value.status == "C" %}
t(key = "Cancelled", lang = "de-DE")
{% elif value.status == "D" %}
t(key = "Deleted", lang = "de-DE")
{% endif %}
: {{ value.count }}
</span>
</div>
在Loco请求中运行Rhai脚本
首先通过ScriptingEngineInitializer
将脚本引擎注入到Loco中。
┌────────────┐
│ src/app.rs │
└────────────┘
async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
Ok(vec![
// Add the scripting engine initializer
Box::new(rhai_loco::ScriptingEngineInitializer),
Box::new(initializers::view_engine::ViewEngineInitializer),
])
}
然后可以使用ScriptingEngine
在请求中提取脚本引擎。
例如,以下示例向登录认证过程添加了自定义脚本支持
┌─────────────────────────┐
│ src/controllers/auth.rs │
└─────────────────────────┘
// Import the scripting engine types
use rhai_loco::{RhaiScript, ScriptingEngine};
pub async fn login(
State(ctx): State<AppContext>,
// Extract the scripting engine
ScriptingEngine(script): ScriptingEngine<RhaiScript>,
Json(mut params): Json<LoginParams>,
) -> Result<Json<LoginResponse>> {
// Use `run_script_if_exists` to run a function `login` from a script
// `on_login.rhai` if it exists under `assets/scripts/`.
//
// Use `run_script` if the script is required to exist or an error is returned.
let result = script
.run_script_if_exists("on_login", &mut params, "login", ())
// ^ script file ^ function name
// ^ data mapped to `this` in script
// ^^ function arguments
.or_else(|err| script.convert_runtime_error(err, |msg| unauthorized(&msg)))?;
// ^^^^^^^^^^^^^^^^^^^^^^^^
// turn any runtime error into an unauthorized response
:
:
}
如果存在,这将调用脚本文件on_login.rhai
中的名为login
的函数
┌──────────────────────────────┐
│ assets/scripts/on_login.rhai │
└──────────────────────────────┘
// Function for custom login logic
fn login() {
// Can import other Rhai modules!
import "super/secure/vault" as vault;
debug(`Trying to login with user = ${this.user} and password = ${this.password}`);
let security_context = vault.extensive_checking(this.user, this.password);
if security_context.passed {
// Data values can be changed!
this.user = security_context.masked_user;
this.password = security_context.masked_password;
return security_context.id;
} else {
vault::black_list(this.user);
throw `The user ${this.user} has been black-listed!`;
}
}
自定义引擎设置
为了自定义Rhai脚本引擎,例如添加自定义函数或自定义类型支持,可以通过ScriptingEngineInitializerWithSetup
轻松地在Rhai引擎上执行自定义设置。
┌────────────┐
│ src/app.rs │
└────────────┘
async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
Ok(vec![
// Add the scripting engine initializer
Box::new(rhai_loco::ScriptingEngineInitializerWithSetup::new_with_setup(|engine| {
:
// ... do custom setup of Rhai engine here ...
:
})),
Box::new(initializers::view_engine::ViewEngineInitializer),
])
}
依赖项
~45–78MB
~1.5M SLoC